Giter Club home page Giter Club logo

pdsl's Introduction




Predicate Domain Specific Language

Read the docs!        




Build Status npm bundle size npm codecov

An expressive declarative toolkit for composing predicates in TypeScript or JavaScript

import p from "pdsl";

const isSoftwareCreator = p`{
  name: string,
  age: > 16,
  occupation: "Engineer" | "Designer" | "Project Manager"
}`;

isSoftwareCreator(someone); // true | false
  • Intuitive
  • Expressive
  • Lightweight - under 6k!
  • No dependencies
  • Small bundle size
  • Fast

Documentation

PDSL Documentation

pdsl's People

Contributors

dependabot[bot] avatar ryardley avatar willheslam avatar zabutonnage avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

pdsl's Issues

BUG: Fails with props starting with underscores

The following returns false when it should return true:

p`{
  action:{
    _typename: "Redirect" 
  }
}`({
  action: { 
    _typename: "Redirect" 
  }
})

Yet this works correctly:

p`{
  action:{
    typename: "Redirect" 
  }
}`({
  action: { 
    typename: "Redirect" 
  }
})

Suspect it is being parsed incorrectly as the extant predicate.

Refactor to allow context to helpers

In order to implement functionality such as validation and optional loose matching we need to pass an optional context to each helper.

Example:

const not = input =>
  function notFn(a) {
    return !val(input)(a);
  };

needs to turn into

const createNot = ctx => input =>
  function notFn(a) {
    return !val(input)(a);
  };

// For public api export
const not = createNot();

Then within the grammer we should use the helper creators and the generator should pass a global context to the creators.

We might run into issues when the helpers were being run within the grammer

runtime: prim(Number)

might need to become

runtime: ctx => prim(ctx)(Number)

Validation and error syntax

Not sure exactly how to do this but having a way to diagnose failure say through an errors array would mean that pdsl could be used to validate form data.

This way I think PDSL could be used for object validation in a way similar to yup but more concise and with more intuitive syntax.

Internal tidyup

We are not using an AST but often refer to the RPN as an AST this should be renamed to be consistent.

Experiment with rewriting helpers in TypeScript

If we rewrite PDSL in TypeScript and then rewrite helpers as guard functions we may be able to create dynamic TypeScript guard functions.

import p from 'pdsl';

function checkInput(input:any): number {
  // next pdsl acts as a guard without requiring a type param.
  if(p`number`(input)){ 
    return input;
  }
  return 0;
}

function getUsernameIfUser(possibleUser:any) {
  if(p`{ username: string, password: string }`(possibleUser)){
    return possibleUser.username; // TypeScript knows this exists and is available
  }
  return null;
}

This would save having to define lots of types for DTOs etc.

Interpolation function for validation

Validation messages should be able to be calculated using an interpolation function.

This was a little technical and not critical to launch the validation feature so spinning it off to its own task here.

This idea first came from #22

This follows from work on the validation functionality PR here: #104

Allow nested schemas

Currently this works:

import p, {schema} from 'pdsl'

const validName = p`string[4] <- "Must be a string 4 chars long"`

const validAge = p`> 21 <- "Must be over 21"`

const validUser = schema`{
  name: ${validName},
  age: ${validAge}
}`;

expect(() => {
  validUser.validateSync({name: "Foo", age: 25}); 
}).toThrow()

However it would be nice to be able to compose full schemas as it simplifies our import boilerplate:

import {schema as p} from 'pdsl'

const validName = p`string[4] <- "Must be a string 4 chars long"`

const validAge = p`> 21 <- "Must be over 21"`

const validUser = p`{
  name: ${validName},
  age: ${validAge}
}`;

expect(() => {
  validUser.validateSync({name: "Foo", age: 25}); 
}).toThrow()

Implement bang predicates

We should be able to use bangs to denote truthy and falsy values.

p`{name: !}`({name:1}); // false
p`{name: !}`({name:0}); // true
p`{name: !}`({name:"false"}); // false
p`{name: !}`({name:false}); // true

p`{name: !!}`({name:0}); // false
p`{name: !!}`({name:1}); // true
p`{name: !!}`({name:"true"}); // true
p`{name: !!}`({name:false}); // false

p`!`(0); // true
p`!!`(false); // false
p`!!`(true); // true

Add comments

It would be nice to be able to add a comment in the template string:

const isUser = `
{
  username: String,
  password: String && { length: > 5 }, // password must be longer than 5 chars 
}
`;

Add an optional property operator

Optional properties should probably look like typescript or flow:

const validate = p`{ foo?: string, name: string }`;
validate({name: 'Fred', foo: 45}); // false
validate({name: 'Fred'}); // true
validate({name: 'Fred', foo: "hello" }); // true

Refactor to monorepo to hold related libs

  • Syntax highlighting
  • Babel plugin
  • Compiler

Along with this we need to ensure we have our release structure organised in a script that will publish the canary as well as the latest tag.

WIP is over at the monorepo branch

Loose matching by default and can be configured

Exact matching objects in the wild is proving too cumbersome especially when dealing with things like graphql query results and nested objects so we need to revert to having it available via syntax:

Default should be loose matching:

p`{
  one: "one",
  two: "two"
}`({one: 'one', two:'two', three: 'three'}); // true

Exact matching should be specified by Flow style object bar syntax:

p`{|
  one: "one",
  two: "two"
|}`({one: 'one', two:'two', three: 'three'}); // false

Thinking that any sub object within this object will have exact matching turned on

p`{|
  one: "one",
  two: "two"
  three: {
    thing: 123
  }
|}`({one: 'one', two:'two', three: { thing: 123, other:'other'}}); // false

Array syntax should remain the same

expect(p`[1, 2]`([1,2])).toBe(true);
expect(p`[1, 2]`([1,2,3])).toBe(false);
p`[? 1]`([2,3,1]); // true

Thoughts on the future direction for pdsl

The following are some draft thoughts and do not indicate a well thought through position feel free to think about it but please reserve comments until draft status is removed.

PDSL thoughts on direction

So I have been thinking about this lib and how it can progress in the future and have come up with a few observations I think need to be addressed before it will become super relevant and compete with existing schema driven validation alternatives such as yup or joi:

Is the exact matching syntax hampering pattern matching usecase?

Exact matching syntax has added some really beneficial things especially around the way we can validate fixed length arrays with mixed values.

Typed arrays has meant we have a concise syntax for dealing with arrays that have a defined item type.

It is common in the wild to be able to express arrays as containing a fixed type say having an array of strings or an array of Person objects. It is less common to need to check for a specific item at an index being a specific type however it is a great feature to be able to do this especially when checking tuples and I think the current syntax around array matching that has come from the exact matching syntax PR is excellent.

However one of the main purposes of pdsl is to act as a shorthand pattern matcher or filter of sorts which means things such as checking if an object has a shape we can use. The key word is 'shorthand'. We almost always want to take something unknown and consolidate it towards a known thing that we can use. In the same way that interfaces allow This means that 90% of the time we only care we can use the object for a certain purpose and we don't care about any extra cruft on that object. Basically it is super annoying to have to add ... all the time because it is very easy to forget. One example where this is often the case would be checking a GraphQL result shape. Most of the time we don't want to care about stray __typename fields for example.

I think a solution here is to provide a set of configuration options that allows an exact matching in objects mode and a mode without exact matching in objects. We then need a way to get around it for each mode too:

import pdsl from 'pdsl';

// loose matching in objects
p.create({strictObjects:false})`{
  name: string,
}`({
  __typename: 'Person',
  name: 'Richard',
}); // true

// Escape hatch to provide strict matching on specific object
p.create({strictObjects:false})`{|
  name: string,
|}`({
  __typename: 'Person',
  name: 'Richard',
}); // false

// strict matching in objects
p.create({
  strictObjects: true
})`{
  name: string
}`({
  __typename: 'Person',
  name: 'Richard'
}); // false

// escape hatch
px`{
  name: string,
  ...
}`({
  __typename: 'Person',
  name: 'Richard'
}); // true

Usage with type-focussing

Secondly TypeScript is becoming more and more important when it comes to JavaScript and being able to synchronise PDSL expressions with TypeScript will make things much easier for developers. There is already a large overlap however there are times where working with typescript is annoying. This is especially true with the situation of working with data fetching layer type generation. Often types will be generated from Schemas and if you follow best practices often you get nullish or incomplete types and the frontend is expected to check the objects for those values to ensure our data is workable.

It may make sense to be able to generate types to a file from all your p expressions in a similar way to the way gql-gen works.

// generated/pdsl.ts
export interface RegisteredUser {
  role: "admin" | "manager" | "editor";
  name: string;
  isCompleteRegistration: true;
}
// generated/graphql.ts
export type GeneratedQueryUser = {
  __typename?: string | null 
  role?: "admin" | "manager" | "editor" | null;
  name?: string | null;
  isCompleteRegistration?: true | null;
}
import {GeneratedQueryUser} from './generated/graphql.ts';

// It might be possible to automatically infer types by using an alias key
// This would mean that the following would automatically be a guard for 
// the RegisteredUser interface. I would have preferred to be able to check 
// against the template string itself but this appears not to be possible with 
// `TemplateStringsArray`
const isRegisteredUser = p.as('RegisteredUser')`
   role: "admin" | "manager" | "editor",
   name: string,
   isCompleteRegistration: true
`;

export function getRegisteredUserList(users:GeneratedQueryUser[]):RegisteredUser[] {
  return users.filter(isRegisteredUser);
}

Does optional chaining make this library obsolete?

Part of what is helpful with this lib is the fact that it makes javascript more concise.

New additions like optional chaining to javascript has helped this out greatly which I think is absolutely fantastic:

const isValidStr = a => typeof a?.thing?.value === 'string' && 
  a?.thing?.value?.length > 3

vs

const isValidStr = a => a.thing && a.thing.value && typeof a.thing.value === 'string' && 
  a.thing.value.length > 3

However it is still shorter and more illustrative to express the validation in PDSL especially with the new length syntax:

const isValidStr = p`{ 
  thing: { 
    value: string[3] 
  } 
}`;

I also think this is cleaner than yup:

const isValidStr = yup.object().shape({
  thing: yup.object().shape({
    value: yup.string().required().min(3)
  })
}).isValidSync

I think realistically whilst this means that there is less of a chance someone would reach for this lib to do simple validation of objects with a couple of properties. When values and structures become more complex PDSL shines.

How can this make sense in the context of form validation say with a lib like formik?

  • Can we derive TypeScript types directly from the schema?
  • How can this be integrated in server side validation say in the context of a graphql schema?

Schema API changes

Thinking schema API should be simpler initially

import {schema as p} from "pdsl";

export default MyForm() {
  return <Formik
      initialValues={{
        email: "",
        firstName: "",
        lastName: ""
      }}
      validationSchema={p`{
        email: 
          _         <- "Required" 
          & Email   <- "Invalid email address",
        firstName: 
          _             <- "Required" 
          & string[>2]  <- "Must be longer than 2 characters"
          & string[<20] <- "Nice try nobody has a first name that long",
        lastName: 
          _             <- "Required" 
          & string[>2]  <- "Must be longer than 2 characters"
          & string[<20] <- "Nice try nobody has a last name that long"
      }`}
      onSubmit={values => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
        }, 500);
      }}
      render={({ errors, touched }) => (
        <Form>
          <label htmlFor="firstName">First Name</label>
          <Field name="firstName" placeholder="Jane" type="text" />

          <ErrorMessage
            name="firstName"
            component="div"
            className="field-error"
          />

          <label htmlFor="lastName">Last Name</label>
          <Field name="lastName" placeholder="Doe" type="text" />
          <ErrorMessage
            name="lastName"
            component="div"
            className="field-error"
          />

          <label htmlFor="email">Email</label>
          <Field name="email" placeholder="[email protected]" type="email" />
          <ErrorMessage name="email" component="div" className="field-error" />

          <button type="submit">Submit</button>
          <Debug />
        </Form>
      )}
    />
}

We can offer an escape hatch:

import {configureSchema} from 'pdsl';
const p = configureSchema({throwErrors: false});

Refactor to create a operator list based on objects

Required by #2 so we can keep our parsing code organised.

All the parser operators should be managed in a configuration list.

const {or, holds} = require('./helpers');

const OPERATORS = [{
  token: "\\|\\|",
  toString: () => '||',
  arity: 2, // -1 for dynamic,
  helper: or
},{
  token: "\\[",
  closingToken: "\\]",
  toString: c => `[${c}`,
  arity: -1,
  helper: holds
}]

The list should be ordered to reflect operator precedence.

Upon module load time the list should be configured to be consumable by the lib so we need not worry about performance.

Bug: String quoting error

The following should pass but it fails validation:

expect(
    p`"This string \"contains\" 'single quotes'"`(
      "This string \"contains\" 'single quotes'"
    )
  ).toBe(true);

Publish docs site

Now that we are working on the monorepo setup we should publish a docs site as part of that workflow.

Discussion: Syntax for between?

I am wondering if the choice of syntax for the between is bad? Mainly concerned about the space.

I am happy to make this a breaking change as this lib is still very young

Some other options:

btw

p`23 < n < 25`;

btw

p`23 << 25`;

btwe

p`23 <=<= 25`;

btwe

p`23 <= n <= 25`;

btw

p`23 >< 25`;

btwe

p`23 =><= 25`;

btwe

p`23 >=<= 25`;

TypeScript type compilation

I think the next thing holding back this lib is the lack of automatic typescript support.

One way to manage this would be to change the way TypeScript support is provided and to create a compiler/watcher for auto compiling types to node_modules.

This would allow PDSL to be strongly typed by simply adding a unique typescript key identifier.

const isUser = p<"isUser">`{
  name: string[>2],
  age: > 21
}`

Behind the scenes types would be compiled to somewhere in node modules:

// node_modules/pdsl/___types.ts
export type PDSLTypes = {
  isUser: { name: string, age: number }
}

The type signature of the returned function would look something like this:

type PredicateFn = <T>(input:any) => input is PDSLTypes[T]

Not sure if it is possible to maintain backward compatibility need to look up if conditionals are possible in TypeScript - I have a feeling they might be....

Bring interpolations into the template string

It would be nice to bring all the interpolations into the string

const isComplexObject = p`
  {
    type: ${/^.+foo$/},
    payload: {
      email: Email && { length: > 5 },
      arr: ![6],
      foo: !6,
      num: -4 < < 100,
      bar: {
        baz: ${/^foo/},
        foo
      }
    }
  }
`;

I am thinking to leave regexes out of the parser because they are tough to parse and are complex anyway however the helpers should be represented with appropriate shorthand syntax.

Convert to Typescript

I hesitated to add TypeScript before however I am starting to change my mind.

As I work more and more with the codebase it becomes more and more complex I am missing typescript for refactoring and catching type bugs.

TypeScript allows us to ensure the lib is more widely compatible as well.

Exact matching syntax

Exact Matching for Objects

I think using flow's exact matching syntax would be useful for specifying when an object cannot have any other properties except for those provided.

I propose the following for consideration:

p`{name, age}`({name: 'Michael', age: 27, pet: 'Cat'});// true
p`{|name, age|}`({name: 'Michael', age: 27, pet: 'Cat'});// false

Exact Matching for Arrays

Currently the Array matching syntax [a,b,c] will return true when the array contains one or more of the values provided.

I propose the following for consideration:

// exact match unordered
p`[|6, 'foo'|]`([6, 'foo']); // true
p`[|6, 'foo'|]`(['foo', 6]); // true
p`[|6, 'foo'|]`([6, 7, 'foo']); // false 

// ordered 
p`[||6, 'foo'||]`(['foo', 6]); // false

This is reminiscent of the exact Object syntax in flow which is why I like it.

Allow RegularExpressions in PDSL directly

If Regular Expressions could be added directly to the p-expression we would save some considerable space and increase legability:

// interpolated Regular Expressions
const isUser = p`{
  username: string & !${/[0-9A-Z]/} & { length: 4..8 },
  password: string & !${/[a-zA-Z0-9]/} & { length: > 8 },
  age: > 17
}`;

// input Regular Expressions directly
const isUser = p`{
  username: string & !/[0-9A-Z]/ & { length: 4..8 },
  password: string & !/[a-zA-Z0-9]/ & { length: > 8 },
  age: > 17
}`;

Should we allow single & and | ?

We will not need bitwise OR or bitwise AND in PDSL perhaps it makes sense to alias && and || with single char versions to make the lang more concise?

const isValidUser = p`{
  username: ${isOnlyLowerCase} & {length: 5 < < 9 },
  password: ${hasExtendedChars} & {length: > 8},
  age: > 17
}`;

Convert to typescript

Working on the PDSL compiler without types is becoming annoying and feels fragile. Should try and switch soon.

Assemble npm readme from docs files

  1. glob srcPath
  2. import doczrc extract menu
  3. transform each file stripping the top config section append in the order provided by doczrc.menu
  4. run the complete file through markdown-toc and append that to the top.
  5. write the file to the outputPath

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.