├── docs ├── CNAME ├── public │ └── pdsl-logo.png ├── numbers.md ├── _config.yml ├── boolean.md ├── values.md ├── kitchen-sink.md ├── troubleshooting.md ├── interpolations.md ├── usage-with-typescript.md ├── usage-with-formik.md ├── usage-with-babel.md ├── null-and-undefined.md ├── composition.md ├── types.md ├── reference.md ├── faq.md ├── strings.md ├── arrays.md ├── helpers.md ├── objects.md ├── validation.md └── index.md ├── examples └── formik │ ├── .gitignore │ ├── package.json │ └── pages │ ├── Debug.js │ └── index.js ├── packages ├── babel-plugin-pdsl │ ├── .eslintignore │ ├── .gitignore │ ├── .gitattributes │ ├── .prettierignore │ ├── testsetup.js │ ├── jest.config.js │ ├── test │ │ ├── fixtures │ │ │ ├── basic-default-schema │ │ │ │ ├── code.js.txt │ │ │ │ └── output.js.txt │ │ │ ├── basic-configured-schema │ │ │ │ ├── code.js.txt │ │ │ │ └── output.js.txt │ │ │ ├── basic-named-specifier │ │ │ │ ├── code.js.txt │ │ │ │ └── output.js.txt │ │ │ ├── multiple-helpers │ │ │ │ ├── code.js.txt │ │ │ │ └── output.js.txt │ │ │ ├── import-default-specifier-schema │ │ │ │ ├── code.js.txt │ │ │ │ └── output.js.txt │ │ │ ├── import-named-specifier │ │ │ │ ├── code.js.txt │ │ │ │ └── output.js.txt │ │ │ └── kitchen-sinc │ │ │ │ ├── code.js.txt │ │ │ │ └── output.js.txt │ │ └── index.spec.js │ ├── scripts │ │ └── release │ ├── src │ │ ├── runtime.js │ │ ├── helpers.js │ │ ├── babel-generator.js │ │ ├── literals.test.js │ │ ├── literals.js │ │ └── index.js │ ├── package.json │ └── README.md └── pdsl │ ├── .npmignore │ ├── .gitignore │ ├── pdsl-logo.png │ ├── tsconfig.declarations.json │ ├── jest.config.js │ ├── src │ ├── helpers │ │ ├── wildcard.ts │ │ ├── entry.ts │ │ ├── extant.ts │ │ ├── arrLen.ts │ │ ├── truthy.ts │ │ ├── falsey.ts │ │ ├── strLen.ts │ │ ├── lt.ts │ │ ├── gt.ts │ │ ├── lte.ts │ │ ├── gte.ts │ │ ├── not.ts │ │ ├── deep.ts │ │ ├── btw.ts │ │ ├── btwe.ts │ │ ├── validation.ts │ │ ├── val.ts │ │ ├── and.ts │ │ ├── regx.ts │ │ ├── or.ts │ │ ├── error-reporter.ts │ │ ├── prim.ts │ │ ├── arrTypeMatch.ts │ │ ├── pred.ts │ │ ├── arrIncludes.ts │ │ ├── arrArgMatch.ts │ │ ├── helpers.ts │ │ ├── obj.ts │ │ ├── helpers.test.ts │ │ ├── index.ts │ │ └── helpers.mdx │ └── lib │ │ ├── pretokenizer.ts │ │ ├── utils.test.ts │ │ ├── errors.ts │ │ ├── errors.test.ts │ │ ├── grammar.test.ts │ │ ├── index.d.ts │ │ ├── lexer.test.ts │ │ ├── generator.ts │ │ ├── i18n.ts │ │ ├── utils.ts │ │ ├── index.ts │ │ ├── lexer.ts │ │ ├── generator.test.ts │ │ ├── parser.test.ts │ │ ├── context.ts │ │ ├── parser.ts │ │ ├── grammar.ts │ │ └── index.test.ts │ ├── tsconfig.json │ ├── rollup.config.js │ ├── README.md │ └── package.json ├── .gitignore ├── pdsl-logo.png ├── lerna.json ├── .travis.yml ├── scripts ├── standard-lib.sh ├── git-check.sh ├── version-bump.sh ├── publish-latest ├── publishing.sh ├── rollback-afterversion └── release ├── CONTRIBUTING.md ├── package.json ├── LICENSE └── README.md /docs/CNAME: -------------------------------------------------------------------------------- 1 | pdsl.site -------------------------------------------------------------------------------- /examples/formik/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.next -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/.eslintignore: -------------------------------------------------------------------------------- 1 | /test/**/*.js -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.log 3 | .DS_Store 4 | /coverage 5 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/.prettierignore: -------------------------------------------------------------------------------- 1 | /test/fixtures/**/*.js -------------------------------------------------------------------------------- /pdsl-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryardley/pdsl/HEAD/pdsl-logo.png -------------------------------------------------------------------------------- /packages/pdsl/.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /docs 3 | .DS_Store 4 | /coverage 5 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/testsetup.js: -------------------------------------------------------------------------------- 1 | process.env.PDSL_SUPPRESS_DEPRICATION_WARNINGS = 1; 2 | -------------------------------------------------------------------------------- /docs/public/pdsl-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryardley/pdsl/HEAD/docs/public/pdsl-logo.png -------------------------------------------------------------------------------- /packages/pdsl/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .DS_Store 3 | /coverage 4 | /lib 5 | /helpers 6 | /index.js -------------------------------------------------------------------------------- /packages/pdsl/pdsl-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryardley/pdsl/HEAD/packages/pdsl/pdsl-logo.png -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: ["/testsetup.js"] 3 | }; 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/fixtures/basic-default-schema/code.js.txt: -------------------------------------------------------------------------------- 1 | import {schema as p} from "pdsl"; 2 | const schema = p`true`; -------------------------------------------------------------------------------- /packages/pdsl/tsconfig.declarations.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "5.2.10", 6 | "npmClient": "yarn", 7 | "useWorkspaces": true 8 | } 9 | -------------------------------------------------------------------------------- /packages/pdsl/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | testPathIgnorePatterns: ["/node_modules/", "\\.js$"] 5 | }; 6 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/fixtures/basic-configured-schema/code.js.txt: -------------------------------------------------------------------------------- 1 | import pdsl from "pdsl"; 2 | const p = pdsl.configureSchema({ someConfig: true }); 3 | const schema = p`true`; -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/fixtures/basic-named-specifier/code.js.txt: -------------------------------------------------------------------------------- 1 | import { configureSchema } from "pdsl"; 2 | const p = configureSchema({ someConfig: true }); 3 | const theSchema = p`true`; -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/wildcard.ts: -------------------------------------------------------------------------------- 1 | export const createWildcard = () => 2 | function wilcard() { 3 | // never going to fail so no need to do error reporting 4 | return true; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/fixtures/basic-default-schema/output.js.txt: -------------------------------------------------------------------------------- 1 | import _pdslHelpers from "pdsl/helpers"; 2 | 3 | const _pdslDefault = _pdslHelpers.createDefault(); 4 | 5 | const { 6 | schema: p 7 | } = _pdslDefault; 8 | const schema = p(_h => _h.val(true)); -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/entry.ts: -------------------------------------------------------------------------------- 1 | import { createVal } from "./val"; 2 | 3 | export const createEntry = ctx => 4 | function entry(name, predicate) { 5 | // never going to fail so no need to do error reporting 6 | return [name, createVal(ctx)(predicate)]; 7 | }; 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | notifications: 4 | email: true 5 | node_js: 6 | - "10" 7 | branches: 8 | only: 9 | - master 10 | script: yarn test 11 | env: CODECOV_TOKEN=68c38489-4793-4bfb-8f76-ebaa324053fa 12 | after_success: 13 | - codecov 14 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/extant.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | 3 | export const createExtant = ctx => 4 | function extant(a, msg?) { 5 | return createErrorReporter("extant", ctx, msg, [a])(() => { 6 | return a !== null && a !== undefined; 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/pretokenizer.ts: -------------------------------------------------------------------------------- 1 | function encode(num) { 2 | return `@{LINK:${num}}`; 3 | } 4 | 5 | export function pretokenizer(stringArray) { 6 | return stringArray.reduce( 7 | (acc, item, index) => 8 | index > 0 ? acc + encode(index - 1) + item : acc + item, 9 | "" 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/fixtures/basic-configured-schema/output.js.txt: -------------------------------------------------------------------------------- 1 | import _pdslHelpers from "pdsl/helpers"; 2 | 3 | const _pdslDefault = _pdslHelpers.createDefault(); 4 | 5 | const pdsl = _pdslDefault; 6 | const p = pdsl.configureSchema({ 7 | someConfig: true 8 | }); 9 | const schema = p(_h => _h.val(true)); -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/fixtures/basic-named-specifier/output.js.txt: -------------------------------------------------------------------------------- 1 | import _pdslHelpers from "pdsl/helpers"; 2 | 3 | const _pdslDefault = _pdslHelpers.createDefault(); 4 | 5 | const { 6 | configureSchema 7 | } = _pdslDefault; 8 | const p = configureSchema({ 9 | someConfig: true 10 | }); 11 | const theSchema = p(_h => _h.val(true)); -------------------------------------------------------------------------------- /packages/pdsl/src/lib/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { debug } from "./utils"; 2 | it("should be happy with code coverage", () => { 3 | const oldLogger = console.log; 4 | console.log = () => {}; 5 | expect( 6 | debug([], [], { token: "", toString: () => "y" }, "type", ["hello"]) 7 | ).toEqual(undefined); 8 | console.log = oldLogger; 9 | }); 10 | -------------------------------------------------------------------------------- /packages/pdsl/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": ".", 4 | "target": "es5", 5 | "module": "esnext", 6 | "allowJs": false, 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "noImplicitAny": false, 10 | "esModuleInterop": true 11 | }, 12 | "include": ["./src"], 13 | "exclude": ["**/*.js"] 14 | } 15 | -------------------------------------------------------------------------------- /scripts/standard-lib.sh: -------------------------------------------------------------------------------- 1 | exit_unless_say_yes() { 2 | read -p "$1" yn 3 | if [[ "$yn" != "y" ]] && [[ "$yn" != "Y" ]]; then 4 | if [[ -n "$2" ]]; then 5 | echo $2 6 | fi 7 | exit 0; 8 | fi 9 | } 10 | 11 | exit_if_empty() { 12 | if [[ -z "$1" ]]; then 13 | if [[ -n "$2" ]]; then 14 | echo "$2" 15 | fi 16 | exit 1 17 | fi 18 | } -------------------------------------------------------------------------------- /scripts/git-check.sh: -------------------------------------------------------------------------------- 1 | 2 | exit_unless_valid_branch() { 3 | local branch=$(git rev-parse --abbrev-ref HEAD) 4 | if [ "$branch" != "$1" ]; then 5 | echo "This script can only be run from the $1 branche"; 6 | exit 1; 7 | fi 8 | } 9 | 10 | exit_unless_clean_git_folder() { 11 | if [ -n "$(git status --porcelain)" ]; then 12 | echo "Please ensure your git folder is clean"; 13 | exit 1; 14 | fi 15 | } 16 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/errors.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file because testing when super fails is irrelavent */ 2 | 3 | type ErrorLike = { 4 | message: string; 5 | path: string; 6 | }; 7 | 8 | export class ValidationError extends Error { 9 | name = "ValidationError"; 10 | 11 | constructor( 12 | message?: string, 13 | public path: string = "", 14 | public inner: ErrorLike[] = [] 15 | ) { 16 | super(message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/fixtures/multiple-helpers/code.js.txt: -------------------------------------------------------------------------------- 1 | import p from "pdsl"; 2 | import _pdslHelpers from "pdsl/helpers"; 3 | const assert = require("assert"); 4 | assert.strictEqual(p`!(null|undefined)`(1), true); 5 | const isNotNil = _pdslHelpers.val( 6 | _pdslHelpers.not( 7 | _pdslHelpers.or( 8 | _pdslHelpers.val(null), 9 | _pdslHelpers.val(undefined) 10 | ) 11 | ) 12 | ); 13 | assert.strictEqual(isNotNil(1), true); -------------------------------------------------------------------------------- /examples/formik/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "formik", 3 | "private": true, 4 | "version": "5.2.10", 5 | "main": "index.js", 6 | "author": "Rudi Yardley", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "next dev", 10 | "start": "next start", 11 | "build": "next build" 12 | }, 13 | "dependencies": { 14 | "formik": "^2.1.4", 15 | "next": "^9.3.2", 16 | "pdsl": "^5.2.10", 17 | "react": "^16.13.1", 18 | "react-dom": "^16.13.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/fixtures/import-default-specifier-schema/code.js.txt: -------------------------------------------------------------------------------- 1 | import pdsl from "pdsl"; 2 | const assert = require("assert"); 3 | 4 | // Config 1 5 | const p = pdsl.configureSchema({ someConfig: true }); 6 | const schema = p`true`; 7 | 8 | assert.rejects(async () => { 9 | await schema.validate(true) 10 | }, true); 11 | 12 | // Config 2 13 | const q = pdsl.predicate({ someOtherConfig: true }); 14 | const predicate = q`true`; 15 | 16 | assert.strictEqual(predicate(true), true); 17 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/errors.test.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from "./errors"; 2 | 3 | it("should hold information", () => { 4 | const err = new ValidationError(); 5 | expect(err.name).toBe("ValidationError"); 6 | expect(err.message).toBe(""); 7 | expect(err.path).toBe(""); 8 | expect(err.inner).toEqual([]); 9 | 10 | const err2 = new ValidationError(""); 11 | expect(err2.name).toBe("ValidationError"); 12 | expect(err2.path).toBe(""); 13 | expect(err2.inner).toEqual([]); 14 | }); 15 | -------------------------------------------------------------------------------- /docs/numbers.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guide 3 | route: /numbers 4 | --- 5 | 6 | # Numbers 7 | 8 | ## Number in Range 9 | 10 | ```javascript 11 | p`1..10`(1); // true 12 | p`1..10`(5); // true 13 | p`1..10`(10); // true 14 | p`1..10`(11); // false 15 | ``` 16 | 17 | ```js 18 | p`1..10`(6); 19 | ``` 20 | 21 | ## Less than and greater than 22 | 23 | ```javascript 24 | p`< 10`(5); // true 25 | p`< 10`(12); // false 26 | p`>= 10`(10); // true 27 | p`> 10`(10); // false 28 | ``` 29 | 30 | ```js 31 | p`< 10 & > 3`(6); 32 | ``` 33 | -------------------------------------------------------------------------------- /scripts/version-bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | version_bump() { 5 | # patch, minor or major 6 | local semver_type=$1 7 | exit_unless_valid_semver "$semver_type" 8 | $(yarn bin)/lerna version "$semver_type" --ignore-changes 'examples/*' 9 | } 10 | 11 | exit_unless_valid_semver() { 12 | local semver_type=$1 13 | if [ "$semver_type" != "patch" ] && [ "$semver_type" != "minor" ] && [ "$semver_type" != "major" ]; then 14 | echo "Please provide update type: patch, minor major" 15 | exit 1; 16 | fi 17 | } -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/fixtures/import-named-specifier/code.js.txt: -------------------------------------------------------------------------------- 1 | import { configureSchema as doschema, predicate } from "pdsl"; 2 | const assert = require("assert"); 3 | 4 | // Config 1 5 | const p = doschema({ someConfig: true }); 6 | const myschema = p`true`; 7 | 8 | assert.rejects(async () => { 9 | await myschema.validate(true) 10 | }, true); 11 | 12 | // Config 2 13 | const q = predicate({ someOtherConfig: true }); 14 | const mypredicate = q`true`; 15 | 16 | assert.strictEqual(mypredicate(true), true); 17 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/arrLen.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | import { createVal } from "./val"; 3 | 4 | export const createArrLen = ctx => 5 | function arrLen(input) { 6 | return function arrLenFn(a, msg?) { 7 | return createErrorReporter( 8 | "arrLen", 9 | ctx, 10 | msg, 11 | [a, input], 12 | true // block downstream 13 | )(() => { 14 | return Array.isArray(a) && createVal(ctx)(input)(a.length); 15 | }); 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/truthy.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | /** 3 | *

Truthy

4 | * A predicate that takes an input value and returns whether or not the value is truthy 5 | * 6 | * @param {function} input The input value 7 | * @return {boolean} Boolean value indicating if the input is truthy 8 | */ 9 | export const createTruthy = ctx => 10 | function truthy(a, msg?) { 11 | return createErrorReporter("truthy", ctx, msg, [a])(() => { 12 | return !!a; 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/falsey.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | 3 | /** 4 | *

Falsey

5 | * A predicate that takes an input value and returns whether or not the value is falsey 6 | * 7 | * @param {function} input The input value 8 | * @return {boolean} Boolean value indicating if the input is falsey 9 | */ 10 | export const createFalsey = ctx => 11 | function falsey(a, msg?) { 12 | return createErrorReporter("falsey", ctx, msg, [a])(() => { 13 | return !a; 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/strLen.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | import { createVal } from "./val"; 3 | 4 | export const createStrLen = ctx => 5 | function strLen(input) { 6 | return function strLenFn(a, msg?) { 7 | return createErrorReporter( 8 | "strLen", 9 | ctx, 10 | msg, 11 | [a, input], 12 | true // block downstream 13 | )(() => { 14 | return typeof a === "string" && createVal(ctx)(input)(a.length); 15 | }); 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /scripts/publish-latest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source ./scripts/git-check.sh 6 | source ./scripts/standard-lib.sh 7 | source ./scripts/publishing.sh 8 | 9 | source_tag=${1:-next} 10 | destination_tag=${2:-latest} 11 | exit_if_empty "$source_tag" 12 | exit_if_empty "$destination_tag" 13 | exit_unless_valid_branch "master" 14 | exit_unless_clean_git_folder 15 | exit_unless_say_yes "This script will move ${source_tag} to ${destination_tag}. Are you sure? (Ny): " "User cancelled action" 16 | promote_tag_to_tag "$source_tag" "$destination_tag" 17 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/fixtures/multiple-helpers/output.js.txt: -------------------------------------------------------------------------------- 1 | import _pdslHelpers2 from "pdsl/helpers"; 2 | const p = _pdslHelpers2.createDefault(); 3 | import _pdslHelpers from "pdsl/helpers"; 4 | const assert = require("assert"); 5 | assert.strictEqual(p(_h => _h.val(_h.not(_h.or(_h.val(null), _h.val(undefined)))))(1), true); 6 | const isNotNil = _pdslHelpers.val( 7 | _pdslHelpers.not( 8 | _pdslHelpers.or( 9 | _pdslHelpers.val(null), 10 | _pdslHelpers.val(undefined) 11 | ) 12 | ) 13 | ); 14 | assert.strictEqual(isNotNil(1), true); -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Release Flow 2 | 3 | 1. New features are created on feature branches and merged to `master` 4 | 1. When a new feature is merged it is published to a semver version on the `next` dist-tag using the `LIVE_RUN=1 yarn release:[minor|major|patch]` script 5 | 1. When the `next` release is considered stable the publish:latest script is run which will bring the `next` tag inline with the `latest` tag. 6 | 7 | ## Running publishing scripts 8 | 9 | Publishing scripts can only be run when the env var is set: 10 | 11 | ``` 12 | LIVE_RUN=1 yarn release:patch 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/lt.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | 3 | /** 4 | *

Less than

5 | * Return a function that checks to see if it's input is less than the given number. 6 | * 7 | * @param {number} a The number to check against. 8 | * @return {function} A function of the form number => boolean 9 | */ 10 | export const createLt = ctx => 11 | function lt(a) { 12 | return function ltFn(n, msg?) { 13 | return createErrorReporter("lt", ctx, msg, [n, a])(() => { 14 | return n < a; 15 | }); 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/gt.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | 3 | /** 4 | *

Greater than

5 | * Return a function that checks to see if it's input is greater than the given number. 6 | * 7 | * @param {number} a The number to check against. 8 | * @return {function} A function of the form number => boolean 9 | */ 10 | export const createGt = ctx => 11 | function gt(a) { 12 | return function gtFn(n, msg?) { 13 | return createErrorReporter("gt", ctx, msg, [n, a])(() => { 14 | return n > a; 15 | }); 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | highlighter: rouge 2 | kramdown: 3 | syntax_highlighter_opts: 4 | disable: true 5 | theme: minima 6 | header_pages: 7 | - index.md 8 | - boolean.md 9 | - numbers.md 10 | - strings.md 11 | - arrays.md 12 | - objects.md 13 | - null-and-undefined.md 14 | - values.md 15 | - interpolations.md 16 | - validation.md 17 | - composition.md 18 | - reference.md 19 | - types.md 20 | - helpers.md 21 | - kitchen-sink.md 22 | - usage-with-babel.md 23 | - usage-with-formik.md 24 | - usage-with-typescript.md 25 | - troubleshooting.md 26 | - faq.md 27 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/scripts/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ -z "$1" ]; then 6 | echo "Please provide update type: patch, minor major" 7 | exit 1; 8 | fi 9 | 10 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 11 | if [[ "$BRANCH" != "master" ]]; then 12 | echo 'This script can only be run from the master branch'; 13 | exit 1; 14 | fi 15 | 16 | if [ -n "$(git status --porcelain)" ]; then 17 | echo "Please ensure your git folder is clean"; 18 | exit 1; 19 | fi 20 | 21 | yarn test 22 | npm version $1 23 | git push --tags && git push 24 | npm publish --access public -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/lte.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | 3 | /** 4 | *

Less than or equal to

5 | * Return a function that checks to see if it's input is less than or equal to the given number. 6 | * 7 | * @param {number} a The number to check against. 8 | * @return {function} A function of the form number => boolean 9 | */ 10 | export const createLte = ctx => 11 | function lte(a) { 12 | return function lteFn(n, msg?) { 13 | return createErrorReporter("lte", ctx, msg, [n, a])(() => { 14 | return n <= a; 15 | }); 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/gte.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | 3 | /** 4 | *

Greater than or equal to

5 | * Return a function that checks to see if it's input is greater than or equal to the given number. 6 | * 7 | * @param {number} a The number to check against. 8 | * @return {function} A function of the form number => boolean 9 | */ 10 | export const createGte = ctx => 11 | function gte(a) { 12 | return function gteFn(n, msg?) { 13 | return createErrorReporter("gte", ctx, msg, [n, a])(() => { 14 | return n >= a; 15 | }); 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /docs/boolean.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guide 3 | route: /boolean 4 | --- 5 | 6 | # Booleans 7 | 8 | ## True and False 9 | 10 | You can test for true and false by simply using true and false: 11 | 12 | ```js 13 | p`true`(true); // true 14 | p`false`(0); // false 15 | p`false`(undefined); // false 16 | p`false`(false); // true 17 | ``` 18 | 19 | ## Truthy and Falsy 20 | 21 | You can check for truthiness using the truthy and falsy predicates. 22 | 23 | ```js 24 | // Truthy 25 | p`!!`(0); // false 26 | p`!!`(1); // true 27 | 28 | // Falsy 29 | p`!`(false); // true 30 | p`!`(null); // true 31 | p`!"hello"`("hello"); // false 32 | ``` 33 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/fixtures/import-default-specifier-schema/output.js.txt: -------------------------------------------------------------------------------- 1 | import _pdslHelpers from "pdsl/helpers"; 2 | 3 | const _pdslDefault = _pdslHelpers.createDefault(); 4 | 5 | const pdsl = _pdslDefault; 6 | 7 | const assert = require("assert"); // Config 1 8 | 9 | 10 | const p = pdsl.configureSchema({ 11 | someConfig: true 12 | }); 13 | const schema = p(_h => _h.val(true)); 14 | assert.rejects(async () => { 15 | await schema.validate(true); 16 | }, true); // Config 2 17 | 18 | const q = pdsl.predicate({ 19 | someOtherConfig: true 20 | }); 21 | const predicate = q(_h => _h.val(true)); 22 | assert.strictEqual(predicate(true), true); 23 | -------------------------------------------------------------------------------- /docs/values.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guide 3 | route: /values 4 | --- 5 | 6 | # Reference equality 7 | 8 | If you pass a value, `pdsl` will match that specific value: 9 | 10 | ```javascript 11 | p`true`(true); // true 12 | p`false`(false); // true 13 | p`9`(9); // true 14 | p`"Rupert"`("Rupert"); // true 15 | ``` 16 | 17 | ```js 18 | p`"Rupert"`("Rupert"); 19 | ``` 20 | 21 | ## Interpolated values 22 | 23 | You can also interpolate values: 24 | 25 | ```javascript 26 | p`${true}`(true); // true 27 | p`${false}`(false); // true 28 | p`> ${9}`(9); // false 29 | p`> ${8}`(9); // true 30 | p`${"Rupert"}`("Rupert"); // true 31 | ``` 32 | 33 | ```js 34 | p`${9}`(9); 35 | ``` 36 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/not.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | import { createVal } from "./val"; 3 | /** 4 | *

Logical NOT

5 | * Takes an input predicate to form a new predicate that NOTs the result of the input predicate. 6 | * 7 | * @param {function} input The input predicate 8 | * @return {function} A function of the form {any => boolean} 9 | */ 10 | export const createNot = ctx => 11 | function not(input) { 12 | return function notFn(a, msg?) { 13 | return createErrorReporter("not", ctx, msg, [input, a])(() => { 14 | return !createVal(ctx)(input)(a); 15 | }); 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /scripts/publishing.sh: -------------------------------------------------------------------------------- 1 | source ./scripts/standard-lib.sh 2 | 3 | publish_to_dist_tag() { 4 | local dist_tag="$1" 5 | exit_if_empty "$dist_tag" "dist_tag is empty exiting." 6 | exit_if_empty "$LIVE_RUN" "Mocking publishing to ${dist_tag}." 7 | yarn build 8 | $(yarn bin)/lerna publish from-package --dist-tag "$dist_tag" --registry https://registry.npmjs.org/ 9 | } 10 | 11 | promote_tag_to_tag() { 12 | local source_tag="$1" 13 | local destination_tag="$2" 14 | exit_if_empty "$LIVE_RUN" "Mocking promoting next to latest." 15 | echo "$source_tag" "$destination_tag" 16 | $(yarn bin)/lerna exec --stream --no-bail --concurrency 1 -- yarn promote-next-latest 17 | } -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/deep.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | 3 | /** 4 | *

Is deep equal to value

5 | * Takes an input value to form a predicate that checks if the input deeply equals the value. 6 | * 7 | * @param {function} value The input value 8 | * @return {function} A function of the form {any => boolean} 9 | */ 10 | export const createDeep = ctx => 11 | function deep(value) { 12 | const st = JSON.stringify(value); 13 | return function isDeepEquals(a, msg?) { 14 | return createErrorReporter("deep", ctx, msg, [a, st])(() => { 15 | return st === JSON.stringify(a); 16 | }); 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/fixtures/import-named-specifier/output.js.txt: -------------------------------------------------------------------------------- 1 | import _pdslHelpers from "pdsl/helpers"; 2 | 3 | const _pdslDefault = _pdslHelpers.createDefault(); 4 | 5 | const { 6 | configureSchema: doschema, 7 | predicate 8 | } = _pdslDefault; 9 | 10 | const assert = require("assert"); // Config 1 11 | 12 | 13 | const p = doschema({ 14 | someConfig: true 15 | }); 16 | const myschema = p(_h => _h.val(true)); 17 | assert.rejects(async () => { 18 | await myschema.validate(true); 19 | }, true); // Config 2 20 | 21 | const q = predicate({ 22 | someOtherConfig: true 23 | }); 24 | const mypredicate = q(_h => _h.val(true)); 25 | assert.strictEqual(mypredicate(true), true); 26 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/fixtures/kitchen-sinc/code.js.txt: -------------------------------------------------------------------------------- 1 | // TODO: The following should eventually be sourced and generated 2 | // from the main tests within the PDSL 3 | const p = require("pdsl"); 4 | const assert = require("assert"); 5 | 6 | assert.strictEqual(p`!(null|undefined)`(1), true); 7 | assert.strictEqual(p`{name}`({name:"foo"}), true); 8 | assert.strictEqual(p`true`(true), true); 9 | assert.strictEqual(p`{name:${a => a.length > 10}}`({name:"12345678901"}), true); 10 | assert.strictEqual(p`_`(1), true); 11 | assert.strictEqual(p`_`(null), false); 12 | assert.strictEqual(p`Array & array[5]`([1, 2, 3, 4, 5]),true); 13 | assert.strictEqual(p`Email`("email@example.com"),true); -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/btw.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | /** 3 | *

Between bounds

4 | * Return a function that checks to see if it's input is between two numbers not including the numbers. 5 | * 6 | * @param {number} a The lower number 7 | * @param {number} b The higher number 8 | * @return {function} A function of the form number => boolean 9 | */ 10 | export const createBtw = ctx => 11 | function btw(a, b) { 12 | return function btwFn(n, msg?) { 13 | return createErrorReporter("btw", ctx, msg, [n, a, b])(() => { 14 | const [min, max] = a < b ? [a, b] : [b, a]; 15 | return n > min && n < max; 16 | }); 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /scripts/rollback-afterversion: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | thisversionraw=$(cat lerna.json | jq '.version') 4 | thisversion=$(sed -e 's/^"//' -e 's/"$//' <<<"$thisversionraw") 5 | lastversionraw=$(git show HEAD~1:lerna.json | jq '.version') 6 | lastversion=$(sed -e 's/^"//' -e 's/"$//' <<<"$lastversionraw") 7 | 8 | echo "Lerna version for this repo is $thisversion whilst the last commit is $lastversion do you want to revert? (yN)" 9 | 10 | read YN 11 | 12 | if [[ "$YN" != "y" ]] && [[ "$YN" != "Y" ]]; then 13 | echo "Aborting script" 14 | exit 0; 15 | fi 16 | 17 | git revert HEAD 18 | git checkout master 19 | git branch -d "v$thisversion" 20 | git tag -d "v$thisversion" 21 | git push origin ":v$thisversion" 22 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/src/runtime.js: -------------------------------------------------------------------------------- 1 | const t = require("@babel/types"); 2 | 3 | // return a function that accepts a list of argument nodes 4 | // to be converted to the relevant function call 5 | // this might be able to read the function name from the 6 | // pdslNode.runtime property 7 | function runtimeCreator(pdslNode, helpersIdentifier) { 8 | return (...babelNodes) => { 9 | const identifier = pdslNode.runtimeIdentifier; 10 | return t.callExpression( 11 | t.memberExpression( 12 | t.identifier(helpersIdentifier), 13 | t.identifier(identifier), 14 | false 15 | ), 16 | babelNodes 17 | ); 18 | }; 19 | } 20 | 21 | module.exports = { runtimeCreator }; 22 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/btwe.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | /** 3 | *

Between bounds or equal to

4 | * Return a function that checks to see if it's input is between two numbers including the numbers. 5 | * 6 | * @param {number} a The lower number 7 | * @param {number} b The higher number 8 | * @return {function} A function of the form number => boolean 9 | */ 10 | export const createBtwe = ctx => 11 | function btwe(a, b) { 12 | return function btweFn(n, msg?) { 13 | return createErrorReporter("btwe", ctx, msg, [n, a, b])(() => { 14 | const [min, max] = a < b ? [a, b] : [b, a]; 15 | return n >= min && n <= max; 16 | }); 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/grammar.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isPrecidenceOperatorClose, 3 | isPrecidenceOperator, 4 | isArgumentSeparator, 5 | isVaradicFunction, 6 | isVaradicFunctionClose, 7 | isPredicateLookup, 8 | isLiteral, 9 | isOperator, 10 | grammar 11 | } from "./grammar"; 12 | 13 | describe("operator predicates", () => { 14 | [ 15 | isPrecidenceOperatorClose, 16 | isPrecidenceOperator, 17 | isArgumentSeparator, 18 | isVaradicFunction, 19 | isVaradicFunctionClose, 20 | isPredicateLookup, 21 | isLiteral, 22 | isOperator 23 | ].forEach(p => { 24 | it("should return false for falsy input", () => { 25 | expect(p(undefined)).toBe(false); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/validation.ts: -------------------------------------------------------------------------------- 1 | import { createEntry } from "./entry"; 2 | import { createExtant } from "./extant"; 3 | 4 | export const createValidation = ctx => msg => 5 | function validation(predicate) { 6 | if (typeof predicate === "string") { 7 | // if this is a string we are looking 8 | // at an entry key without a predicate. 9 | // eg { name <- "Some message" } 10 | // Here we need to expand out the predicate to a 11 | // full entry 12 | const newPredicate = createExtant(ctx); 13 | return createEntry(ctx)(predicate, a => newPredicate(a, msg)); 14 | } 15 | return (...args) => { 16 | return predicate(...args, msg); // add msg as the final arg 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /docs/kitchen-sink.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Kitchen Sink 3 | menu: Examples 4 | route: /kitchen-sink 5 | --- 6 | 7 | # Kitchen sink 8 | 9 | ```js 10 | const isKitchenSinc = p` 11 | { 12 | type: ${/^.+foo$/}, 13 | payload: { 14 | email: string[>5] & Email, 15 | arr: [6,'foo', ...], 16 | foo: !true, 17 | num: 1..10, 18 | bar: { 19 | baz: ${/^foo/}, 20 | foo: !! 21 | } 22 | } 23 | } 24 | `; 25 | 26 | isKitchenSinc({ 27 | type: "snafoo", 28 | payload: { 29 | email: "hello@world.com", 30 | arr: [6, "foo", 1, 2, 3, 4, 5, 6], 31 | foo: false, 32 | num: 2, 33 | bar: { 34 | baz: "food", 35 | foo: "I am truthy" 36 | } 37 | } 38 | }); // true 39 | ``` 40 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/val.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | /** 3 | *

Is strict equal to value

4 | * Takes an input value to form a predicate that checks if the input strictly equals by reference the value. 5 | * 6 | * @param {function|*} value The input value if already a fuction it will be returned 7 | * @return {function} A function of the form {any => boolean} 8 | */ 9 | export const createVal = ctx => 10 | function val(value) { 11 | return typeof value === "function" 12 | ? value 13 | : function isVal(a, msg?) { 14 | return createErrorReporter("val", ctx, msg, [a, value])(() => { 15 | return a === value; 16 | }); 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Troubleshooting 3 | route: /troubleshooting 4 | --- 5 | 6 | # Troubleshooting 7 | 8 | This should work however this project is young and there is a chance you may find bugs that are not covered by our test suite. Not all safety checks are in place and you may find issues around this. 9 | 10 | Please help this open source project by [creating issues](https://github.com/ryardley/pdsl/issues/new). 11 | 12 | Pull requests appreciated! [Feel free to help with open issues](https://github.com/ryardley/pdsl/issues). 13 | 14 | This Syntax is DRAFT and we are open for [RFCs on the syntax](https://github.com/ryardley/pdsl/issues/new). 15 | 16 | All feedback welcome. If you want to be a maintainer [create a pull request](https://github.com/ryardley/pdsl/pulls) 17 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pdsl/babel-plugin-pdsl", 3 | "version": "5.2.10", 4 | "main": "./src/index.js", 5 | "license": "MIT", 6 | "repository": "github:ryardley/babel-plugin-pdsl", 7 | "dependencies": { 8 | "@babel/generator": "^7.5.5", 9 | "@babel/helper-module-imports": "^7.0.0", 10 | "@babel/template": "^7.4.4", 11 | "@babel/traverse": "^7.5.5", 12 | "@babel/types": "^7.5.5" 13 | }, 14 | "scripts": { 15 | "test": "jest", 16 | "promote-next-latest": "pkg_version=$(npm v . dist-tags.next); [ -n $pkg_version ] && ( npm dist-tag add @pdsl/babel-plugin-pdsl@$pkg_version latest ) || exit 0" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.5.5", 20 | "babel-plugin-tester": "^7.0.1", 21 | "jest": "^24.9.0", 22 | "pdsl": "^5.2.10" 23 | }, 24 | "peerDependencies": { 25 | "pdsl": ">=3.5.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/interpolations.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guide 3 | route: /interpolations 4 | --- 5 | 6 | # Interpolations 7 | 8 | ## Interpolated values 9 | 10 | You can interpolate values: 11 | 12 | ```javascript 13 | p`${true}`(true); // true 14 | p`${false}`(false); // true 15 | p`> ${9}`(9); // false 16 | p`> ${8}`(9); // true 17 | p`${"Rupert"}`("Rupert"); // true 18 | ``` 19 | 20 | ```js 21 | p`${"Rupert"}`("Rupert"); // true 22 | ``` 23 | 24 | ## Regular expression predicates 25 | 26 | You can use a regular expression by interpolating it like you would a predicate function. 27 | 28 | ```js 29 | p`${/^foo/}`("food"); // true 30 | ``` 31 | 32 | ## Javascript predicate functions 33 | 34 | Any function passed as an expression to the template literal will be used as a test. 35 | 36 | ```js 37 | const startsWithFoo = a => a.indexOf("foo") === 0; 38 | 39 | p`{ eat: ${startsWithFoo} }`({ eat: "food" }); // true 40 | ``` 41 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/and.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | import { createVal } from "./val"; 3 | /** 4 | *

Logical AND

5 | * Combine predicates to form a new predicate that ANDs the result of the input predicates. 6 | * 7 | * @param {function} left The first predicate 8 | * @param {function} right The second predicate 9 | * @return {function} A function of the form {any => boolean} 10 | */ 11 | export const createAnd = ctx => 12 | function and(left, right) { 13 | return function andFn(a, msg?) { 14 | return createErrorReporter( 15 | "and", 16 | ctx, 17 | msg, 18 | [a, left, right], 19 | false, // dont block downstream 20 | true // disable default 21 | )(() => { 22 | const val = createVal(ctx); 23 | return val(left)(a) && val(right)(a); 24 | }); 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/src/helpers.js: -------------------------------------------------------------------------------- 1 | const t = require("@babel/types"); 2 | 3 | // return a babelNode that wraps the babelNode in a val() 4 | // helper function call 5 | function val(babelNode, helpersIdentifier) { 6 | return t.callExpression( 7 | t.memberExpression( 8 | t.identifier(helpersIdentifier), 9 | t.identifier("val"), 10 | false 11 | ), 12 | [babelNode] 13 | ); 14 | } 15 | 16 | // Use Babel API to wrap the babel Node in a helper `pred` function, 17 | function wrapPred(babelNode, helpersIdentifier) { 18 | return t.callExpression( 19 | t.memberExpression( 20 | t.identifier(helpersIdentifier), 21 | t.identifier("pred"), 22 | false 23 | ), 24 | [babelNode] 25 | ); 26 | } 27 | 28 | const lookupPredicateFunction = (node, funcs, helpersIdentifier) => { 29 | return wrapPred(funcs[node.token], helpersIdentifier); 30 | }; 31 | module.exports = { val, lookupPredicateFunction }; 32 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source ./scripts/version-bump.sh 6 | source ./scripts/git-check.sh 7 | source ./scripts/publishing.sh 8 | 9 | echo "Have you set the env var for actually publishing? Eg." 10 | echo "" 11 | echo " LIVE_RUN=1 yarn release:patch" 12 | echo "" 13 | echo "Press a key when ready..." 14 | read 15 | 16 | semver_type=$1 17 | dist_tag=${2:-next} 18 | exit_unless_valid_semver "$semver_type" 19 | exit_unless_valid_branch "master" 20 | exit_unless_clean_git_folder 21 | yarn test 22 | version_bump "$semver_type" 23 | if [[ -n "$LIVE_RUN" ]]; then 24 | thisversionraw=$(cat lerna.json | jq '.version') 25 | thisversion=$(sed -e 's/^"//' -e 's/"$//' <<<"$thisversionraw") 26 | echo "Pushing tags to remote on branch v$thisversion" 27 | git checkout -b "version-bump-v$thisversion" && git push --tags && git push -u origin "version-bump-v$thisversion" 28 | git checkout master 29 | fi 30 | publish_to_dist_tag "$dist_tag" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "pretest": "yarn build", 6 | "test": "lerna run test --concurrency=1 --stream", 7 | "docs:build": "lerna run --scope docs build --stream", 8 | "build": "lerna run --scope pdsl build --stream", 9 | "bootstrap": "lerna bootstrap --no-ci", 10 | "clean": "yarn clean:artifacts && yarn clean:packages", 11 | "clean:artifacts": "lerna run clean --parallel", 12 | "clean:packages": "lerna clean --yes", 13 | "publish:latest": "./scripts/publish-latest", 14 | "release:patch": "./scripts/release patch", 15 | "release:minor": "./scripts/release minor", 16 | "release:major": "./scripts/release major" 17 | }, 18 | "resolutions": { 19 | "acorn": "^6.4.1", 20 | "minimist": "^1.2.2", 21 | "kind-of": "^6.0.3" 22 | }, 23 | "devDependencies": { 24 | "lerna": "^3.16.4" 25 | }, 26 | "workspaces": [ 27 | "packages/*", 28 | "examples/*" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/regx.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | 3 | /** 4 | *

Regular Expression predicate

5 | * Forms a predicate from a given regular expression 6 | * 7 | * @param {RegExp} rx The input value 8 | * @return {function} A function of the form {any => boolean} 9 | */ 10 | export const createRegx = ctx => 11 | function regx(rx) { 12 | const rgx = typeof rx === "function" ? rx(ctx) : rx; 13 | return function testRegx(a, msg?) { 14 | return createErrorReporter("regx", ctx, msg, [a, rx])(() => { 15 | return rgx.test(a); 16 | }); 17 | }; 18 | }; 19 | 20 | export const Email = () => 21 | /^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]+)$/; 22 | export const Xc = () => /(?=.*[^a-zA-Z0-9\s]).*/; 23 | export const Nc = () => /(?=.*[0-9]).*/; 24 | export const Lc = () => /(?=.*[a-z]).*/; 25 | export const Uc = () => /(?=.*[A-Z]).*/; 26 | export const LUc = () => /(?=.*[a-z])(?=.*[A-Z]).*/; 27 | -------------------------------------------------------------------------------- /docs/usage-with-typescript.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Integrations 3 | name: Usage with TypeScript 4 | route: /typescript 5 | --- 6 | 7 | # Usage with TypeScript 8 | 9 | PDSL is really quite useful in TypeScript as guard functions are important to a good type management strategy. To use in TypeScript simply pass in the guard type you want your predicate to determine as a type prop. 10 | 11 | ```ts 12 | import p from "pdsl"; 13 | 14 | // pass in string 15 | const isString = p`string`; 16 | 17 | type User = { 18 | name: string; 19 | password: string; 20 | }; 21 | 22 | // pass in User 23 | const isUser = p`{ 24 | name: string[3..8], 25 | password: string[>5] 26 | }`; 27 | 28 | function doStuff(input: string | User) { 29 | // input is either string or User 30 | if (isString(input)) { 31 | // input is now considered a string 32 | return input.toLowerCase(); 33 | } 34 | 35 | if (isUser(input)) { 36 | // input is now considered a User 37 | return input.name; 38 | } 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | type PredicateFn = (input: any) => input is T; 2 | 3 | type PdslOptions = { 4 | schemaMode?: boolean; 5 | abortEarly?: boolean; 6 | captureErrors?: boolean; 7 | throwErrors?: boolean; 8 | }; 9 | 10 | type PdslError = { 11 | path: string; 12 | message: string; 13 | }; 14 | 15 | type PdslSchema = { 16 | validateSync(a: any): Array; 17 | validate(a: any): Promise>; 18 | }; 19 | 20 | type schemaCreator = ( 21 | strings: TemplateStringsArray, 22 | ...expressions: any[] 23 | ) => PdslSchema; 24 | 25 | type predicateCreatorType = typeof predicateCreator; 26 | 27 | declare function predicateCreator( 28 | strings: TemplateStringsArray, 29 | ...expressions: any[] 30 | ): PredicateFn; 31 | 32 | declare namespace predicateCreator { 33 | var schema: schemaCreator; 34 | var predicate: (options?: PdslOptions) => predicateCreatorType; 35 | var createSchema: (options?: PdslOptions) => schemaCreator; 36 | } 37 | 38 | export default predicateCreator; 39 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/fixtures/kitchen-sinc/output.js.txt: -------------------------------------------------------------------------------- 1 | const _pdslHelpers = require("pdsl/helpers"); 2 | 3 | // TODO: The following should eventually be sourced and generated 4 | // from the main tests within the PDSL 5 | const _pdslDefault = _pdslHelpers.createDefault(); 6 | 7 | const p = _pdslDefault; 8 | 9 | const assert = require("assert"); 10 | 11 | assert.strictEqual(p(_h => _h.val(_h.not(_h.or(_h.val(null), _h.val(undefined)))))(1), true); 12 | assert.strictEqual(p(_h => _h.val(_h.obj("name")))({ 13 | name: "foo" 14 | }), true); 15 | assert.strictEqual(p(_h => _h.val(true))(true), true); 16 | assert.strictEqual(p(_h => _h.val(_h.obj(_h.entry("name", _h.pred(a => a.length > 10)))))({ 17 | name: "12345678901" 18 | }), true); 19 | assert.strictEqual(p(_h => _h.val(_h.extant))(1), true); 20 | assert.strictEqual(p(_h => _h.val(_h.extant))(null), false); 21 | assert.strictEqual(p(_h => _h.val(_h.and(_h.arrTypeMatch(_h.prim(Number)), _h.arrLen(5))))([1, 2, 3, 4, 5]), true); 22 | assert.strictEqual(p(_h => _h.val(_h.regx(_h.Email)))("email@example.com"), true); -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/or.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | import { createVal } from "./val"; 3 | 4 | /** 5 | *

Logical OR

6 | * Combine predicates to form a new predicate that ORs the result of the input predicates. 7 | * 8 | * @param {function} left The first predicate 9 | * @param {function} right The second predicate 10 | * @return {function} A function of the form {any => boolean} 11 | */ 12 | export const createOr = ctx => 13 | function or(left, right) { 14 | return function orFn(a, msg?) { 15 | return createErrorReporter( 16 | "or", 17 | ctx, 18 | msg, 19 | [a, left, right], 20 | false, // dont block downstream 21 | true // disable default 22 | )(() => { 23 | const val = createVal(ctx); 24 | ctx.batchStart(); 25 | 26 | const result = val(left)(a) || val(right)(a); 27 | if (!result) { 28 | ctx.batchCommit(); 29 | } 30 | ctx.batchPurge(); 31 | return result; 32 | }); 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /examples/formik/pages/Debug.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormikConsumer } from "formik"; 3 | 4 | export const Debug = () => ( 5 |
13 |
26 | Formik State 27 |
28 | 29 | {({ validationSchema, validate, onSubmit, ...rest }) => ( 30 |
37 |           {JSON.stringify(rest, null, 2)}
38 |         
39 | )} 40 |
41 |
42 | ); 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rudi Yardley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/error-reporter.ts: -------------------------------------------------------------------------------- 1 | export const createErrorReporter = ( 2 | key, 3 | ctx, 4 | msg, 5 | args, 6 | blockDownstream = false, 7 | disableDefault = false 8 | ) => callback => { 9 | // Much of the complexity here is determining if we should show downstream errors 10 | if (!ctx || !ctx.captureErrors) return callback(); 11 | 12 | ctx.pushErrStack(key); 13 | const blockFurther = msg || blockDownstream; 14 | 15 | if (blockFurther) { 16 | ctx.blockErrors = ctx.errStack.join("."); 17 | } 18 | 19 | const result = callback(); 20 | 21 | if (result === false && ctx) { 22 | const errPath = ctx.errStack.join("."); 23 | const errAllowed = 24 | !ctx.blockErrors || 25 | msg || 26 | (ctx.blockErrors.length !== errPath.length && 27 | errPath.indexOf(ctx.blockErrors) !== 0); 28 | 29 | const message = msg || (!disableDefault && ctx.lookup(key)); 30 | 31 | if (errAllowed && message) { 32 | ctx.reportError(message, ...args); 33 | } 34 | } 35 | if (blockFurther) { 36 | ctx.blockErrors = ""; 37 | } 38 | ctx.popErrStack(); 39 | return result; 40 | }; 41 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/lexer.test.ts: -------------------------------------------------------------------------------- 1 | import { lexer } from "./lexer"; 2 | import { tokens, grammar } from "./grammar"; 3 | import { rpnToString } from "./utils"; 4 | 5 | describe("lexer", () => { 6 | it("should tokenize", () => { 7 | expect( 8 | rpnToString( 9 | lexer(" { name : @{LINK:0} && @{LINK:2} ,age:@{LINK:1}}") 10 | ) 11 | ).toEqual("{0·name·:·@{LINK:0}·&&·@{LINK:2}·,·age·:·@{LINK:1}·}"); 12 | }); 13 | it("should strip comments", () => { 14 | expect( 15 | rpnToString( 16 | lexer(`{ 17 | name: "Rudi", // foo 18 | // thing 19 | age 20 | }`) 21 | ) 22 | ).toEqual('{0·name·:·"Rudi"·,·age·}'); 23 | }); 24 | 25 | it("should differentiate between ! as a unary operator and as a predicate", () => { 26 | expect(JSON.stringify(lexer(`{ name: ! }`))).toEqual( 27 | JSON.stringify([ 28 | grammar[tokens.OBJ]("{"), 29 | grammar[tokens.IDENTIFIER]("name"), 30 | grammar[tokens.ENTRY](":"), 31 | grammar[tokens.FALSY_KEYWORD]("!"), 32 | grammar[tokens.OBJ_CLOSE]("}") 33 | ]) 34 | ); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/prim.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | /** 3 | *

Primative predicate

4 | * Forms a predicate from a given JavaSCript primative object to act as a typeof check for the input value. 5 | * 6 | * Eg.

 7 |  * prim(Function)(() => {}); // true
 8 |  * prim(Number)(6); // true
 9 |  * 
10 | * 11 | * @param {object} primative The input primative one of Array, Boolean, Number, Symbol, BigInt, String, Function, Object 12 | * @return {function} A function of the form {any => boolean} 13 | */ 14 | export const createPrim = ctx => 15 | function prim(primative) { 16 | if (primative.name === "Array") { 17 | return function isArray(a, msg?) { 18 | return createErrorReporter("prim", ctx, msg, [a, primative.name])( 19 | () => { 20 | return Array.isArray(a); 21 | } 22 | ); 23 | }; 24 | } 25 | return function isPrimative(a, msg?) { 26 | return createErrorReporter("prim", ctx, msg, [a, primative.name])(() => { 27 | return typeof a === primative.name.toLowerCase(); 28 | }); 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/pdsl/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "rollup-plugin-typescript2"; 2 | 3 | const config = { 4 | watch: { 5 | include: "src/**" 6 | } 7 | }; 8 | 9 | const tsconfig = { 10 | rollupCommonJSResolveHack: false, 11 | clean: true 12 | }; 13 | 14 | export default [ 15 | { 16 | input: "./src/helpers/index.ts", 17 | output: { 18 | file: "./helpers/index.js", 19 | name: "helpers", 20 | format: "cjs", 21 | exports: "named" 22 | }, 23 | plugins: [typescript(tsconfig)], 24 | ...config 25 | }, 26 | { 27 | input: "./src/lib/index.ts", 28 | output: { 29 | file: "./index.js", 30 | name: "lib", 31 | format: "cjs", 32 | exports: "named" 33 | }, 34 | plugins: [ 35 | typescript({ 36 | ...tsconfig, 37 | tsconfig: "./tsconfig.declarations.json" 38 | }) 39 | ], 40 | ...config 41 | }, 42 | { 43 | input: "./src/lib/grammar.ts", 44 | output: { 45 | file: "./lib/grammar.js", 46 | name: "lib", 47 | format: "cjs", 48 | exports: "named" 49 | }, 50 | plugins: [typescript(tsconfig)], 51 | ...config 52 | } 53 | ]; 54 | -------------------------------------------------------------------------------- /docs/usage-with-formik.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Integrations 3 | name: Formik form validation 4 | route: /formik-validation 5 | --- 6 | 7 | # Formik form validation 8 | 9 | PDSL provides a handy schema named export that can be used to integrate directly with Formik: 10 | 11 | ```tsx 12 | 13 | import {schema as p} from "pdsl" 14 | 15 | () => ( 16 | 2] <- "Must be longer than 2 characters" 29 | & string[<20] <- "Nice try nobody has a first name that long", 30 | lastName: 31 | _ <- "Required" 32 | & string[>2] <- "Must be longer than 2 characters" 33 | & string[<20] <- "Nice try nobody has a last name that long" 34 | }`} 35 | onSubmit={values => { 36 | // submit values 37 | }} 38 | render={({ errors, touched }) => ( 39 | // render form 40 | )} 41 | /> 42 | ) 43 | 44 | ``` 45 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/generator.ts: -------------------------------------------------------------------------------- 1 | import { getRawHelpers } from "../helpers/index"; 2 | const { val } = getRawHelpers(); 3 | import { 4 | isPredicateLookup, 5 | isVaradicFunction, 6 | isOperator, 7 | isLiteral 8 | } from "./grammar"; 9 | 10 | const lookupPredicateFunction = (node, funcs) => { 11 | return funcs[node.token]; 12 | }; 13 | 14 | export function generator(input, funcs, ctx) { 15 | const [runnable] = input.reduce((stack, node) => { 16 | if (isPredicateLookup(node)) { 17 | stack.push(lookupPredicateFunction(node, funcs)); 18 | return stack; 19 | } 20 | 21 | if (isLiteral(node)) { 22 | stack.push(node.runtime(ctx)); 23 | return stack; 24 | } 25 | 26 | /* istanbul ignore next 27 | because it flags the else as never 28 | happening however I am not comfortable 29 | enough to remove the if completely 30 | even though it would probably work */ 31 | if (isOperator(node) || isVaradicFunction(node)) { 32 | const { arity, runtime } = node; 33 | const args = stack.splice(-1 * arity); 34 | stack.push(runtime(ctx)(...args)); 35 | return stack; 36 | } 37 | }, []); 38 | 39 | return val(ctx)(runnable); 40 | } 41 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/i18n.ts: -------------------------------------------------------------------------------- 1 | const dict = { 2 | btw: "Number $1 is not between $2 and $3", 3 | btwe: "Number $1 is not in range [$2..$3]", 4 | lt: "Number $1 is not less than $2", 5 | lte: "Number $1 is not less than or equal to $2", 6 | gt: "Number $1 is not greater than $2", 7 | gte: "Number $1 is not greater than or equal to $2", 8 | arrArgMatch: "Array $1 does not match given predicate pattern", 9 | arrTypeMatch: "Array $1 does not match given type", 10 | arrIncl: "Array $1 does not include specified types", 11 | or: "Value $1 does not satisfy 'or' predicate", 12 | and: "Value $1 does not satisfy 'and' predicate", 13 | not: "Value $1 does not satisfy 'not' predicate", 14 | extant: "Value $1 is either null or undefined", 15 | truthy: "Value $1 is not truthy", 16 | falsey: "Value $1 is not falsey", 17 | obj: "Value $1 did not match object predicates", 18 | val: "Value $1 did not match value $2", 19 | deep: "Value $1 did not match value $2", 20 | regx: "Value $1 did not match regexp $2", 21 | prim: "Value $1 is not of type $2", 22 | strLen: "Length of string $1 does not match", 23 | arrLen: "Length of array $1 does not match" 24 | }; 25 | 26 | export function lookup(key: string) { 27 | return dict[key as keyof typeof dict]; 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |


2 |

3 | 4 |

5 | 6 |

Predicate Domain Specific Language

7 |

8 | Read the docs!         9 |

10 |


11 | 12 | [![Build Status](https://travis-ci.com/ryardley/pdsl.svg?branch=master)](https://travis-ci.com/ryardley/pdsl) 13 | ![npm bundle size](https://img.shields.io/bundlephobia/minzip/pdsl.svg) 14 | ![npm](https://img.shields.io/npm/v/pdsl.svg) 15 | [![codecov](https://codecov.io/gh/ryardley/pdsl/branch/master/graph/badge.svg)](https://codecov.io/gh/ryardley/pdsl) 16 | 17 | #### An expressive declarative toolkit for composing predicates in TypeScript or JavaScript 18 | 19 | ```js 20 | import p from "pdsl"; 21 | 22 | const isSoftwareCreator = p`{ 23 | name: string, 24 | age: > 16, 25 | occupation: "Engineer" | "Designer" | "Project Manager" 26 | }`; 27 | 28 | isSoftwareCreator(someone); // true | false 29 | ``` 30 | 31 | - [x] Intuitive 32 | - [x] Expressive 33 | - [x] Lightweight - under 6k! 34 | - [x] No dependencies 35 | - [x] Small bundle size 36 | - [x] Fast 37 | 38 |
39 | 40 | ## Documentation 41 | 42 | [PDSL Documentation](https://pdsl.site) 43 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/src/babel-generator.js: -------------------------------------------------------------------------------- 1 | // This generator unlike the one in PDSL actually generates babel AST 2 | 3 | const { 4 | isPredicateLookup, 5 | isVaradicFunction, 6 | isOperator, 7 | isLiteral 8 | } = require("pdsl/lib/grammar"); 9 | 10 | const { literal } = require("./literals"); 11 | const { runtimeCreator } = require("./runtime"); 12 | const { val, lookupPredicateFunction } = require("./helpers"); 13 | 14 | function generator(input, babelExpressionList, helpersIdentifier) { 15 | const [runnable] = input.reduce((stack, node) => { 16 | if (isPredicateLookup(node)) { 17 | stack.push( 18 | lookupPredicateFunction(node, babelExpressionList, helpersIdentifier) 19 | ); 20 | return stack; 21 | } 22 | 23 | if (isLiteral(node)) { 24 | stack.push(literal(node, helpersIdentifier)); 25 | return stack; 26 | } 27 | 28 | if (isOperator(node) || isVaradicFunction(node)) { 29 | const { arity } = node; 30 | const args = stack.splice(-1 * arity); 31 | const runtime = runtimeCreator(node, helpersIdentifier); 32 | stack.push(runtime(...args)); 33 | return stack; 34 | } 35 | }, []); 36 | 37 | return val(runnable, helpersIdentifier); 38 | } 39 | 40 | module.exports = { generator }; 41 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | export function isRegEx(expression) { 2 | return expression instanceof RegExp; 3 | } 4 | 5 | export function isPDSLSchema(expression) { 6 | return expression && typeof expression.unsafe_predicate === "function"; 7 | } 8 | 9 | export function isPrimative(expression) { 10 | return ( 11 | [ 12 | "Array", 13 | "Boolean", 14 | "Number", 15 | "Symbol", 16 | // "BigInt", // waiting for stage 4 17 | "String", 18 | "Function", 19 | "Object" 20 | ].indexOf(expression.name) > -1 21 | ); 22 | } 23 | export function rpnToString(rpn) { 24 | return rpn.map(a => a.toString()).join("·"); 25 | } 26 | 27 | export function debug(output, stack, node, type, msg) { 28 | console.log( 29 | [ 30 | `token: ${node.token}`, 31 | `type: ${type}`, 32 | ...msg.map(m => ` msg:${m}`), 33 | `stack: ${rpnToString(stack)}`, 34 | `output: ${rpnToString(output)}` 35 | ].join("\n") 36 | ); 37 | } 38 | 39 | export function isDeepVal(expression) { 40 | return ["{}", "[]", '""'].indexOf(JSON.stringify(expression)) > -1; 41 | } 42 | 43 | export function isFunction(expression) { 44 | return typeof expression === "function"; 45 | } 46 | 47 | export const identity = a => a; 48 | 49 | // 3.5.11 50 | -------------------------------------------------------------------------------- /packages/pdsl/README.md: -------------------------------------------------------------------------------- 1 |


2 | 3 |

4 | 5 |

6 | 7 |

Predicate Domain Specific Language

8 |

9 | Read the docs!         10 |

11 |


12 | 13 | [![Build Status](https://travis-ci.com/ryardley/pdsl.svg?branch=master)](https://travis-ci.com/ryardley/pdsl) 14 | ![npm bundle size](https://img.shields.io/bundlephobia/minzip/pdsl.svg) 15 | ![npm](https://img.shields.io/npm/v/pdsl.svg) 16 | [![codecov](https://codecov.io/gh/ryardley/pdsl/branch/master/graph/badge.svg)](https://codecov.io/gh/ryardley/pdsl) 17 | 18 | #### An expressive declarative toolkit for composing predicates in TypeScript or JavaScript 19 | 20 | ```js 21 | import p from "pdsl"; 22 | 23 | const isSoftwareCreator = p`{ 24 | name: string, 25 | age: > 16, 26 | occupation: "Engineer" | "Designer" | "Project Manager" 27 | }`; 28 | 29 | isSoftwareCreator(someone); // true | false 30 | ``` 31 | 32 | - [x] Intuitive 33 | - [x] Expressive 34 | - [x] Lightweight - under 6k! 35 | - [x] No dependencies 36 | - [x] Small bundle size 37 | - [x] Fast 38 | 39 |
40 | 41 | ## Documentation 42 | 43 | [PDSL Documentation](https://pdsl.site) 44 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/arrTypeMatch.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | import { createVal } from "./val"; 3 | /** 4 | *

Array type match

5 | * Return a function that checks to see if an array contains only the values listed or if the predicate function provided returns true when run over all items in the array. 6 | * Eg, 7 | *

 8 |  * // Helper functions
 9 |  * const isNumeric = a => typeof a === 'number';
10 |  * const isString = a => typeof a === 'string';
11 |  *
12 |  * arrTypeMatch(isNumeric)([1,2,3]); // true
13 |  * arrTypeMatch(isNumeric)([1,2,'3']); // false
14 |  * arrTypeMatch(isNumeric)([]); // true
15 |  * 
16 | * 17 | * @param {function|*} test predicate function used to test the contents of the array. 18 | * @return {function} A function of the form {array => boolean} 19 | */ 20 | export const createArrTypeMatch = ctx => 21 | function arrTypeMatch(test) { 22 | const predicate = createVal(ctx)(test); 23 | return function matchFn(arr, msg?) { 24 | return createErrorReporter("arrTypeMatch", ctx, msg, [arr])(() => { 25 | if (!Array.isArray(arr)) return false; 26 | 27 | let matches = true; 28 | for (let i = 0; i < arr.length; i++) { 29 | matches = matches && predicate(arr[i]); 30 | } 31 | return matches; 32 | }); 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/pred.ts: -------------------------------------------------------------------------------- 1 | import { createVal } from "./val"; 2 | import { createRegx } from "./regx"; 3 | import { createPrim } from "./prim"; 4 | import { createDeep } from "./deep"; 5 | import { 6 | identity, 7 | isDeepVal, 8 | isFunction, 9 | isPrimative, 10 | isPDSLSchema, 11 | isRegEx 12 | } from "../lib/utils"; 13 | 14 | function createExpressionParser(ctx, expression) { 15 | // Composing functions 16 | if (isFunction(expression)) { 17 | if (isPrimative(expression)) return createPrim(ctx); 18 | return identity; 19 | } 20 | 21 | // Allow composing schemas 22 | if (isPDSLSchema(expression)) { 23 | return expression => expression.unsafe_predicate; 24 | } 25 | 26 | // Composing regex 27 | if (isRegEx(expression)) return createRegx(ctx); 28 | 29 | // {} [] etc. 30 | if (isDeepVal(expression)) return createDeep(ctx); 31 | 32 | return createVal(ctx); 33 | } 34 | 35 | /** 36 | *

Predicate

37 | * Creates an appropriate predicate based on an input value. This will choose a predicate transformer dynamically based on the type of input. 38 | * 39 | * @param {*} input Anything parsable 40 | * @return {function} A function of the form {any => boolean} 41 | */ 42 | export const createPred = ctx => 43 | function pred(input) { 44 | const expParser = createExpressionParser(ctx, input); 45 | return expParser(input); 46 | }; 47 | -------------------------------------------------------------------------------- /docs/usage-with-babel.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Integrations 3 | name: Precompiling with Babel 4 | route: /precompiling-with-babel 5 | --- 6 | 7 | # Precompiling with Babel 8 | 9 | PDSL comes with a [babel plugin](https://github.com/ryardley/pdsl/tree/monorepo/packages/babel-plugin-pdsl). 10 | 11 | This plugin speeds up [`pdsl`](https://github.com/ryardley/pdsl) in babelified codebases by pre-compiling p-expressions to predicate function definitions. 12 | 13 | ```bash 14 | yarn add --dev @pdsl/babel-plugin-pdsl 15 | ``` 16 | 17 | You should ensure it is placed before any plugins that affect module import syntax. 18 | 19 | ```javascript 20 | { 21 | plugins: ["@pdsl/babel-plugin-pdsl"]; 22 | } 23 | ``` 24 | 25 | ## How precompiling works 26 | 27 | Conceptually this plugin parses p-expressions and replaces them with function chains: 28 | 29 | Example Input: 30 | 31 | ```javascript 32 | import p from "pdsl"; 33 | 34 | const notNil = p`!(null|undefined)`; 35 | const hasName = p`{name}`; 36 | const isTrue = p`true`; 37 | const hasNameWithFn = p`{name:${a => a.length > 10}}`; 38 | ``` 39 | 40 | Example Output 41 | 42 | ```javascript 43 | import { val, not, or, obj, entry, pred } from "pdsl/helpers"; 44 | 45 | const notNil = val(not(or(val(null), val(undefined)))); 46 | const hasName = val(obj("name")); 47 | const isTrue = val(true); 48 | const hasNameWithFn = val( 49 | obj( 50 | entry( 51 | "name", 52 | pred(a => a.length > 10) 53 | ) 54 | ) 55 | ); 56 | ``` 57 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/README.md: -------------------------------------------------------------------------------- 1 | # `@pdsl/babel-plugin-pdsl` 2 | 3 | This plugin speeds up [`pdsl`](https://github.com/ryardley/pdsl) in babelified codebases by pre-compiling p-expressions to predicate function definitions. 4 | 5 | _This should be considered experimental for the time being._ 6 | 7 | ## Prerequisites 8 | 9 | Ensure you have [`pdsl`](https://github.com/ryardley/pdsl) installed in your project. 10 | 11 | ## Installation 12 | 13 | Install the plugin with yarn or npm. 14 | 15 | ```bash 16 | yarn add --dev @pdsl/babel-plugin-pdsl 17 | ``` 18 | 19 | ```bash 20 | npm install -D @pdsl/babel-plugin-pdsl 21 | ``` 22 | 23 | ## Configuration 24 | 25 | Add the plugin to your `.bablerc`: 26 | 27 | ```json 28 | { 29 | "plugins": ["@pdsl/babel-plugin-pdsl"] 30 | } 31 | ``` 32 | 33 | _NOTE: Ensure the plugin is placed before any module resolution plugins._ 34 | 35 | ## How this works 36 | 37 | This plugin parses p-expressions and repaces them with function calls: 38 | 39 | #### Input 40 | 41 | ```js 42 | import p from "pdsl"; 43 | 44 | const notNil = p`!(null|undefined)`; 45 | const hasName = p`{name}`; 46 | const isTrue = p`true`; 47 | const hasNameWithFn = p`{name:${a => a.length > 10}}`; 48 | ``` 49 | 50 | #### Output 51 | 52 | ```js 53 | import { val, not, or, obj, entry, pred } from "pdsl/helpers"; 54 | 55 | const notNil = val(not(or(val(null), val(undefined)))); 56 | const hasName = val(obj("name")); 57 | const isTrue = val(true); 58 | const hasNameWithFn = val(obj(entry("name", pred(a => a.length > 10)))); 59 | ``` 60 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { parser } from "./parser"; 2 | import { lexer } from "./lexer"; 3 | import { generator } from "./generator"; 4 | import { getRawHelpers, createDefault } from "../helpers/index"; 5 | import { pretokenizer } from "./pretokenizer"; 6 | 7 | const { pred } = getRawHelpers(); 8 | 9 | // Utility functions 10 | const flow = (...funcs) => input => 11 | funcs.reduce((out, func) => func(out), input); 12 | 13 | const debugRpnArray = rpnArray => rpnArray.map(a => a.toString()).join(" "); 14 | 15 | function debugRpn(strings: TemplateStringsArray, ..._expressions: any[]) { 16 | return flow(toRpnArray, debugRpnArray)(strings); 17 | } 18 | 19 | function debugTokens(strings: TemplateStringsArray, ..._expressions: any[]) { 20 | return flow(pretokenizer, lexer, debugRpnArray)(strings); 21 | } 22 | 23 | const cleanup = a => a.filter(Boolean); 24 | 25 | const toRpnArray = flow(pretokenizer, lexer, parser, cleanup); 26 | 27 | const compileTemplateLiteral = (strings, expressions, ctx) => { 28 | const predicateFn = generator( 29 | toRpnArray(strings), 30 | expressions.map(pred(ctx)), 31 | ctx 32 | ); 33 | predicateFn.unsafe_rpn = () => debugRpn(strings); 34 | return predicateFn; 35 | }; 36 | 37 | export const unsafe_toRpnArray = toRpnArray; 38 | 39 | // Create the default export for runtime compiling 40 | const defaultObject = createDefault(compileTemplateLiteral); 41 | 42 | export const unsafe_rpn = debugRpn; 43 | export const unsafe_tokens = debugTokens; 44 | 45 | const { configureSchema, schema, predicate } = defaultObject; 46 | export { configureSchema, schema, predicate }; 47 | 48 | export default defaultObject; 49 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/test/index.spec.js: -------------------------------------------------------------------------------- 1 | const pluginTester = require("babel-plugin-tester"); 2 | const plugin = require("../src"); 3 | const path = require("path"); 4 | const fs = require("fs"); 5 | 6 | function loadFixtureSync(folder) { 7 | const code = fs.readFileSync( 8 | path.resolve(__dirname, `./fixtures/${folder}/code.js.txt`), 9 | "utf-8" 10 | ); 11 | 12 | const output = fs.readFileSync( 13 | path.resolve(__dirname, `./fixtures/${folder}/output.js.txt`), 14 | "utf-8" 15 | ); 16 | 17 | return { code, output }; 18 | } 19 | 20 | // White space is really annoying in these not sure if its 21 | // possible to do anythinng about it 22 | const tests = [ 23 | { 24 | title: "Kitchen Sinc", 25 | ...loadFixtureSync("kitchen-sinc"), 26 | runoutput: true 27 | }, 28 | { 29 | title: "Import named specifier", 30 | ...loadFixtureSync("import-named-specifier") 31 | }, 32 | { 33 | title: "Import default specifier", 34 | ...loadFixtureSync("import-default-specifier-schema") 35 | }, 36 | { 37 | title: "Basic default schema", 38 | ...loadFixtureSync("basic-default-schema") 39 | }, 40 | { 41 | title: "Basic configured schema", 42 | ...loadFixtureSync("basic-configured-schema") 43 | }, 44 | { 45 | title: "Basic named specifier", 46 | ...loadFixtureSync("basic-named-specifier") 47 | } 48 | ]; 49 | pluginTester({ 50 | plugin, 51 | tests, 52 | endOfLine: "preserve" 53 | }); 54 | 55 | test("The code runs as expected", () => { 56 | tests.forEach(({ output, runOutput }) => { 57 | if (runOutput) { 58 | expect(() => { 59 | eval(output); 60 | }).not.toThrow(); 61 | } 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /docs/null-and-undefined.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guide 3 | route: /null-undefined 4 | --- 5 | 6 | # Null checking 7 | 8 | ## Explicit null checking 9 | 10 | To check if a value is specifically `undefined` or specifically `null` you just use the term you want to check for: 11 | 12 | ```javascript 13 | p`undefined`(0); // false 14 | p`undefined`(undefined); // true 15 | p`null`(null); // true 16 | p`null`(undefined); // false 17 | ``` 18 | 19 | ```js 20 | p`null`(undefined); // false 21 | ``` 22 | 23 | ## Extant operator 24 | 25 | To check if a value is not null or undefined you can use the extant operator `_`: 26 | 27 | ```javascript 28 | p`_`(0); // true 29 | p`_`(null); // false 30 | p`_`(undefined); // false 31 | p`_`(NaN); // true 32 | ``` 33 | 34 | You can always reverse the polarity using the boolean not operator to specifically test for `null` or `undefined`: 35 | 36 | ```javascript 37 | p`!_`(null); // true 38 | ``` 39 | 40 | or you can be explicit: 41 | 42 | ```javascript 43 | p`null|undefined`(null); // true 44 | p`null|undefined`(0); // false 45 | ``` 46 | 47 | ```js 48 | p`_`(0); // true 49 | ``` 50 | 51 | ## Nullish things 52 | 53 | if you want to do boolean coercion you can use the boolean coercion (falsey) operator: 54 | 55 | ```javascript 56 | p`!`(null); // true 57 | p`!`(undefined); // true 58 | p`!`(NaN); // true 59 | p`!`(0); // true 60 | ``` 61 | 62 | ```js 63 | p`!`(null); 64 | ``` 65 | 66 | ## Empty comparisons 67 | 68 | Checking for empty things works as you would expect: 69 | 70 | ```javascript 71 | p`[]`([]); // true 72 | p`{}`({}); // true 73 | p`""`(""); // true 74 | p`undefined`(undefined); // true 75 | p`null`(null); // true 76 | ``` 77 | 78 | ```js 79 | p`[]`([]); // true 80 | ``` 81 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/arrIncludes.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | import { createVal } from "./val"; 3 | 4 | /** 5 | *

Array arrIncl

6 | * Return a function that checks to see if an array contains either any of the values listed or if any of the predicate functions provided return true when run over all items in the array. 7 | * Eg, 8 | *

 9 |  * arrIncl(a => a > 3, 2)([1,2,3]); // true
10 |  * arrIncl(1, 2)([1,3]); // false
11 |  * 
12 | * 13 | * @param {...function|*} args Either values or predicate functions used to test the contents of the array. 14 | * @return {function} A function of the form {array => boolean} 15 | */ 16 | export const createArrIncludes = ctx => 17 | function arrIncl(...args) { 18 | return function holdsFn(n, msg?) { 19 | return createErrorReporter("arrIncl", ctx, msg, [n, ...args])(() => { 20 | let i, j; 21 | let fns: any[] = []; 22 | let success: any[] = []; 23 | 24 | // prepare args as an array of predicate fns and an array to keep track of success 25 | for (i = 0; i < args.length; i++) { 26 | const arg = args[i]; 27 | fns.push(createVal(ctx)(arg)); 28 | success.push(false); 29 | } 30 | 31 | // loop through array only once 32 | for (i = 0; i < n.length; i++) { 33 | const item = n[i]; 34 | for (j = 0; j < fns.length; j++) { 35 | if (!success[j]) { 36 | const fn = fns[j]; 37 | success[j] = success[j] || fn(item); 38 | } 39 | } 40 | } 41 | 42 | return success.reduce((a, b) => a && b); 43 | }); 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /packages/pdsl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pdsl", 3 | "version": "5.2.10", 4 | "description": "Predicate DSL", 5 | "main": "index.js", 6 | "types": "lib/index.d.ts", 7 | "author": { 8 | "name": "Rudi Yardley", 9 | "url": "https://medium.com/@ryardley" 10 | }, 11 | "license": "MIT", 12 | "repository": "github:ryardley/pdsl", 13 | "scripts": { 14 | "test": "jest --coverage --coverageDirectory ../../coverage", 15 | "docs": "jsdoc --destination ../../docs helpers.js", 16 | "build": "rollup -c", 17 | "dev": "rollup -c -w", 18 | "promote-next-latest": "pkg_version=$(npm v . dist-tags.next); [ -n $pkg_version ] && ( npm dist-tag add pdsl@$pkg_version latest ) || exit 0" 19 | }, 20 | "homepage": "https://pdsl.site", 21 | "bugs": { 22 | "url": "https://github.com/ryardley/pdsl/issues" 23 | }, 24 | "files": [ 25 | "helpers/*", 26 | "lib/*", 27 | "helpers.js", 28 | "lib.js" 29 | ], 30 | "keywords": [ 31 | "functional", 32 | "predicate", 33 | "predicates", 34 | "pred", 35 | "type", 36 | "runtime", 37 | "predicate compiler", 38 | "validation", 39 | "object validation", 40 | "input validation", 41 | "expression", 42 | "predicate expression", 43 | "boolean expression", 44 | "boolean functions", 45 | "form validation", 46 | "pdsl" 47 | ], 48 | "devDependencies": { 49 | "@rollup/plugin-node-resolve": "^7.1.1", 50 | "@types/jest": "^25.1.4", 51 | "codecov": "^3.5.0", 52 | "jest": "^25.2.4", 53 | "jsdoc": "^3.6.3", 54 | "rollup": "^2.3.1", 55 | "rollup-plugin-typescript2": "^0.27.0", 56 | "ts-jest": "^25.3.0", 57 | "tslib": "^1.11.1", 58 | "typescript": "^3.8.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /docs/composition.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guide 3 | route: /composition 4 | --- 5 | 6 | # Composition 7 | 8 | You can compose p expressions easily. 9 | 10 | ```javascript 11 | const Nums = /[0-9]/; 12 | const UpCase = /[A-Z]/; 13 | const NotNumsAndUpCase = p`!${Nums} & !${UpCase}`; 14 | const Extended = /[^a-zA-Z0-9]/; 15 | 16 | const isValidUser = p`{ 17 | username: string[4..8] & ${NotNumsAndUpCase}, 18 | password: string[>8] & ${Extended}, 19 | age: > 17 20 | }`; 21 | 22 | isValidUser({ username: "ryardley", password: "Hello1234!", age: 21 }); //true 23 | isValidUser({ username: "ryardley", password: "Hello1234!", age: 17 }); //false 24 | isValidUser({ username: "Ryardley", password: "Hello1234!", age: 21 }); //false 25 | isValidUser({ username: "12345", password: "Hello1234!", age: 21 }); //false 26 | isValidUser({ username: "ryardley", password: "12345678", age: 21 }); //false 27 | ``` 28 | 29 | Have a go: 30 | 31 | ```js 32 | const Nums = /[0-9]/; 33 | const UpCase = /[A-Z]/; 34 | const NotNumsAndUpCase = p`!${Nums} & !${UpCase}`; 35 | const Extended = /[^a-zA-Z0-9]/; 36 | 37 | const isValidUser = p`{ 38 | username: string[4..8] & ${NotNumsAndUpCase}, 39 | password: string[>8] & ${Extended}, 40 | age: > 17 41 | }`; 42 | 43 | isValidUser({ username: "ryardley", password: "Hello1234!", age: 21 }); //true 44 | ``` 45 | 46 | The more complex things get, the more PDSL shines. See the above example in vanilla JS: 47 | 48 | ```javascript 49 | const isValidUser = input => 50 | input && 51 | input.username && 52 | typeof input.username === "string" && 53 | !input.username.match(/[^0-9]/) && 54 | !input.username.match(/[^A-Z]/) && 55 | input.username.length >= 4 && 56 | input.username.length <= 8 && 57 | typeof input.password === "string" && 58 | input.password.match(/[^a-zA-Z0-9]/) && 59 | input.password.length > 8 && 60 | input.age > 17; 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/types.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guide 3 | route: /types 4 | --- 5 | 6 | # Primitive matching 7 | 8 | If you pass a JavaScript primitive object, you will get the appropriate typeof check. 9 | 10 | ```javascript 11 | const isNumeric = p`Number`; // typeof value === 'number' 12 | const isBoolean = p`Boolean`; // typeof value === 'boolean' 13 | const isString = p`String`; // typeof value === 'string' 14 | const isSymbol = p`Symbol`; // typeof value === 'symbol' 15 | const isArray = p`Array`; // Array.isArray(value) 16 | const isObject = p`Object`; // typeof value === 'object' 17 | const isFunction = p`Function`; // typeof value === 'function' 18 | // const isBigInt = p`BigInt`;// BigInt will be coming soon once standardised 19 | ``` 20 | 21 | For consistency with typesystems such as TypeScript and Flow you can use lower case for the following: 22 | 23 | ```javascript 24 | const isNumeric = p`number`; // typeof value === 'number' 25 | const isBoolean = p`boolean`; // typeof value === 'boolean' 26 | const isString = p`string`; // typeof value === 'string' 27 | const isSymbol = p`symbol`; // typeof value === 'symbol' 28 | const isArray = p`array`; // Array.isArray(value) 29 | ``` 30 | 31 | You can test both the type and length of strings and arrays by using the length syntax: 32 | 33 | ```javascript 34 | p`string[5]`("12345"); // true 35 | p`string[5]`("1234"); // false 36 | p`string[<5]`("1234"); // true 37 | 38 | p`array[5]`([1, 2, 3, 4, 5]); // true 39 | p`array[5]`([1, 2, 3, 4]); // false 40 | p`array[<5]`([1, 2, 3, 4]); // true 41 | ``` 42 | 43 | You can also pass in a JavaScript primitive to the template string. 44 | 45 | ```javascript 46 | const isNumeric = p`${Number}`; // typeof value === 'number' 47 | const isBoolean = p`${Boolean}`; // typeof value === 'boolean' 48 | const isString = p`${String}`; // typeof value === 'string' 49 | ``` 50 | 51 | ```js 52 | p`number`(6); 53 | ``` 54 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/lexer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | grammar, 3 | tokens, 4 | isBooleanable, 5 | isOperator, 6 | hasToken 7 | } from "./grammar"; 8 | 9 | const rexString = `(${Object.values(tokens).join("|")})`; 10 | 11 | const rex = new RegExp(rexString, "g"); 12 | 13 | const grammers = Object.entries(grammar); 14 | 15 | function toNode(token) { 16 | for (let i = 0; i < grammers.length; i++) { 17 | const [test, createNode] = grammers[i]; 18 | 19 | // HACK: remove lookahead 20 | // need to work out a better way to do this 21 | const testWithoutLookahead = test === tokens.GT ? "\\>" : test; 22 | 23 | const testPassed = new RegExp(`^${testWithoutLookahead}$`).test(token); 24 | 25 | if (testPassed) { 26 | return createNode(token); 27 | } 28 | } 29 | } 30 | 31 | // HACK: can we do this using a regex and look ahead? 32 | function disambiguateNotOperator(node, index, arr) { 33 | // Find a not operator 34 | if (!(isOperator(node) && hasToken(node, "!"))) { 35 | return node; 36 | } 37 | 38 | // if next node is not a booleanable 39 | // thing this is meant to be the falsey operator 40 | const nextNode = arr[index + 1]; 41 | 42 | if (isBooleanable(nextNode)) { 43 | return node; 44 | } 45 | 46 | return grammar[tokens.FALSY_KEYWORD]("!"); 47 | } 48 | 49 | export function lexer(input) { 50 | rex.lastIndex = 0; 51 | return ( 52 | input 53 | // remove comments 54 | .replace(/\/\/.*(\n|$)/g, "") 55 | // Remove closing string bracket to make 56 | // string[x] and array[x] a a unary operator 57 | // eg. "string[ > 7]" -> "string[ > 7" 58 | .replace(/((?:string|array)\[)([^\]]*)(\])/g, "$1($2)") 59 | // go and globally tokenise everything for parsing 60 | .match(rex) 61 | // convert to an object 62 | .map(toNode) 63 | .map(disambiguateNotOperator) 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/generator.test.ts: -------------------------------------------------------------------------------- 1 | import { generator } from "./generator"; 2 | import { grammar, tokens } from "./grammar"; 3 | import Context from "./context"; 4 | 5 | const link = grammar[tokens.PREDICATE_LOOKUP]; 6 | const not = grammar[tokens.NOT]; 7 | const or = grammar[tokens.OR]; 8 | const and = grammar[tokens.AND]; 9 | const obj = grammar[tokens.OBJ]; 10 | const symbol = grammar[tokens.IDENTIFIER]; 11 | const entry = grammar[tokens.ENTRY]; 12 | 13 | function stringifyAst(rpn) { 14 | return rpn.map(n => n.toString()).join(" "); 15 | } 16 | 17 | describe("generator", () => { 18 | [ 19 | { rpn: [link("@{LINK:0}")], fns: [() => true], out: true }, 20 | { rpn: [link("@{LINK:0}")], fns: [() => false], out: false }, 21 | { rpn: [link("@{LINK:0}"), not("!")], fns: [() => false], out: true }, 22 | { 23 | rpn: [link("@{LINK:0}"), link("@{LINK:1}"), or("||")], 24 | fns: [() => false, () => true], 25 | out: true 26 | }, 27 | { 28 | rpn: [link("@{LINK:0}"), link("@{LINK:1}"), and("&&")], 29 | fns: [() => false, () => true], 30 | out: false 31 | }, 32 | { 33 | rpn: [ 34 | symbol("name"), 35 | link("@{LINK:0}"), 36 | entry(":"), 37 | symbol("age"), 38 | link("@{LINK:1}"), 39 | entry(":"), 40 | obj("{2") 41 | ], 42 | fns: [a => a === "foo", a => a === 41], 43 | inp: { name: "foo", age: 41 }, 44 | out: true 45 | }, 46 | { 47 | skip: false, 48 | only: false, 49 | rpn: [symbol("name"), obj("{1")], 50 | fns: [], 51 | inp: { name: "foo" }, 52 | out: true 53 | } 54 | ].forEach(({ rpn, skip, only, fns, inp, out }) => { 55 | const itFunc = skip ? it.skip : only ? it.only : it; 56 | const ctx = new Context(); 57 | itFunc(stringifyAst(rpn), () => { 58 | expect(generator(rpn, fns, ctx)(inp)).toBe(out); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/arrArgMatch.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | import { createVal } from "./val"; 3 | import { createWildcard } from "./wildcard"; 4 | 5 | /** 6 | *

Array match

7 | * Return a function that checks to see if an array contains either any of the values listed or if any of the predicate functions provided return true when run over all items in the array. 8 | * Eg, 9 | *

10 |  * // Helper functions
11 |  * const isNumeric = a => typeof a === 'number';
12 |  * const isString = a => typeof a === 'string';
13 |  *
14 |  * arrArgMatch(isNumeric, isNumeric, isNumeric)([1,2,3]); // true
15 |  * arrArgMatch(isNumeric, isNumeric, isNumeric, '...')([1,2,3]); // true
16 |  * arrArgMatch(isString, isNumeric, isNumeric, '...')([1,2,3]); // false
17 |  * arrArgMatch(isString, isNumeric, isNumeric, '...')(['1',2,3]); // true
18 |  * arrArgMatch(isNumeric, isNumeric, isNumeric, '...')([1,2,3,4]); // true
19 |  * arrArgMatch(1, 2)([1,3]); // false
20 |  * 
21 | * 22 | * @param {...function|*} tests Either values, `['...', predicate]` or predicate functions used to test the contents of the array. 23 | * @return {function} A function of the form {array => boolean} 24 | */ 25 | export const createArrArgMatch = ctx => 26 | function arrArgMatch(...tests) { 27 | return function matchFn(arr, msg?) { 28 | return createErrorReporter("arrArgMatch", ctx, msg, [arr])(() => { 29 | const hasWildcard = tests.slice(-1)[0] === "..."; 30 | let matches = hasWildcard || arr.length === tests.length; 31 | for (let i = 0; i < tests.length; i++) { 32 | const testVal = tests[i]; 33 | const predicate = 34 | testVal === "..." ? createWildcard() : createVal(ctx)(testVal); 35 | const pass = predicate(arr[i]); 36 | matches = matches && pass; 37 | } 38 | return matches; 39 | }); 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guide 3 | route: /reference 4 | --- 5 | 6 | # Reference 7 | 8 | | Operator | Description | 9 | | ------------------------ | -------------------------------- | 10 | | `&` | Boolean AND | 11 | | `&&` | Boolean AND alternative | 12 | | `[1,2,3]` | Array argument match | 13 | | `[? string, number ]` | Array includes | 14 | | `array` | Array type match | 15 | | `array[4]` | Array length match | 16 | | `Array` | Array type match | 17 | | `1..100` | Number in range (incl) | 18 | | `1<<100` | Number in range (excl) | 19 | | `_` | Extant | 20 | | `!` | Falsey | 21 | | `>` | Greater than | 22 | | `>=` | Greater than or equals | 23 | | `<` | Less than | 24 | | `<=` | Less than or equals | 25 | | `!` | Boolean NOT | 26 | | `{ prop: string }` | Object | 27 | | \| | Boolean OR | 28 | | \|\| | Boolean OR alternative | 29 | | `string` | Value is String | 30 | | `string[4]` | String length match | 31 | | `number` | Value is Number | 32 | | `Number`, `String`, etc. | JavaScript types to test against | 33 | | `!!` | Truthy | 34 | | `<- "We found an issue"` | Error validation | 35 | | `*` | Wildcard (always true) | 36 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: F.A.Q. 3 | route: /faq 4 | --- 5 | 6 | # FAQ 7 | 8 | ## What does pdsl stand for? 9 | 10 | Predicate Domain Specific Language. 11 | 12 | ## Why did you write this? 13 | 14 | @ryardley had a need for it when filtering on events in an app working with [ts-bus](https://github.com/ryardley/ts-bus). He also wanted to learn how to create a compiler from scratch. 15 | 16 | ## Why would I ever use this? 17 | 18 | We think PDSL is a great addition to working with predicates in JavaScript and hope you feel that way too. If there is something stopping you from wanting to use this in your projects we would like to know so [let us know about it here](https://github.com/ryardley/pdsl/issues/new) - perhaps we can fix your problem or prioritize it in our roadmap! 19 | 20 | We don't know what's in your head and we want to make libraries that help more people get the most out of programming. 21 | 22 | ## How does this work? 23 | 24 | It is comprised of a [grammar](packages/pdsl/grammar.js), a [lexer](packages/pdsl/lexer.js) a [parser](packages/pdsl/parser.js) and a [code generator](packages/pdsl/generator.js). It uses a version of the [shunting yard algorhythm](https://en.wikipedia.org/wiki/Shunting-yard_algorithm) to create the basic parser storing the output in [RPN](https://en.wikipedia.org/wiki/Reverse_Polish_notation) but using objects in an array instead of a tree. Then parsing was added for Varadic Functions. A lot of it was by trial and error. 25 | 26 | There are better ways to do it. There are [plans to refactor to use a transducer pattern](https://github.com/ryardley/pdsl/issues/33) but there is also a plan to create a babel plugin which will [remove the need for compiler performance enhancement](https://github.com/ryardley/pdsl/issues/32). Nevertheless if you have tips and know how to do it better, faster, stronger or smaller, retaining semantic flexability and with no dependencies - we want to learn - [let us know about it here](https://github.com/ryardley/pdsl/issues/new). 27 | -------------------------------------------------------------------------------- /docs/strings.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guide 3 | route: /strings 4 | --- 5 | 6 | # String 7 | 8 | generally you can simply use either double or single quotes to check for string equivalence `""`. 9 | 10 | ## Test for type string 11 | 12 | You can test that you have a string by using the string type test 13 | 14 | ```javascript 15 | p`string`("I am a string!"); // true 16 | p`string`(1234); // false 17 | ``` 18 | 19 | ```js 20 | p`string`("I am a string!"); 21 | ``` 22 | 23 | ## Test string length 24 | 25 | You can test both the type and length of strings and arrays by using the length syntax: 26 | 27 | ```javascript 28 | p`string[5]`("12345"); // true 29 | p`string[5]`("1234"); // false 30 | ``` 31 | 32 | You can also have any numeric test to apply to the strings length. 33 | 34 | ```javascript 35 | p`string[<5]`("1234"); // true 36 | p`string[<5 | > 20]`("123456"); // false 37 | ``` 38 | 39 | ```js 40 | p`string[<5]`("1234"); 41 | ``` 42 | 43 | ## Empty Strings 44 | 45 | Checking for empty strings: 46 | 47 | ```javascript 48 | p`''`(""); // true 49 | p`""`(""); // true 50 | p`""`("Not empty"); // false 51 | ``` 52 | 53 | ```js 54 | p`''`(""); 55 | ``` 56 | 57 | ## String literals 58 | 59 | You can check for string literals. 60 | 61 | ```javascript 62 | p`'Hello pdsl!'`("Hello pdsl!"); // true 63 | p`"Hello pdsl!"`("Hello pdsl!"); // true 64 | ``` 65 | 66 | ```js 67 | p`'Hello pdsl!'`("Hello pdsl!"); 68 | ``` 69 | 70 | ## Discriminated Unions 71 | 72 | You can check a list of discriminated unions using the `|` operator. 73 | 74 | ```javascript 75 | p`"Doctor" | "Lawyer" | "Dentist" | "Teacher"`("Doctor"); // true 76 | p`"Doctor" | "Lawyer" | "Dentist" | "Teacher"`("Politician"); // false 77 | ``` 78 | 79 | ```js 80 | p`"Doctor" | "Lawyer" | "Dentist" | "Teacher"`("Doctor"); 81 | ``` 82 | 83 | ## Escaping characters 84 | 85 | You can escape characters by using alternative quotes. 86 | 87 | ```js 88 | p`"This string contains \`backticks\`"`("This string contains `backticks`"); // true 89 | ``` 90 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/helpers.ts: -------------------------------------------------------------------------------- 1 | import { createAnd } from "./and"; 2 | import { createArrArgMatch } from "./arrArgMatch"; 3 | import { createArrIncludes } from "./arrIncludes"; 4 | import { createArrLen } from "./arrLen"; 5 | import { createArrTypeMatch } from "./arrTypeMatch"; 6 | import { createBtw } from "./btw"; 7 | import { createBtwe } from "./btwe"; 8 | import { createDeep } from "./deep"; 9 | import { createEntry } from "./entry"; 10 | import { createExtant } from "./extant"; 11 | import { createFalsey } from "./falsey"; 12 | import { createGt } from "./gt"; 13 | import { createGte } from "./gte"; 14 | import { createLt } from "./lt"; 15 | import { createLte } from "./lte"; 16 | import { createNot } from "./not"; 17 | import { createObj } from "./obj"; 18 | import { createOr } from "./or"; 19 | import { createPred } from "./pred"; 20 | import { createPrim } from "./prim"; 21 | import { createRegx, Email, Xc, Nc, Lc, Uc, LUc } from "./regx"; 22 | import { createStrLen } from "./strLen"; 23 | import { createTruthy } from "./truthy"; 24 | import { createVal } from "./val"; 25 | import { createWildcard } from "./wildcard"; 26 | import { createValidation } from "./validation"; 27 | 28 | const helpers = { 29 | Email, 30 | Xc, 31 | Nc, 32 | Lc, 33 | Uc, 34 | LUc, 35 | btw: createBtw, 36 | btwe: createBtwe, 37 | lt: createLt, 38 | lte: createLte, 39 | gt: createGt, 40 | gte: createGte, 41 | arrIncl: createArrIncludes, 42 | or: createOr, 43 | and: createAnd, 44 | not: createNot, 45 | obj: createObj, 46 | val: createVal, 47 | regx: createRegx, 48 | entry: createEntry, 49 | prim: createPrim, 50 | pred: createPred, 51 | deep: createDeep, 52 | extant: createExtant, 53 | truthy: createTruthy, 54 | falsey: createFalsey, 55 | arrArgMatch: createArrArgMatch, 56 | arrTypeMatch: createArrTypeMatch, 57 | wildcard: createWildcard, 58 | strLen: createStrLen, 59 | arrLen: createArrLen, 60 | validation: createValidation 61 | }; 62 | 63 | export default helpers; 64 | -------------------------------------------------------------------------------- /docs/arrays.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guide 3 | route: /arrays 4 | --- 5 | 6 | # Arrays 7 | 8 | ## Empty Array 9 | 10 | You can test for an empty array using `[]` 11 | 12 | ```javascript 13 | p`[]`(null); //false 14 | p`[]`([]); //true 15 | p`[]`([1]); //false 16 | ``` 17 | 18 | ```js 19 | p`[]`([]); 20 | ``` 21 | 22 | ## Typed Arrays 23 | 24 | Typed arrays look like TypeScript typed arrays and can contain only the type specified. 25 | 26 | ```javascript 27 | p`Array`([1, 2, 3, 4]); // true 28 | p`Array`([1, 2, true, 4]); // false 29 | p`Array`(["1", "2", "3", "4"]); // true 30 | ``` 31 | 32 | ```js 33 | p`Array`([1, 2, 3, 4]); 34 | ``` 35 | 36 | ## Array length 37 | 38 | You can specify an array length using the `array` keyword followed by a length expression in square brackets. 39 | 40 | ```javascript 41 | p`array[4]`([1, 2, 3, 4]); // true 42 | p`array[4]`([1, 2, 3, 4, 5]); // false 43 | p`array[>4]`([1, 2, 3, 4, 5]); // true 44 | p`array[>4]`([1, 2, 3]); // false 45 | ``` 46 | 47 | ```js 48 | p`array[>4]`([1, 2, 3, 4, 5]); 49 | ``` 50 | 51 | ## Array with specific items 52 | 53 | Simply specify the items in a set of array brackets. 54 | 55 | You can use a `*` to indicate any possible type including undefined or null, `_` to indicate anything that is not null or undefined and `...` to indicate that you don't care to match the rest of the array. 56 | 57 | ```javascript 58 | p`[ 1, 2, 3, 4 ]`([1, 2, 3, 4]); // true 59 | p`[ 1, 2, *, 4 ]`([1, 2, "thing", 4]); // true 60 | p`[ 1, 2, ... ]`([1, 2, "thing", 4, { foo: "foo" }]); // true 61 | ``` 62 | 63 | ```js 64 | const arg = [7, "seven", NaN, { type: "SEVEN" }]; 65 | p`[ number, string, *, { type: "SEVEN" } ]`(arg); 66 | ``` 67 | 68 | ## Array includes 69 | 70 | You can check if an array includes an item by using the array includes syntax: 71 | 72 | ```javascript 73 | p`[? 5]`([1, 2, 3, 4, 5]); // true 74 | p`[? >10]`([1, 2, 30, 4, 5]); // true 75 | p`[? 5]`([1, 2, 3, 4]); // false 76 | ``` 77 | 78 | ```js 79 | p`[? "admin"]`(["editor", "admin", "helper"]); 80 | ``` 81 | -------------------------------------------------------------------------------- /examples/formik/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Formik, Field, Form, ErrorMessage } from "formik"; 3 | import { schema as p } from "pdsl"; 4 | import { Debug } from "./Debug"; 5 | const SignUp = () => ( 6 |
7 |

Sign up

8 | 2] <- "Must be longer than 2 characters" 22 | & string[<20] <- "Nice try nobody has a first name that long", 23 | 24 | lastName: 25 | _ <- "Required" 26 | & string[>2] <- "Must be longer than 2 characters" 27 | & string[<20] <- "Nice try nobody has a last name that long" 28 | }`} 29 | onSubmit={values => { 30 | setTimeout(() => { 31 | alert(JSON.stringify(values, null, 2)); 32 | }, 500); 33 | }} 34 | render={({ errors, touched }) => ( 35 |
36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | )} 61 | /> 62 |
63 | ); 64 | 65 | export default SignUp; 66 | -------------------------------------------------------------------------------- /docs/helpers.md: -------------------------------------------------------------------------------- 1 | --- 2 | route: /helpers 3 | --- 4 | 5 | # Helpers (experimental) 6 | 7 | NOTE: This should be considered unsafe API for the time being as it may change. 8 | 9 | PDSL provides a number of helpers that can be exported from the `pdsl/helpers` package and may be used standalone or as part of a `p` expression. 10 | 11 | ```javascript 12 | import { Email, pred, btw, gt, regx } from "pdsl/helpers"; 13 | 14 | btw(1, 10)(20); // false 15 | regx(/^foo/)("food"); // true 16 | gt(100)(100); // false 17 | gte(100)(100); // true 18 | pred(9)(9); // true 19 | pred(9)(10); // false 20 | pred(Email)("hello@world.com"); // true 21 | pred(Number)(1); // true 22 | pred(String)("Hello"); // true 23 | ``` 24 | 25 | Available helpers: 26 | 27 | | Helper | Description | PDSL Operator | 28 | | ------------- | ------------------------------------------- | ---------------------- | 29 | | [and](#and) | Logical AND | `a & b` or `a && b` | 30 | | [btw](#btw) | Between | `10 < < 100` | 31 | | [btwe](#btwe) | Between or equals | `10..100` | 32 | | [deep](#deep) | Deep equality | N/A | 33 | | [gt](#gt) | Greater than | `> 5` | 34 | | [gte](#gte) | Greater than or equals | `>= 5` | 35 | | [lt](#lt) | Less than | `< 5` | 36 | | [lte](#lte) | Less than equals | `<= 5` | 37 | | [not](#not) | Logical NOT | `!6` | 38 | | [or](#or) | Logical OR | `a \| b` or `a \|\| b` | 39 | | [pred](#pred) | Select the correct predicate based on input | `${myVal}` | 40 | | [prim](#prim) | Primitive typeof checking | `Array` etc. | 41 | | [regx](#regx) | Regular expression predicate | `${/^foo/}` | 42 | | [val](#val) | Strict equality | N/A | 43 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/parser.test.ts: -------------------------------------------------------------------------------- 1 | import { parser } from "./parser"; 2 | import { lexer } from "./lexer"; 3 | 4 | function prepare(parserOut) { 5 | return parserOut.map(p => p.toString()).join(" "); 6 | } 7 | 8 | describe("parser", () => { 9 | [ 10 | { input: "a||b", output: "a b ||" }, 11 | { input: "a||b&&c", output: "a b c && ||" }, 12 | { input: "!a||b&&c", output: "a !1 b c && ||" }, 13 | { input: "!(a||b)&&c", output: "a b || !1 c &&" }, 14 | { 15 | input: ` 16 | { 17 | fee, 18 | fi, 19 | fo, 20 | fum, 21 | { 22 | i, 23 | smell, 24 | the, 25 | blood, 26 | { 27 | of, 28 | an, 29 | englishman 30 | } 31 | } 32 | }`, 33 | output: "fee fi fo fum i smell the blood of an englishman {3 {5 {5" 34 | }, 35 | { input: "{ name , age }", output: "name age {2" }, 36 | { 37 | input: "{ name : a, age : b }", 38 | output: "name a : age b : {2" 39 | }, 40 | { 41 | input: "{ name : {foo, bar}, age : b }", 42 | output: "name foo bar {2 : age b : {2" 43 | }, 44 | { 45 | input: "{ name : {foo:{1,2}, bar}, age : b }", 46 | output: "name foo 1 2 {2 : bar {2 : age b : {2" 47 | }, 48 | { input: "{ name : ! { bar } }", output: "name bar {1 !1 : {1" }, 49 | { 50 | input: "a || { username : b , password : c && { length : d } }", 51 | output: "a username b : password c length d : {1 && : {2 ||" 52 | }, 53 | { input: "{ age : > 5 }", output: "age 5 > : {1" }, 54 | { input: "{ age : < 5 }", output: "age 5 < : {1" }, 55 | { input: "10 < < 100", output: "10 100 < <" }, 56 | { input: ">=100", output: "100 >=" }, 57 | { input: "<=100", output: "100 <=" }, 58 | { input: "!8", output: "8 !1" }, 59 | { input: "{name: ! }", output: "name ! : {1" }, 60 | { input: "{name: !! }", output: "name !! : {1" }, 61 | { input: "> 5 & < 10", output: "5 > 10 < &", skip: false, only: false } 62 | ].forEach(({ input, output, skip, only }) => { 63 | const itFunc = skip ? it.skip : only ? it.only : it; 64 | 65 | itFunc(`"${input}" -> "${output}"`, () => { 66 | const actual = prepare(parser(lexer(input))); 67 | expect(actual).toEqual(output); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/obj.ts: -------------------------------------------------------------------------------- 1 | import { createErrorReporter } from "./error-reporter"; 2 | import { createExtant } from "./extant"; 3 | 4 | export const createObj = (ctx, isExactMatching = false) => 5 | function obj(...entriesWithRest) { 6 | return function objFn(a, msg?) { 7 | return createErrorReporter( 8 | "obj", 9 | ctx, 10 | msg, 11 | [a, entriesWithRest], 12 | false, 13 | true 14 | )(() => { 15 | const isExtant = createExtant(ctx); 16 | 17 | const isDeepParentExactMatch = ctx 18 | .getObjExactStack() 19 | .reduce((acc, item) => acc || item, false); 20 | 21 | let isLooseMatching = !(isExactMatching || isDeepParentExactMatch); 22 | let entriesMatch = true; 23 | let entryCount = 0; 24 | 25 | for (let i = 0; i < entriesWithRest.length; i++) { 26 | const entry = entriesWithRest[i]; 27 | 28 | // Ignore rest and note we have one 29 | if (entry === "...") { 30 | isLooseMatching = isLooseMatching || true; 31 | continue; 32 | } 33 | 34 | // Extract key and predicate from the entry and run the predicate against the value 35 | const [key, predicate] = Array.isArray(entry) 36 | ? entry 37 | : [entry, isExtant]; 38 | 39 | // Storing the object key on a global stack for errors reporting 40 | ctx.pushObjStack(key, isExactMatching); 41 | 42 | let result; 43 | if (ctx.abortEarly) { 44 | result = isExtant(a) && predicate(a[key]); 45 | } else { 46 | // Ensure that the test is run no matter what the previous tests were 47 | // This is important for collecting all errors 48 | result = predicate( 49 | isExtant(a) ? a[key] : /* istanbul ignore next */ undefined 50 | ); 51 | } 52 | 53 | // Popping the object path off the global stack 54 | ctx.popObjStack(); 55 | 56 | entriesMatch = entriesMatch && result; 57 | // We just logged an entry track it 58 | entryCount++; 59 | } 60 | 61 | // If there was a rest arg we don't need to check length 62 | if (isLooseMatching) return entriesMatch; 63 | 64 | // Check entry length 65 | return entriesMatch && Object.keys(a).length === entryCount; 66 | }); 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /docs/objects.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guide 3 | route: /objects 4 | --- 5 | 6 | # Objects 7 | 8 | ## Empty Objects 9 | 10 | Checking for empty objects: 11 | 12 | ```javascript 13 | p`{}`({}); // true 14 | p`{}`(undefined); // false 15 | p`{}`({ name: "John" }); // false 16 | ``` 17 | 18 | ```js 19 | p`{}`({}); 20 | ``` 21 | 22 | ## Object properties 23 | 24 | Test for an object property's existence by simply providing an object with a property name. 25 | 26 | ```javascript 27 | p`{ name }`({ name: "Rudi" }); // true 28 | p`{ age }`({ age: 0 }); // true 29 | p`{ color }`({ color: undefined }); // false 30 | p`{ type }`({ type: null }); // false 31 | ``` 32 | 33 | ```js 34 | p`{ name }`({ name: "Rudi" }); 35 | ``` 36 | 37 | This is the same as using `!(null | undefined)` which is also the same as using the shorthand: `_`. 38 | 39 | ```javascript 40 | // These are all equivalent 41 | p`{ name }`; 42 | p`{ name: _ }`; 43 | p`{ name: !(null|undefined) }`; 44 | ``` 45 | 46 | You can pass expressions to test agains the object's value: 47 | 48 | ```javascript 49 | p`{ name: "Goodbye" | "Hello" }`({ name: "Hello" }); // true 50 | p`{ name: >19 & <25 }`({ name: 20 }); // true 51 | p`{ name: 19..25 }`({ name: 20 }); // true 52 | ``` 53 | 54 | ```js 55 | p`{ name: "Goodbye" | "Hello" }`({ name: "Hello" }); 56 | ``` 57 | 58 | The property can also contain nested objects. 59 | 60 | ```js 61 | const validate = p`{ 62 | name, 63 | payload: { 64 | listening: true, 65 | num: >4 66 | } 67 | }`; 68 | 69 | validate({ 70 | name: "Hello", 71 | payload: { 72 | listening: true, 73 | num: 5 74 | } 75 | }); // true 76 | ``` 77 | 78 | ## Exact matching syntax 79 | 80 | PDSL is loose matches objects by default which means the following: 81 | 82 | ```javascript 83 | p`{ name }`({ name: "A name", age: 234 }); // true 84 | ``` 85 | 86 | Exact object matching mode can be turned on by using objects with pipes `|`: 87 | 88 | ```javascript 89 | p`{| name |}`({ name: "A name", age: 234 }); // false 90 | p`{| name |}`({ name: "A name" }); // true 91 | ``` 92 | 93 | All nested normal objects will become exact matching too within the exact matching tokens: 94 | 95 | ```js 96 | p`{| 97 | name, 98 | age, 99 | sub: { 100 | num: 100 101 | } 102 | |}`({ 103 | name: "Fred", 104 | age: 12, 105 | sub: { 106 | num: 100, 107 | foo: "foo" 108 | } 109 | }); // false 110 | ``` 111 | 112 | ## Loose matching operator 113 | 114 | Once you turn exact matching on in an object tree you can only turn it off by using the `...` loose matching operator: 115 | 116 | ```js 117 | p`{| 118 | name: "foo", 119 | exact: { 120 | hello:"hello" 121 | }, 122 | loose: { 123 | hello: "hello", 124 | ... 125 | } 126 | |}`({ 127 | name: "foo", 128 | exact: { 129 | hello: "hello" 130 | }, 131 | loose: { 132 | hello: "hello", 133 | extra: true, 134 | other: 10 135 | } 136 | }); 137 | ``` 138 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/context.ts: -------------------------------------------------------------------------------- 1 | import { lookup } from "./i18n"; 2 | 3 | const defaultConfig: ContextOptions = { abortEarly: true }; 4 | 5 | // TODO: Get this to compose state helper objects to 6 | // handle the objet stack and exact matching depth analysis 7 | type ErrorLike = { path: string; message: string }; 8 | 9 | type ContextOptions = { 10 | schemaMode?: boolean; 11 | abortEarly?: boolean; 12 | captureErrors?: boolean; 13 | throwErrors?: boolean; 14 | }; 15 | 16 | class Context { 17 | errs: ErrorLike[] = []; 18 | errStack: string[] = []; 19 | objStack: string[] = []; // store our nested object path keys 20 | objExactStack: boolean[] = []; // store nested exact matching objects 21 | config = {}; 22 | batch: ErrorLike[] = []; 23 | isBatching: boolean = false; 24 | blockErrors = ""; 25 | schemaMode: boolean = false; 26 | abortEarly: boolean = false; 27 | captureErrors: boolean = false; 28 | throwErrors: boolean = true; 29 | 30 | constructor(options: ContextOptions = {}) { 31 | this.reset(); 32 | Object.assign(this, defaultConfig, options); 33 | } 34 | 35 | reset(options: ContextOptions = {}) { 36 | this.errs = []; 37 | this.errStack = []; 38 | this.objStack = []; // store our nested object path keys 39 | this.objExactStack = []; // store nested exact matching objects 40 | this.config = {}; 41 | this.batch = []; 42 | this.isBatching = false; 43 | this.blockErrors = ""; 44 | Object.assign(this, options); 45 | } 46 | 47 | batchStart() { 48 | this.isBatching = true; 49 | } 50 | 51 | batchCommit() { 52 | this.errs = this.errs.concat(this.batch); 53 | } 54 | 55 | batchPurge() { 56 | this.batch = []; 57 | this.isBatching = false; 58 | } 59 | 60 | reportError(msg: string, ...argstore: any[]) { 61 | /* istanbul ignore next */ 62 | if (!msg) return; 63 | 64 | const message = msg 65 | // interpolate values 66 | .replace(/\$(\d+)/g, (...matchArgs) => { 67 | const [, argIndex] = matchArgs.slice(0, -2); 68 | return JSON.stringify(argstore[Number(argIndex) - 1]); 69 | }) 70 | // Fix up encoding issue with escaped quotes 71 | .replace(/\\\"/g, '"'); 72 | 73 | const collection = this.isBatching ? this.batch : this.errs; 74 | 75 | collection.push({ 76 | path: this.objStack.join("."), 77 | message 78 | }); 79 | } 80 | 81 | lookup(key: string) { 82 | return lookup(key); 83 | } 84 | 85 | pushObjStack(key: string, isExactMatching: boolean) { 86 | this.objExactStack.push(isExactMatching); 87 | const out = this.objStack.push(key); 88 | return out; 89 | } 90 | 91 | popObjStack() { 92 | this.objExactStack.pop(); 93 | const key = this.objStack.pop(); 94 | return key; 95 | } 96 | 97 | getObjExactStack() { 98 | return this.objExactStack; 99 | } 100 | 101 | pushErrStack(key: string) { 102 | const out = this.errStack.push(key); 103 | return out; 104 | } 105 | 106 | popErrStack() { 107 | const key = this.errStack.pop(); 108 | return key; 109 | } 110 | 111 | getErrors() { 112 | return this.errs; 113 | } 114 | } 115 | 116 | export default Context; 117 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/parser.ts: -------------------------------------------------------------------------------- 1 | // This article really helped work this out: 2 | // http://wcipeg.com/wiki/Shunting_yard_algorithm#Variadic_functions 3 | 4 | const DEBUG = process && process.env && process.env.DEBUG; 5 | 6 | import { 7 | isPrecidenceOperatorClose, 8 | isPrecidenceOperator, 9 | isArgumentSeparator, 10 | isVaradicFunction, 11 | isVaradicFunctionClose, 12 | isPredicateLookup, 13 | isLiteral, 14 | isOperator 15 | } from "./grammar"; 16 | 17 | import { debug } from "./utils"; 18 | 19 | const peek = a => a[a.length - 1]; 20 | 21 | export function parser(input) { 22 | const stack: any[] = []; 23 | const arity: any[] = []; 24 | const varadics: any[] = []; 25 | try { 26 | const finalOut = input 27 | .reduce((output, node) => { 28 | let type; 29 | let msg: any[] = []; 30 | 31 | if (isLiteral(node) || isPredicateLookup(node)) { 32 | type = "operand"; 33 | // send to output 34 | output.push(node); 35 | } 36 | 37 | if (isVaradicFunction(node)) { 38 | type = "varadic"; 39 | stack.push(node); 40 | arity.push(1); 41 | varadics.push(node); 42 | } 43 | 44 | if (isArgumentSeparator(node)) { 45 | type = "comma"; 46 | while (stack.length > 0 && !isVaradicFunction(peek(stack))) { 47 | output.push(stack.pop()); 48 | } 49 | arity.push(arity.pop() + 1); 50 | } 51 | 52 | if (isVaradicFunctionClose(node)) { 53 | type = "varadic-close"; 54 | while (stack.length > 0 && !isVaradicFunction(peek(stack), node)) { 55 | output.push(stack.pop()); 56 | } 57 | varadics.pop(); 58 | const fn = stack.pop(); 59 | 60 | fn.arity = arity.pop(); 61 | output.push(fn); 62 | } 63 | 64 | if (isOperator(node)) { 65 | type = "operator"; 66 | while ( 67 | stack.length > 0 && 68 | !isPrecidenceOperator(peek(stack)) && 69 | peek(stack).prec < node.prec 70 | ) { 71 | msg.push("flushing stack"); 72 | output.push(stack.pop()); 73 | } 74 | stack.push(node); 75 | } 76 | 77 | if (isPrecidenceOperator(node)) { 78 | type = "precedence"; 79 | stack.push(node); 80 | } 81 | 82 | if (isPrecidenceOperatorClose(node)) { 83 | type = "precedence-close"; 84 | while (!isPrecidenceOperator(peek(stack))) { 85 | output.push(stack.pop()); 86 | } 87 | 88 | stack.pop(); 89 | } 90 | 91 | /* istanbul ignore next */ 92 | if (DEBUG) debug(output, stack, node, type, msg); 93 | 94 | return output; 95 | }, []) 96 | .concat(stack.reverse()); 97 | 98 | if (varadics.length > 0) { 99 | throw new Error("Mismatched varadic function"); 100 | } 101 | return finalOut; 102 | } catch (e) { 103 | // TODO: How can we get meaningful errors that highlight where the error occured? 104 | // Pass character location to tokenisation 105 | throw new Error( 106 | `Malformed Input! pdsl could not parse the tokenized input stream : ${input.join( 107 | " " 108 | )}` 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /docs/validation.md: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guide 3 | route: /validation 4 | --- 5 | 6 | # Validation 7 | 8 | PDSL supports the creation of yup style validation schemas. 9 | 10 | To use this feature you export the `schema` named export from the pdsl package. 11 | 12 | ```javascript 13 | import { schema as p } from "pdsl"; 14 | ``` 15 | 16 | You can then create a normal p-expression but include validation messages to the right of your predicates starting with an arrow and followed by a message in double quotes. 17 | 18 | ```go 19 | <- "This is a validation message" 20 | ``` 21 | 22 | You can then call the async `validate` or the synchronous `validateSync` methods on the object returned. 23 | 24 | ```js 25 | async function doValidation() { 26 | const { validate } = p.schema`{ 27 | name: string <- "Hey name has to be a string!" 28 | }`; 29 | 30 | try { 31 | await validate({ name: 100 }); 32 | } catch (err) { 33 | console.error(err.message); // "Hey name has to be a string!" 34 | } 35 | } 36 | 37 | doValidation(); 38 | ``` 39 | 40 | Here is an example 41 | 42 | ```js 43 | async function runValidation() { 44 | const { validate, validateSync } = p.schema`{ 45 | name: 46 | string <- "Name must be a string" 47 | & string[>7] <- "Name must be longer than 7 characters", 48 | age: 49 | (number & > 18) <- "Age must be numeric and over 18" 50 | }`; 51 | 52 | try { 53 | await validate({ name: "Rick" }); 54 | } catch (err) { 55 | console.log(err); // "Name must be longer than 7 characters" 56 | } 57 | 58 | try { 59 | validateSync({ name: 100, age: 24 }); 60 | } catch (err) { 61 | console.log(err); // "Name must be a string" 62 | } 63 | 64 | try { 65 | await validate({ name: "Rickardo", age: 16 }); 66 | } catch (err) { 67 | console.log(err); // "Age must be numeric and over 18" 68 | } 69 | 70 | try { 71 | validateSync({ name: "Rickardo", age: 24 }); 72 | } catch (err) { 73 | console.log("Ye gads!"); 74 | } 75 | } 76 | 77 | runValidation(); 78 | ``` 79 | 80 | ## Form validation 81 | 82 | You can use this technique to create form validators and then plug them into libraries like formik: 83 | 84 | ```javascript 85 | import {schema as p} from "pdsl" 86 | 87 | () => ( 88 | 2] <- "Must be longer than 2 characters" 101 | & string[<20] <- "Nice try nobody has a first name that long", 102 | lastName: 103 | _ <- "Required" 104 | & string[>2] <- "Must be longer than 2 characters" 105 | & string[<20] <- "Nice try nobody has a last name that long" 106 | }`} 107 | onSubmit={values => { 108 | // submit values 109 | }} 110 | render={({ errors, touched }) => ( 111 | // render form 112 | )} 113 | /> 114 | ) 115 | 116 | ``` 117 | 118 | ## Validation Configuration 119 | 120 | If you wish to receive errors as an array of objects instead you can configure the schema not to throw errors. 121 | 122 | ```js 123 | const pp = p.configureSchema({ throwErrors: false }); 124 | 125 | const { validateSync } = pp`{ 126 | email: 127 | _ <- "Required" 128 | & Email <- "Invalid email address", 129 | firstName: 130 | _ <- "Required" 131 | & string[>2] <- "Must be longer than 2 characters" 132 | & string[<20] <- "Nice try nobody has a first name that long", 133 | lastName: 134 | _ <- "Required" 135 | & string[>2] <- "Must be longer than 2 characters" 136 | & string[<20] <- "Nice try nobody has a last name that long" 137 | }`; 138 | 139 | validateSync({ 140 | email: "foo" 141 | }); 142 | 143 | /* [ 144 | {message:"Invalid email address", path: "email"}, 145 | {message:"Required", path: "firstName"}, 146 | {message:"Required", path: "lastName"}, 147 | ] */ 148 | ``` 149 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/helpers.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | btw, 3 | btwe, 4 | deep, 5 | Email, 6 | gt, 7 | gte, 8 | arrIncl, 9 | lt, 10 | lte, 11 | obj, 12 | regx, 13 | prim, 14 | val, 15 | extant, 16 | arrArgMatch, 17 | strLen, 18 | arrLen 19 | } from "./index"; 20 | 21 | it("should obj", () => { 22 | expect( 23 | obj( 24 | ["name", a => a === "foo"], 25 | ["age", a => a === 41] 26 | )({ 27 | name: "foo", 28 | age: 41 29 | }) 30 | ).toBe(true); 31 | }); 32 | 33 | it("should check email", () => { 34 | expect(val(regx(Email))("contact@rudiyardley.com")).toBe(true); 35 | }); 36 | 37 | it("should extant", () => { 38 | expect(extant(false)).toBe(true); 39 | expect(extant(undefined)).toBe(false); 40 | expect(extant(null)).toBe(false); 41 | }); 42 | 43 | it("should Email", () => { 44 | expect(Email.test("foo@bar.com")).toBe(true); 45 | expect(Email.test("hello")).toBe(false); 46 | }); 47 | 48 | it("should btw", () => { 49 | expect(btw(10, 100)(50)).toBe(true); 50 | expect(btw(10, 100)(-50)).toBe(false); 51 | expect(btw(100, 10)(-50)).toBe(false); 52 | expect(btw(10, 100)(100)).toBe(false); 53 | expect(btw(10, 100)(10)).toBe(false); 54 | }); 55 | 56 | it("should btwe", () => { 57 | expect(btwe(10, 100)(50)).toBe(true); 58 | expect(btwe(100, 10)(50)).toBe(true); 59 | expect(btwe(10, 100)(-50)).toBe(false); 60 | expect(btwe(10, 100)(100)).toBe(true); 61 | expect(btwe(10, 100)(10)).toBe(true); 62 | }); 63 | 64 | it("should lt", () => { 65 | expect(lt(10)(10)).toBe(false); 66 | expect(lt(10)(50)).toBe(false); 67 | expect(lt(10)(-50)).toBe(true); 68 | }); 69 | 70 | it("should lte", () => { 71 | expect(lte(10)(10)).toBe(true); 72 | expect(lte(10)(50)).toBe(false); 73 | expect(lte(10)(-50)).toBe(true); 74 | }); 75 | 76 | it("should gt", () => { 77 | expect(gt(10)(10)).toBe(false); 78 | expect(gt(10)(50)).toBe(true); 79 | expect(gt(10)(-50)).toBe(false); 80 | }); 81 | 82 | it("should gte", () => { 83 | expect(gte(10)(10)).toBe(true); 84 | expect(gte(10)(50)).toBe(true); 85 | expect(gte(10)(-50)).toBe(false); 86 | }); 87 | 88 | it("should arrIncl", () => { 89 | expect(arrIncl(10)([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).toBe(true); 90 | expect(arrIncl(10)([1, 2, 3, 4, 5, 6, 7, 8, 9, 9])).toBe(false); 91 | expect(arrIncl(3, 10)([1, 2, 3, 10])).toBe(true); 92 | expect(arrIncl(3, 10)([1, 2, 3])).toBe(false); 93 | expect(arrIncl(3, 10)([1, 2])).toBe(false); 94 | expect(arrIncl(gt(3))([4])).toBe(true); 95 | expect(arrIncl(gt(3), 1)([1, 4])).toBe(true); 96 | expect(arrIncl(gt(3), 1)([4])).toBe(false); 97 | }); 98 | 99 | it("should val", () => { 100 | expect(val(10)(10)).toBe(true); 101 | expect(val(10)(50)).toBe(false); 102 | }); 103 | 104 | it("should deep", () => { 105 | expect(deep(10)(10)).toBe(true); 106 | expect(deep({ a: "foo", b: "bar" })({ a: "foo", b: "bar", c: 12 })).toBe( 107 | false 108 | ); 109 | expect(deep({ a: "foo", b: "bar" })({ a: "foo", b: "bar" })).toBe(true); 110 | }); 111 | 112 | it("should regex", () => { 113 | expect(regx(/^foo/)("food")).toBe(true); 114 | expect(regx(/^foo/)("thing")).toBe(false); 115 | }); 116 | 117 | it("should prim", () => { 118 | expect(prim(Number)(10)).toBe(true); 119 | expect(prim(Number)("10")).toBe(false); 120 | expect(prim(Array)([10])).toBe(true); 121 | expect(prim(Array)(10)).toBe(false); 122 | }); 123 | 124 | it("should arrArgMatch", () => { 125 | const isNumeric = a => typeof a === "number"; 126 | const isString = a => typeof a === "string"; 127 | expect(arrArgMatch(isNumeric)([1])).toBe(true); 128 | expect(arrArgMatch(isNumeric)([1, 2])).toBe(false); 129 | expect(arrArgMatch(isNumeric, isNumeric)([1, 2])).toBe(true); 130 | expect(arrArgMatch(isNumeric, isNumeric)([1])).toBe(false); 131 | expect(arrArgMatch(isString)([1])).toBe(false); 132 | expect(arrArgMatch(isString)(["1"])).toBe(true); 133 | expect(arrArgMatch(1)([1])).toBe(true); 134 | expect(arrArgMatch(1)(["1"])).toBe(false); 135 | 136 | // With wildcard 137 | expect(arrArgMatch(1, "...")([1, 2, "foo"])).toBe(true); 138 | expect(arrArgMatch(1, "...")([2, 2, "foo"])).toBe(false); 139 | expect(arrArgMatch("...")([2, 2, "foo"])).toBe(true); 140 | expect(arrArgMatch(1, "...")([1, "two", "three", "ten"])).toBe(true); 141 | expect(arrArgMatch(3, "...")([1, "two", "three", "ten"])).toBe(false); 142 | expect(arrArgMatch(3, 4, 5, "...")([3, 4, "two", "three", "ten"])).toBe( 143 | false 144 | ); 145 | expect(arrArgMatch(3, 4, 5, "...")([3, 4, 5, 6, "two", "three", "ten"])).toBe( 146 | true 147 | ); 148 | }); 149 | 150 | it("should strLen", () => { 151 | expect(strLen(10)("1234567890")).toBe(true); 152 | expect(strLen(10)("12345678910")).toBe(false); 153 | }); 154 | 155 | it("should arrLen", () => { 156 | expect(arrLen(10)("1234567890")).toBe(false); 157 | expect(arrLen(10)([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])).toBe(true); 158 | expect(arrLen(10)([1, 2, 3, 4, 5, 6, 7, 8, 9])).toBe(false); 159 | }); 160 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/src/literals.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | predicateLiteral, 3 | booleanLiteral, 4 | symbolLiteral, 5 | numericLiteral, 6 | stringLiteral 7 | } = require("./literals"); 8 | const { grammar, tokens } = require("pdsl/lib/grammar"); 9 | const generate = require("@babel/generator").default; 10 | 11 | describe("numericLiteral", () => { 12 | test("3.1415", () => { 13 | expect(generate(numericLiteral({ runtime: () => 3.1415 })).code).toBe( 14 | "3.1415" 15 | ); 16 | }); 17 | }); 18 | describe("symbolLiteral", () => { 19 | test("foo", () => { 20 | expect(generate(symbolLiteral({ runtime: () => "foo" })).code).toBe( 21 | '"foo"' 22 | ); 23 | }); 24 | }); 25 | 26 | describe("stringLiteral", () => { 27 | test("'foo'", () => { 28 | expect(generate(stringLiteral({ runtime: () => "foo" })).code).toBe( 29 | '"foo"' 30 | ); 31 | }); 32 | }); 33 | 34 | describe("booleanLiteral", () => { 35 | test("true", () => { 36 | expect(generate(booleanLiteral({ runtime: () => true })).code).toBe("true"); 37 | }); 38 | 39 | test("false", () => { 40 | expect(generate(booleanLiteral({ runtime: () => false })).code).toBe( 41 | "false" 42 | ); 43 | }); 44 | }); 45 | 46 | describe("predicateLiteral", () => { 47 | const predicateLiteralTests = [ 48 | { 49 | name: "Email", 50 | input: tokens.EMAIL_REGX, 51 | expected: "helpers.regx(helpers.Email)" 52 | }, 53 | { 54 | name: "{}", 55 | input: tokens.EMPTY_OBJ, 56 | expected: "helpers.deep({})" 57 | }, 58 | { 59 | name: "[]", 60 | input: tokens.EMPTY_ARRAY, 61 | expected: "helpers.deep([])" 62 | }, 63 | { 64 | name: '""', 65 | input: tokens.EMPTY_STRING_DOUBLE, 66 | expected: 'helpers.deep("")' 67 | }, 68 | { 69 | name: "''", 70 | input: tokens.EMPTY_STRING_SINGLE, 71 | expected: 'helpers.deep("")' 72 | }, 73 | { 74 | name: "Number", 75 | input: tokens.PRIM_NUMBER, 76 | expected: "helpers.prim(Number)" 77 | }, 78 | { 79 | name: "number", 80 | input: tokens.PRIM_NUMBER_VAL, 81 | expected: "helpers.prim(Number)" 82 | }, 83 | { 84 | name: "Object", 85 | input: tokens.PRIM_OBJECT, 86 | expected: "helpers.prim(Object)" 87 | }, 88 | { 89 | name: "Array", 90 | input: tokens.PRIM_ARRAY, 91 | expected: "helpers.prim(Array)" 92 | }, 93 | { 94 | name: "null", 95 | input: tokens.NULL, 96 | expected: "helpers.val(null)" 97 | }, 98 | { 99 | name: "undefined", 100 | input: tokens.UNDEFINED, 101 | expected: "helpers.val(undefined)" 102 | }, 103 | { 104 | name: "Boolean", 105 | input: tokens.PRIM_BOOLEAN_VAL, 106 | expected: "helpers.prim(Boolean)" 107 | }, 108 | { 109 | name: "symbol", 110 | input: tokens.PRIM_SYMBOL_VAL, 111 | expected: "helpers.prim(Symbol)" 112 | }, 113 | { 114 | name: "string", 115 | input: tokens.PRIM_STRING_VAL, 116 | expected: "helpers.prim(String)" 117 | }, 118 | { 119 | name: "array", 120 | input: tokens.PRIM_ARRAY_VAL, 121 | expected: "helpers.prim(Array)" 122 | }, 123 | { 124 | name: "boolean", 125 | input: tokens.PRIM_BOOLEAN, 126 | expected: "helpers.prim(Boolean)" 127 | }, 128 | { 129 | name: "String", 130 | input: tokens.PRIM_STRING, 131 | expected: "helpers.prim(String)" 132 | }, 133 | { 134 | name: "Symbol", 135 | input: tokens.PRIM_SYMBOL, 136 | expected: "helpers.prim(Symbol)" 137 | }, 138 | { 139 | name: "Function", 140 | input: tokens.PRIM_FUNCTION, 141 | expected: "helpers.prim(Function)" 142 | }, 143 | { 144 | name: "_", 145 | input: tokens.EXTANT_PREDICATE, 146 | expected: "helpers.extant" 147 | }, 148 | { 149 | name: "*", 150 | input: tokens.WILDCARD_PREDICATE, 151 | expected: "helpers.wildcard" 152 | }, 153 | { 154 | name: "!!", 155 | input: tokens.TRUTHY, 156 | expected: "helpers.truthy" 157 | }, 158 | { 159 | name: "falsey", 160 | input: tokens.FALSY_KEYWORD, 161 | expected: "helpers.falsey" 162 | } 163 | ]; 164 | 165 | const EXEMPTIONS = ["Xc", "Nc", "Lc", "Uc", "LUc"]; 166 | 167 | // Test to ensure we have a test here for all new predicate literals 168 | Object.entries(grammar) 169 | .filter(([test, creator]) => { 170 | return ( 171 | creator('"foo"').type === "PredicateLiteral" && 172 | !EXEMPTIONS.includes(test) 173 | ); 174 | }) 175 | .map(([test]) => test) 176 | .forEach(test => { 177 | const hasTest = 178 | predicateLiteralTests.filter(t => t.input === test).length > 0; 179 | it(`Have test for ${test}`, () => { 180 | expect(hasTest).toBe(true); 181 | }); 182 | }); 183 | 184 | predicateLiteralTests.forEach(({ name, input, expected, only }) => { 185 | const testFn = only ? test.only : test; 186 | testFn(name, () => { 187 | const node = grammar[input](); 188 | const rpn = predicateLiteral(node, "helpers"); 189 | expect(generate(rpn).code).toBe(expected); 190 | }); 191 | }); 192 | }); 193 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from "../lib/errors"; 2 | import Context from "../lib/context"; 3 | 4 | import Helpers from "./helpers"; 5 | 6 | export const createSchema = (compiler: any, ctx: Context) => { 7 | return (...args: any[]) => { 8 | const predicateFn = compiler(ctx)(...args); 9 | function validateSync(input: any) { 10 | // Setting abortEarly to false 11 | // enables us to collect all errors 12 | ctx.reset({ abortEarly: false, captureErrors: true }); 13 | 14 | // Run the test 15 | predicateFn(input); 16 | const errs = ctx.getErrors(); 17 | 18 | // Throw errors 19 | if (ctx.throwErrors && errs.length > 0) { 20 | throw new ValidationError(errs[0].message, errs[0].path, errs); 21 | } 22 | 23 | // Return the errors 24 | return errs; 25 | } 26 | async function validate(input: any) { 27 | return validateSync(input); 28 | } 29 | return { 30 | unsafe_predicate: predicateFn, 31 | validateSync, 32 | validate, 33 | unsafe_rpn: predicateFn.unsafe_rpn 34 | }; 35 | }; 36 | }; 37 | 38 | function passContextToHelpers(ctx: Context, helpers: typeof Helpers) { 39 | return { 40 | Email: helpers.Email(), 41 | Xc: helpers.Xc(), 42 | Nc: helpers.Nc(), 43 | Lc: helpers.Lc(), 44 | Uc: helpers.Uc(), 45 | LUc: helpers.LUc(), 46 | btw: helpers.btw(ctx), 47 | btwe: helpers.btwe(ctx), 48 | lt: helpers.lt(ctx), 49 | lte: helpers.lte(ctx), 50 | gt: helpers.gt(ctx), 51 | gte: helpers.gte(ctx), 52 | arrIncl: helpers.arrIncl(ctx), 53 | or: helpers.or(ctx), 54 | and: helpers.and(ctx), 55 | not: helpers.not(ctx), 56 | obj: helpers.obj(ctx), 57 | val: helpers.val(ctx), 58 | regx: helpers.regx(ctx), 59 | entry: helpers.entry(ctx), 60 | prim: helpers.prim(ctx), 61 | pred: helpers.pred(ctx), 62 | deep: helpers.deep(ctx), 63 | extant: helpers.extant(ctx), 64 | truthy: helpers.truthy(ctx), 65 | falsey: helpers.falsey(ctx), 66 | arrArgMatch: helpers.arrArgMatch(ctx), 67 | arrTypeMatch: helpers.arrTypeMatch(ctx), 68 | wildcard: helpers.wildcard(), 69 | strLen: helpers.strLen(ctx), 70 | arrLen: helpers.arrLen(ctx), 71 | validation: helpers.validation(ctx) 72 | }; 73 | } 74 | 75 | type PredicateFn = (input?: any) => input is T; 76 | 77 | type BabelPredicateCallback = ( 78 | a: ReturnType 79 | ) => PredicateFn; 80 | 81 | type Engine = RuntimeEngine | BabelEngine; 82 | 83 | type RuntimeEngine = ( 84 | strings: TemplateStringsArray, 85 | ...expressions: any[] 86 | ) => PredicateFn; 87 | 88 | type BabelEngine = (cb: BabelPredicateCallback) => PredicateFn; 89 | 90 | type Compiler

= (ctx: Context) => P; 91 | 92 | const createRuntimeCompiler = ( 93 | engine: RuntimeEngine 94 | ): Compiler => (ctx: Context) => ( 95 | strings: TemplateStringsArray, 96 | ...expressions: any[] 97 | ) => { 98 | const predicateFn = engine(strings, expressions, ctx); 99 | return predicateFn; 100 | }; 101 | 102 | const createBabelCompiler = (): Compiler => ctx => ( 103 | predicateCallback: BabelPredicateCallback 104 | ) => { 105 | const predicateFn = predicateCallback(passContextToHelpers(ctx, Helpers)); 106 | return predicateFn; 107 | }; 108 | 109 | type Schema = ReturnType; 110 | 111 | type ConfigureSchema = (options?: any) => Schema; 112 | 113 | type CreateDefaultReturnType

= P & { 114 | configureSchema: ConfigureSchema; 115 | schema: Schema; 116 | predicate: (options: any) => P; 117 | }; 118 | 119 | export function createDefault( 120 | engine: RuntimeEngine 121 | ): CreateDefaultReturnType>; 122 | export function createDefault( 123 | engine: void 124 | ): CreateDefaultReturnType>; 125 | export function createDefault(engine: any): CreateDefaultReturnType { 126 | const compiler = 127 | typeof engine !== "undefined" 128 | ? createRuntimeCompiler(engine) 129 | : createBabelCompiler(); 130 | 131 | const predicateEngine = compiler(new Context()); 132 | 133 | const configureSchema = (options?) => { 134 | const ctx = new Context({ 135 | schemaMode: true, 136 | abortEarly: false, 137 | captureErrors: true, 138 | throwErrors: true, 139 | ...options 140 | }); 141 | 142 | return createSchema(compiler, ctx); 143 | }; 144 | 145 | const predicate = options => { 146 | return compiler(new Context(options)); 147 | }; 148 | 149 | const schema = configureSchema(); 150 | 151 | return Object.assign(predicateEngine, { 152 | configureSchema, 153 | schema, 154 | predicate 155 | }); 156 | } 157 | 158 | export const { 159 | Email, 160 | Xc, 161 | Nc, 162 | Lc, 163 | Uc, 164 | LUc, 165 | btw, 166 | btwe, 167 | lt, 168 | lte, 169 | gt, 170 | gte, 171 | arrIncl, 172 | or, 173 | and, 174 | not, 175 | obj, 176 | val, 177 | regx, 178 | entry, 179 | prim, 180 | pred, 181 | deep, 182 | extant, 183 | truthy, 184 | falsey, 185 | arrArgMatch, 186 | arrTypeMatch, 187 | wildcard, 188 | strLen, 189 | arrLen, 190 | validation 191 | } = passContextToHelpers(new Context(), Helpers); 192 | 193 | export const getRawHelpers = () => Helpers; 194 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/src/literals.js: -------------------------------------------------------------------------------- 1 | const t = require("@babel/types"); 2 | const { types } = require("pdsl/lib/grammar"); 3 | 4 | function booleanLiteral(pdslNode) { 5 | return t.booleanLiteral(pdslNode.runtime()); 6 | } 7 | 8 | function predicateLiteral(pdslNode, helpersIdentifier) { 9 | switch (`${pdslNode}`) { 10 | case "Email": 11 | return t.callExpression( 12 | t.memberExpression( 13 | t.identifier(helpersIdentifier), 14 | t.identifier("regx") 15 | ), 16 | [ 17 | t.memberExpression( 18 | t.identifier(helpersIdentifier), 19 | t.identifier("Email") 20 | ) 21 | ] 22 | ); 23 | case "{}": 24 | return t.callExpression( 25 | t.memberExpression( 26 | t.identifier(helpersIdentifier), 27 | t.identifier("deep") 28 | ), 29 | [t.objectExpression([])] 30 | ); 31 | case "[]": 32 | return t.callExpression( 33 | t.memberExpression( 34 | t.identifier(helpersIdentifier), 35 | t.identifier("deep") 36 | ), 37 | [t.arrayExpression([])] 38 | ); 39 | case '""': 40 | return t.callExpression( 41 | t.memberExpression( 42 | t.identifier(helpersIdentifier), 43 | t.identifier("deep") 44 | ), 45 | [t.stringLiteral("")] 46 | ); 47 | case "Number": 48 | case "number": 49 | return t.callExpression( 50 | t.memberExpression( 51 | t.identifier(helpersIdentifier), 52 | t.identifier("prim") 53 | ), 54 | [t.identifier("Number")] 55 | ); 56 | case "Object": 57 | return t.callExpression( 58 | t.memberExpression( 59 | t.identifier(helpersIdentifier), 60 | t.identifier("prim") 61 | ), 62 | [t.identifier("Object")] 63 | ); 64 | case "Array": 65 | case "array": 66 | return t.callExpression( 67 | t.memberExpression( 68 | t.identifier(helpersIdentifier), 69 | t.identifier("prim") 70 | ), 71 | [t.identifier("Array")] 72 | ); 73 | case "null": 74 | return t.callExpression( 75 | t.memberExpression( 76 | t.identifier(helpersIdentifier), 77 | t.identifier("val") 78 | ), 79 | [t.identifier("null")] 80 | ); 81 | case "undefined": 82 | return t.callExpression( 83 | t.memberExpression( 84 | t.identifier(helpersIdentifier), 85 | t.identifier("val") 86 | ), 87 | [t.identifier("undefined")] 88 | ); 89 | case "Function": 90 | return t.callExpression( 91 | t.memberExpression( 92 | t.identifier(helpersIdentifier), 93 | t.identifier("prim") 94 | ), 95 | [t.identifier("Function")] 96 | ); 97 | case "Symbol": 98 | case "symbol": 99 | return t.callExpression( 100 | t.memberExpression( 101 | t.identifier(helpersIdentifier), 102 | t.identifier("prim") 103 | ), 104 | [t.identifier("Symbol")] 105 | ); 106 | case "String": 107 | case "string": 108 | return t.callExpression( 109 | t.memberExpression( 110 | t.identifier(helpersIdentifier), 111 | t.identifier("prim") 112 | ), 113 | [t.identifier("String")] 114 | ); 115 | case "Boolean": 116 | case "boolean": 117 | return t.callExpression( 118 | t.memberExpression( 119 | t.identifier(helpersIdentifier), 120 | t.identifier("prim") 121 | ), 122 | [t.identifier("Boolean")] 123 | ); 124 | case "!": 125 | return t.memberExpression( 126 | t.identifier(helpersIdentifier), 127 | t.identifier("falsey") 128 | ); 129 | case "!!": 130 | return t.memberExpression( 131 | t.identifier(helpersIdentifier), 132 | t.identifier("truthy") 133 | ); 134 | case "*": 135 | return t.memberExpression( 136 | t.identifier(helpersIdentifier), 137 | t.identifier("wildcard") 138 | ); 139 | case "_": 140 | return t.memberExpression( 141 | t.identifier(helpersIdentifier), 142 | t.identifier("extant") 143 | ); 144 | default: 145 | throw new Error("Unknown pdslNode: ", pdslNode.toString()); 146 | } 147 | } 148 | 149 | function symbolLiteral(pdslNode) { 150 | const { runtime } = pdslNode; 151 | return t.stringLiteral(runtime()); 152 | } 153 | 154 | function numericLiteral(pdslNode) { 155 | const { runtime } = pdslNode; 156 | return t.numericLiteral(runtime()); 157 | } 158 | 159 | function stringLiteral(pdslNode) { 160 | const { runtime } = pdslNode; 161 | return t.stringLiteral(runtime()); 162 | } 163 | 164 | // return the relevant babel node for the given pdslNode literal 165 | function literal(pdslNode, helpersIdentifier) { 166 | const toBabelNode = { 167 | [types.BooleanLiteral]: booleanLiteral, 168 | [types.PredicateLiteral]: predicateLiteral, 169 | [types.SymbolLiteral]: symbolLiteral, 170 | [types.NumericLiteral]: numericLiteral, 171 | [types.StringLiteral]: stringLiteral 172 | }[pdslNode.type]; 173 | 174 | return toBabelNode(pdslNode, helpersIdentifier); 175 | } 176 | 177 | module.exports = { 178 | predicateLiteral, 179 | booleanLiteral, 180 | symbolLiteral, 181 | numericLiteral, 182 | stringLiteral, 183 | literal 184 | }; 185 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | route: / 3 | name: Welcome to PDSL 4 | --- 5 | 6 |

7 | Welcome to PDSL 8 |
9 | 10 |

11 | 12 |

Welcome to PDSL

13 |

The expressive declarative toolkit for composing predicates in TypeScript or JavaScript

14 | 15 | ```js 16 | const isSoftwareCreator = p`{ 17 | name: string, 18 | age: > 16, 19 | occupation: "Engineer" | "Designer" | "Project Manager" 20 | }`; 21 | 22 | isSoftwareCreator({ 23 | name: "Jane Kenith", 24 | age: 22, 25 | occupation: "Engineer" 26 | }); // true 27 | ``` 28 | 29 | **Predicate functions are just easier with PDSL** 30 | 31 | Creating predicate functions in JavaScript is often verbose, especially for checking the format of complex object types. We need predicate functions all the time when filtering an array, validating input, determining the type of an unknown object or creating guard conditions in TypeScript. 32 | 33 | PDSL provides the developer a simple but powerful shorthand based on a combination of template strings and helper functions for defining predicate functions that makes it easy to understand intent. With `pdsl` we can easily visualize the expected input's structure and intent using it's intuitive predicate composition language. 34 | 35 | PDSL is: 36 | 37 | - Intuitive 38 | - Expressive 39 | - Lightweight (under 6k)! 40 | - No dependencies 41 | - Small bundle size 42 | - Fast 43 | 44 | ## Installation 45 | 46 | Install with npm or yarn 47 | 48 | ```bash 49 | yarn add pdsl 50 | ``` 51 | 52 | ```bash 53 | npm install pdsl 54 | ``` 55 | 56 | Then import the default package into your JavaScript or TypeScript file: 57 | 58 | ```javascript 59 | import p from "pdsl"; 60 | 61 | p`{ name: string }`({ name: "Jane" }); 62 | ``` 63 | 64 | If you have Babel in your build pipeline we recommend to use the [babel plugin](/precompiling-with-babel). 65 | 66 | ## New in Version 5 67 | 68 | ### Exact matching on objects is now off by default 69 | 70 | New in version 5.2+ objects no longer have exact matching turned on by default. If you wish to continue using exact matching you can use the [exact matching syntax](/objects#exact-matching-syntax): 71 | 72 | ```javascript 73 | p`{ name: "foo" }`({ name: "foo", age: 20 }); // true 74 | ``` 75 | 76 | Exact object matching mode can be turned on by using objects with pipes `|`: 77 | 78 | ```javascript 79 | p`{| name: "foo" |}`({ name: "foo", age: 20 }); // false 80 | ``` 81 | 82 | Once you turn exact matching on in an object tree you can only turn it off by using the rest operator: 83 | 84 | ```javascript 85 | p`{| 86 | name: "foo", 87 | exact: { 88 | hello:"hello" 89 | }, 90 | loose: { 91 | hello: "hello", 92 | ... 93 | } 94 | |}`({ 95 | name: "foo", 96 | exact: { 97 | hello: "hello" 98 | }, 99 | loose: { 100 | hello: "hello", 101 | extra: true, 102 | another: [] 103 | } 104 | }); // true 105 | ``` 106 | 107 | ### New validation syntax 108 | 109 | We now have a new [validation syntax](/validation)! 110 | 111 | ```javascript 112 | async function runValidation() { 113 | const { validate, validateSync } = p.schema`{ 114 | name: 115 | string <- "Name must be a string" 116 | & string[>7] <- "Name must be longer than 7 characters", 117 | age: 118 | (number & > 18) <- "Age must be numeric and over 18" 119 | }`; 120 | 121 | try { 122 | await validate({ name: "Rick" }); 123 | } catch (err) { 124 | console.log(err); // "Name must be longer than 7 characters" 125 | } 126 | 127 | try { 128 | validateSync({ name: 100, age: 24 }); 129 | } catch (err) { 130 | console.log(err); // "Name must be a string" 131 | } 132 | 133 | try { 134 | await validate({ name: "Rickardo", age: 16 }); 135 | } catch (err) { 136 | console.log(err); // "Age must be numeric and over 18" 137 | } 138 | 139 | try { 140 | validateSync({ name: "Rickardo", age: 24 }); 141 | } catch (err) { 142 | console.log("Ye gads!"); 143 | } 144 | } 145 | 146 | runValidation(); 147 | ``` 148 | 149 | ### New array includes syntax 150 | 151 | Also new we have an [array includes](/arrays#array-includes) function: 152 | 153 | ``` 154 | [? ] 155 | ``` 156 | 157 | ```javascript 158 | p`[? >50 ]`([1, 2, 100, 12]); // true because 100 is greater than 50 159 | ``` 160 | 161 | ### Formik compatability 162 | 163 | We also now have formik compatability! 164 | 165 | ```ts 166 | import {schema as p} from 'pdsl'; 167 | 168 | export default () => ( 169 | 2] <- "Must be longer than 2 characters" 182 | & string[<20] <- "Nice try nobody has a first name that long", 183 | lastName: 184 | _ <- "Required" 185 | & string[>2] <- "Must be longer than 2 characters" 186 | & string[<20] <- "Nice try nobody has a last name that long" 187 | }`} 188 | onSubmit={values => { 189 | // submit values 190 | }} 191 | render={({ errors, touched }) => ( 192 | // render form 193 | )} 194 | /> 195 | ) 196 | ``` 197 | 198 | ## Roadmap 199 | 200 | Help organise our priorities by [telling us what is the most important to you](https://github.com/ryardley/pdsl/issues/new) 201 | 202 | - Basic Language Design (✓) 203 | - PDSL Compiler (✓) 204 | - Comprehensive Test cases (✓) 205 | - Babel Plugin to remove compiler perf overhead (✓) 206 | - Validation errors (✓) 207 | - Exact matching syntax (✓) 208 | - Automatic type creation from queries 209 | - Syntax Highlighting / VSCode Autocomplete / Prettier formatting 210 | -------------------------------------------------------------------------------- /packages/babel-plugin-pdsl/src/index.js: -------------------------------------------------------------------------------- 1 | const { generator } = require("./babel-generator"); 2 | const { unsafe_toRpnArray } = require("pdsl"); 3 | const t = require("@babel/types"); 4 | 5 | // 1. Find TaggedTemplateExpression 6 | // 2. If the identifier is p then continue 7 | // 3. Get strings and nodeList 8 | // 4. pass strings to the pdsl toAst function 9 | // 5. pass the output from the toAst function to the babelGenerator along with the nodeList 10 | // 6. The result of the babelGenerator will be the ast for the tagged template expression along side the helper Identifiers used. 11 | // 7. Add the helper identifiers to the stored list of helper identifiers for the file. 12 | // 8. Write out the imports to the head of the file 13 | 14 | function findStringsAndAstNodeList(path) { 15 | // if (path.node.tag.name !== "p") return; 16 | const { expressions: nodeList, quasis: templateElements } = path.node.quasi; 17 | const strings = templateElements.map(e => e.value.raw); 18 | return { strings, nodeList }; 19 | } 20 | 21 | function replaceTaggedTemplateLiteralWithFunctions(path) { 22 | // Get the strings and function nodes 23 | const { strings, nodeList } = findStringsAndAstNodeList(path); 24 | 25 | // Create the PDSL rpn array 26 | const pdslRpnArray = unsafe_toRpnArray(strings); 27 | 28 | // Turn that into a babel AST 29 | const ast = generator(pdslRpnArray, nodeList, "_h"); 30 | 31 | // Replace the node 32 | path.replaceWith( 33 | t.callExpression(t.identifier(path.node.tag.name), [ 34 | t.arrowFunctionExpression([t.identifier("_h")], ast) 35 | ]) 36 | ); 37 | } 38 | 39 | // Swap out import path 40 | // TODO: just add the helpers import with a unique identifier 41 | function addHelpersImportPath( 42 | path, 43 | helpersIdentifier, 44 | specifiers, 45 | useRequire = false, 46 | { template } 47 | ) { 48 | // console.log(specifiers.map(s => s.node.local.name)); 49 | const defaults = specifiers 50 | .filter(s => s.type === "ImportDefaultSpecifier") 51 | .map(s => s.node.local.name) 52 | .join(""); // should only be one 53 | 54 | const named = specifiers.filter(s => s.type === "ImportSpecifier"); 55 | 56 | if (useRequire) { 57 | path.insertBefore( 58 | template.ast(`const ${helpersIdentifier} = require("pdsl/helpers");`) 59 | ); 60 | } else { 61 | path.insertBefore( 62 | template.ast(`import ${helpersIdentifier} from "pdsl/helpers";`) 63 | ); 64 | } 65 | 66 | const internalId = getUid(path, "pdslDefault"); 67 | 68 | path.replaceWith( 69 | template.ast( 70 | `const ${internalId.name} = ${helpersIdentifier}.createDefault();` 71 | ) 72 | ); 73 | 74 | if (defaults) { 75 | path.insertAfter(template.ast(`const ${defaults} = ${internalId.name};`)); 76 | } 77 | 78 | if (named.length > 0) { 79 | const namedImports = named.map(s => { 80 | const local = s.node.local.name; 81 | const imported = s.node.imported.name; 82 | if (local === imported) { 83 | return local; 84 | } 85 | return [imported, local].join(":"); 86 | }); 87 | if (namedImports) { 88 | path.insertAfter( 89 | template.ast(`const { ${namedImports} } = ${internalId.name};`) 90 | ); 91 | } 92 | } 93 | } 94 | 95 | function getUid(path, seed) { 96 | return path.scope.generateUidIdentifier(seed); 97 | } 98 | 99 | function flatMap(arr, mapFn) { 100 | return arr.reduce((acc, x) => acc.concat(mapFn(x)), []); 101 | } 102 | 103 | function findVariableReferencesAsTemplateExpressionFromPath(path) { 104 | const varDeclarator = path.findParent( 105 | path => path.type === "VariableDeclarator" 106 | ); 107 | const varBindings = varDeclarator.scope.bindings[varDeclarator.node.id.name]; 108 | 109 | return varBindings.referencePaths 110 | .map(p => p.parentPath) 111 | .filter(pat => { 112 | return pat.type === "TaggedTemplateExpression"; 113 | }); 114 | } 115 | 116 | function handleDefaultSpecifier(rootPath, specifierName, config) { 117 | const bindings = rootPath.scope.bindings[specifierName]; 118 | 119 | flatMap(bindings.referencePaths, refPath => { 120 | // pickup when called directly: pdsl`{name}` 121 | if (refPath.container.type === "TaggedTemplateExpression") { 122 | const defaultImportTaggedTemplateLiteral = refPath.parentPath; 123 | return defaultImportTaggedTemplateLiteral; 124 | } 125 | 126 | // pickup when called as member expression: const foo = pdsl.predicate(); foo`{name}` 127 | if (refPath.container.type === "MemberExpression") { 128 | if ( 129 | refPath.container.property.name === "predicate" || 130 | refPath.container.property.name === "configureSchema" 131 | ) { 132 | return findVariableReferencesAsTemplateExpressionFromPath(refPath); 133 | } 134 | } 135 | // Still need to cover the case pdsl.schema()`foo` 136 | }).forEach(ttl => { 137 | replaceTaggedTemplateLiteralWithFunctions(ttl, config); 138 | }); 139 | } 140 | 141 | function handleNamedSpecifier(rootPath, specifierName, config) { 142 | const bindings = rootPath.scope.bindings[specifierName]; 143 | 144 | flatMap(bindings.referencePaths, refPath => { 145 | // pickup when called directly: pdsl`{name}` 146 | if (refPath.container.type === "TaggedTemplateExpression") { 147 | const defaultImportTaggedTemplateLiteral = refPath.parentPath; 148 | return defaultImportTaggedTemplateLiteral; 149 | } 150 | return findVariableReferencesAsTemplateExpressionFromPath(refPath); 151 | }).forEach(ttl => { 152 | replaceTaggedTemplateLiteralWithFunctions(ttl, config); 153 | }); 154 | } 155 | 156 | module.exports = function pdslPlugin(config) { 157 | return { 158 | name: "pdsl", 159 | visitor: { 160 | ImportDeclaration(path) { 161 | if (path.node.source.value !== "pdsl") return; 162 | const helpersIdentifier = getUid(path, "pdslHelpers"); 163 | 164 | const specifiers = path.get("specifiers"); 165 | specifiers.forEach(specifierPath => { 166 | const specifierName = specifierPath.node.local.name; 167 | const rootPath = specifierPath.parentPath.parentPath; 168 | switch (specifierPath.type) { 169 | case "ImportDefaultSpecifier": 170 | handleDefaultSpecifier(rootPath, specifierName, config); 171 | break; 172 | case "ImportSpecifier": 173 | handleNamedSpecifier(rootPath, specifierName, config); 174 | } 175 | }); 176 | 177 | addHelpersImportPath( 178 | path, 179 | helpersIdentifier.name, 180 | specifiers, 181 | false, 182 | config 183 | ); 184 | }, 185 | Identifier(path) { 186 | if (path.node.name !== "require") return; 187 | if ( 188 | path.parentPath.node.arguments[0] && 189 | path.parentPath.node.arguments[0].value !== "pdsl" 190 | ) { 191 | return; 192 | } 193 | const helpersIdentifier = getUid(path, "pdslHelpers"); 194 | const varDeclaration = path.findParent( 195 | path => path.type === "VariableDeclaration" 196 | ); 197 | const varDeclarator = path.findParent( 198 | path => path.type === "VariableDeclarator" 199 | ); 200 | const specifierName = varDeclarator.node.id.name; 201 | const rootPath = varDeclarator.parentPath.parentPath; 202 | 203 | handleDefaultSpecifier(rootPath, specifierName, config); 204 | addHelpersImportPath( 205 | varDeclaration, 206 | helpersIdentifier.name, 207 | [ 208 | { 209 | type: "ImportDefaultSpecifier", 210 | node: { local: { name: specifierName } } 211 | } 212 | ], 213 | true, 214 | config 215 | ); 216 | } 217 | } 218 | }; 219 | }; 220 | -------------------------------------------------------------------------------- /packages/pdsl/src/helpers/helpers.mdx: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## `const createBtw = ctx => function btw(a, b)` 4 | 5 |

6 | Between bounds 7 |

Return a function that checks to see if it's input is between two numbers not including the numbers. 8 | 9 | - **Parameters:** 10 | - `a` — `number` — The lower number 11 | - `b` — `number` — The higher number 12 | - **Returns:** `function` — A function of the form number => boolean 13 | 14 | ## `const createBtwe = ctx => function btwe(a, b)` 15 | 16 |

17 | Between bounds or equal to 18 |

Return a function that checks to see if it's input is between two numbers including the numbers. 19 | 20 | - **Parameters:** 21 | - `a` — `number` — The lower number 22 | - `b` — `number` — The higher number 23 | - **Returns:** `function` — A function of the form number => boolean 24 | 25 | ## `const createLt = ctx => function lt(a)` 26 | 27 |

28 | Less than 29 |

Return a function that checks to see if it's input is less than the given number. 30 | 31 | - **Parameters:** `a` — `number` — The number to check against. 32 | - **Returns:** `function` — A function of the form number => boolean 33 | 34 | ## `const createLte = ctx => function lte(a)` 35 | 36 |

37 | Less than or equal to 38 |

Return a function that checks to see if it's input is less than or equal to the given number. 39 | 40 | - **Parameters:** `a` — `number` — The number to check against. 41 | - **Returns:** `function` — A function of the form number => boolean 42 | 43 | ## `const createGt = ctx => function gt(a)` 44 | 45 |

46 | Greater than 47 |

Return a function that checks to see if it's input is greater than the given number. 48 | 49 | - **Parameters:** `a` — `number` — The number to check against. 50 | - **Returns:** `function` — A function of the form number => boolean 51 | 52 | ## `const createGte = ctx => function gte(a)` 53 | 54 |

55 | Greater than or equal to 56 |

Return a function that checks to see if it's input is greater than or equal to the given number. 57 | 58 | - **Parameters:** `a` — `number` — The number to check against. 59 | - **Returns:** `function` — A function of the form number => boolean 60 | 61 | ## `const createArrArgMatch = ctx => function arrArgMatch(...tests)` 62 | 63 |

64 | Array match 65 |

Return a function that checks to see if an array contains either any of the values listed or if any of the predicate functions provided return true when run over all items in the array. Eg, 66 |
 67 | 
 68 | arrArgMatch(isNumeric, isNumeric, isNumeric)([1,2,3]); // true arrArgMatch(isNumeric, isNumeric, isNumeric, '...')([1,2,3]); // true arrArgMatch(isString, isNumeric, isNumeric, '...')([1,2,3]); // false arrArgMatch(isString, isNumeric, isNumeric, '...')(['1',2,3]); // true arrArgMatch(isNumeric, isNumeric, isNumeric, '...')([1,2,3,4]); // true arrArgMatch(1, 2)([1,3]); // false 
69 | 70 | - **Parameters:** `tests` — `...function|*` — Either values, `['...', predicate]` or predicate functions used to test the contents of the array. 71 | - **Returns:** `function` — A function of the form {array => boolean} 72 | 73 | ## `const createArrTypeMatch = ctx => function arrTypeMatch(test)` 74 | 75 |

76 | Array type match 77 |

Return a function that checks to see if an array contains only the values listed or if the predicate function provided returns true when run over all items in the array. Eg, 78 |
 79 | 
 80 | arrTypeMatch(isNumeric)([1,2,3]); // true arrTypeMatch(isNumeric)([1,2,'3']); // false arrTypeMatch(isNumeric)([]); // true 
81 | 82 | - **Parameters:** `test` — `function|*` — predicate function used to test the contents of the array. 83 | - **Returns:** `function` — A function of the form {array => boolean} 84 | 85 | ## `const createArrIncludes = ctx => function arrIncl(...args)` 86 | 87 |

88 | Array arrIncl 89 |

Return a function that checks to see if an array contains either any of the values listed or if any of the predicate functions provided return true when run over all items in the array. Eg, 90 |
 91 |   
 92 |     {" "}
 93 |     arrIncl(a => a > 3, 2)([1,2,3]); // true arrIncl(1, 2)([1,3]); // false{" "}
 94 |   
 95 | 
96 | 97 | - **Parameters:** `args` — `...function|*` — Either values or predicate functions used to test the contents of the array. 98 | - **Returns:** `function` — A function of the form {array => boolean} 99 | 100 | ## `const createOr = ctx => function or(left, right)` 101 | 102 |

103 | Logical OR 104 |

Combine predicates to form a new predicate that ORs the result of the input predicates. 105 | 106 | - **Parameters:** 107 | - `left` — `function` — The first predicate 108 | - `right` — `function` — The second predicate 109 | - **Returns:** `function` — A function of the form {any => boolean} 110 | 111 | ## `const createAnd = ctx => function and(left, right)` 112 | 113 |

114 | Logical AND 115 |

Combine predicates to form a new predicate that ANDs the result of the input predicates. 116 | 117 | - **Parameters:** 118 | - `left` — `function` — The first predicate 119 | - `right` — `function` — The second predicate 120 | - **Returns:** `function` — A function of the form {any => boolean} 121 | 122 | ## `const createNot = ctx => function not(input)` 123 | 124 |

125 | Logical NOT 126 |

Takes an input predicate to form a new predicate that NOTs the result of the input predicate. 127 | 128 | - **Parameters:** `input` — `function` — The input predicate 129 | - **Returns:** `function` — A function of the form {any => boolean} 130 | 131 | ## `const createTruthy = ctx => function truthy(a, msg)` 132 | 133 |

134 | Truthy 135 |

A predicate that takes an input value and returns whether or not the value is truthy 136 | 137 | - **Parameters:** `input` — `function` — The input value 138 | - **Returns:** `boolean` — Boolean value indicating if the input is truthy 139 | 140 | ## `const createFalsey = ctx => function falsey(a, msg)` 141 | 142 |

143 | Falsey 144 |

A predicate that takes an input value and returns whether or not the value is falsey 145 | 146 | - **Parameters:** `input` — `function` — The input value 147 | - **Returns:** `boolean` — Boolean value indicating if the input is falsey 148 | 149 | ## `const createVal = ctx => function val(value)` 150 | 151 |

152 | Is strict equal to value 153 |

Takes an input value to form a predicate that checks if the input strictly equals by reference the value. 154 | 155 | - **Parameters:** `value` — `function|*` — The input value if already a fuction it will be returned 156 | - **Returns:** `function` — A function of the form {any => boolean} 157 | 158 | ## `const createDeep = ctx => function deep(value)` 159 | 160 |

161 | Is deep equal to value 162 |

Takes an input value to form a predicate that checks if the input deeply equals the value. 163 | 164 | - **Parameters:** `value` — `function` — The input value 165 | - **Returns:** `function` — A function of the form {any => boolean} 166 | 167 | ## `const createRegx = ctx => function regx(rx)` 168 | 169 |

170 | Regular Expression predicate 171 |

Forms a predicate from a given regular expression 172 | 173 | - **Parameters:** `rx` — `RegExp` — The input value 174 | - **Returns:** `function` — A function of the form {any => boolean} 175 | 176 | ## `const createPrim = ctx => function prim(primative)` 177 | 178 |

179 | Primative predicate 180 |

Forms a predicate from a given JavaSCript primative object to act as a typeof check for the input value. 181 | 182 | Eg.
 prim(Function)(() => {}); // true prim(Number)(6); // true 
183 | 184 | - **Parameters:** `primative` — `object` — The input primative one of Array, Boolean, Number, Symbol, BigInt, String, Function, Object 185 | - **Returns:** `function` — A function of the form {any => boolean} 186 | 187 | ## `const createPred = ctx => function pred(input)` 188 | 189 |

190 | Predicate 191 |

Creates an appropriate predicate based on an input value. This will choose a predicate transformer dynamically based on the type of input. 192 | 193 | - **Parameters:** `input` — `*` — Anything parsable 194 | - **Returns:** `function` — A function of the form {any => boolean} 195 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/grammar.ts: -------------------------------------------------------------------------------- 1 | import { getRawHelpers } from "../helpers/index"; 2 | const { 3 | and, 4 | arrArgMatch, 5 | arrLen, 6 | arrTypeMatch, 7 | arrIncl, 8 | btw, 9 | btwe, 10 | deep, 11 | Email, 12 | entry, 13 | extant, 14 | falsey, 15 | gt, 16 | gte, 17 | Lc, 18 | lt, 19 | lte, 20 | LUc, 21 | Nc, 22 | not, 23 | obj, 24 | or, 25 | prim, 26 | regx, 27 | strLen, 28 | truthy, 29 | Uc, 30 | val, 31 | validation, 32 | wildcard, 33 | Xc 34 | } = getRawHelpers(); 35 | 36 | // In order of global greedy token parsing 37 | export const tokens = { 38 | TRUE: "true", 39 | FALSE: "false", 40 | EMAIL_REGX: "Email", 41 | EXTENDED_CHARS_REGX: "Xc", 42 | NUM_CHARS_REGX: "Nc", 43 | LOW_CHARS_REGX: "Lc", 44 | UP_CHARS_REGX: "Uc", 45 | LOW_UP_CHARS_REGX: "LUc", 46 | VALIDATION_MSG: `<-\\s*\\"((?:\\\\\\"|[^\\"])*)\\"`, 47 | EMPTY_OBJ: "\\{\\}", 48 | EMPTY_ARRAY: "\\[\\]", 49 | EMPTY_STRING_DOUBLE: `\\"\\"`, 50 | EMPTY_STRING_SINGLE: "\\'\\'", 51 | STRING_LENGTH: "string\\[", 52 | ARRAY_LENGTH: "array\\[", 53 | PRIM_NUMBER: "Number", 54 | PRIM_OBJECT: "Object", 55 | ARRAY_TYPED: "Array<", 56 | PRIM_ARRAY: "Array", 57 | NULL: "null", 58 | UNDEFINED: "undefined", 59 | PRIM_NUMBER_VAL: "number", 60 | PRIM_BOOLEAN_VAL: "boolean", 61 | PRIM_SYMBOL_VAL: "symbol", 62 | PRIM_STRING_VAL: "string", 63 | PRIM_ARRAY_VAL: "array", 64 | PRIM_BOOLEAN: "Boolean", 65 | PRIM_STRING: "String", 66 | PRIM_SYMBOL: "Symbol", 67 | PRIM_FUNCTION: "Function", 68 | WILDCARD_PREDICATE: "\\*", 69 | TRUTHY: "\\!\\!", 70 | FALSY_KEYWORD: "falsey", // Using literal falsey as if we use "\\!" it will be picked up all the not operators 71 | IDENTIFIER: "[a-zA-Z_]+[a-zA-Z0-9_-]*", 72 | EXTANT_PREDICATE: "_", 73 | REST_SYMBOL: "\\.\\.\\.", 74 | NUMBER: "-?\\d+(\\.\\d+)?", 75 | STRING_DOUBLE: `\\"[^\\"]*\\"`, 76 | STRING_SINGLE: `\\'[^\\']*\\'`, 77 | PREDICATE_LOOKUP: "@{LINK:(\\d+)}", 78 | OBJ_EXACT: "\\{\\|", 79 | OBJ_EXACT_CLOSE: "\\|\\}", 80 | NOT: "\\!", 81 | AND: "\\&\\&", 82 | AND_SHORT: "\\&", 83 | OR: "\\|\\|", 84 | OR_SHORT: "\\|", 85 | BTW: "\\<\\s\\<", 86 | BTWE: "\\.\\.", 87 | GTE: "\\>\\=", 88 | LTE: "\\<\\=", 89 | GT: "\\>(?=(?:\\s*)?[\\.\\d])", // disambiguation checks if followed by a number 90 | LT: "\\<", 91 | ENTRY: "\\:", 92 | OBJ: "\\{", 93 | OBJ_CLOSE: "\\}", 94 | ARRAY_INCLUDES: "\\[\\?", 95 | ARRAY: "\\[", 96 | ARRAY_CLOSE: "\\]", 97 | ARG: "\\,", 98 | PRECEDENCE: "\\(", 99 | PRECEDENCE_CLOSE: "\\)" 100 | }; 101 | 102 | export const types = { 103 | BooleanLiteral: "BooleanLiteral", 104 | PredicateLiteral: "PredicateLiteral", 105 | SymbolLiteral: "SymbolLiteral", 106 | NumericLiteral: "NumericLiteral", 107 | StringLiteral: "StringLiteral", 108 | PredicateLookup: "PredicateLookup", 109 | Operator: "Operator", 110 | VariableArityOperator: "VariableArityOperator", 111 | VariableArityOperatorClose: "VariableArityOperatorClose", 112 | ArgumentSeparator: "ArgumentSeparator", 113 | PrecidenceOperator: "PrecidenceOperator", 114 | PrecidenceOperatorClose: "PrecidenceOperatorClose" 115 | }; 116 | 117 | export const grammar = { 118 | // LITERALS 119 | [tokens.TRUE]: token => ({ 120 | type: types.BooleanLiteral, 121 | token, 122 | runtime: () => true, 123 | runtimeIdentifier: undefined, 124 | toString() { 125 | return token; 126 | } 127 | }), 128 | [tokens.FALSE]: token => ({ 129 | type: types.BooleanLiteral, 130 | token, 131 | runtime: () => false, 132 | runtimeIdentifier: undefined, 133 | toString() { 134 | return token; 135 | } 136 | }), 137 | [tokens.EMAIL_REGX]: token => ({ 138 | type: types.PredicateLiteral, 139 | token, 140 | runtime: ctx => regx(ctx)(Email), 141 | runtimeIdentifier: "regx", 142 | toString() { 143 | return "Email"; 144 | } 145 | }), 146 | [tokens.EXTENDED_CHARS_REGX]: token => ({ 147 | type: types.PredicateLiteral, 148 | token, 149 | runtime: ctx => regx(ctx)(Xc), 150 | runtimeIdentifier: "regx", 151 | toString() { 152 | return "Xc"; 153 | } 154 | }), 155 | [tokens.NUM_CHARS_REGX]: token => ({ 156 | type: types.PredicateLiteral, 157 | token, 158 | runtime: ctx => regx(ctx)(Nc), 159 | runtimeIdentifier: "regx", 160 | toString() { 161 | return "Nc"; 162 | } 163 | }), 164 | [tokens.LOW_CHARS_REGX]: token => ({ 165 | type: types.PredicateLiteral, 166 | token, 167 | runtime: ctx => regx(ctx)(Lc), 168 | runtimeIdentifier: "regx", 169 | toString() { 170 | return "Lc"; 171 | } 172 | }), 173 | [tokens.UP_CHARS_REGX]: token => ({ 174 | type: types.PredicateLiteral, 175 | token, 176 | runtime: ctx => regx(ctx)(Uc), 177 | runtimeIdentifier: "regx", 178 | toString() { 179 | return "Uc"; 180 | } 181 | }), 182 | [tokens.LOW_UP_CHARS_REGX]: token => ({ 183 | type: types.PredicateLiteral, 184 | token, 185 | runtime: ctx => regx(ctx)(LUc), 186 | runtimeIdentifier: "regx", 187 | toString() { 188 | return "LUc"; 189 | } 190 | }), 191 | [tokens.EMPTY_OBJ]: token => ({ 192 | type: types.PredicateLiteral, 193 | token, 194 | runtime: ctx => deep(ctx)({}), 195 | runtimeIdentifier: "deep", 196 | toString() { 197 | return "{}"; 198 | } 199 | }), 200 | [tokens.EMPTY_ARRAY]: token => ({ 201 | type: types.PredicateLiteral, 202 | token, 203 | runtime: ctx => deep(ctx)([]), 204 | runtimeIdentifier: "deep", 205 | toString() { 206 | return "[]"; 207 | } 208 | }), 209 | [tokens.EMPTY_STRING_DOUBLE]: token => ({ 210 | type: types.PredicateLiteral, 211 | token, 212 | runtime: ctx => deep(ctx)(""), 213 | runtimeIdentifier: "deep", 214 | toString() { 215 | return `""`; 216 | } 217 | }), 218 | [tokens.EMPTY_STRING_SINGLE]: token => ({ 219 | type: types.PredicateLiteral, 220 | token, 221 | runtime: ctx => deep(ctx)(""), 222 | runtimeIdentifier: "deep", 223 | toString() { 224 | return `""`; 225 | } 226 | }), 227 | [tokens.PRIM_NUMBER]: token => ({ 228 | type: types.PredicateLiteral, 229 | token, 230 | runtime: ctx => prim(ctx)(Number), 231 | runtimeIdentifier: "prim", 232 | toString() { 233 | return "Number"; 234 | } 235 | }), 236 | [tokens.PRIM_OBJECT]: token => ({ 237 | type: types.PredicateLiteral, 238 | token, 239 | runtime: ctx => prim(ctx)(Object), 240 | runtimeIdentifier: "prim", 241 | toString() { 242 | return "Object"; 243 | } 244 | }), 245 | [tokens.PRIM_ARRAY]: token => ({ 246 | type: types.PredicateLiteral, 247 | token, 248 | runtime: ctx => prim(ctx)(Array), 249 | runtimeIdentifier: "prim", 250 | toString() { 251 | return "Array"; 252 | } 253 | }), 254 | [tokens.NULL]: token => ({ 255 | type: types.PredicateLiteral, 256 | token, 257 | runtime: ctx => val(ctx)(null), 258 | runtimeIdentifier: "val", 259 | toString() { 260 | return "null"; 261 | } 262 | }), 263 | [tokens.UNDEFINED]: token => ({ 264 | type: types.PredicateLiteral, 265 | token, 266 | runtime: ctx => val(ctx)(undefined), 267 | runtimeIdentifier: "val", 268 | toString() { 269 | return "undefined"; 270 | } 271 | }), 272 | [tokens.PRIM_NUMBER_VAL]: token => ({ 273 | type: types.PredicateLiteral, 274 | token, 275 | runtime: ctx => prim(ctx)(Number), 276 | runtimeIdentifier: "prim", 277 | toString() { 278 | return "number"; 279 | } 280 | }), 281 | [tokens.PRIM_BOOLEAN_VAL]: token => ({ 282 | type: types.PredicateLiteral, 283 | token, 284 | runtime: ctx => prim(ctx)(Boolean), 285 | runtimeIdentifier: "prim", 286 | toString() { 287 | return "boolean"; 288 | } 289 | }), 290 | [tokens.PRIM_SYMBOL_VAL]: token => ({ 291 | type: types.PredicateLiteral, 292 | token, 293 | runtime: ctx => prim(ctx)(Symbol), 294 | runtimeIdentifier: "prim", 295 | toString() { 296 | return "symbol"; 297 | } 298 | }), 299 | [tokens.PRIM_STRING_VAL]: token => ({ 300 | type: types.PredicateLiteral, 301 | token, 302 | runtime: ctx => prim(ctx)(String), 303 | runtimeIdentifier: "prim", 304 | toString() { 305 | return "string"; 306 | } 307 | }), 308 | [tokens.PRIM_ARRAY_VAL]: token => ({ 309 | type: types.PredicateLiteral, 310 | token, 311 | runtime: /* istanbul ignore next */ ctx => prim(ctx)(Array), 312 | runtimeIdentifier: "prim", 313 | toString() { 314 | return "array"; 315 | } 316 | }), 317 | [tokens.PRIM_BOOLEAN]: token => ({ 318 | type: types.PredicateLiteral, 319 | token, 320 | runtime: ctx => prim(ctx)(Boolean), 321 | runtimeIdentifier: "prim", 322 | toString() { 323 | return "Boolean"; 324 | } 325 | }), 326 | [tokens.PRIM_STRING]: token => ({ 327 | type: types.PredicateLiteral, 328 | token, 329 | runtime: ctx => prim(ctx)(String), 330 | runtimeIdentifier: "prim", 331 | toString() { 332 | return "String"; 333 | } 334 | }), 335 | [tokens.PRIM_SYMBOL]: token => ({ 336 | type: types.PredicateLiteral, 337 | token, 338 | runtime: ctx => prim(ctx)(Symbol), 339 | runtimeIdentifier: "prim", 340 | toString() { 341 | return "Symbol"; 342 | } 343 | }), 344 | [tokens.PRIM_FUNCTION]: token => ({ 345 | type: types.PredicateLiteral, 346 | token, 347 | runtime: ctx => prim(ctx)(Function), 348 | runtimeIdentifier: "prim", 349 | toString() { 350 | return "Function"; 351 | } 352 | }), 353 | [tokens.EXTANT_PREDICATE]: token => ({ 354 | type: types.PredicateLiteral, 355 | token, 356 | runtime: ctx => extant(ctx), 357 | runtimeIdentifier: "extant", 358 | toString() { 359 | return "_"; 360 | } 361 | }), 362 | [tokens.WILDCARD_PREDICATE]: token => ({ 363 | type: types.PredicateLiteral, 364 | token, 365 | runtime: ctx => wildcard(), 366 | runtimeIdentifier: "wildcard", 367 | toString() { 368 | return "*"; 369 | } 370 | }), 371 | [tokens.TRUTHY]: token => { 372 | return { 373 | type: types.PredicateLiteral, 374 | token, 375 | runtime: ctx => truthy(ctx), 376 | runtimeIdentifier: "truthy", 377 | toString() { 378 | return "!!"; 379 | } 380 | }; 381 | }, 382 | [tokens.FALSY_KEYWORD]: token => { 383 | return { 384 | type: types.PredicateLiteral, 385 | token, 386 | runtime: ctx => falsey(ctx), 387 | runtimeIdentifier: "falsey", 388 | toString() { 389 | return "!"; 390 | } 391 | }; 392 | }, 393 | [tokens.IDENTIFIER]: token => ({ 394 | type: types.SymbolLiteral, 395 | token, 396 | runtime: () => token, 397 | runtimeIdentifier: undefined, 398 | toString() { 399 | return token; 400 | } 401 | }), 402 | [tokens.REST_SYMBOL]: token => ({ 403 | type: types.SymbolLiteral, 404 | token, 405 | runtime: () => token, 406 | runtimeIdentifier: undefined, 407 | toString() { 408 | return token; 409 | } 410 | }), 411 | [tokens.NUMBER]: token => ({ 412 | type: types.NumericLiteral, 413 | token, 414 | runtime: () => Number(token), 415 | runtimeIdentifier: undefined, 416 | toString() { 417 | return token; 418 | } 419 | }), 420 | [tokens.STRING_DOUBLE]: token => { 421 | const t = token.match(/\"(.*)\"/); 422 | /* istanbul ignore next because __deafult never matches in tests */ 423 | const value = t ? t[1] : "__default"; 424 | return { 425 | type: types.StringLiteral, 426 | token: value, 427 | runtime: ctx => val(ctx)(value), 428 | runtimeIdentifier: "val", 429 | toString() { 430 | return token; 431 | } 432 | }; 433 | }, 434 | [tokens.STRING_SINGLE]: token => { 435 | const t = token.match(/\'(.*)\'/); 436 | /* istanbul ignore next because __deafult never matches in tests */ 437 | const value = t ? t[1] : "__default"; 438 | return { 439 | type: types.StringLiteral, 440 | token: value, 441 | runtime: ctx => val(ctx)(value), 442 | runtimeIdentifier: "val", 443 | toString() { 444 | return token; 445 | } 446 | }; 447 | }, 448 | [tokens.PREDICATE_LOOKUP]: token => { 449 | const t = token.match(/@{LINK:(\d+)}/); 450 | /* istanbul ignore next because __deafult never matches in tests */ 451 | const val = t ? t[1] : "__default"; 452 | return { 453 | type: types.PredicateLookup, 454 | token: val, 455 | runtime: /* istanbul ignore next as not used */ () => val, 456 | runtimeIdentifier: undefined, 457 | toString() { 458 | return token; 459 | } 460 | }; 461 | }, 462 | 463 | // OPERATORS 464 | 465 | [tokens.NOT]: token => ({ 466 | type: types.Operator, 467 | token, 468 | arity: 1, 469 | runtime: ctx => not(ctx), 470 | runtimeIdentifier: "not", 471 | toString() { 472 | return token + this.arity; 473 | }, 474 | prec: 10 475 | }), 476 | [tokens.AND]: token => ({ 477 | type: types.Operator, 478 | token, 479 | arity: 2, 480 | runtime: ctx => and(ctx), 481 | runtimeIdentifier: "and", 482 | prec: 60, 483 | toString() { 484 | return token; 485 | } 486 | }), 487 | [tokens.AND_SHORT]: token => ({ 488 | type: types.Operator, 489 | token, 490 | arity: 2, 491 | runtime: ctx => and(ctx), 492 | runtimeIdentifier: "and", 493 | prec: 60, 494 | toString() { 495 | return token; 496 | } 497 | }), 498 | 499 | [tokens.OR]: token => ({ 500 | type: types.Operator, 501 | token, 502 | arity: 2, 503 | runtime: ctx => or(ctx), 504 | runtimeIdentifier: "or", 505 | prec: 60, 506 | toString() { 507 | return token; 508 | } 509 | }), 510 | [tokens.OR_SHORT]: token => ({ 511 | type: types.Operator, 512 | token, 513 | arity: 2, 514 | runtime: ctx => or(ctx), 515 | runtimeIdentifier: "or", 516 | prec: 60, 517 | toString() { 518 | return token; 519 | } 520 | }), 521 | [tokens.BTW]: token => ({ 522 | type: types.Operator, 523 | token, 524 | arity: 2, 525 | runtime: ctx => btw(ctx), 526 | runtimeIdentifier: "btw", 527 | prec: 50, 528 | toString() { 529 | return token; 530 | } 531 | }), 532 | [tokens.BTWE]: token => ({ 533 | type: types.Operator, 534 | token, 535 | arity: 2, 536 | runtime: ctx => btwe(ctx), 537 | runtimeIdentifier: "btwe", 538 | prec: 50, 539 | toString() { 540 | return token; 541 | } 542 | }), 543 | [tokens.GTE]: token => ({ 544 | type: types.Operator, 545 | token, 546 | arity: 1, 547 | runtime: ctx => gte(ctx), 548 | runtimeIdentifier: "gte", 549 | prec: 50, 550 | toString() { 551 | return token; 552 | } 553 | }), 554 | [tokens.LTE]: token => ({ 555 | type: types.Operator, 556 | token, 557 | arity: 1, 558 | runtime: ctx => lte(ctx), 559 | runtimeIdentifier: "lte", 560 | prec: 50, 561 | toString() { 562 | return token; 563 | } 564 | }), 565 | [tokens.GT]: token => ({ 566 | type: types.Operator, 567 | token, 568 | arity: 1, 569 | runtime: ctx => gt(ctx), 570 | runtimeIdentifier: "gt", 571 | prec: 50, 572 | toString() { 573 | return token; 574 | } 575 | }), 576 | [tokens.LT]: token => ({ 577 | type: types.Operator, 578 | token, 579 | arity: 1, 580 | runtime: ctx => lt(ctx), 581 | runtimeIdentifier: "lt", 582 | prec: 50, 583 | toString() { 584 | return token; 585 | } 586 | }), 587 | [tokens.VALIDATION_MSG]: token => { 588 | const [, msg] = token.match( 589 | /<-\s*\"((?:\\\"|[^\"])*)\"/ 590 | ) || /* istanbul ignore next because it is tested in babel plugin */ [, ""]; 591 | 592 | return { 593 | type: types.Operator, 594 | token: ":e:", 595 | arity: 1, 596 | prec: 55, //?? 597 | runtime: ctx => validation(ctx)(msg.trim()), 598 | runtimeIdentifier: "validation", 599 | toString() { 600 | return ":e:" + msg.slice(0, 3) + ":"; 601 | } 602 | }; 603 | }, 604 | 605 | // functions have highest precidence 606 | [tokens.ENTRY]: token => ({ 607 | type: types.Operator, 608 | token, 609 | arity: 2, 610 | runtime: ctx => entry(ctx), 611 | runtimeIdentifier: "entry", 612 | prec: 100, 613 | toString() { 614 | return token; 615 | } 616 | }), 617 | 618 | [tokens.OBJ_EXACT]: token => ({ 619 | type: types.VariableArityOperator, 620 | token, 621 | arity: 0, 622 | runtime: ctx => obj(ctx, true), 623 | runtimeIdentifier: "obj", 624 | prec: 100, 625 | closingToken: "|}", 626 | toString() { 627 | return token + this.arity; 628 | } 629 | }), 630 | 631 | [tokens.OBJ_EXACT_CLOSE]: token => ({ 632 | type: types.VariableArityOperatorClose, 633 | token, 634 | toString() { 635 | return token; 636 | } 637 | }), 638 | 639 | [tokens.OBJ]: token => ({ 640 | type: types.VariableArityOperator, 641 | token, 642 | arity: 0, 643 | runtime: ctx => obj(ctx), 644 | runtimeIdentifier: "obj", 645 | prec: 100, 646 | closingToken: "}", 647 | toString() { 648 | return token + this.arity; 649 | } 650 | }), 651 | 652 | [tokens.OBJ_CLOSE]: token => ({ 653 | type: types.VariableArityOperatorClose, 654 | token, 655 | toString() { 656 | return token; 657 | } 658 | }), 659 | 660 | [tokens.ARRAY_INCLUDES]: token => ({ 661 | type: types.VariableArityOperator, 662 | token, 663 | arity: 0, 664 | runtime: ctx => arrIncl(ctx), 665 | runtimeIdentifier: "arrIncl", 666 | prec: 100, 667 | closingToken: "]", 668 | toString() { 669 | return token + this.arity; 670 | } 671 | }), 672 | 673 | [tokens.ARRAY]: token => ({ 674 | type: types.VariableArityOperator, 675 | token, 676 | arity: 0, 677 | runtime: ctx => arrArgMatch(ctx), 678 | runtimeIdentifier: "arrArgMatch", 679 | prec: 100, 680 | closingToken: "]", 681 | toString() { 682 | return token + this.arity; 683 | } 684 | }), 685 | 686 | [tokens.ARRAY_CLOSE]: token => ({ 687 | type: types.VariableArityOperatorClose, 688 | token, 689 | toString() { 690 | return token; 691 | } 692 | }), 693 | 694 | [tokens.ARG]: token => ({ 695 | type: types.ArgumentSeparator, 696 | token, 697 | toString() { 698 | return token; 699 | } 700 | }), 701 | 702 | [tokens.PRECEDENCE]: token => ({ 703 | type: types.PrecidenceOperator, 704 | token, 705 | toString() { 706 | return token; 707 | } 708 | }), 709 | [tokens.PRECEDENCE_CLOSE]: token => ({ 710 | type: types.PrecidenceOperatorClose, 711 | token, 712 | toString() { 713 | return token; 714 | } 715 | }), 716 | [tokens.ARRAY_TYPED]: token => ({ 717 | type: types.Operator, 718 | token, 719 | arity: 1, 720 | prec: 50, 721 | runtime: ctx => arrTypeMatch(ctx), 722 | runtimeIdentifier: "arrTypeMatch", 723 | toString() { 724 | return "Array<"; 725 | } 726 | }), 727 | [tokens.STRING_LENGTH]: token => ({ 728 | type: types.Operator, 729 | token, 730 | arity: 1, 731 | prec: 50, 732 | runtime: strLen, 733 | runtimeIdentifier: "strLen", 734 | toString() { 735 | return "string["; 736 | } 737 | }), 738 | [tokens.ARRAY_LENGTH]: token => ({ 739 | type: types.Operator, 740 | token, 741 | arity: 1, 742 | prec: 50, 743 | runtime: ctx => arrLen(ctx), 744 | runtimeIdentifier: "arrLen", 745 | toString() { 746 | return "array["; 747 | } 748 | }) 749 | }; 750 | 751 | export function isOperator(node) { 752 | if (!node) return false; 753 | return node.type === types.Operator; 754 | } 755 | 756 | export function isLiteral(node) { 757 | if (!node) return false; 758 | return ( 759 | { 760 | NumericLiteral: 1, 761 | StringLiteral: 1, 762 | SymbolLiteral: 1, 763 | BooleanLiteral: 1, 764 | PredicateLiteral: 1 765 | }[node.type] || false 766 | ); 767 | } 768 | 769 | export function isPredicateLookup(node) { 770 | if (!node) return false; 771 | return node.type === types.PredicateLookup; 772 | } 773 | export function isVaradicFunctionClose(node) { 774 | if (!node) return false; 775 | return node.type === types.VariableArityOperatorClose; 776 | } 777 | 778 | export function isVaradicFunction(node, closingNode?) { 779 | if (!node) return false; 780 | 781 | const isVaradicStart = node.type === types.VariableArityOperator; 782 | 783 | if (!closingNode) return isVaradicStart; 784 | 785 | return isVaradicStart && node.closingToken === closingNode.token; 786 | } 787 | 788 | export function isBooleanable(node) { 789 | return ( 790 | isLiteral(node) || 791 | isPredicateLookup(node) || 792 | isVaradicFunction(node) || 793 | isPrecidenceOperator(node) 794 | ); 795 | } 796 | 797 | export function isArgumentSeparator(node) { 798 | if (!node) return false; 799 | return node.type === types.ArgumentSeparator; 800 | } 801 | export function isPrecidenceOperator(node) { 802 | if (!node) return false; 803 | return node.type === types.PrecidenceOperator; 804 | } 805 | 806 | export function isPrecidenceOperatorClose(node) { 807 | if (!node) return false; 808 | return node.type === types.PrecidenceOperatorClose; 809 | } 810 | 811 | export function hasToken(node, token) { 812 | return node && node.token === token; 813 | } 814 | -------------------------------------------------------------------------------- /packages/pdsl/src/lib/index.test.ts: -------------------------------------------------------------------------------- 1 | import p, { 2 | unsafe_rpn, 3 | unsafe_tokens, 4 | predicate, 5 | configureSchema, 6 | schema 7 | } from "./index"; 8 | import * as helpers from "../helpers/index"; 9 | import { ValidationError } from "./errors"; 10 | const { Email, gt } = helpers; 11 | 12 | describe("value predicates", () => { 13 | it("should return strict equality with any value", () => { 14 | expect(p`${true}`(true)).toBe(true); 15 | expect(p`${false}`(false)).toBe(true); 16 | expect(p`${false}`(true)).toBe(false); 17 | expect(p`${null}`(true)).toBe(false); 18 | expect(p`${null}`(null)).toBe(true); 19 | expect(p`${undefined}`(undefined)).toBe(true); 20 | expect(p`${undefined}`(null)).toBe(false); 21 | expect(p`${null}`(undefined)).toBe(false); 22 | expect(p`${0}`(0)).toBe(true); 23 | expect(p`${0}`(1)).toBe(false); 24 | expect(p`${"Rupert"}`("Rupert")).toBe(true); 25 | }); 26 | }); 27 | 28 | describe("function predicates", () => { 29 | it("should use the given function ", () => { 30 | expect(p`${n => n > 6 && n < 9}`(7)).toBe(true); 31 | expect(p`${n => n > 6 && n < 9}`(6)).toBe(false); 32 | }); 33 | }); 34 | 35 | describe("RegEx predicates", () => { 36 | it("should use the regex as a predicate", () => { 37 | expect(p`${/^foo/}`("food")).toBe(true); 38 | expect(p`${/^foo/}`("drink")).toBe(false); 39 | }); 40 | }); 41 | 42 | describe("Javascript primitives", () => { 43 | it("should accept primative classes and test typeof associations", () => { 44 | expect(p`${Number}`(5)).toBe(true); 45 | expect(p`${String}`(5)).toBe(false); 46 | expect(p`${Boolean}`(false)).toBe(true); 47 | expect(p`${String}`("Foo")).toBe(true); 48 | expect(p`${Symbol}`(Symbol("Foo"))).toBe(true); 49 | expect(p`${Function}`(() => {})).toBe(true); 50 | expect(p`${Array}`([1, 2, 3, 4])).toBe(true); 51 | expect(p`${Array}`(1234)).toBe(false); 52 | expect(p`${Object}`({ foo: "foo" })).toBe(true); 53 | }); 54 | it("should accept primative value definitions and test typeof associations", () => { 55 | expect(p`number`(5)).toBe(true); 56 | expect(p`string`(5)).toBe(false); 57 | expect(p`boolean`(false)).toBe(true); 58 | expect(p`string`("Foo")).toBe(true); 59 | expect(p`symbol`(Symbol("Foo"))).toBe(true); 60 | }); 61 | }); 62 | 63 | describe("Deep value predicates", () => { 64 | it("should interperet stuff on the deep value whitelist as being value checked", () => { 65 | expect(p`${{}}`({})).toBe(true); 66 | expect(p`${[]}`([])).toBe(true); 67 | expect(p`${""}`("")).toBe(true); 68 | expect(p`${{}}`([])).toBe(false); 69 | expect(p`${[]}`({})).toBe(false); 70 | expect(p`${""}`([])).toBe(false); 71 | }); 72 | }); 73 | 74 | // TODO add more tests 75 | it("should be able to use object template properties", () => { 76 | expect(p`{name:${"Rudi"}}`({ name: "Rudi" })).toBe(true); 77 | }); 78 | 79 | it("should be able to use brackets and or in template properties", () => { 80 | expect(p`{name:(${"Rudi"} || ${"Gregor"})}`({ name: "Rudi" })).toBe(true); 81 | expect(p`{name:(${"Rudi"} || ${"Gregor"})}`({ name: "Gregor" })).toBe(true); 82 | expect(p`{name:(${"Rudi"} || ${"Gregor"})}`({ name: "Other" })).toBe(false); 83 | }); 84 | 85 | it("should have an extant predicate", () => { 86 | expect(p`_`(true)).toBe(true); 87 | expect(p`_`(false)).toBe(true); 88 | expect(p`_`(null)).toBe(false); 89 | expect(p`_`(undefined)).toBe(false); 90 | expect(p`_`(NaN)).toBe(true); 91 | expect(p`!`(null)).toBe(true); 92 | expect(p`!!`(undefined)).toBe(false); 93 | expect(p`!`(NaN)).toBe(true); 94 | expect(p`{ name : _ }`({ name: false })).toBe(true); 95 | expect(p`{ name : _ }`({ name: undefined })).toBe(false); 96 | expect(p`{ name : _ }`({ name: null })).toBe(false); 97 | }); 98 | 99 | it("should use the extant predicate as the default object checking behaviour", () => { 100 | expect(p`{ name }`({ name: false })).toBe(true); 101 | expect(p`{ name }`({ name: undefined })).toBe(false); 102 | expect(p`{ name }`({ name: null })).toBe(false); 103 | }); 104 | 105 | it("should be loose matching by default", () => { 106 | expect(p`{ name }`({ name: "Fred", age: 12 })).toBe(true); 107 | expect(p`{ name }`({ name: "Fred", age: 12 })).toBe(true); 108 | expect(p`{ name, age }`({ name: "Fred", age: 12 })).toBe(true); 109 | }); 110 | 111 | it("should use exact matching", () => { 112 | expect(p`{| name |}`({ name: "Fred" })).toBe(true); 113 | expect(p`{| name |}`({ name: "Fred", age: 12 })).toBe(false); 114 | expect(p`{| name, age |}`({ name: "Fred", age: 12 })).toBe(true); 115 | }); 116 | 117 | it("should match exactly all the way down the object tree unless you use a rest", () => { 118 | expect( 119 | p`{| name, age, sub: { num: 100 } |}`({ 120 | name: "Fred", 121 | age: 12, 122 | sub: { num: 100 } 123 | }) 124 | ).toBe(true); 125 | expect( 126 | p`{| name, age, sub: { num: 100 } |}`({ 127 | name: "Fred", 128 | age: 12, 129 | sub: { num: 100, foo: "foo" } 130 | }) 131 | ).toBe(false); 132 | expect( 133 | p`{| name, age, sub: { num: 100, ... } |}`({ 134 | name: "Fred", 135 | age: 12, 136 | sub: { num: 100, foo: "foo" } 137 | }) 138 | ).toBe(true); 139 | expect( 140 | p`{| name, age, sub: { num: 100, foo: { strict: true }, ... } |}`({ 141 | name: "Fred", 142 | age: 12, 143 | sub: { 144 | num: 100, 145 | foo: { strict: true, other: "stuff" }, 146 | bar: "bar" 147 | } 148 | }) 149 | ).toBe(false); 150 | expect( 151 | p`{| name, age, sub: { num: 100, foo: { strict: true }, ... } |}`({ 152 | name: "Fred", 153 | age: 12, 154 | sub: { 155 | num: 100, 156 | foo: { strict: true }, 157 | bar: "bar" 158 | } 159 | }) 160 | ).toBe(true); 161 | 162 | expect( 163 | p`{| name, age, sub: [{ num: 100, foo: { strict: true }, ... }] |}`({ 164 | name: "Fred", 165 | age: 12, 166 | sub: [ 167 | { 168 | num: 100, 169 | foo: { strict: true }, 170 | bar: "bar" 171 | } 172 | ] 173 | }) 174 | ).toBe(true); 175 | }); 176 | 177 | it("should throw when mismatching strict object syntax", () => { 178 | expect(() => { 179 | p`{| name }`; 180 | }).toThrow(); 181 | 182 | expect(() => { 183 | p`{| name: [ "foo" |} `; 184 | }).toThrow(); 185 | 186 | expect(() => { 187 | p`{| name `; 188 | }).toThrow(); 189 | }); 190 | 191 | it("should be able to use nested object property templates", () => { 192 | expect( 193 | p`{ meta: { remote }, ...}`({ type: "shared.foo", meta: { remote: true } }) 194 | ).toBe(true); 195 | expect( 196 | p`{ meta: !{ remote }, ...}`({ type: "shared.foo", meta: { thing: "foo" } }) 197 | ).toBe(true); 198 | expect( 199 | p`{ meta: !{ remote }, ...}`({ 200 | type: "shared.foo", 201 | meta: { remote: "thing" } 202 | }) 203 | ).toBe(false); 204 | expect( 205 | p`{ meta: !{ remote }, ...}`({ 206 | type: "shared.foo", 207 | meta: { remote: false } 208 | }) 209 | ).toBe(false); 210 | expect( 211 | p`{ meta: { remote:${false} }, ...}`({ 212 | type: "shared.foo", 213 | meta: { remote: false } 214 | }) 215 | ).toBe(true); 216 | }); 217 | 218 | it("should match the examples", () => { 219 | expect(p`{length: ${5}, ...}`("12345")).toBe(true); 220 | expect(p`{length: ${gt(5)}, ...}`("123456")).toBe(true); 221 | expect(p`{foo:{length:${5}, ...}}`({ foo: "12345" })).toBe(true); 222 | expect(p`{foo:{length:${gt(5)}, ...}}`({ foo: "123456" })).toBe(true); 223 | expect( 224 | p` 225 | { 226 | type: ${/^.+foo$/}, 227 | payload: { 228 | email: ${Email} && { length: > 5, ... }, 229 | arr: ![6], 230 | foo: !true, 231 | num: -4 < < 100, 232 | bar: { 233 | baz: ${/^foo/}, 234 | foo 235 | } 236 | } 237 | }`({ 238 | type: "asdsadfoo", 239 | payload: { 240 | email: "a@b.com", 241 | arr: [3, 3, 3, 3, 3], 242 | foo: false, 243 | num: 10, 244 | bar: { 245 | baz: "food", 246 | foo: true 247 | } 248 | } 249 | }) 250 | ).toBe(true); 251 | 252 | expect( 253 | p`${String} || { 254 | username: ${String}, 255 | password: ${String} && { 256 | length: ${gt(3)} 257 | } 258 | }`({}) 259 | ).toBe(false); 260 | 261 | expect( 262 | p`${String} || { 263 | username: ${String}, 264 | password: ${String} && { 265 | length: ${gt(3)} 266 | } 267 | }`({ username: "hello", password: "mi" }) 268 | ).toBe(false); 269 | 270 | expect(p`${String} && { length: ${6}, ... }`("123456")).toBe(true); 271 | expect(p`${String} && { length: ${7}, ... }`("123456")).toBe(false); 272 | }); 273 | 274 | it("should be able to debug the rpn", () => { 275 | expect( 276 | unsafe_tokens` 277 | { 278 | type: ${/^.+foo$/}, 279 | payload: { 280 | email: (${Email} && { length: > 5 }), 281 | arr: ![6], 282 | foo: !true, 283 | num: -4 < < 100, 284 | bar: { 285 | baz: ${/^foo/}, 286 | foo 287 | } 288 | } 289 | }` 290 | ).toBe( 291 | "{0 type : @{LINK:0} , payload : {0 email : ( @{LINK:1} && {0 length : > 5 } ) , arr : !1 [0 6 ] , foo : !1 true , num : -4 < < 100 , bar : {0 baz : @{LINK:2} , foo } } }" 292 | ); 293 | expect( 294 | unsafe_rpn` 295 | { 296 | type: ${/^.+foo$/}, 297 | payload: { 298 | email: (${Email} && { length: > 5 }), 299 | arr: ![6], 300 | foo: !true, 301 | num: -4 < < 100, 302 | bar: { 303 | baz: ${/^foo/}, 304 | foo 305 | } 306 | } 307 | }` 308 | ).toBe( 309 | "type @{LINK:0} : payload email @{LINK:1} length 5 > : {1 && : arr 6 [1 !1 : foo true !1 : num -4 100 < < : bar baz @{LINK:2} : foo {2 : {5 : {2" 310 | ); 311 | }); 312 | 313 | it("should test the toString() calls for code coverage", () => { 314 | expect( 315 | unsafe_tokens` 316 | [1..4] | 317 | 'foo' | 318 | Function | 319 | Symbol | 320 | String | 321 | Boolean | 322 | array | 323 | string | 324 | symbol | 325 | boolean | 326 | number | 327 | undefined | 328 | null | 329 | Array | 330 | Object | 331 | Number | 332 | '' | 333 | "" | 334 | [] | 335 | {} | 336 | LUc | 337 | Uc | 338 | Lc | 339 | Nc | 340 | Xc | 341 | Email | 342 | false | 343 | true | 344 | ! 3 | 345 | ! | 346 | !! | 347 | >= 2 | 348 | <= 2 | 349 | < 2 | 350 | _ | 351 | ... | 352 | Array< | 353 | string[ | 354 | array[ | 355 | [? |` 356 | ).toBe( 357 | [ 358 | "[0 1 .. 4 ]", 359 | "'foo'", 360 | "Function", 361 | "Symbol", 362 | "String", 363 | "Boolean", 364 | "array", 365 | "string", 366 | "symbol", 367 | "boolean", 368 | "number", 369 | "undefined", 370 | "null", 371 | "Array", 372 | "Object", 373 | "Number", 374 | '""', 375 | '""', 376 | "[]", 377 | "{}", 378 | "LUc", 379 | "Uc", 380 | "Lc", 381 | "Nc", 382 | "Xc", 383 | "Email", 384 | "false", 385 | "true", 386 | "!1 3", 387 | "!", 388 | "!!", 389 | ">= 2", 390 | "<= 2", 391 | "< 2", 392 | "_", 393 | "...", 394 | "Array<", 395 | "string[", 396 | "array[", 397 | "[?0" 398 | ].join(" | ") + " |" 399 | ); 400 | }); 401 | 402 | it("should handle complex objects", () => { 403 | expect( 404 | p`string || { 405 | username: string, 406 | password: string && { 407 | length: > 3, ... 408 | } 409 | }`({ username: "hello", password: "mike" }) 410 | ).toBe(true); 411 | }); 412 | 413 | it("should handle greater than ", () => { 414 | expect(p`>5`(6)).toBe(true); 415 | expect(p`>5`(5)).toBe(false); 416 | expect(p`{age:>5}`({ age: 34 })).toBe(true); 417 | expect(p`string[<5 | >20]`("1234567")).toBe(false); 418 | }); 419 | 420 | it("should handle a wildcard", () => { 421 | expect(p`*`()).toBe(true); 422 | expect(p`*`("Foo")).toBe(true); 423 | expect(p`*`(false)).toBe(true); 424 | expect(p`*`(undefined)).toBe(true); 425 | expect(p`*`(null)).toBe(true); 426 | expect(p`*`(NaN)).toBe(true); 427 | }); 428 | 429 | it("should respect a wildcard on an object", () => { 430 | expect(p`{|name: *|}`({ name: undefined })).toBe(true); 431 | expect(p`{|name: *|}`({ name: null })).toBe(true); 432 | expect(p`{|name: *|}`({ name: false })).toBe(true); 433 | expect(p`{|name: *|}`({})).toBe(false); 434 | }); 435 | 436 | it("should handle greater than equals ", () => { 437 | expect(p`>=5`(6)).toBe(true); 438 | expect(p`>=5`(5)).toBe(true); 439 | expect(p`<=5`(4)).toBe(true); 440 | expect(p`>=5`(4)).toBe(false); 441 | expect(p`{age:>=5}`({ age: 34 })).toBe(true); 442 | }); 443 | 444 | it("should handle less than ", () => { 445 | expect(p`<5`(4)).toBe(true); 446 | expect(p`<5`(5)).toBe(false); 447 | expect(p`{age : < 5}`({ age: 4 })).toBe(true); 448 | }); 449 | 450 | it("should handle between ", () => { 451 | expect(p`10 < < 100`(15)).toBe(true); 452 | expect(p`-10 < < 10`(0)).toBe(true); 453 | expect(p`-10 < < 10`(-20)).toBe(false); 454 | expect(p`{age :1 < < 5}`({ age: 4 })).toBe(true); 455 | }); 456 | 457 | it("should know when to turn a value into a predicate", () => { 458 | expect(p`10`(10)).toBe(true); 459 | expect(p`!10`(9)).toBe(true); 460 | expect(p`true`(true)).toBe(true); 461 | expect(p`!true && 6`(6)).toBe(true); 462 | expect(p`"Rudi"`("Rudi")).toBe(true); 463 | expect(p`{foo:>=10}`({ foo: 10 })).toBe(true); 464 | expect(p`{foo:!10}`({ foo: "hello" })).toBe(true); 465 | expect(p`{foo}`({ foo: "hello" })).toBe(true); 466 | }); 467 | 468 | it("should support the array exact matching syntax", () => { 469 | expect(p`[4]`([4])).toBe(true); 470 | expect(p`[4]`([])).toBe(false); 471 | expect(p`[4]`([1, 2, 3])).toBe(false); 472 | expect(p`[1,2,3,4]`([1, 2, 3, 4])).toBe(true); 473 | expect(p`[4,{name}]`([{ name: "foo" }, 4])).toBe(false); 474 | expect(p`[{name}, 4]`([{ name: "foo" }, 4])).toBe(true); 475 | }); 476 | 477 | it("should support the array loose matching syntax", () => { 478 | expect(p`[4]`([4])).toBe(true); 479 | expect(p`[4]`([])).toBe(false); 480 | expect(p`[4]`([1, 2, 3])).toBe(false); 481 | expect(p`[1,2,3,4]`([1, 2, 3, 4])).toBe(true); 482 | expect(p`[4, {name}]`([{ name: "foo" }, 4])).toBe(false); 483 | expect(p`[{name}, 4]`([{ name: "foo" }, 4])).toBe(true); 484 | }); 485 | 486 | it("should support Email", () => { 487 | expect(p`Email`("as@as.com")).toBe(true); 488 | expect(p`Email`("Rudi")).toBe(false); 489 | }); 490 | 491 | it("should support Number", () => { 492 | expect(p`Number`("as@as.com")).toBe(false); 493 | expect(p`Number`(4)).toBe(true); 494 | expect(p`Number`(0)).toBe(true); 495 | expect(p`Number`("4")).toBe(false); 496 | expect(p`Number`(NaN)).toBe(true); 497 | expect(p`Number`(123.123)).toBe(true); 498 | }); 499 | 500 | it("should support Array", () => { 501 | expect(p`Array`("as@as.com")).toBe(false); 502 | expect(p`Array`([4])).toBe(true); 503 | expect(p`Array`("4")).toBe(false); 504 | }); 505 | 506 | it("should support String", () => { 507 | expect(p`String`("as@as.com")).toBe(true); 508 | expect(p`String`({ foo: "asd" })).toBe(false); 509 | expect(p`String`(4)).toBe(false); 510 | expect(p`String && {length: > 3, ...}`("Hi")).toBe(false); 511 | expect(p`String && {length: > 3, ...}`("Hello")).toBe(true); 512 | }); 513 | 514 | it("should support Object", () => { 515 | expect(p`Object`("as@as.com")).toBe(false); 516 | expect(p`Object`({ foo: "asd" })).toBe(true); 517 | expect(p`Object`(4)).toBe(false); 518 | }); 519 | 520 | it("should support null and undefined", () => { 521 | expect(p`undefined`(undefined)).toBe(true); 522 | expect(p`undefined`(0)).toBe(false); 523 | expect(p`null`(undefined)).toBe(false); 524 | expect(p`null`(null)).toBe(true); 525 | expect(p`null`(0)).toBe(false); 526 | expect(p`null || undefined`(null)).toBe(true); 527 | expect(p`null || undefined`(false)).toBe(false); 528 | expect(p`!(null||undefined)`(undefined)).toBe(false); 529 | expect(p`!(null||undefined)`("")).toBe(true); 530 | expect(p`!(null||undefined)`("Hi")).toBe(true); 531 | expect(p`!(null|undefined)`(undefined)).toBe(false); 532 | expect(p`!(null|undefined)`("")).toBe(true); 533 | expect(p`!(null|undefined)`("Hi")).toBe(true); 534 | }); 535 | 536 | it("should handle emptys", () => { 537 | expect(p`""`("")).toBe(true); 538 | expect(p`{}`({})).toBe(true); 539 | expect(p`[]`([])).toBe(true); 540 | expect(p`[]`([1])).toBe(false); 541 | }); 542 | 543 | it("should handle a user credentials object", () => { 544 | const isOnlyLowerCase = p`String & !Nc & !Uc`; 545 | const hasExtendedChars = p`String & Xc`; 546 | 547 | const isValidUser = p`{ 548 | username: ${isOnlyLowerCase} && {length: 5 < < 9, ... }, 549 | password: ${hasExtendedChars} && {length: > 8, ...}, 550 | age: > 17 551 | }`; 552 | 553 | expect( 554 | isValidUser({ username: "ryardley", password: "Hello1234!", age: 21 }) 555 | ).toBe(true); 556 | expect( 557 | isValidUser({ username: "ryardley", password: "Hello1234!", age: 17 }) 558 | ).toBe(false); 559 | expect( 560 | isValidUser({ username: "Ryardley", password: "Hello1234!", age: 21 }) 561 | ).toBe(false); 562 | expect( 563 | isValidUser({ username: "123456", password: "Hello1234!", age: 21 }) 564 | ).toBe(false); 565 | expect( 566 | isValidUser({ username: "ryardley", password: "12345678", age: 21 }) 567 | ).toBe(false); 568 | }); 569 | 570 | it("should handle roughly PI", () => { 571 | expect(p`3.1415 < < 3.1416`(Math.PI)).toBe(true); 572 | expect(p`3.1415 < < 3.1416`(3.1417)).toBe(false); 573 | }); 574 | 575 | it("should notNil", () => { 576 | const notNil = p`!(null | undefined)`; 577 | 578 | expect(notNil("something")).toBe(true); 579 | expect(notNil(false)).toBe(true); 580 | expect(notNil(0)).toBe(true); 581 | expect(notNil(null)).toBe(false); 582 | expect(notNil(undefined)).toBe(false); 583 | }); 584 | 585 | it("should handle comments", () => { 586 | const isValidUser = p`{ 587 | username: String, // foo 588 | // thing 589 | password: String, ... 590 | }`; 591 | expect( 592 | isValidUser({ username: "ryardley", password: "Hello1234!", age: 21 }) 593 | ).toBe(true); 594 | }); 595 | 596 | it("should handle trailing commas", () => { 597 | const isValidUser = p`{ 598 | username: String, 599 | password: String, ..., 600 | }`; 601 | expect( 602 | isValidUser({ username: "ryardley", password: "Hello1234!", age: 21 }) 603 | ).toBe(true); 604 | }); 605 | 606 | it("should deal with garbage input", () => { 607 | expect(() => { 608 | p`}{asdjklh askasd h*&%^6 `; 609 | }).toThrow("Malformed Input"); 610 | }); 611 | 612 | it("should handle .. operator", () => { 613 | expect(p`1..10`(7)).toBe(true); 614 | expect(p`1..10`(1)).toBe(true); 615 | expect(p`1..10`(10)).toBe(true); 616 | expect(p`1..10`(11)).toBe(false); 617 | }); 618 | 619 | it("should handle all the symbols", () => { 620 | expect(p`true`(true)).toBe(true); 621 | expect(p`false`(false)).toBe(true); 622 | expect(p`""`("")).toBe(true); 623 | expect(p`''`("")).toBe(true); 624 | expect(p`Boolean`(false)).toBe(true); 625 | expect(p`Boolean`(0)).toBe(false); 626 | expect(p`Symbol`(Symbol("hello"))).toBe(true); 627 | expect(p`Symbol`("hello")).toBe(false); 628 | expect(p`Function`(() => {})).toBe(true); 629 | expect(p`'A single string'`("A single string")).toBe(true); 630 | 631 | // TODO: These will be deprecated 632 | expect(p`Lc`("abcdef")).toBe(true); 633 | expect(p`Lc`("ABCDEF")).toBe(false); 634 | expect(p`Lc`("aBCDEF")).toBe(true); 635 | expect(p`LUc`("ABCDEF")).toBe(false); 636 | expect(p`LUc`("AbCDEF")).toBe(true); 637 | expect(p`LUc`("abcdef")).toBe(false); 638 | }); 639 | 640 | it("should handle strings with weird characters", () => { 641 | expect( 642 | p`"This string contains \`backticks\`"`("This string contains `backticks`") 643 | ).toBe(true); 644 | 645 | expect( 646 | p`"This string contains 'single quotes'"`( 647 | "This string contains 'single quotes'" 648 | ) 649 | ).toBe(true); 650 | }); 651 | 652 | it("should be able to use a truthy operator", () => { 653 | expect(p`!`(true)).toBe(false); 654 | expect(p`!`(false)).toBe(true); 655 | expect(p`!!`(true)).toBe(true); 656 | expect(p`!!`(false)).toBe(false); 657 | expect(p`{name: !}`({ name: 1 })).toBe(false); 658 | expect(p`{name: !}`({ name: 0 })).toBe(true); 659 | }); 660 | 661 | it("should only match exact array values", () => { 662 | expect( 663 | p`["one", number, {name: string}]`(["one", 123, { name: "a name" }]) 664 | ).toBe(true); 665 | 666 | expect( 667 | p`["one", number, {name: string}]`(["one", 123, { name: "a name" }, 1234]) 668 | ).toBe(false); 669 | 670 | expect( 671 | p`[ {name:string}, number , * ]`([{ name: "Foo" }, 1, undefined]) 672 | ).toBe(true); 673 | }); 674 | 675 | it("should match array values with greedy wildcard", () => { 676 | expect( 677 | p`[number, number, string, ...]`([1, 2, "3", "four", undefined, undefined]) 678 | ).toBe(true); 679 | }); 680 | 681 | it("should be able to match array includes syntax", () => { 682 | expect(p`[? "four"]`([1, 2, "3", "four", undefined, undefined])).toBe(true); 683 | expect(p`[? "four"]`([1, 2, "3", undefined, undefined])).toBe(false); 684 | expect(p`[? "four", "3"]`([1, 2, "3", undefined, undefined])).toBe(false); 685 | expect(p`[?1,"3"]`([1, 2, "3", undefined, undefined])).toBe(true); 686 | expect(p`[? 5]`([1, 2, 3, 4, 5])).toBe(true); 687 | expect(p`[? 5]`([1, 2, 3, 4])).toBe(false); 688 | expect(p`[? 5 | "5"]`([1, 2, 3, 4, "5"])).toBe(true); 689 | expect(p`[? 5 | "5"]`([1, 2, 3, 4, 50])).toBe(false); 690 | expect(p`[? > 5]`([1, 2, 3, 4, 50])).toBe(true); 691 | expect(p`[? > 5]`([1, 2, 3, 4])).toBe(false); 692 | }); 693 | 694 | it("should match Array syntax", () => { 695 | expect(p`Array`([1, 2, 3, 4, 5])).toBe(true); 696 | expect(p`Array`([1, 2, 3, 4, "5"])).toBe(false); 697 | expect(p`Array<{name:string}>`([{ name: "foo" }])).toBe(true); 698 | expect(p`Array<{name:string|number}>`([{ name: "foo" }, { name: 3 }])).toBe( 699 | true 700 | ); 701 | expect(p`{property: Array}`({ property: [1] })).toBe(true); 702 | expect(p`{property: Array<>6>}`({ property: [7, 8, 9] })).toBe(true); 703 | expect(p`{property: Array<>6>}`({ property: [1, 8, 9] })).toBe(false); 704 | expect(p`Array<{name}>`([{ name: "Rudi" }])).toBe(true); 705 | expect(p`Array<{name}>`([{ name: undefined }])).toBe(false); 706 | expect(p`Array<{name}>`({ name: undefined })).toBe(false); 707 | expect(p`Array<{name}> & {length:4, ...}`([{ name: "Rudi" }])).toBe(false); 708 | expect(p`Array<{name}> & {length:1, ...}`([{ name: "Rudi" }])).toBe(true); 709 | }); 710 | 711 | it("should support string length syntax", () => { 712 | expect(p`string[4]`("1")).toBe(false); 713 | expect(p`string[4]`("1234")).toBe(true); 714 | expect(p`string[4]`("12345")).toBe(false); 715 | expect(p`string[>4]`("12345")).toBe(true); 716 | expect(p`string[>4]`("1234")).toBe(false); 717 | expect(p`string[${4}]`("1234")).toBe(true); 718 | expect(p`string[4..6]`("123")).toBe(false); 719 | expect(p`string[4..6]`("1234")).toBe(true); 720 | expect(p`string[4..6]`("123456")).toBe(true); 721 | expect(p`string[4..6]`("1234567")).toBe(false); 722 | expect(p`{password: string[>10]}`({ password: "abcdefghijk" })).toBe(true); 723 | }); 724 | 725 | it("should support array length syntax", () => { 726 | expect(p`array[4]`([1])).toBe(false); 727 | expect(p`array[4]`([1, 2, 3, 4])).toBe(true); 728 | expect(p`Array & array[5]`([1, 2, 3, 4, 5])).toBe(true); 729 | expect(p`array[>4]`([1, 2, 3, 4, 5])).toBe(true); 730 | expect(p`array[5] & Array & [_,_,3, ...]`([1, 2, 3, 4, 5])).toBe( 731 | true 732 | ); 733 | expect(p`array[5] & Array & [_,*,3]`([1, 2, 3, 4, 5])).toBe(false); 734 | }); 735 | 736 | it("should parse logic in numeric value calulations", () => { 737 | expect(p`>5`(6)).toBe(true); 738 | expect(p`> 5 & < 10`(6)).toBe(true); 739 | expect(p`5..7 | 10..13`(6)).toBe(true); 740 | }); 741 | 742 | it("should handle nested properties starting with underscores", () => { 743 | expect( 744 | p`{ 745 | action:{ 746 | _typename: "Redirect" 747 | } 748 | }`({ 749 | action: { _typename: "Redirect" } 750 | }) 751 | ).toBe(true); 752 | }); 753 | 754 | it("should be able to pass a config object in", () => { 755 | expect(predicate({})`>5`(6)).toBe(true); 756 | expect(predicate({})`>5`(5)).toBe(false); 757 | }); 758 | 759 | describe("validation", () => { 760 | const pdsl = configureSchema({ 761 | throwErrors: false 762 | }); 763 | 764 | it("should be able to use var substitution in error messages", () => { 765 | const expression = pdsl`>5 <- "Value $1 must be greater than 5!"`; 766 | expect(expression.unsafe_rpn()).toBe("5 > :e:Val:"); 767 | expect(expression.validateSync(4)).toEqual([ 768 | { path: "", message: "Value 4 must be greater than 5!" } 769 | ]); 770 | }); 771 | 772 | it("should be handle when var substitution is out of bounds", () => { 773 | const expression = pdsl`>5 <- "Value $7 must be greater than 5!"`; 774 | expect(expression.unsafe_rpn()).toBe("5 > :e:Val:"); 775 | expect(expression.validateSync(4)).toEqual([ 776 | { path: "", message: "Value undefined must be greater than 5!" } 777 | ]); 778 | }); 779 | 780 | it("should be able to accept whitespace", () => { 781 | const expression = pdsl`>5 <- "Value must be greater than 5! "`; 782 | expect(expression.unsafe_rpn()).toBe("5 > :e:Val:"); 783 | expect(expression.validateSync(4)).toEqual([ 784 | { path: "", message: "Value must be greater than 5!" } 785 | ]); 786 | }); 787 | 788 | it("should work on object properties", () => { 789 | const expression = pdsl`{ 790 | name: string <- "Name is not a string!", 791 | age: number <- "Age is not a number!" 792 | }`; 793 | 794 | expect(expression.validateSync({ name: 1234, age: "12342134" })).toEqual([ 795 | { 796 | message: "Name is not a string!", 797 | path: "name" 798 | }, 799 | { 800 | message: "Age is not a number!", 801 | path: "age" 802 | } 803 | ]); 804 | }); 805 | 806 | it("should work on arrays", () => { 807 | const expression = pdsl`[1,2,3] <- "Array is not [1,2,3]"`; 808 | expect(expression.validateSync([1, 2, 3, 4])).toEqual([ 809 | { path: "", message: "Array is not [1,2,3]" } 810 | ]); 811 | expect(expression.validateSync([1, 2, 3])).toEqual([]); 812 | }); 813 | 814 | it("should handle precedence and cling to whatever is before it", () => { 815 | const expression = pdsl`{ 816 | name: string <- "Name must be a string" 817 | & string[>7] <- "Name must be longer than 7 characters", 818 | age: (number & > 18) <- "Age must be numeric and over 18" 819 | }`; 820 | 821 | expect(expression.validateSync({ name: "12345678", age: 20 })).toEqual([]); 822 | 823 | expect(expression.validateSync({ name: "123456", age: 20 })).toEqual([ 824 | { 825 | message: "Name must be longer than 7 characters", 826 | path: "name" 827 | } 828 | ]); 829 | expect(expression.validateSync({ name: "12345", age: 17 })).toEqual([ 830 | { 831 | message: "Name must be longer than 7 characters", 832 | path: "name" 833 | }, 834 | { 835 | message: "Age must be numeric and over 18", 836 | path: "age" 837 | } 838 | ]); 839 | 840 | expect(expression.validateSync({ name: "12345678", age: 16 })).toEqual([ 841 | { 842 | message: "Age must be numeric and over 18", 843 | path: "age" 844 | } 845 | ]); 846 | }); 847 | 848 | it("should throw the right errors", () => { 849 | const expression = schema`{ 850 | name: string <- "Name must be a string" 851 | & string[>7] <- "Name must be longer than 7 characters", 852 | age: (number & > 18) <- "Age must be numeric and over 18" 853 | }`.validateSync; 854 | 855 | try { 856 | expression({ name: "Foo", age: 20 }); 857 | } catch (err) { 858 | expect( 859 | p`{ 860 | path: "name", 861 | message: "Name must be longer than 7 characters" 862 | }`(err.inner[0]) 863 | ).toBe(true); 864 | } 865 | 866 | try { 867 | expression({ name: 123, age: 20 }); 868 | } catch (err) { 869 | expect( 870 | p`{ 871 | path: "name", 872 | message: "Name must be a string" 873 | }`(err.inner[0]) 874 | ).toBe(true); 875 | } 876 | }); 877 | 878 | it("should skip over all errors from OR decisions", () => { 879 | const expression = pdsl`({ 880 | name: string <- "Name must be a string" 881 | } | { 882 | age: (number & > 18) <- "Age must be numeric and over 18" 883 | }) <- "You are not verified"`; 884 | expect(expression.validateSync({ name: 100 })).toEqual([ 885 | { path: "name", message: "Name must be a string" }, 886 | { path: "age", message: "Age must be numeric and over 18" }, 887 | { path: "", message: "You are not verified" } 888 | ]); 889 | expect(expression.validateSync({ name: "100" })).toEqual([]); 890 | expect(expression.validateSync({ age: 100 })).toEqual([]); 891 | 892 | expect(expression.validateSync({ foo: "bar" })).toEqual([ 893 | { path: "name", message: "Name must be a string" }, 894 | { path: "age", message: "Age must be numeric and over 18" }, 895 | { path: "", message: "You are not verified" } 896 | ]); 897 | }); 898 | 899 | it("should work with literal strings", () => { 900 | const expression = pdsl`"hello" <- "This should be hello"`; 901 | expect(expression.unsafe_rpn()).toBe('"hello" :e:Thi:'); 902 | expect(expression.validateSync("nope")).toEqual([ 903 | { path: "", message: "This should be hello" } 904 | ]); 905 | }); 906 | 907 | it("should work with escaped quotes", () => { 908 | // Unfortunately because of the way template strings work we 909 | // have to use double backslash to escape quotes :( 910 | const expression = pdsl`"hello" <- "This \\"should\\" be hello"`; 911 | 912 | expect(expression.validateSync("nope")).toEqual([ 913 | { path: "", message: 'This "should" be hello' } 914 | ]); 915 | }); 916 | 917 | it("should work with nested objects", () => { 918 | const expression = pdsl`{ 919 | name: string <- "Name must be a string", 920 | age: (number & > 18) <- "Age must be numeric and over 18", 921 | school: { 922 | type: "summer" <- "Summer must be type", 923 | thing: "winter" <- "Winter must be thing" 924 | } <- "School object problems" 925 | }`; 926 | 927 | expect( 928 | expression.validateSync({ 929 | name: "rudi", 930 | age: 123, 931 | school: { type: "foo" } 932 | }) 933 | ).toEqual([ 934 | { path: "school.type", message: "Summer must be type" }, 935 | { path: "school.thing", message: "Winter must be thing" }, 936 | { path: "school", message: "School object problems" } 937 | ]); 938 | }); 939 | 940 | it("should work with typed arrays", () => { 941 | const expression = pdsl`Array`; 942 | 943 | expect(expression.validateSync(["a", "b"])).toEqual([ 944 | { 945 | message: 'Value "a" is not of type "Number"', 946 | path: "" 947 | }, 948 | { 949 | message: 'Array ["a","b"] does not match given type', 950 | path: "" 951 | } 952 | ]); 953 | }); 954 | 955 | it("should validate object shorthands", () => { 956 | const expression = pdsl`{ 957 | name <- "Name is not provided!", 958 | }`; 959 | expect(expression.validateSync({ name: undefined })).toEqual([ 960 | { 961 | message: "Name is not provided!", 962 | path: "name" 963 | } 964 | ]); 965 | }); 966 | 967 | it("should validate asynchronously", async () => { 968 | const expression = pdsl`{ 969 | name: string <- "Name is not a string!", 970 | age: number <- "Age is not a number!" 971 | }`; 972 | 973 | expect(await expression.validate({ name: "Fred", age: "16" })).toEqual([ 974 | { 975 | message: "Age is not a number!", 976 | path: "age" 977 | } 978 | ]); 979 | }); 980 | 981 | it("should throw errors that can be parsed by formik", async () => { 982 | const schema = configureSchema({ throwErrors: true })`{ 983 | name: string <- "Name is not a string!", 984 | age: number <- "Age is not a number!", 985 | thing: _ <- "Thing is not null or undefined" 986 | }`; 987 | 988 | expect(schema.unsafe_rpn()).toBe( 989 | "name string :e:Nam: : age number :e:Age: : thing _ :e:Thi: : {3" 990 | ); 991 | 992 | let myerror; 993 | 994 | try { 995 | await schema.validate({ name: 1234, age: "16", thing: undefined }); 996 | } catch (err) { 997 | myerror = err; 998 | } 999 | 1000 | expect(myerror.name).toBe("ValidationError"); 1001 | expect(myerror.inner).toEqual([ 1002 | { 1003 | message: "Name is not a string!", 1004 | path: "name" 1005 | }, 1006 | { 1007 | message: "Age is not a number!", 1008 | path: "age" 1009 | }, 1010 | { 1011 | message: "Thing is not null or undefined", 1012 | path: "thing" 1013 | } 1014 | ]); 1015 | }); 1016 | 1017 | it("should provide reasonable defaults", () => { 1018 | const expression = pdsl`{ 1019 | name: string, 1020 | age: number & > 20, 1021 | thing: _ 1022 | }`; 1023 | 1024 | expect(expression.validateSync({})).toEqual([ 1025 | { 1026 | message: 'Value undefined is not of type "String"', 1027 | path: "name" 1028 | }, 1029 | { 1030 | message: 'Value undefined is not of type "Number"', 1031 | path: "age" 1032 | }, 1033 | { 1034 | message: "Value undefined is either null or undefined", 1035 | path: "thing" 1036 | } 1037 | ]); 1038 | }); 1039 | 1040 | it("should not have an inner property if there is only one error", async () => { 1041 | const expression = configureSchema({ throwErrors: true })`{ 1042 | name: string <- "Name is not a string!", 1043 | age: number <- "Age is not a number!" 1044 | }`; 1045 | 1046 | let myerror; 1047 | 1048 | try { 1049 | await expression.validate({ name: 1234, age: 16 }); 1050 | } catch (err) { 1051 | myerror = err; 1052 | } 1053 | 1054 | expect(myerror.name).toBe("ValidationError"); 1055 | expect(myerror.inner[0].message).toBe("Name is not a string!"); 1056 | expect(myerror.inner[0].path).toBe("name"); 1057 | }); 1058 | 1059 | it("should parse expressions with multiple string length calls", () => { 1060 | expect( 1061 | p`{ 1062 | email: 1063 | _ <- "Required" 1064 | & Email <- "Invalid email address", 1065 | 1066 | firstName: 1067 | _ <- "Required" 1068 | & string[>2] <- "Must be longer than 2 characters" 1069 | & string[<20] <- "Nice try nobody has a first name that long", 1070 | 1071 | lastName: 1072 | _ <- "Required" 1073 | & string[>2] <- "Must be longer than 2 characters" 1074 | & string[<20] <- "Nice try nobody has a last name that long" 1075 | }`({ 1076 | email: "contact@example.com", 1077 | firstName: "Foo", 1078 | lastName: "Barr" 1079 | }) 1080 | ).toBe(true); 1081 | }); 1082 | 1083 | describe("schema mode", () => { 1084 | it("should not return a function when using schema mode", async () => { 1085 | const schema = pdsl`"Hello"`; 1086 | 1087 | expect(typeof schema).toBe("object"); 1088 | }); 1089 | 1090 | it("should be able to compose other p expressions", () => { 1091 | const validEmail = p`_ <- "Required" & Email <- "Invalid email address"`; 1092 | 1093 | const schemaObject = schema`{ email: ${validEmail} }`; 1094 | 1095 | expect(() => { 1096 | schemaObject.validateSync({ email: "foo@bar.com" }); 1097 | }).not.toThrow(); 1098 | }); 1099 | 1100 | it("should be able to compose other schemas", () => { 1101 | const validEmail = schema`_ <- "Required" & Email <- "Invalid email address"`; 1102 | 1103 | const schemaObj = schema`{ 1104 | email: ${validEmail} 1105 | }`; 1106 | 1107 | expect(() => { 1108 | schemaObj.validateSync({ email: "foo@bar.com" }); 1109 | }).not.toThrow(); 1110 | }); 1111 | 1112 | it("should pass a sanity test", async () => { 1113 | const schemaObj = schema`{ greeting: "Hello", object: "World" }`; 1114 | 1115 | expect(schemaObj.validate).not.toBeUndefined(); 1116 | expect(schemaObj.validateSync).not.toBeUndefined(); 1117 | 1118 | let error; 1119 | try { 1120 | await schemaObj.validate("Hello World"); 1121 | } catch (err) { 1122 | error = err; 1123 | } 1124 | 1125 | expect(error.name).toBe("ValidationError"); 1126 | expect(error.message).toBe('Value undefined did not match value "Hello"'); 1127 | expect(error.path).toBe("greeting"); 1128 | expect(error.inner).toEqual([ 1129 | { 1130 | message: 'Value undefined did not match value "Hello"', 1131 | path: "greeting" 1132 | }, 1133 | { 1134 | message: 'Value undefined did not match value "World"', 1135 | path: "object" 1136 | } 1137 | ]); 1138 | 1139 | error = undefined; 1140 | 1141 | try { 1142 | await schemaObj.validate({ greeting: "Hello", object: "World" }); 1143 | } catch (err) { 1144 | error = err; 1145 | } 1146 | 1147 | expect(error).toBeUndefined(); 1148 | }); 1149 | 1150 | it("should match a typical object validation schema", async () => { 1151 | const schemaObj = schema`{ 1152 | email: Email <- "Invalid email address", 1153 | firstName: string <- "Invalid firstName", 1154 | lastName: string <- "Invalid lastName" 1155 | }`; 1156 | 1157 | let error: ValidationError; 1158 | try { 1159 | await schemaObj.validate({ 1160 | email: "foo", 1161 | firstName: "Rudi", 1162 | lastName: "Yardley" 1163 | }); 1164 | } catch (error) { 1165 | expect((error as ValidationError).inner).toEqual([ 1166 | { 1167 | message: "Invalid email address", 1168 | path: "email" 1169 | } 1170 | ]); 1171 | } 1172 | }); 1173 | }); 1174 | }); 1175 | 1176 | describe("precompiled babel API", () => { 1177 | test("default", () => { 1178 | const pdsl = helpers.createDefault(); 1179 | const itWorks = pdsl(_h => _h.val("works!")); 1180 | 1181 | expect(itWorks("works!")).toBe(true); 1182 | expect(itWorks("nope")).toBe(false); 1183 | }); 1184 | 1185 | test("predicate()", () => { 1186 | const pdsl = helpers.createDefault(); 1187 | const itWorks = pdsl.predicate({})(_h => _h.val("works!")); 1188 | 1189 | expect(itWorks("works!")).toBe(true); 1190 | expect(itWorks("nope")).toBe(false); 1191 | }); 1192 | 1193 | test("schema()", () => { 1194 | const pdsl = helpers.createDefault(); 1195 | const itWorks = pdsl.schema(_h => _h.val("works!")).validateSync; 1196 | 1197 | expect(itWorks("works!")).toEqual([]); 1198 | 1199 | try { 1200 | itWorks("nope!"); 1201 | } catch (error) { 1202 | expect((error as ValidationError).inner[0].message).toEqual( 1203 | 'Value "nope!" did not match value "works!"' 1204 | ); 1205 | } 1206 | }); 1207 | }); 1208 | --------------------------------------------------------------------------------