├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .prettierrc ├── LICENSE.txt ├── README.md ├── assets ├── button-api-docs-dark.png ├── button-api-docs-light.png ├── button-playground.png ├── logo-dark.png ├── logo-light.png ├── perf-dark.svg └── perf-light.svg ├── package-lock.json ├── package.json ├── rewrite.mjs ├── src ├── main │ ├── Type.ts │ ├── ValidationError.ts │ ├── coerce │ │ ├── array.ts │ │ ├── bigint.ts │ │ ├── boolean.ts │ │ ├── const.ts │ │ ├── date.ts │ │ ├── map.ts │ │ ├── never.ts │ │ ├── number.ts │ │ └── string.ts │ ├── constants.ts │ ├── core.ts │ ├── dsl │ │ ├── any.ts │ │ ├── array.ts │ │ ├── bigint.ts │ │ ├── boolean.ts │ │ ├── const.ts │ │ ├── convert.ts │ │ ├── date.ts │ │ ├── enum.ts │ │ ├── function.ts │ │ ├── instanceOf.ts │ │ ├── intersection.ts │ │ ├── lazy.ts │ │ ├── map.ts │ │ ├── nan.ts │ │ ├── never.ts │ │ ├── not.ts │ │ ├── null.ts │ │ ├── number.ts │ │ ├── object.ts │ │ ├── promise.ts │ │ ├── record.ts │ │ ├── set.ts │ │ ├── string.ts │ │ ├── symbol.ts │ │ ├── tuple.ts │ │ ├── undefined.ts │ │ ├── union.ts │ │ ├── unknown.ts │ │ └── void.ts │ ├── index.ts │ ├── inspect.ts │ ├── internal │ │ ├── arrays.ts │ │ ├── bitmasks.ts │ │ ├── lang.ts │ │ ├── objects.ts │ │ ├── shapes.ts │ │ └── types.ts │ ├── plugin │ │ ├── array-essentials.ts │ │ ├── bigint-essentials.ts │ │ ├── date-essentials.ts │ │ ├── number-essentials.ts │ │ ├── object-essentials.ts │ │ ├── object-eval.ts │ │ ├── set-essentials.ts │ │ └── string-essentials.ts │ ├── shape │ │ ├── ArrayShape.ts │ │ ├── BigIntShape.ts │ │ ├── BooleanShape.ts │ │ ├── ConstShape.ts │ │ ├── DateShape.ts │ │ ├── EnumShape.ts │ │ ├── FunctionShape.ts │ │ ├── InstanceShape.ts │ │ ├── IntersectionShape.ts │ │ ├── LazyShape.ts │ │ ├── MapShape.ts │ │ ├── NeverShape.ts │ │ ├── NumberShape.ts │ │ ├── ObjectShape.ts │ │ ├── PromiseShape.ts │ │ ├── ReadonlyShape.ts │ │ ├── RecordShape.ts │ │ ├── SetShape.ts │ │ ├── Shape.ts │ │ ├── StringShape.ts │ │ ├── SymbolShape.ts │ │ └── UnionShape.ts │ ├── types.ts │ └── utils.ts └── test │ ├── README.test.ts │ ├── ValidationError.test.ts │ ├── coerce │ ├── bigint.test.ts │ ├── boolean.test.ts │ ├── const.test.ts │ ├── date.test.ts │ ├── number.test.ts │ └── string.test.ts │ ├── dsl │ ├── and.test-d.ts │ ├── any.test-d.ts │ ├── any.test.ts │ ├── array.test-d.ts │ ├── array.test.ts │ ├── bigint.test.ts │ ├── boolean.test.ts │ ├── const.test-d.ts │ ├── const.test.ts │ ├── convert.test-d.ts │ ├── convert.test.ts │ ├── date.test-d.ts │ ├── date.test.ts │ ├── enum.test-d.ts │ ├── enum.test.ts │ ├── function.test-d.ts │ ├── function.test.ts │ ├── instanceOf.test-d.ts │ ├── instanceOf.test.ts │ ├── intersection.test-d.ts │ ├── intersection.test.ts │ ├── lazy.test-d.ts │ ├── lazy.test.ts │ ├── map.test-d.ts │ ├── map.test.ts │ ├── nan.test.ts │ ├── never.test-d.ts │ ├── never.test.ts │ ├── not.test-d.ts │ ├── not.test.ts │ ├── null.test.ts │ ├── number.test-d.ts │ ├── number.test.ts │ ├── object.test-d.ts │ ├── object.test.ts │ ├── or.test-d.ts │ ├── or.test.ts │ ├── promise.test-d.ts │ ├── promise.test.ts │ ├── record.test-d.ts │ ├── record.test.ts │ ├── set.test-d.ts │ ├── set.test.ts │ ├── string.test-d.ts │ ├── string.test.ts │ ├── symbol.test.ts │ ├── tuple.test-d.ts │ ├── tuple.test.ts │ ├── undefined.test.ts │ ├── union.test-d.ts │ ├── union.test.ts │ ├── unknonwn.test.ts │ └── void.test.ts │ ├── inspect.test.ts │ ├── internal │ ├── arrays.test.ts │ ├── bitmasks.test.ts │ ├── lang.test.ts │ ├── objects.test.ts │ └── types.test.ts │ ├── perf │ ├── any.perf.js │ ├── array.perf.js │ ├── function.perf.js │ ├── inspect.perf.js │ ├── intersection.perf.js │ ├── lazy.perf.js │ ├── number.perf.js │ ├── object.perf.js │ ├── overall.perf.js │ ├── record.perf.js │ ├── set.perf.js │ ├── string.perf.js │ └── union.perf.js │ ├── plugin │ ├── array-essentials.test.ts │ ├── bigint-essentials.test.ts │ ├── date-essentials.test.ts │ ├── number-essentials.test.ts │ ├── object-essentials.test.ts │ ├── object-eval.test.ts │ ├── set-essentials.test.ts │ └── string-essentials.test.ts │ ├── shape │ ├── ArrayShape.test.ts │ ├── BigIntShape.test.ts │ ├── BooleanShape.test.ts │ ├── ConstShape.test.ts │ ├── DateShape.test.ts │ ├── EnumShape.test.ts │ ├── FunctionShape.test.ts │ ├── InstanceShape.test.ts │ ├── IntersectionShape.test.ts │ ├── LazyShape.test.ts │ ├── MapShape.test.ts │ ├── NeverShape.test.ts │ ├── NumberShape.test.ts │ ├── ObjectShape.test.ts │ ├── PromiseShape.test.ts │ ├── ReadonlyShape.test.ts │ ├── RecordShape.test.ts │ ├── SetShape.test.ts │ ├── Shape.test.ts │ ├── StringShape.test.ts │ ├── SymbolShape.test.ts │ ├── UnionShape.test.ts │ └── mocks.ts │ └── utils.test.ts ├── toofast.json ├── tsconfig.build.json ├── tsconfig.json ├── tsdoc.json └── typedoc.json /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | tags: [ "v[0-9]+.[0-9]+.[0-9]+" ] 6 | branches: [ "next" ] 7 | paths: 8 | - "src/**" 9 | 10 | env: 11 | NPM_TAG: ${{ github.ref_type == 'tag' && 'latest' || 'next' }} 12 | 13 | jobs: 14 | 15 | publish: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: "20" 23 | cache: npm 24 | registry-url: "https://registry.npmjs.org" 25 | 26 | - name: Configure git user 27 | run: | 28 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 29 | git config user.name "github-actions[bot]" 30 | 31 | - name: Checkout latest branch 32 | if: env.NPM_TAG == 'latest' 33 | run: | 34 | set -x 35 | 36 | git checkout -b latest 37 | 38 | find . \( -name '*.ts' -o -name '*.md' \) \ 39 | -exec sed -i \ 40 | -e 's/smikhalevski.github.io\/doubter\/next\//smikhalevski.github.io\/doubter\/latest\//g' \ 41 | -e 's/github.com\/smikhalevski\/doubter#/github.com\/smikhalevski\/doubter\/tree\/latest#/g' \ 42 | {} \; 43 | 44 | git add . 45 | git commit -m 'Updated doc links' 46 | git push --force origin latest 47 | 48 | - name: Update next version 49 | if: env.NPM_TAG == 'next' 50 | run: | 51 | VERSION="$(npm pkg get version | xargs)-next.${GITHUB_SHA::7}" 52 | npm version --no-git-tag-version ${VERSION} 53 | echo "::notice title=Version::${VERSION}" 54 | 55 | - run: npm ci 56 | - run: npm run build 57 | - run: npm run docs 58 | - run: npm test 59 | - run: npm run test:definitions 60 | 61 | - name: Publish package 62 | working-directory: ./lib 63 | run: npm publish --tag ${{ env.NPM_TAG }} 64 | env: 65 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 66 | 67 | - name: Publish docs 68 | run: | 69 | set -x 70 | 71 | mv ./docs /tmp/docs 72 | 73 | git reset HEAD --hard 74 | git fetch origin ghpages:ghpages 75 | git checkout ghpages 76 | git rm -rf --ignore-unmatch ./${{ env.NPM_TAG }} 77 | git clean -fxd 78 | 79 | mv /tmp/docs ./${{ env.NPM_TAG }} 80 | 81 | git add . 82 | 83 | ! git diff --staged --quiet --exit-code || exit 0 84 | 85 | git commit -m "Updated ${{ env.NPM_TAG }} docs (${GITHUB_SHA::7})" 86 | git push origin ghpages 87 | 88 | - name: Create release draft 89 | if: env.NPM_TAG == 'latest' 90 | run: gh release create ${{ github.ref_name }} --generate-notes --draft 91 | env: 92 | GH_TOKEN: ${{ github.token }} 93 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "src/**" 7 | - "package-lock.json" 8 | - "tsconfig.json" 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.head_ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | 16 | test: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: "20" 23 | cache: npm 24 | - run: npm ci 25 | - run: npm run build 26 | - run: npm test 27 | 28 | test-definitions: 29 | needs: test 30 | runs-on: ubuntu-latest 31 | strategy: 32 | matrix: 33 | typescript: [ "4.1", "4.9", "5.0" ] 34 | name: typescript@${{ matrix.typescript }} 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: actions/setup-node@v4 38 | with: 39 | node-version: "20" 40 | cache: npm 41 | - run: npm ci 42 | - run: npm run build 43 | - run: npm install --save-exact typescript@${{ matrix.typescript }} 44 | - run: npm run test:definitions 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | docs/ 4 | coverage/ 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "endOfLine": "auto", 12 | "singleAttributePerLine": true 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Savva Mikhalevski 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 | -------------------------------------------------------------------------------- /assets/button-api-docs-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smikhalevski/doubter/e4e15abcdb358534dcfb58fb6737d71496836d1d/assets/button-api-docs-dark.png -------------------------------------------------------------------------------- /assets/button-api-docs-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smikhalevski/doubter/e4e15abcdb358534dcfb58fb6737d71496836d1d/assets/button-api-docs-light.png -------------------------------------------------------------------------------- /assets/button-playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smikhalevski/doubter/e4e15abcdb358534dcfb58fb6737d71496836d1d/assets/button-playground.png -------------------------------------------------------------------------------- /assets/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smikhalevski/doubter/e4e15abcdb358534dcfb58fb6737d71496836d1d/assets/logo-dark.png -------------------------------------------------------------------------------- /assets/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smikhalevski/doubter/e4e15abcdb358534dcfb58fb6737d71496836d1d/assets/logo-light.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doubter", 3 | "version": "5.1.1", 4 | "description": "Runtime validation and transformation library.", 5 | "main": "./index.js", 6 | "module": "./index.mjs", 7 | "types": "./index.d.ts", 8 | "type": "module", 9 | "exports": { 10 | ".": { 11 | "types": "./index.d.ts", 12 | "import": "./index.mjs", 13 | "require": "./index.js" 14 | }, 15 | "./core": { 16 | "types": "./core.d.ts", 17 | "import": "./core.mjs", 18 | "require": "./core.js" 19 | }, 20 | "./utils": { 21 | "types": "./utils.d.ts", 22 | "import": "./utils.mjs", 23 | "require": "./utils.js" 24 | }, 25 | "./plugin/array-essentials": { 26 | "types": "./plugin/array-essentials.d.ts", 27 | "import": "./plugin/array-essentials.mjs", 28 | "require": "./plugin/array-essentials.js" 29 | }, 30 | "./plugin/bigint-essentials": { 31 | "types": "./plugin/bigint-essentials.d.ts", 32 | "import": "./plugin/bigint-essentials.mjs", 33 | "require": "./plugin/bigint-essentials.js" 34 | }, 35 | "./plugin/date-essentials": { 36 | "types": "./plugin/date-essentials.d.ts", 37 | "import": "./plugin/date-essentials.mjs", 38 | "require": "./plugin/date-essentials.js" 39 | }, 40 | "./plugin/number-essentials": { 41 | "types": "./plugin/number-essentials.d.ts", 42 | "import": "./plugin/number-essentials.mjs", 43 | "require": "./plugin/number-essentials.js" 44 | }, 45 | "./plugin/object-essentials": { 46 | "types": "./plugin/object-essentials.d.ts", 47 | "import": "./plugin/object-essentials.mjs", 48 | "require": "./plugin/object-essentials.js" 49 | }, 50 | "./plugin/set-essentials": { 51 | "types": "./plugin/set-essentials.d.ts", 52 | "import": "./plugin/set-essentials.mjs", 53 | "require": "./plugin/set-essentials.js" 54 | }, 55 | "./plugin/string-essentials": { 56 | "types": "./plugin/string-essentials.d.ts", 57 | "import": "./plugin/string-essentials.mjs", 58 | "require": "./plugin/string-essentials.js" 59 | }, 60 | "./package.json": "./package.json" 61 | }, 62 | "sideEffects": [ 63 | "./index.js", 64 | "./index.mjs", 65 | "./plugin/array-essentials.js", 66 | "./plugin/array-essentials.mjs", 67 | "./plugin/bigint-essentials.js", 68 | "./plugin/bigint-essentials.mjs", 69 | "./plugin/date-essentials.js", 70 | "./plugin/date-essentials.mjs", 71 | "./plugin/number-essentials.js", 72 | "./plugin/number-essentials.mjs", 73 | "./plugin/object-essentials.js", 74 | "./plugin/object-essentials.mjs", 75 | "./plugin/object-eval.js", 76 | "./plugin/object-eval.mjs", 77 | "./plugin/set-essentials.js", 78 | "./plugin/set-essentials.mjs", 79 | "./plugin/string-essentials.js", 80 | "./plugin/string-essentials.mjs" 81 | ], 82 | "scripts": { 83 | "build": "tsc --project tsconfig.build.json && node rewrite.mjs lib && tsc --project tsconfig.build.json --module CommonJS && cp package.json README.md LICENSE.txt lib/ && cd lib && npm pkg delete type scripts devDependencies", 84 | "clean": "rimraf lib docs coverage", 85 | "test": "vitest run", 86 | "test:definitions": "tsd --typings lib/index.d.ts --files 'src/test/**/*.test-d.ts'", 87 | "perf": "toofast", 88 | "docs": "typedoc" 89 | }, 90 | "repository": { 91 | "type": "git", 92 | "url": "git+https://github.com/smikhalevski/doubter.git" 93 | }, 94 | "keywords": [ 95 | "typings", 96 | "validate", 97 | "parse", 98 | "runtime", 99 | "union", 100 | "lazy" 101 | ], 102 | "author": "Savva Mikhalevski ", 103 | "license": "MIT", 104 | "bugs": { 105 | "url": "https://github.com/smikhalevski/doubter/issues" 106 | }, 107 | "homepage": "https://github.com/smikhalevski/doubter#readme", 108 | "devDependencies": { 109 | "@badrap/valita": "^0.4.4", 110 | "@types/qs": "^6.9.18", 111 | "ajv": "^8.17.1", 112 | "myzod": "^1.12.1", 113 | "prettier": "^3.5.3", 114 | "qs": "^6.14.0", 115 | "rimraf": "^6.0.1", 116 | "toofast": "^3.0.3", 117 | "tsd": "^0.32.0", 118 | "tslib": "^2.8.1", 119 | "typedoc": "^0.28.4", 120 | "typedoc-plugin-mdn-links": "^5.0.1", 121 | "typescript": "^5.8.3", 122 | "valibot": "^1.1.0", 123 | "vitest": "^3.1.3", 124 | "zod": "^4.0.0-beta.20250505T195954" 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /rewrite.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | process.chdir(process.argv[2]); 4 | 5 | for (const file of fs.readdirSync('.', { recursive: true })) { 6 | if (file.endsWith('.js')) { 7 | fs.writeFileSync(file.replaceAll('.js', '.mjs'), fs.readFileSync(file, 'utf8').replaceAll(".js'", ".mjs'")); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/Type.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from './internal/lang.js'; 2 | 3 | /** 4 | * The enum-like class that describes a value type. 5 | * 6 | * @template T The name of the value type. 7 | * @see {@link Shape.inputs} 8 | * @group Type Inference 9 | */ 10 | export class Type { 11 | static readonly ARRAY = new Type('array'); 12 | static readonly BIGINT = new Type('bigint'); 13 | static readonly BOOLEAN = new Type('boolean'); 14 | static readonly DATE = new Type('date'); 15 | static readonly FUNCTION = new Type('function'); 16 | static readonly MAP = new Type('map'); 17 | static readonly NULL = new Type('null'); 18 | static readonly NUMBER = new Type('number'); 19 | static readonly OBJECT = new Type('object'); 20 | static readonly PROMISE = new Type('promise'); 21 | static readonly SET = new Type('set'); 22 | static readonly STRING = new Type('string'); 23 | static readonly SYMBOL = new Type('symbol'); 24 | static readonly UNDEFINED = new Type('undefined'); 25 | static readonly UNKNOWN = new Type('unknown'); 26 | 27 | private constructor( 28 | /** 29 | * The name of the type. 30 | */ 31 | readonly name: T 32 | ) {} 33 | 34 | /** 35 | * Returns the type of the given value. If value is a type itself, it is returned as is. 36 | * 37 | * @param value The value to get type of. 38 | * @return The type of the value. 39 | */ 40 | static of(value: unknown): Type { 41 | const type = typeof value; 42 | 43 | if (type === 'undefined') { 44 | return Type.UNDEFINED; 45 | } 46 | if (type === 'boolean') { 47 | return Type.BOOLEAN; 48 | } 49 | if (type === 'number') { 50 | return Type.NUMBER; 51 | } 52 | if (type === 'string') { 53 | return Type.STRING; 54 | } 55 | if (type === 'function') { 56 | return Type.FUNCTION; 57 | } 58 | if (type === 'symbol') { 59 | return Type.SYMBOL; 60 | } 61 | if (type === 'bigint') { 62 | return Type.BIGINT; 63 | } 64 | if (value === null) { 65 | return Type.NULL; 66 | } 67 | if (isArray(value)) { 68 | return Type.ARRAY; 69 | } 70 | if (value instanceof Type) { 71 | return value; 72 | } 73 | if (value instanceof Date) { 74 | return Type.DATE; 75 | } 76 | if (value instanceof Promise) { 77 | return Type.PROMISE; 78 | } 79 | if (value instanceof Set) { 80 | return Type.SET; 81 | } 82 | if (value instanceof Map) { 83 | return Type.MAP; 84 | } 85 | return Type.OBJECT; 86 | } 87 | 88 | /** 89 | * @internal 90 | */ 91 | toString(): string { 92 | return this.name; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/ValidationError.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from './inspect.js'; 2 | import { Issue } from './types.js'; 3 | 4 | /** 5 | * An error thrown if parsing failed. Custom check callbacks, refinement predicates, converters, and fallback 6 | * functions can throw this error to notify that the operation has failed. 7 | * 8 | * @group Other 9 | */ 10 | export class ValidationError extends TypeError { 11 | /** 12 | * Creates a new {@link ValidationError} instance. 13 | * 14 | * @param issues The array of issues that caused the error. 15 | * @param message The error message. 16 | */ 17 | constructor( 18 | /** 19 | * The array of issues that caused the error. 20 | */ 21 | public issues: Issue[], 22 | message?: string 23 | ) { 24 | super(message !== undefined ? message : inspect(issues)); 25 | } 26 | } 27 | 28 | ValidationError.prototype.name = 'ValidationError'; 29 | -------------------------------------------------------------------------------- /src/main/coerce/array.ts: -------------------------------------------------------------------------------- 1 | import { unique } from '../internal/arrays.js'; 2 | import { getCanonicalValue, isArray, isIterableObject } from '../internal/lang.js'; 3 | 4 | /** 5 | * Coerces a value to an array. 6 | * 7 | * @param input The value to coerce. 8 | * @returns An array. 9 | */ 10 | export function coerceToArray(input: unknown): unknown[] { 11 | if (isIterableObject(getCanonicalValue(input))) { 12 | return Array.from(input as Iterable); 13 | } 14 | return [input]; 15 | } 16 | 17 | /** 18 | * Coerces a value to an array of unique values. 19 | * 20 | * @param input The value to coerce. 21 | * @returns An array of unique values. 22 | */ 23 | export function coerceToUniqueArray(input: unknown): unknown[] { 24 | return unique(isArray(input) ? input : coerceToArray(input)); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/coerce/bigint.ts: -------------------------------------------------------------------------------- 1 | import { getCanonicalValue, isArray } from '../internal/lang.js'; 2 | import { Type } from '../Type.js'; 3 | import { NEVER } from './never.js'; 4 | 5 | /** 6 | * The array of inputs that are coercible to a bigint with {@link coerceToBigInt}. 7 | */ 8 | export const bigintCoercibleInputs = Object.freeze([ 9 | Type.ARRAY, 10 | Type.BIGINT, 11 | Type.OBJECT, 12 | Type.STRING, 13 | Type.NUMBER, 14 | Type.BOOLEAN, 15 | null, 16 | undefined, 17 | ]); 18 | 19 | /** 20 | * Coerces a value to a bigint. 21 | * 22 | * @param input The value to coerce. 23 | * @returns A bigint value, or {@link NEVER} if coercion isn't possible. 24 | */ 25 | export function coerceToBigInt(input: unknown): bigint { 26 | if (isArray(input) && input.length === 1 && typeof (input = input[0]) === 'bigint') { 27 | return input; 28 | } 29 | if (input === null || input === undefined) { 30 | return BigInt(0); 31 | } 32 | 33 | input = getCanonicalValue(input); 34 | 35 | if ( 36 | typeof input === 'bigint' || 37 | typeof input === 'number' || 38 | typeof input === 'string' || 39 | typeof input === 'boolean' 40 | ) { 41 | try { 42 | return BigInt(input); 43 | } catch {} 44 | } 45 | return NEVER; 46 | } 47 | -------------------------------------------------------------------------------- /src/main/coerce/boolean.ts: -------------------------------------------------------------------------------- 1 | import { getCanonicalValue, isArray } from '../internal/lang.js'; 2 | import { Type } from '../Type.js'; 3 | import { NEVER } from './never.js'; 4 | 5 | /** 6 | * The array of inputs that are coercible to a boolean with {@link coerceToBoolean}. 7 | */ 8 | export const booleanCoercibleInputs = Object.freeze([ 9 | Type.ARRAY, 10 | Type.OBJECT, 11 | Type.BOOLEAN, 12 | 'false', 13 | 'true', 14 | 0, 15 | 1, 16 | null, 17 | undefined, 18 | ]); 19 | 20 | /** 21 | * Coerces a value to a boolean. 22 | * 23 | * @param input The value to coerce. 24 | * @returns A boolean value, or {@link NEVER} if coercion isn't possible. 25 | */ 26 | export function coerceToBoolean(input: unknown): boolean { 27 | if (isArray(input) && input.length === 1 && typeof (input = input[0]) === 'boolean') { 28 | return input; 29 | } 30 | 31 | input = getCanonicalValue(input); 32 | 33 | if (input === 0 || input === 'false' || input === false || input === null || input === undefined) { 34 | return false; 35 | } 36 | if (input === 1 || input === 'true' || input === true) { 37 | return true; 38 | } 39 | return NEVER; 40 | } 41 | -------------------------------------------------------------------------------- /src/main/coerce/const.ts: -------------------------------------------------------------------------------- 1 | import { getCanonicalValue, isArray, isEqual } from '../internal/lang.js'; 2 | import { Type } from '../Type.js'; 3 | import { bigintCoercibleInputs, coerceToBigInt } from './bigint.js'; 4 | import { booleanCoercibleInputs, coerceToBoolean } from './boolean.js'; 5 | import { coerceToDate, dateCoercibleInputs } from './date.js'; 6 | import { NEVER } from './never.js'; 7 | import { coerceToNumber, numberCoercibleInputs } from './number.js'; 8 | import { coerceToString, stringCoercibleInputs } from './string.js'; 9 | 10 | /** 11 | * The array of inputs that are coercible to `NaN` with {@link coerceToConst}. 12 | */ 13 | const nanCoercibleInputs = Object.freeze([ 14 | Type.ARRAY, 15 | Type.OBJECT, // new Number(NaN) 16 | NaN, 17 | ]); 18 | 19 | /** 20 | * Returns the array of types that are coercible to a constant value with {@link coerceToConst}. 21 | */ 22 | export function getConstCoercibleInputs(value: unknown): readonly unknown[] { 23 | const canonicalValue = getCanonicalValue(value); 24 | 25 | if (typeof canonicalValue === 'bigint') { 26 | return bigintCoercibleInputs; 27 | } 28 | if (typeof canonicalValue === 'number') { 29 | return canonicalValue !== canonicalValue ? nanCoercibleInputs : numberCoercibleInputs; 30 | } 31 | if (typeof canonicalValue === 'string') { 32 | return stringCoercibleInputs; 33 | } 34 | if (typeof canonicalValue === 'boolean') { 35 | return booleanCoercibleInputs; 36 | } 37 | if (value instanceof Date) { 38 | return dateCoercibleInputs; 39 | } 40 | return [Type.ARRAY, value]; 41 | } 42 | 43 | /** 44 | * Coerces an input value to the given constant value. 45 | * 46 | * @param value The literal value to which an `input` must be coerced. 47 | * @param input The input to coerce. 48 | * @returns `value` if `input` is coercible to `value` or {@link NEVER} if coercion isn't possible. 49 | */ 50 | export function coerceToConst(value: T, input: unknown): T { 51 | if (isArray(input) && input.length === 1 && isEqual((input = input[0]), value)) { 52 | return value; 53 | } 54 | 55 | const canonicalValue = getCanonicalValue(value); 56 | 57 | let coercedInput; 58 | 59 | switch (typeof canonicalValue) { 60 | case 'bigint': 61 | coercedInput = coerceToBigInt(input); 62 | break; 63 | 64 | case 'number': 65 | coercedInput = (input = getCanonicalValue(input)) !== input ? input : coerceToNumber(input); 66 | break; 67 | 68 | case 'string': 69 | coercedInput = coerceToString(input); 70 | break; 71 | 72 | case 'boolean': 73 | coercedInput = coerceToBoolean(input); 74 | break; 75 | 76 | // Date 77 | case 'object': 78 | if ( 79 | value !== null && 80 | value instanceof Date && 81 | (input = coerceToDate(input)) !== NEVER && 82 | (input as Date).getTime() === value.getTime() 83 | ) { 84 | coercedInput = value; 85 | } else { 86 | coercedInput = input; 87 | } 88 | break; 89 | 90 | default: 91 | coercedInput = input; 92 | break; 93 | } 94 | 95 | if (isEqual(coercedInput, canonicalValue)) { 96 | return value; 97 | } 98 | return NEVER; 99 | } 100 | -------------------------------------------------------------------------------- /src/main/coerce/date.ts: -------------------------------------------------------------------------------- 1 | import { getCanonicalValue, isArray, isValidDate } from '../internal/lang.js'; 2 | import { Type } from '../Type.js'; 3 | import { NEVER } from './never.js'; 4 | 5 | /** 6 | * The array of inputs that are coercible to a Date with {@link coerceToDate}. 7 | */ 8 | export const dateCoercibleInputs = Object.freeze([Type.ARRAY, Type.DATE, Type.STRING, Type.NUMBER]); 9 | 10 | /** 11 | * Coerces a value to a Date. 12 | * 13 | * @param input The value to coerce. 14 | * @returns A `Date` value, or {@link NEVER} if coercion isn't possible. 15 | */ 16 | export function coerceToDate(input: unknown): Date { 17 | if (isArray(input) && input.length === 1 && isValidDate((input = input[0]))) { 18 | return input; 19 | } 20 | 21 | input = getCanonicalValue(input); 22 | 23 | if ( 24 | (typeof input === 'string' || typeof input === 'number' || input instanceof Date) && 25 | isValidDate((input = new Date(input))) 26 | ) { 27 | return input; 28 | } 29 | return NEVER; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/coerce/map.ts: -------------------------------------------------------------------------------- 1 | import { getCanonicalValue, isArray, isIterableObject, isMapEntry, isObjectLike } from '../internal/lang.js'; 2 | import { NEVER } from './never.js'; 3 | 4 | /** 5 | * Coerces a value to an array of Map entries. 6 | * 7 | * @param input A value to coerce. 8 | * @returns An array of entries, or {@link NEVER} if coercion isn't possible. 9 | */ 10 | export function coerceToMapEntries(input: unknown): [unknown, unknown][] { 11 | if (isArray(input)) { 12 | return input.every(isMapEntry) ? input : NEVER; 13 | } 14 | 15 | input = getCanonicalValue(input); 16 | 17 | if (isIterableObject(input)) { 18 | return (input = Array.from(input)).every(isMapEntry) ? input : NEVER; 19 | } 20 | if (isObjectLike(input)) { 21 | return Object.entries(input); 22 | } 23 | return NEVER; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/coerce/never.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The marker object that is used to denote an impossible value. 3 | * 4 | * @group Other 5 | */ 6 | export const NEVER = Object.freeze({} as never); 7 | -------------------------------------------------------------------------------- /src/main/coerce/number.ts: -------------------------------------------------------------------------------- 1 | import { getCanonicalValue, isArray } from '../internal/lang.js'; 2 | import { Type } from '../Type.js'; 3 | import { NEVER } from './never.js'; 4 | 5 | /** 6 | * The array of inputs that are coercible to a number with {@link coerceToNumber}. 7 | */ 8 | export const numberCoercibleInputs = Object.freeze([ 9 | Type.ARRAY, 10 | Type.OBJECT, 11 | Type.NUMBER, 12 | Type.STRING, 13 | Type.BOOLEAN, 14 | Type.BIGINT, 15 | Type.DATE, 16 | null, 17 | undefined, 18 | ]); 19 | 20 | /** 21 | * Coerces a value to a number (not `NaN`). 22 | * 23 | * @param input The value to coerce. 24 | * @returns A number value, or {@link NEVER} if coercion isn't possible. 25 | */ 26 | export function coerceToNumber(input: unknown): number { 27 | if (isArray(input) && input.length === 1 && typeof (input = input[0]) === 'number') { 28 | return input === input ? input : NEVER; 29 | } 30 | if (input === null || input === undefined) { 31 | return 0; 32 | } 33 | 34 | input = getCanonicalValue(input); 35 | 36 | if ( 37 | (typeof input === 'string' || typeof input === 'boolean' || typeof input === 'number' || input instanceof Date) && 38 | (input = +input) === input 39 | ) { 40 | return input as number; 41 | } 42 | if (typeof input === 'bigint' && input >= Number.MIN_SAFE_INTEGER && input <= Number.MAX_SAFE_INTEGER) { 43 | return Number(input); 44 | } 45 | return NEVER; 46 | } 47 | -------------------------------------------------------------------------------- /src/main/coerce/string.ts: -------------------------------------------------------------------------------- 1 | import { getCanonicalValue, isArray, isValidDate } from '../internal/lang.js'; 2 | import { Type } from '../Type.js'; 3 | import { NEVER } from './never.js'; 4 | 5 | /** 6 | * The array of inputs that are coercible to a string with {@link coerceToString}. 7 | */ 8 | export const stringCoercibleInputs = Object.freeze([ 9 | Type.ARRAY, 10 | Type.OBJECT, 11 | Type.STRING, 12 | Type.NUMBER, 13 | Type.BOOLEAN, 14 | Type.BIGINT, 15 | Type.DATE, 16 | null, 17 | undefined, 18 | ]); 19 | 20 | /** 21 | * Coerces a value to a string. 22 | * 23 | * @param input The value to coerce. 24 | * @returns A string value, or {@link NEVER} if coercion isn't possible. 25 | */ 26 | export function coerceToString(input: unknown): string { 27 | if (isArray(input) && input.length === 1 && typeof (input = input[0]) === 'string') { 28 | return input; 29 | } 30 | if (input === null || input === undefined) { 31 | return ''; 32 | } 33 | 34 | input = getCanonicalValue(input); 35 | 36 | if (typeof input === 'string') { 37 | return input; 38 | } 39 | if ((typeof input === 'number' && isFinite(input)) || typeof input === 'boolean' || typeof input === 'bigint') { 40 | return '' + input; 41 | } 42 | if (isValidDate(input)) { 43 | return input.toISOString(); 44 | } 45 | return NEVER; 46 | } 47 | -------------------------------------------------------------------------------- /src/main/core.ts: -------------------------------------------------------------------------------- 1 | export { any } from './dsl/any.js'; 2 | export { array } from './dsl/array.js'; 3 | export { bigint } from './dsl/bigint.js'; 4 | export { boolean, boolean as bool } from './dsl/boolean.js'; 5 | export { const_ as const } from './dsl/const.js'; 6 | export { date } from './dsl/date.js'; 7 | export { enum_ as enum } from './dsl/enum.js'; 8 | export { function_ as function, function_ as fn } from './dsl/function.js'; 9 | export { instanceOf } from './dsl/instanceOf.js'; 10 | export { intersection, intersection as and } from './dsl/intersection.js'; 11 | export { lazy } from './dsl/lazy.js'; 12 | export { map } from './dsl/map.js'; 13 | export { nan } from './dsl/nan.js'; 14 | export { never } from './dsl/never.js'; 15 | export { not } from './dsl/not.js'; 16 | export { null_ as null } from './dsl/null.js'; 17 | export { number } from './dsl/number.js'; 18 | export { object } from './dsl/object.js'; 19 | export { promise } from './dsl/promise.js'; 20 | export { record } from './dsl/record.js'; 21 | export { set } from './dsl/set.js'; 22 | export { string } from './dsl/string.js'; 23 | export { symbol } from './dsl/symbol.js'; 24 | export { convert } from './dsl/convert.js'; 25 | export { tuple } from './dsl/tuple.js'; 26 | export { undefined_ as undefined } from './dsl/undefined.js'; 27 | export { union, union as or } from './dsl/union.js'; 28 | export { unknown } from './dsl/unknown.js'; 29 | export { void_ as void } from './dsl/void.js'; 30 | 31 | export { ArrayShape } from './shape/ArrayShape.js'; 32 | export { BigIntShape } from './shape/BigIntShape.js'; 33 | export { BooleanShape } from './shape/BooleanShape.js'; 34 | export { ConstShape } from './shape/ConstShape.js'; 35 | export { DateShape } from './shape/DateShape.js'; 36 | export { EnumShape } from './shape/EnumShape.js'; 37 | export { FunctionShape } from './shape/FunctionShape.js'; 38 | export { InstanceShape } from './shape/InstanceShape.js'; 39 | export { IntersectionShape } from './shape/IntersectionShape.js'; 40 | export { LazyShape } from './shape/LazyShape.js'; 41 | export { MapShape } from './shape/MapShape.js'; 42 | export { NeverShape } from './shape/NeverShape.js'; 43 | export { NumberShape } from './shape/NumberShape.js'; 44 | export { ObjectShape } from './shape/ObjectShape.js'; 45 | export { PromiseShape } from './shape/PromiseShape.js'; 46 | export { ReadonlyShape } from './shape/ReadonlyShape.js'; 47 | export { RecordShape } from './shape/RecordShape.js'; 48 | export { SetShape } from './shape/SetShape.js'; 49 | export { CatchShape, DenyShape, ExcludeShape, PipeShape, ReplaceShape, Shape, ConvertShape } from './shape/Shape.js'; 50 | export { StringShape } from './shape/StringShape.js'; 51 | export { SymbolShape } from './shape/SymbolShape.js'; 52 | export { UnionShape } from './shape/UnionShape.js'; 53 | export { ValidationError } from './ValidationError.js'; 54 | export { Type } from './Type.js'; 55 | export { NEVER } from './coerce/never.js'; 56 | 57 | export type { ObjectKeysMode } from './shape/ObjectShape.js'; 58 | export type { 59 | AllowShape, 60 | AnyShape, 61 | Branded, 62 | DeepPartialProtocol, 63 | DeepPartialShape, 64 | Input, 65 | NotShape, 66 | Output, 67 | RefineShape, 68 | } from './shape/Shape.js'; 69 | export type * from './types.js'; 70 | -------------------------------------------------------------------------------- /src/main/dsl/any.ts: -------------------------------------------------------------------------------- 1 | import { AnyShape, Shape } from '../shape/Shape.js'; 2 | import { Message, ParseOptions, RefineOptions } from '../types.js'; 3 | 4 | /** 5 | * Creates the unconstrained shape. 6 | * 7 | * You can specify compile-time type to enhance type inference. 8 | * 9 | * This provides _no runtime type-safety_! 10 | * 11 | * @template Value The input and the output value. 12 | * @group DSL 13 | */ 14 | export function any(): Shape; 15 | 16 | /** 17 | * Creates the shape that is constrained with a 18 | * [narrowing predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html). 19 | * 20 | * @param cb The type predicate that returns `true` if value conforms the required type, or `false` otherwise. 21 | * @param options The issue options or the issue message. 22 | * @returns The shape that has the narrowed output. 23 | * @template Value The input and the output value. 24 | * @group DSL 25 | */ 26 | export function any( 27 | /** 28 | * @param value The input value. 29 | * @param options Parsing options. 30 | */ 31 | cb: (value: any, options: ParseOptions) => value is Value, 32 | options?: RefineOptions | Message 33 | ): Shape; 34 | 35 | /** 36 | * Creates the shape that is constrained with a predicate. 37 | * 38 | * @param cb The predicate that returns truthy result if value is valid, or returns falsy result otherwise. 39 | * @param options The issue options or the issue message. 40 | * @template Value The input and the output value. 41 | * @group DSL 42 | */ 43 | export function any( 44 | /** 45 | * @param value The input value. 46 | * @param options Parsing options. 47 | */ 48 | cb: (value: any, options: ParseOptions) => boolean, 49 | options?: RefineOptions | Message 50 | ): Shape; 51 | 52 | export function any(cb?: (value: any, options: ParseOptions) => boolean, options?: RefineOptions | Message): AnyShape { 53 | const shape = new Shape(); 54 | 55 | return cb === null || cb === undefined ? shape : shape.refine(cb, options); 56 | } 57 | -------------------------------------------------------------------------------- /src/main/dsl/array.ts: -------------------------------------------------------------------------------- 1 | import { ArrayShape } from '../shape/ArrayShape.js'; 2 | import { AnyShape, Shape } from '../shape/Shape.js'; 3 | import { IssueOptions, Message } from '../types.js'; 4 | 5 | /** 6 | * Creates the unconstrained array shape. 7 | * 8 | * @param options The issue options or the issue message. 9 | * @group DSL 10 | */ 11 | export function array(options?: IssueOptions | Message): ArrayShape<[], Shape>; 12 | 13 | /** 14 | * Creates the array shape with elements that conform the element shape. 15 | * 16 | * @param shape The shape of an array element. 17 | * @param options The issue options or the issue message. 18 | * @template ValueShape The shape of an array element. 19 | * @group DSL 20 | */ 21 | export function array( 22 | shape: ValueShape, 23 | options?: IssueOptions | Message 24 | ): ArrayShape<[], ValueShape>; 25 | 26 | export function array( 27 | shape?: AnyShape | IssueOptions | Message, 28 | options?: IssueOptions | Message 29 | ): ArrayShape<[], AnyShape> { 30 | if (shape instanceof Shape) { 31 | return new ArrayShape([], shape, options); 32 | } else { 33 | return new ArrayShape([], new Shape(), shape); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/dsl/bigint.ts: -------------------------------------------------------------------------------- 1 | import { BigIntShape } from '../shape/BigIntShape.js'; 2 | import { IssueOptions, Message } from '../types.js'; 3 | 4 | /** 5 | * Creates the bigint shape. 6 | * 7 | * @param options The issue options or the issue message. 8 | * @group DSL 9 | */ 10 | export function bigint(options?: IssueOptions | Message): BigIntShape { 11 | return new BigIntShape(options); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/dsl/boolean.ts: -------------------------------------------------------------------------------- 1 | import { BooleanShape } from '../shape/BooleanShape.js'; 2 | import { IssueOptions, Message } from '../types.js'; 3 | 4 | /** 5 | * Creates the boolean shape. 6 | * 7 | * @param options The issue options or the issue message. 8 | * @group DSL 9 | */ 10 | export function boolean(options?: IssueOptions | Message): BooleanShape { 11 | return new BooleanShape(options); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/dsl/const.ts: -------------------------------------------------------------------------------- 1 | import { ConstShape } from '../shape/ConstShape.js'; 2 | import { Any, IssueOptions, Message } from '../types.js'; 3 | 4 | /** 5 | * Creates the constant value shape. 6 | * 7 | * @param value The value to which the input must be strictly equal. 8 | * @param options The issue options or the issue message. 9 | * @template Value The expected value. 10 | * @group DSL 11 | */ 12 | export function const_(value: Value, options?: IssueOptions | Message): ConstShape { 13 | return new ConstShape(value, options); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/dsl/convert.ts: -------------------------------------------------------------------------------- 1 | import { ConvertShape, Shape } from '../shape/Shape.js'; 2 | import { ParseOptions } from '../types.js'; 3 | 4 | /** 5 | * Creates the shape that synchronously converts the input value. 6 | * 7 | * @param cb The callback that converts the input value. Throw a {@link ValidationError} to notify that the conversion 8 | * cannot be successfully completed. 9 | * @template Value The input value. 10 | * @template ConvertedValue The output value. 11 | * @group DSL 12 | */ 13 | export function convert( 14 | /** 15 | * @param value The input value. 16 | * @param options Parsing options. 17 | */ 18 | cb: (value: any, options: ParseOptions) => ConvertedValue 19 | ): Shape { 20 | return new ConvertShape(cb); 21 | } 22 | 23 | /** 24 | * Creates the shape that asynchronously converts the input value. 25 | * 26 | * @param cb The callback that converts the input value asynchronously. The returned promise can be rejected with a 27 | * {@link ValidationError} to notify that the conversion cannot be successfully completed. 28 | * @template Value The input value. 29 | * @template ConvertedValue The output value. 30 | * @group DSL 31 | */ 32 | export function convertAsync( 33 | /** 34 | * @param value The input value. 35 | * @param options Parsing options. 36 | */ 37 | cb: (value: any, options: ParseOptions) => PromiseLike 38 | ): Shape { 39 | return new ConvertShape(cb, true); 40 | } 41 | -------------------------------------------------------------------------------- /src/main/dsl/date.ts: -------------------------------------------------------------------------------- 1 | import { DateShape } from '../shape/DateShape.js'; 2 | import { IssueOptions, Message } from '../types.js'; 3 | 4 | /** 5 | * Creates the {@link !Date} shape. 6 | * 7 | * @param options The issue options or the issue message. 8 | * @group DSL 9 | */ 10 | export function date(options?: IssueOptions | Message): DateShape { 11 | return new DateShape(options); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/dsl/enum.ts: -------------------------------------------------------------------------------- 1 | import { ReadonlyDict } from '../internal/objects.js'; 2 | import { EnumShape } from '../shape/EnumShape.js'; 3 | import { Any, IssueOptions, Message } from '../types.js'; 4 | 5 | /** 6 | * Creates the shape that constrains input with the array of values. 7 | * 8 | * @param values The array of values allowed for the input. 9 | * @param options The issue options or the issue message. 10 | * @template Value The union of allowed enum values. 11 | * @template ValuesArray The array of allowed values. 12 | * @group DSL 13 | */ 14 | export function enum_( 15 | values: ValuesArray, 16 | options?: IssueOptions | Message 17 | ): EnumShape; 18 | 19 | /** 20 | * Creates the shape that constrains input with values of 21 | * [the enum-like object](https://www.typescriptlang.org/docs/handbook/enums.html). 22 | * 23 | * @param values The native enum or a mapping object. 24 | * @param options The issue options or the issue message. 25 | * @template Value The union of allowed enum values. 26 | * @template ValuesDict The object that maps from the key to an enum value. 27 | * @group DSL 28 | */ 29 | export function enum_>( 30 | values: ValuesDict, 31 | options?: IssueOptions | Message 32 | ): EnumShape; 33 | 34 | export function enum_(source: any[] | ReadonlyDict, options?: IssueOptions | Message): EnumShape { 35 | return new EnumShape(source, options); 36 | } 37 | -------------------------------------------------------------------------------- /src/main/dsl/function.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from '../internal/lang.js'; 2 | import { ArrayShape } from '../shape/ArrayShape.js'; 3 | import { FunctionShape } from '../shape/FunctionShape.js'; 4 | import { AnyShape, Shape } from '../shape/Shape.js'; 5 | import { IssueOptions, Message } from '../types.js'; 6 | 7 | /** 8 | * Creates a shape of a function that has no arguments. 9 | * 10 | * @param options The issue options or the issue message. 11 | * @group DSL 12 | */ 13 | export function function_(options?: IssueOptions | Message): FunctionShape, null, null>; 14 | 15 | /** 16 | * Creates a shape of a function with arguments parsed by corresponding shapes in the `argShapes` array. 17 | * 18 | * @param argShapes The array of argument shapes. 19 | * @param options The issue options or the issue message. 20 | * @template ArgShapes The array of argument shapes. 21 | * @group DSL 22 | */ 23 | export function function_( 24 | argShapes: ArgShapes, 25 | options?: IssueOptions | Message 26 | ): FunctionShape, null, null>; 27 | 28 | /** 29 | * Creates a shape of a function with arguments parsed by an array shape. 30 | * 31 | * @param argsShape The shape of the array of arguments. 32 | * @param options The issue options or the issue message. 33 | * @template InputArgs The array of input arguments. 34 | * @template OutputArgs The array of input arguments. 35 | * @template ArgsShape The shape of the array of arguments. 36 | * @group DSL 37 | */ 38 | export function function_< 39 | InputArgs extends readonly any[], 40 | OutputArgs extends readonly any[], 41 | ArgsShape extends Shape, 42 | >(argsShape: ArgsShape, options?: IssueOptions | Message): FunctionShape; 43 | 44 | export function function_(argShapes?: Shape | AnyShape[] | IssueOptions | Message, options?: IssueOptions | Message) { 45 | if (isArray(argShapes)) { 46 | argShapes = new ArrayShape(argShapes, null); 47 | } 48 | if (!(argShapes instanceof Shape)) { 49 | options = argShapes; 50 | argShapes = new ArrayShape([], null); 51 | } 52 | return new FunctionShape(argShapes as Shape, null, null, options); 53 | } 54 | -------------------------------------------------------------------------------- /src/main/dsl/instanceOf.ts: -------------------------------------------------------------------------------- 1 | import { InstanceShape } from '../shape/InstanceShape.js'; 2 | import { IssueOptions, Message } from '../types.js'; 3 | 4 | /** 5 | * Creates the class instance shape. 6 | * 7 | * @param ctor The instance constructor. 8 | * @param options The issue options or the issue message. 9 | * @template Ctor The instance constructor. 10 | * @group DSL 11 | */ 12 | export function instanceOf any>( 13 | ctor: Ctor, 14 | options?: IssueOptions | Message 15 | ): InstanceShape { 16 | return new InstanceShape(ctor, options); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/dsl/intersection.ts: -------------------------------------------------------------------------------- 1 | import { IntersectionShape } from '../shape/IntersectionShape.js'; 2 | import { AnyShape } from '../shape/Shape.js'; 3 | import { IssueOptions, Message } from '../types.js'; 4 | 5 | /** 6 | * Creates an intersection shape that tries to parse the input with all provided shapes and merge parsing results. 7 | * 8 | * @param shapes The array of shapes. 9 | * @param options The issue options or the issue message. 10 | * @template Shapes The tuple of intersected shapes. 11 | * @group DSL 12 | */ 13 | export function intersection( 14 | shapes: Shapes, 15 | options?: IssueOptions | Message 16 | ): IntersectionShape { 17 | return new IntersectionShape(shapes, options); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/dsl/lazy.ts: -------------------------------------------------------------------------------- 1 | import { identity } from '../internal/lang.js'; 2 | import { LazyShape } from '../shape/LazyShape.js'; 3 | import { AnyShape, Input } from '../shape/Shape.js'; 4 | 5 | /** 6 | * Creates the shape that resolves the underlying shape on-demand. 7 | * 8 | * @param shapeProvider The provider that returns the resolved shape. 9 | * @template ProvidedShape The provided shape. 10 | * @group DSL 11 | */ 12 | export function lazy( 13 | shapeProvider: () => ProvidedShape 14 | ): LazyShape> { 15 | return new LazyShape(shapeProvider, identity); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/dsl/map.ts: -------------------------------------------------------------------------------- 1 | import { MapShape } from '../shape/MapShape.js'; 2 | import { AnyShape } from '../shape/Shape.js'; 3 | import { IssueOptions, Message } from '../types.js'; 4 | 5 | /** 6 | * Creates the {@link !Map} instance shape. 7 | * 8 | * @param keyShape The key shape. 9 | * @param valueShape The value shape. 10 | * @param options The issue options or the issue message. 11 | * @template KeyShape The key shape. 12 | * @template ValueShape The value shape. 13 | * @group DSL 14 | */ 15 | export function map( 16 | keyShape: KeyShape, 17 | valueShape: ValueShape, 18 | options?: IssueOptions | Message 19 | ): MapShape { 20 | return new MapShape(keyShape, valueShape, options); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/dsl/nan.ts: -------------------------------------------------------------------------------- 1 | import { ConstShape } from '../shape/ConstShape.js'; 2 | import { IssueOptions, Message } from '../types.js'; 3 | 4 | /** 5 | * Creates the shape that requires an input to be equal to `NaN`. 6 | * 7 | * @param options The issue options or the issue message. 8 | * @group DSL 9 | */ 10 | export function nan(options?: IssueOptions | Message): ConstShape { 11 | return new ConstShape(NaN, options); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/dsl/never.ts: -------------------------------------------------------------------------------- 1 | import { NeverShape } from '../shape/NeverShape.js'; 2 | import { IssueOptions, Message } from '../types.js'; 3 | 4 | /** 5 | * Creates the shape that always raises an issue. 6 | * 7 | * @param options The issue options or the issue message. 8 | * @group DSL 9 | */ 10 | export function never(options?: IssueOptions | Message): NeverShape { 11 | return new NeverShape(options); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/dsl/not.ts: -------------------------------------------------------------------------------- 1 | import { AnyShape, NotShape, Shape } from '../shape/Shape.js'; 2 | import { IssueOptions, Message } from '../types.js'; 3 | 4 | /** 5 | * Creates the shape that only allows values that don't conform the shape. 6 | * 7 | * @param shape The shape to which the output must not conform. 8 | * @param options The issue options or the issue message. 9 | * @template ExcludedShape The shape to which the output must not conform. 10 | * @group DSL 11 | */ 12 | export function not( 13 | shape: ExcludedShape, 14 | options?: IssueOptions | Message 15 | ): NotShape { 16 | return new Shape().not(shape, options); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/dsl/null.ts: -------------------------------------------------------------------------------- 1 | import { ConstShape } from '../shape/ConstShape.js'; 2 | import { IssueOptions, Message } from '../types.js'; 3 | 4 | /** 5 | * Creates the shape that requires an input to be equal to `null`. 6 | * 7 | * @param options The issue options or the issue message. 8 | * @group DSL 9 | */ 10 | export function null_(options?: IssueOptions | Message): ConstShape { 11 | return new ConstShape(null, options); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/dsl/number.ts: -------------------------------------------------------------------------------- 1 | import { NumberShape } from '../shape/NumberShape.js'; 2 | import { IssueOptions, Message } from '../types.js'; 3 | 4 | /** 5 | * Creates the number shape. 6 | * 7 | * @param options The issue options or the issue message. 8 | * @group DSL 9 | */ 10 | export function number(options?: IssueOptions | Message): NumberShape { 11 | return new NumberShape(options); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/dsl/object.ts: -------------------------------------------------------------------------------- 1 | import { ReadonlyDict } from '../internal/objects.js'; 2 | import { ObjectShape } from '../shape/ObjectShape.js'; 3 | import { AnyShape } from '../shape/Shape.js'; 4 | import { IssueOptions, Message } from '../types.js'; 5 | 6 | /** 7 | * Creates the object shape. 8 | * 9 | * @param shapes The mapping from an object key to a corresponding shape. 10 | * @param options The issue options or the issue message. 11 | * @template PropShapes The mapping from a string object key to a corresponding value shape. 12 | * @group DSL 13 | */ 14 | export function object>( 15 | shapes: PropShapes, 16 | options?: IssueOptions | Message 17 | ): ObjectShape { 18 | return new ObjectShape(shapes, null, options); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/dsl/promise.ts: -------------------------------------------------------------------------------- 1 | import { PromiseShape } from '../shape/PromiseShape.js'; 2 | import { AnyShape, Shape } from '../shape/Shape.js'; 3 | import { IssueOptions, Message } from '../types.js'; 4 | 5 | /** 6 | * Creates the {@link !Promise} instance shape. 7 | * 8 | * @param options The issue options or the issue message. 9 | * @template ValueShape The shape of the resolved value. 10 | * @group DSL 11 | */ 12 | export function promise(options?: IssueOptions | Message): PromiseShape; 13 | 14 | /** 15 | * Creates the {@link !Promise} instance shape that validates the fulfillment value. 16 | * 17 | * @param shape The shape of the resolved value. 18 | * @param options The issue options or the issue message. 19 | * @template ValueShape The shape of the resolved value. 20 | * @group DSL 21 | */ 22 | export function promise( 23 | shape: ValueShape, 24 | options?: IssueOptions | Message 25 | ): PromiseShape; 26 | 27 | export function promise( 28 | shape?: AnyShape | IssueOptions | Message, 29 | options?: IssueOptions | Message 30 | ): PromiseShape { 31 | if (shape instanceof Shape) { 32 | return new PromiseShape(shape, options); 33 | } else { 34 | return new PromiseShape(null, shape); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/dsl/record.ts: -------------------------------------------------------------------------------- 1 | import { anyKeyShape, RecordShape } from '../shape/RecordShape.js'; 2 | import { AnyShape, Shape } from '../shape/Shape.js'; 3 | import { IssueOptions, Message } from '../types.js'; 4 | 5 | /** 6 | * Creates the shape that describes an object with string keys and values that conform the given shape. 7 | * 8 | * @param valuesShape The shape of record values. 9 | * @param options The issue options or the issue message. 10 | * @template ValuesShape The shape of record values. 11 | * @group DSL 12 | */ 13 | export function record( 14 | valuesShape: ValuesShape, 15 | options?: IssueOptions | Message 16 | ): RecordShape; 17 | 18 | /** 19 | * Creates the shape that describes an object with string keys and values that conform the given shape. 20 | * 21 | * @param keysShape The shape of record keys. 22 | * @param valuesShape The shape of record values. 23 | * @param options The issue options or the issue message. 24 | * @template KeysShape The shape of record keys. 25 | * @template ValuesShape The shape of record values. 26 | * @group DSL 27 | */ 28 | export function record, ValuesShape extends AnyShape>( 29 | keysShape: KeysShape, 30 | valuesShape: ValuesShape, 31 | options?: IssueOptions | Message 32 | ): RecordShape; 33 | 34 | export function record( 35 | keysShape: AnyShape, 36 | valuesShape?: AnyShape | IssueOptions | Message, 37 | options?: IssueOptions | Message 38 | ) { 39 | if (valuesShape instanceof Shape) { 40 | return new RecordShape(keysShape, valuesShape, options); 41 | } else { 42 | return new RecordShape(anyKeyShape, keysShape, options); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/dsl/set.ts: -------------------------------------------------------------------------------- 1 | import { SetShape } from '../shape/SetShape.js'; 2 | import { AnyShape } from '../shape/Shape.js'; 3 | import { IssueOptions, Message } from '../types.js'; 4 | 5 | /** 6 | * Creates the {@link !Set} instance shape. 7 | * 8 | * @param shape The value shape 9 | * @param options The issue options or the issue message. 10 | * @template ValueShape The value shape. 11 | * @group DSL 12 | */ 13 | export function set( 14 | shape: ValueShape, 15 | options?: IssueOptions | Message 16 | ): SetShape { 17 | return new SetShape(shape, options); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/dsl/string.ts: -------------------------------------------------------------------------------- 1 | import { StringShape } from '../shape/StringShape.js'; 2 | import { IssueOptions, Message } from '../types.js'; 3 | 4 | /** 5 | * Creates the string shape. 6 | * 7 | * @param options The issue options or the issue message. 8 | * @group DSL 9 | */ 10 | export function string(options?: IssueOptions | Message): StringShape { 11 | return new StringShape(options); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/dsl/symbol.ts: -------------------------------------------------------------------------------- 1 | import { SymbolShape } from '../shape/SymbolShape.js'; 2 | import { IssueOptions, Message } from '../types.js'; 3 | 4 | /** 5 | * Creates the symbol shape. 6 | * 7 | * @param options The issue options or the issue message. 8 | * @group DSL 9 | */ 10 | export function symbol(options?: IssueOptions | Message): SymbolShape { 11 | return new SymbolShape(options); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/dsl/tuple.ts: -------------------------------------------------------------------------------- 1 | import { ArrayShape } from '../shape/ArrayShape.js'; 2 | import { AnyShape, Shape } from '../shape/Shape.js'; 3 | import { IssueOptions, Message } from '../types.js'; 4 | 5 | /** 6 | * Creates the tuple shape. 7 | * 8 | * @param shapes The array of tuple element shapes. 9 | * @param options The issue options or the issue message. 10 | * @template HeadShapes The head tuple elements. 11 | * @group DSL 12 | */ 13 | export function tuple( 14 | shapes: HeadShapes, 15 | options?: IssueOptions | Message 16 | ): ArrayShape; 17 | 18 | /** 19 | * Creates the tuple shape with rest elements. 20 | * 21 | * @param headShapes The array of tuple element shapes. 22 | * @param restShape The shape of rest elements. 23 | * @param options The issue options or the issue message. 24 | * @template HeadShapes The head tuple elements. 25 | * @template RestShape The rest tuple elements. 26 | * @group DSL 27 | */ 28 | export function tuple( 29 | headShapes: HeadShapes, 30 | restShape: RestShape, 31 | options?: IssueOptions | Message 32 | ): ArrayShape; 33 | 34 | export function tuple( 35 | shapes: [AnyShape, ...AnyShape[]], 36 | restShape?: AnyShape | IssueOptions | Message | null, 37 | options?: IssueOptions | Message 38 | ): ArrayShape<[AnyShape, ...AnyShape[]], AnyShape | null> { 39 | if (restShape === null || restShape === undefined || restShape instanceof Shape) { 40 | return new ArrayShape(shapes, restShape || null, options); 41 | } else { 42 | return new ArrayShape(shapes, null, restShape); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/dsl/undefined.ts: -------------------------------------------------------------------------------- 1 | import { ConstShape } from '../shape/ConstShape.js'; 2 | import { IssueOptions, Message } from '../types.js'; 3 | 4 | /** 5 | * Creates the shape that requires an input to be equal to `undefined`. 6 | * 7 | * @param options The issue options or the issue message. 8 | * @group DSL 9 | */ 10 | export function undefined_(options?: IssueOptions | Message): ConstShape { 11 | return new ConstShape(undefined, options); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/dsl/union.ts: -------------------------------------------------------------------------------- 1 | import { AnyShape } from '../shape/Shape.js'; 2 | import { UnionShape } from '../shape/UnionShape.js'; 3 | import { IssueOptions, Message } from '../types.js'; 4 | 5 | /** 6 | * Creates a union shape that tries to parse the input with one of the provided shapes. 7 | * 8 | * @param shapes The array of shapes to try. 9 | * @param options The issue options or the issue message. 10 | * @template Shapes The tuple of united shapes. 11 | * @group DSL 12 | */ 13 | export function union( 14 | shapes: Shapes, 15 | options?: IssueOptions | Message 16 | ): UnionShape { 17 | return new UnionShape(shapes, options); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/dsl/unknown.ts: -------------------------------------------------------------------------------- 1 | import { Shape } from '../shape/Shape.js'; 2 | 3 | /** 4 | * Creates the unconstrained shape with unknown value. 5 | * 6 | * @group DSL 7 | */ 8 | export function unknown(): Shape { 9 | return new Shape(); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/dsl/void.ts: -------------------------------------------------------------------------------- 1 | import { ConstShape } from '../shape/ConstShape.js'; 2 | import { IssueOptions, Message } from '../types.js'; 3 | 4 | /** 5 | * Creates a shape that requires an input to be `undefined` at runtime and typed as `void`. 6 | * 7 | * @param options The issue options or the issue message. 8 | * @group DSL 9 | */ 10 | export function void_(options?: IssueOptions | Message): ConstShape { 11 | return new ConstShape(undefined, options); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The module with the core Doubter functionality. 3 | * 4 | * ```ts 5 | * import * as d from 'doubter/core'; 6 | * ``` 7 | * 8 | * @module core 9 | */ 10 | 11 | import './plugin/array-essentials.js'; 12 | import './plugin/bigint-essentials.js'; 13 | import './plugin/date-essentials.js'; 14 | import './plugin/number-essentials.js'; 15 | import './plugin/object-essentials.js'; 16 | import './plugin/object-eval.js'; 17 | import './plugin/set-essentials.js'; 18 | import './plugin/string-essentials.js'; 19 | 20 | export type * from './plugin/array-essentials.js'; 21 | export type * from './plugin/bigint-essentials.js'; 22 | export type * from './plugin/date-essentials.js'; 23 | export type * from './plugin/number-essentials.js'; 24 | export type * from './plugin/object-essentials.js'; 25 | export type * from './plugin/set-essentials.js'; 26 | export type * from './plugin/string-essentials.js'; 27 | 28 | export * from './core.js'; 29 | -------------------------------------------------------------------------------- /src/main/internal/arrays.ts: -------------------------------------------------------------------------------- 1 | export function unique(values: T[]): T[]; 2 | 3 | export function unique(values: readonly T[]): readonly T[]; 4 | 5 | export function unique(values: readonly T[]): T[] { 6 | let arr = values as T[]; 7 | 8 | for (let i = 0; i < values.length; ++i) { 9 | if (arr !== values) { 10 | if (!arr.includes(values[i])) { 11 | arr.push(values[i]); 12 | } 13 | } else if (values.includes(values[i], i + 1)) { 14 | arr = values.slice(0, i + 1); 15 | } 16 | } 17 | return arr; 18 | } 19 | 20 | /** 21 | * Converts `k` to a number if it represents a valid array index, or returns -1 if `k` isn't an index. 22 | */ 23 | export function toArrayIndex(k: any): number { 24 | return (typeof k === 'number' || (typeof k === 'string' && k === '' + (k = +k))) && k >>> 0 === k ? k : -1; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/internal/bitmasks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A bitmask that can hold up to 2^31 - 1 number of bits. 3 | */ 4 | export type Bitmask = number[] | number; 5 | 6 | /** 7 | * Toggles the bit in the bitmask at given position. 8 | * 9 | * @param bitmask The mutable mask to update. 10 | * @param position The index at which the bit must be toggled. 11 | * @returns The updated mask. 12 | */ 13 | export function toggleBit(bitmask: Bitmask, position: number): Bitmask { 14 | if (typeof bitmask === 'number') { 15 | if (position < 32) { 16 | return bitmask ^ (1 << position); 17 | } 18 | bitmask = [bitmask, 0, 0]; 19 | } 20 | 21 | const bucketIndex = position >>> 5; 22 | 23 | bitmask[bucketIndex] ^= 1 << (position - (bucketIndex << 5)); 24 | 25 | return bitmask; 26 | } 27 | 28 | /** 29 | * Returns the bit value at given position. 30 | */ 31 | export function getBit(bitmask: Bitmask, position: number): number { 32 | if (typeof bitmask === 'number') { 33 | return (bitmask >>> position) & 0b1; 34 | } 35 | 36 | const bucketIndex = position >>> 5; 37 | 38 | return (bitmask[bucketIndex] >>> (position - (bucketIndex << 5))) & 0b1; 39 | } 40 | -------------------------------------------------------------------------------- /src/main/internal/lang.ts: -------------------------------------------------------------------------------- 1 | export const isArray = Array.isArray; 2 | 3 | export function identity(value: T): T { 4 | return value; 5 | } 6 | 7 | /** 8 | * [SameValueZero](https://tc39.es/ecma262/multipage/abstract-operations.html#sec-samevaluezero) comparison. 9 | */ 10 | export function isEqual(a: unknown, b: unknown): boolean { 11 | return a === b || (a !== a && b !== b); 12 | } 13 | 14 | export function isObjectLike>(value: unknown): value is T { 15 | return value !== null && typeof value === 'object'; 16 | } 17 | 18 | export function isObject(value: unknown): boolean { 19 | return isObjectLike(value) && !isArray(value); 20 | } 21 | 22 | export function isPlainObject(value: unknown): value is object { 23 | return isObjectLike(value) && ((value = Object.getPrototypeOf(value)) === null || value === Object.prototype); 24 | } 25 | 26 | export function isIterableObject(value: any): value is Iterable { 27 | return isObjectLike(value) && ((typeof Symbol !== 'undefined' && Symbol.iterator in value) || !isNaN(value.length)); 28 | } 29 | 30 | export function isEqualOrSubclass(ctor: Function, superCtor: Function): boolean { 31 | return ctor === superCtor || superCtor.prototype.isPrototypeOf(ctor.prototype); 32 | } 33 | 34 | export function isValidDate(value: unknown): value is Date { 35 | return value instanceof Date && (value = value.getTime()) === value; 36 | } 37 | 38 | export function isMapEntry(value: unknown): value is [unknown, unknown] { 39 | return isArray(value) && value.length === 2; 40 | } 41 | 42 | /** 43 | * Returns primitive if 44 | * [an object is a wrapper](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values), 45 | * is provided or returns the value as is. 46 | * 47 | * @param value The value to unwrap. 48 | */ 49 | export function getCanonicalValue(value: unknown): unknown { 50 | if ( 51 | isObjectLike(value) && 52 | ((typeof BigInt !== 'undefined' && value instanceof BigInt) || 53 | value instanceof String || 54 | value instanceof Number || 55 | value instanceof Boolean || 56 | value instanceof Symbol) 57 | ) { 58 | return value.valueOf(); 59 | } 60 | return value; 61 | } 62 | -------------------------------------------------------------------------------- /src/main/internal/objects.ts: -------------------------------------------------------------------------------- 1 | export interface ReadonlyDict { 2 | readonly [key: string]: T; 3 | } 4 | 5 | export interface Dict { 6 | [key: string]: T; 7 | } 8 | 9 | export function defineReadonlyProperty(obj: object, key: PropertyKey, value: T): T { 10 | Object.defineProperty(obj, key, { value, configurable: true }); 11 | return value; 12 | } 13 | 14 | /** 15 | * Updates object property value, prevents prototype pollution. 16 | */ 17 | export function setProperty(obj: any, key: PropertyKey, value: T): T { 18 | if (key === '__proto__') { 19 | Object.defineProperty(obj, key, { value, configurable: true, writable: true, enumerable: true }); 20 | } else { 21 | obj[key] = value; 22 | } 23 | return value; 24 | } 25 | 26 | /** 27 | * Shallow-clones a dictionary-like object. 28 | */ 29 | export function cloneObject(dict: ReadonlyDict): Dict { 30 | const obj = Object.create(Object.getPrototypeOf(dict)); 31 | 32 | for (const key in dict) { 33 | setProperty(obj, key, dict[key]); 34 | } 35 | return obj; 36 | } 37 | 38 | /** 39 | * Shallow-clones a dictionary-like object picking only a given set of keys. 40 | */ 41 | export function pickKeys(dict: ReadonlyDict, keys: readonly string[]): Dict { 42 | const obj = Object.create(Object.getPrototypeOf(dict)); 43 | 44 | for (const key of keys) { 45 | if (key in dict) { 46 | setProperty(obj, key, dict[key]); 47 | } 48 | } 49 | return obj; 50 | } 51 | 52 | /** 53 | * Shallow-clones a dictionary-like object picking only the first `count` number of keys. 54 | */ 55 | export function cloneRecord(dict: ReadonlyDict, count: number): Dict { 56 | const obj = Object.create(Object.getPrototypeOf(dict)); 57 | 58 | let index = 0; 59 | 60 | for (const key in dict) { 61 | if (index >= count) { 62 | break; 63 | } 64 | setProperty(obj, key, dict[key]); 65 | ++index; 66 | } 67 | return obj; 68 | } 69 | -------------------------------------------------------------------------------- /src/main/internal/types.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '../Type.js'; 2 | import { isEqual } from './lang.js'; 3 | 4 | export function isType(value: unknown): value is Type { 5 | return value instanceof Type; 6 | } 7 | 8 | /** 9 | * Returns `true` if type or literal `a` is assignable to the type or literal `b`. 10 | */ 11 | export function isAssignable(a: unknown, b: unknown): boolean { 12 | return b === Type.UNKNOWN || isEqual(a, b) || Type.of(a) === b; 13 | } 14 | 15 | /** 16 | * Returns an array of unique types and literals that comprise a union. 17 | */ 18 | export function unionTypes(types: readonly unknown[]): readonly unknown[] { 19 | let t = types as unknown[]; 20 | 21 | next: for (let i = 0; i < t.length; ++i) { 22 | const ti = t[i]; 23 | 24 | for (let j = i + 1; j < t.length; ++j) { 25 | const tj = t[j]; 26 | 27 | if (isAssignable(ti, tj)) { 28 | if (t === types) { 29 | t = types.slice(0); 30 | } 31 | t.splice(i--, 1); 32 | continue next; 33 | } 34 | 35 | if (isAssignable(tj, ti)) { 36 | if (t === types) { 37 | t = types.slice(0); 38 | } 39 | t.splice(j--, 1); 40 | } 41 | } 42 | } 43 | 44 | return t; 45 | } 46 | 47 | /** 48 | * Takes an array of arrays of types that are treated as an intersection of unions, and applies the distribution rule 49 | * that produces a union. 50 | * 51 | * ``` 52 | * (a | b) & (a | b | c) → a | b 53 | * ``` 54 | */ 55 | export function distributeTypes(types: ReadonlyArray>): unknown[] { 56 | if (types.length === 0) { 57 | return []; 58 | } 59 | 60 | const t = []; 61 | const t0 = types[0]; 62 | 63 | for (let i = 0; i < t0.length; ++i) { 64 | const t0i = t0[i]; 65 | 66 | for (let k = 1; k < types.length; ++k) { 67 | const tk = types[k]; 68 | 69 | for (let j = 0; j < tk.length; ++j) { 70 | const tkj = tk[j]; 71 | 72 | if (isAssignable(tkj, t0i)) { 73 | t.push(tkj); 74 | } else if (isAssignable(t0i, tkj)) { 75 | t.push(t0i); 76 | } 77 | } 78 | } 79 | } 80 | 81 | return t; 82 | } 83 | -------------------------------------------------------------------------------- /src/main/plugin/bigint-essentials.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The plugin that enhances {@link core!BigIntShape BigIntShape} with additional methods. 3 | * 4 | * ```ts 5 | * import * as d from 'doubter/core'; 6 | * import 'doubter/plugin/bigint-essentials'; 7 | * 8 | * d.bigint().nonNegative(); 9 | * ``` 10 | * 11 | * @module plugin/bigint-essentials 12 | */ 13 | 14 | import { CODE_BIGINT_MAX, CODE_BIGINT_MIN, MESSAGE_BIGINT_MAX, MESSAGE_BIGINT_MIN } from '../constants.js'; 15 | import { BigIntShape } from '../shape/BigIntShape.js'; 16 | import { IssueOptions, Message } from '../types.js'; 17 | import { createIssue } from '../utils.js'; 18 | 19 | declare module '../core.js' { 20 | export interface BigIntShape { 21 | /** 22 | * Constrains the bigint to be greater than zero. 23 | * 24 | * @param options The issue options or the issue message. 25 | * @returns The clone of the shape. 26 | * @group Plugin Methods 27 | * @plugin {@link plugin/bigint-essentials! plugin/bigint-essentials} 28 | */ 29 | positive(options?: IssueOptions | Message): this; 30 | 31 | /** 32 | * Constrains the bigint to be less than zero. 33 | * 34 | * @param options The issue options or the issue message. 35 | * @returns The clone of the shape. 36 | * @group Plugin Methods 37 | * @plugin {@link plugin/bigint-essentials! plugin/bigint-essentials} 38 | */ 39 | negative(options?: IssueOptions | Message): this; 40 | 41 | /** 42 | * Constrains the bigint to be less or equal to zero. 43 | * 44 | * @param options The issue options or the issue message. 45 | * @returns The clone of the shape. 46 | * @group Plugin Methods 47 | * @plugin {@link plugin/bigint-essentials! plugin/bigint-essentials} 48 | */ 49 | nonPositive(options?: IssueOptions | Message): this; 50 | 51 | /** 52 | * Constrains the bigint to be greater or equal to zero. 53 | * 54 | * @param options The issue options or the issue message. 55 | * @returns The clone of the shape. 56 | * @group Plugin Methods 57 | * @plugin {@link plugin/bigint-essentials! plugin/bigint-essentials} 58 | */ 59 | nonNegative(options?: IssueOptions | Message): this; 60 | 61 | /** 62 | * Constrains the bigint to be greater than or equal to the value. 63 | * 64 | * @param value The inclusive minimum value. 65 | * @param options The issue options or the issue message. 66 | * @returns The clone of the shape. 67 | * @group Plugin Methods 68 | * @plugin {@link plugin/bigint-essentials! plugin/bigint-essentials} 69 | */ 70 | min(value: bigint | number | string, options?: IssueOptions | Message): this; 71 | 72 | /** 73 | * Constrains the number to be less than or equal to the value. 74 | * 75 | * @param value The inclusive maximum value. 76 | * @param options The issue options or the issue message. 77 | * @returns The clone of the shape. 78 | * @group Plugin Methods 79 | * @plugin {@link plugin/bigint-essentials! plugin/bigint-essentials} 80 | */ 81 | max(value: bigint | number | string, options?: IssueOptions | Message): this; 82 | } 83 | } 84 | 85 | BigIntShape.prototype.positive = function (issueOptions) { 86 | return this.min(1, issueOptions); 87 | }; 88 | 89 | BigIntShape.prototype.negative = function (issueOptions) { 90 | return this.max(-1, issueOptions); 91 | }; 92 | 93 | BigIntShape.prototype.nonPositive = function (issueOptions) { 94 | return this.max(0, issueOptions); 95 | }; 96 | 97 | BigIntShape.prototype.nonNegative = function (issueOptions) { 98 | return this.min(0, issueOptions); 99 | }; 100 | 101 | BigIntShape.prototype.min = function (value, issueOptions) { 102 | return this.addOperation( 103 | (value, param, options) => { 104 | if (value >= param) { 105 | return null; 106 | } 107 | return [createIssue(CODE_BIGINT_MIN, value, MESSAGE_BIGINT_MIN, param, options, issueOptions)]; 108 | }, 109 | { type: CODE_BIGINT_MIN, param: BigInt(value) } 110 | ); 111 | }; 112 | 113 | BigIntShape.prototype.max = function (value, issueOptions) { 114 | return this.addOperation( 115 | (value, param, options) => { 116 | if (value <= param) { 117 | return null; 118 | } 119 | return [createIssue(CODE_BIGINT_MAX, value, MESSAGE_BIGINT_MAX, param, options, issueOptions)]; 120 | }, 121 | { type: CODE_BIGINT_MAX, param: BigInt(value) } 122 | ); 123 | }; 124 | -------------------------------------------------------------------------------- /src/main/plugin/date-essentials.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The plugin that enhances {@link core!DateShape DateShape} with additional methods. 3 | * 4 | * ```ts 5 | * import * as d from 'doubter/core'; 6 | * import 'doubter/plugin/date-essentials'; 7 | * 8 | * d.date().after(Date.now()); 9 | * ``` 10 | * 11 | * @module plugin/date-essentials 12 | */ 13 | 14 | import { CODE_DATE_MAX, CODE_DATE_MIN, MESSAGE_DATE_MAX, MESSAGE_DATE_MIN } from '../constants.js'; 15 | import { DateShape } from '../shape/DateShape.js'; 16 | import { Shape } from '../shape/Shape.js'; 17 | import { IssueOptions, Message } from '../types.js'; 18 | import { createIssue } from '../utils.js'; 19 | 20 | declare module '../core.js' { 21 | export interface DateShape { 22 | /** 23 | * Constrains the input date to be greater than or equal to another date. 24 | * 25 | * @param value The inclusive minimum date. 26 | * @param options The issue options or the issue message. 27 | * @returns The clone of the shape. 28 | * @group Plugin Methods 29 | * @plugin {@link plugin/date-essentials! plugin/date-essentials} 30 | */ 31 | min(value: Date | number | string, options?: IssueOptions | Message): this; 32 | 33 | /** 34 | * Constrains the input date to be less than or equal to another date. 35 | * 36 | * @param value The inclusive maximum date. 37 | * @param options The issue options or the issue message. 38 | * @returns The clone of the shape. 39 | * @group Plugin Methods 40 | * @plugin {@link plugin/date-essentials! plugin/date-essentials} 41 | */ 42 | max(value: Date | number | string, options?: IssueOptions | Message): this; 43 | 44 | /** 45 | * Constrains the input date to be greater than or equal to another date. 46 | * 47 | * @param value The inclusive minimum date. 48 | * @param options The issue options or the issue message. 49 | * @returns The clone of the shape. 50 | * @alias {@link DateShape.min} 51 | * @group Plugin Methods 52 | * @plugin {@link plugin/date-essentials! plugin/date-essentials} 53 | */ 54 | after(value: Date | number | string, options?: IssueOptions | Message): this; 55 | 56 | /** 57 | * Constrains the input date to be less than or equal to another date. 58 | * 59 | * @param value The inclusive maximum date. 60 | * @param options The issue options or the issue message. 61 | * @returns The clone of the shape. 62 | * @alias {@link DateShape.max} 63 | * @group Plugin Methods 64 | * @plugin {@link plugin/date-essentials! plugin/date-essentials} 65 | */ 66 | before(value: Date | number | string, options?: IssueOptions | Message): this; 67 | 68 | /** 69 | * Converts date to an ISO string. 70 | * 71 | * @group Plugin Methods 72 | * @plugin {@link plugin/date-essentials! plugin/date-essentials} 73 | */ 74 | toISOString(): Shape; 75 | 76 | /** 77 | * Converts date to a timestamp integer number. 78 | * 79 | * @group Plugin Methods 80 | * @plugin {@link plugin/date-essentials! plugin/date-essentials} 81 | */ 82 | toTimestamp(): Shape; 83 | } 84 | } 85 | 86 | DateShape.prototype.min = DateShape.prototype.after = function (value, issueOptions) { 87 | return this.addOperation( 88 | (value, param, options) => { 89 | if (value.getTime() >= param.getTime()) { 90 | return null; 91 | } 92 | return [createIssue(CODE_DATE_MIN, value, MESSAGE_DATE_MIN, param, options, issueOptions)]; 93 | }, 94 | { type: CODE_DATE_MIN, param: new Date(value) } 95 | ); 96 | }; 97 | 98 | DateShape.prototype.max = DateShape.prototype.before = function (value, issueOptions) { 99 | return this.addOperation( 100 | (value, param, options) => { 101 | if (value.getTime() <= param.getTime()) { 102 | return null; 103 | } 104 | return [createIssue(CODE_DATE_MAX, value, MESSAGE_DATE_MAX, param, options, issueOptions)]; 105 | }, 106 | { type: CODE_DATE_MAX, param: new Date(value) } 107 | ); 108 | }; 109 | 110 | DateShape.prototype.toISOString = function () { 111 | return this.convert(toISOString); 112 | }; 113 | 114 | DateShape.prototype.toTimestamp = function () { 115 | return this.convert(toTimestamp); 116 | }; 117 | 118 | function toISOString(date: Date): string { 119 | return date.toISOString(); 120 | } 121 | 122 | function toTimestamp(date: Date): number { 123 | return date.getTime(); 124 | } 125 | -------------------------------------------------------------------------------- /src/main/plugin/object-eval.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The plugin that enables runtime optimization of {@link core!ObjectShape ObjectShape}. Object shapes would dynamically 3 | * compile code fragments to increase runtime performance. 4 | * 5 | * ```ts 6 | * import * as d from 'doubter/core'; 7 | * import 'doubter/plugin/object-eval'; 8 | * 9 | * d.object({ foo: d.string() }).plain(); 10 | * ``` 11 | * 12 | * @module plugin/object-eval 13 | */ 14 | 15 | import { cloneObject, defineReadonlyProperty, setProperty } from '../internal/objects.js'; 16 | import { concatIssues, unshiftIssuesPath } from '../internal/shapes.js'; 17 | import { ObjectShape } from '../shape/ObjectShape.js'; 18 | 19 | try { 20 | // Assert code evaluation support 21 | new Function(''); 22 | 23 | Object.defineProperty(ObjectShape.prototype, '_applyRestUnchecked', { 24 | configurable: true, 25 | 26 | get() { 27 | return defineReadonlyProperty(this, '_applyRestUnchecked', compileApplyRestUnchecked(this)); 28 | }, 29 | }); 30 | } catch { 31 | // Code evaluation isn't supported 32 | } 33 | 34 | function compileApplyRestUnchecked(shape: ObjectShape) { 35 | let source = 'return function(input, options, nonce) { var issues = null, output = input, result;'; 36 | 37 | for (const key in shape.propShapes) { 38 | const keyStr = JSON.stringify(key); 39 | 40 | source += 41 | 'if ((result = this.propShapes[' + 42 | keyStr + 43 | ']._apply(input[' + 44 | keyStr + 45 | '], options, nonce)) !== null)' + 46 | 'if (Array.isArray(result)) {' + 47 | 'unshiftIssuesPath(result, ' + 48 | keyStr + 49 | ');' + 50 | 'if (options.earlyReturn) return result;' + 51 | 'issues = concatIssues(issues, result);' + 52 | '} else if (issues === null || this.operations.length !== 0)' + 53 | 'setProperty(input === output ? output = cloneObject(input) : output, ' + 54 | keyStr + 55 | ', result.value);'; 56 | } 57 | 58 | source += 'return this._applyOperations(input, output, options, issues)}'; 59 | 60 | const factory = new Function('unshiftIssuesPath', 'concatIssues', 'cloneObject', 'setProperty', source); 61 | 62 | return factory(unshiftIssuesPath, concatIssues, cloneObject, setProperty); 63 | } 64 | -------------------------------------------------------------------------------- /src/main/plugin/set-essentials.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The plugin that enhances {@link core!SetShape SetShape} with additional methods. 3 | * 4 | * ```ts 5 | * import * as d from 'doubter/core'; 6 | * import 'doubter/plugin/set-essentials'; 7 | * 8 | * d.set(d.string()).size(5); 9 | * ``` 10 | * 11 | * @module plugin/set-essentials 12 | */ 13 | import { CODE_SET_MAX, CODE_SET_MIN, MESSAGE_SET_MAX, MESSAGE_SET_MIN } from '../constants.js'; 14 | import { SetShape } from '../shape/SetShape.js'; 15 | import { AnyShape } from '../shape/Shape.js'; 16 | import { IssueOptions, Message } from '../types.js'; 17 | import { createIssue } from '../utils.js'; 18 | 19 | declare module '../core.js' { 20 | export interface SetShape { 21 | /** 22 | * Constrains the set size. 23 | * 24 | * @param size The minimum set size. 25 | * @param options The issue options or the issue message. 26 | * @returns The clone of the shape. 27 | * @group Plugin Methods 28 | * @plugin {@link plugin/set-essentials! plugin/set-essentials} 29 | */ 30 | size(size: number, options?: IssueOptions | Message): this; 31 | 32 | /** 33 | * Constrains the minimum set size. 34 | * 35 | * @param size The minimum set size. 36 | * @param options The issue options or the issue message. 37 | * @returns The clone of the shape. 38 | * @group Plugin Methods 39 | * @plugin {@link plugin/set-essentials! plugin/set-essentials} 40 | */ 41 | min(size: number, options?: IssueOptions | Message): this; 42 | 43 | /** 44 | * Constrains the maximum set size. 45 | * 46 | * @param size The maximum set size. 47 | * @param options The issue options or the issue message. 48 | * @returns The clone of the shape. 49 | * @group Plugin Methods 50 | * @plugin {@link plugin/set-essentials! plugin/set-essentials} 51 | */ 52 | max(size: number, options?: IssueOptions | Message): this; 53 | 54 | /** 55 | * Constrains the {@link !Set} to contain at least one element. 56 | * 57 | * @param options The issue options or the issue message. 58 | * @returns The clone of the shape. 59 | * @group Plugin Methods 60 | * @plugin {@link plugin/set-essentials! plugin/set-essentials} 61 | */ 62 | nonEmpty(options?: IssueOptions | Message): this; 63 | } 64 | } 65 | 66 | SetShape.prototype.size = function (size, issueOptions) { 67 | return this.min(size, issueOptions).max(size, issueOptions); 68 | }; 69 | 70 | SetShape.prototype.min = function (size, issueOptions) { 71 | return this.addOperation( 72 | (value, param, options) => { 73 | if (value.size >= param) { 74 | return null; 75 | } 76 | return [createIssue(CODE_SET_MIN, value, MESSAGE_SET_MIN, param, options, issueOptions)]; 77 | }, 78 | { type: CODE_SET_MIN, param: size } 79 | ); 80 | }; 81 | 82 | SetShape.prototype.max = function (size, issueOptions) { 83 | return this.addOperation( 84 | (value, param, options) => { 85 | if (value.size <= param) { 86 | return null; 87 | } 88 | return [createIssue(CODE_SET_MAX, value, MESSAGE_SET_MAX, param, options, issueOptions)]; 89 | }, 90 | { type: CODE_SET_MAX, param: size } 91 | ); 92 | }; 93 | 94 | SetShape.prototype.nonEmpty = function (issueOptions) { 95 | return this.min(1, issueOptions); 96 | }; 97 | -------------------------------------------------------------------------------- /src/main/shape/BigIntShape.ts: -------------------------------------------------------------------------------- 1 | import { bigintCoercibleInputs, coerceToBigInt } from '../coerce/bigint.js'; 2 | import { NEVER } from '../coerce/never.js'; 3 | import { CODE_TYPE_BIGINT, MESSAGE_TYPE_BIGINT } from '../constants.js'; 4 | import { Type } from '../Type.js'; 5 | import { IssueOptions, Message, ParseOptions, Result } from '../types.js'; 6 | import { createIssue } from '../utils.js'; 7 | import { Shape } from './Shape.js'; 8 | 9 | const bigintInputs = Object.freeze([Type.BIGINT]); 10 | 11 | /** 12 | * The shape of a bigint value. 13 | * 14 | * @group Shapes 15 | */ 16 | export class BigIntShape extends Shape { 17 | /** 18 | * `true` if this shape coerces input values to the required type during parsing, or `false` otherwise. 19 | */ 20 | isCoercing = false; 21 | 22 | /** 23 | * The type issue options or the type issue message. 24 | */ 25 | protected _options; 26 | 27 | /** 28 | * Creates a new {@link BigIntShape} instance. 29 | * 30 | * @param options The issue options or the issue message. 31 | */ 32 | constructor(options?: IssueOptions | Message) { 33 | super(); 34 | 35 | this._options = options; 36 | } 37 | 38 | /** 39 | * Enables an input value coercion. 40 | * 41 | * @returns The clone of the shape. 42 | */ 43 | coerce(): this { 44 | const shape = this._clone(); 45 | shape.isCoercing = true; 46 | return shape; 47 | } 48 | 49 | protected _getInputs(): readonly unknown[] { 50 | return this.isCoercing ? bigintCoercibleInputs : bigintInputs; 51 | } 52 | 53 | protected _apply(input: any, options: ParseOptions, _nonce: number): Result { 54 | let output = input; 55 | 56 | if (typeof output !== 'bigint' && (!this.isCoercing || (output = coerceToBigInt(input)) === NEVER)) { 57 | return [createIssue(CODE_TYPE_BIGINT, input, MESSAGE_TYPE_BIGINT, undefined, options, this._options)]; 58 | } 59 | return this._applyOperations(input, output, options, null) as Result; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/shape/BooleanShape.ts: -------------------------------------------------------------------------------- 1 | import { booleanCoercibleInputs, coerceToBoolean } from '../coerce/boolean.js'; 2 | import { NEVER } from '../coerce/never.js'; 3 | import { CODE_TYPE_BOOLEAN, MESSAGE_TYPE_BOOLEAN } from '../constants.js'; 4 | import { Type } from '../Type.js'; 5 | import { IssueOptions, Message, ParseOptions, Result } from '../types.js'; 6 | import { createIssue } from '../utils.js'; 7 | import { Shape } from './Shape.js'; 8 | 9 | const booleanInputs = Object.freeze([Type.BOOLEAN]); 10 | 11 | /** 12 | * The shape of a boolean value. 13 | * 14 | * @group Shapes 15 | */ 16 | export class BooleanShape extends Shape { 17 | /** 18 | * `true` if this shape coerces input values to the required type during parsing, or `false` otherwise. 19 | */ 20 | isCoercing = false; 21 | 22 | /** 23 | * The type issue options or the type issue message. 24 | */ 25 | protected _options; 26 | 27 | /** 28 | * Creates a new {@link BooleanShape} instance. 29 | * 30 | * @param options The issue options or the issue message. 31 | */ 32 | constructor(options?: IssueOptions | Message) { 33 | super(); 34 | 35 | this._options = options; 36 | } 37 | 38 | /** 39 | * Enables an input value coercion. 40 | * 41 | * @returns The clone of the shape. 42 | */ 43 | coerce(): this { 44 | const shape = this._clone(); 45 | shape.isCoercing = true; 46 | return shape; 47 | } 48 | 49 | protected _getInputs(): readonly unknown[] { 50 | return this.isCoercing ? booleanCoercibleInputs : booleanInputs; 51 | } 52 | 53 | protected _apply(input: any, options: ParseOptions, _nonce: number): Result { 54 | let output = input; 55 | 56 | if (typeof output !== 'boolean' && (!this.isCoercing || (output = coerceToBoolean(input)) === NEVER)) { 57 | return [createIssue(CODE_TYPE_BOOLEAN, input, MESSAGE_TYPE_BOOLEAN, undefined, options, this._options)]; 58 | } 59 | return this._applyOperations(input, output, options, null) as Result; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/shape/ConstShape.ts: -------------------------------------------------------------------------------- 1 | import { coerceToConst, getConstCoercibleInputs } from '../coerce/const.js'; 2 | import { NEVER } from '../coerce/never.js'; 3 | import { CODE_TYPE_CONST, MESSAGE_TYPE_CONST } from '../constants.js'; 4 | import { IssueOptions, Message, ParseOptions, Result } from '../types.js'; 5 | import { createIssue } from '../utils.js'; 6 | import { Shape } from './Shape.js'; 7 | 8 | const nullInputs = Object.freeze([null]); 9 | const undefinedInputs = Object.freeze([undefined]); 10 | 11 | /** 12 | * The shape of a constant value. 13 | * 14 | * @template Value The expected constant value. 15 | * @group Shapes 16 | */ 17 | export class ConstShape extends Shape { 18 | /** 19 | * `true` if this shape coerces input values to the required type during parsing, or `false` otherwise. 20 | */ 21 | isCoercing = false; 22 | 23 | /** 24 | * Returns `true` if an input is equal to the const value, or `false` otherwise. 25 | */ 26 | protected _predicate: (input: unknown) => boolean; 27 | 28 | /** 29 | * The type issue options or the type issue message. 30 | */ 31 | protected _options; 32 | 33 | /** 34 | * Creates a new {@link ConstShape} instance. 35 | * 36 | * @param value The expected value. 37 | * @param options The issue options or the issue message. 38 | * @template Value The expected value. 39 | */ 40 | constructor( 41 | /** 42 | * The expected constant value. 43 | */ 44 | readonly value: Value, 45 | options?: IssueOptions | Message 46 | ) { 47 | super(); 48 | 49 | this._options = options; 50 | this._predicate = value !== value ? Number.isNaN : input => value === input; 51 | } 52 | 53 | /** 54 | * Enables an input value coercion. 55 | * 56 | * @returns The clone of the shape. 57 | */ 58 | coerce(): this { 59 | const shape = this._clone(); 60 | shape.isCoercing = true; 61 | return shape; 62 | } 63 | 64 | protected _getInputs(): readonly unknown[] { 65 | const { value } = this; 66 | 67 | if (this.isCoercing) { 68 | return getConstCoercibleInputs(value); 69 | } 70 | if (value === undefined) { 71 | return undefinedInputs; 72 | } 73 | if (value === null) { 74 | return nullInputs; 75 | } 76 | return [value]; 77 | } 78 | 79 | protected _apply(input: unknown, options: ParseOptions, _nonce: number): Result { 80 | let output = input; 81 | 82 | if (!this._predicate(input) && (!this.isCoercing || (output = coerceToConst(this.value, input)) === NEVER)) { 83 | return [createIssue(CODE_TYPE_CONST, input, MESSAGE_TYPE_CONST, this.value, options, this._options)]; 84 | } 85 | return this._applyOperations(input, output, options, null) as Result; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/shape/DateShape.ts: -------------------------------------------------------------------------------- 1 | import { coerceToDate, dateCoercibleInputs } from '../coerce/date.js'; 2 | import { NEVER } from '../coerce/never.js'; 3 | import { CODE_TYPE_DATE, MESSAGE_TYPE_DATE } from '../constants.js'; 4 | import { isValidDate } from '../internal/lang.js'; 5 | import { Type } from '../Type.js'; 6 | import { IssueOptions, Message, ParseOptions, Result } from '../types.js'; 7 | import { createIssue } from '../utils.js'; 8 | import { Shape } from './Shape.js'; 9 | 10 | const dateInputs = Object.freeze([Type.DATE]); 11 | 12 | /** 13 | * The shape of the {@link !Date} object. 14 | * 15 | * @group Shapes 16 | */ 17 | export class DateShape extends Shape { 18 | /** 19 | * `true` if this shape coerces input values to the required type during parsing, or `false` otherwise. 20 | */ 21 | isCoercing = false; 22 | 23 | /** 24 | * The type issue options or the type issue message. 25 | */ 26 | protected _options; 27 | 28 | /** 29 | * Creates a new {@link DateShape} instance. 30 | * 31 | * @param options The issue options or the issue message. 32 | */ 33 | constructor(options?: IssueOptions | Message) { 34 | super(); 35 | 36 | this._options = options; 37 | } 38 | 39 | /** 40 | * Enables an input value coercion. 41 | * 42 | * @returns The clone of the shape. 43 | */ 44 | coerce(): this { 45 | const shape = this._clone(); 46 | shape.isCoercing = true; 47 | return shape; 48 | } 49 | 50 | protected _getInputs(): readonly unknown[] { 51 | return this.isCoercing ? dateCoercibleInputs : dateInputs; 52 | } 53 | 54 | protected _apply(input: any, options: ParseOptions, _nonce: number): Result { 55 | let output = input; 56 | 57 | if (!isValidDate(input) && (!this.isCoercing || (output = coerceToDate(input)) === NEVER)) { 58 | return [createIssue(CODE_TYPE_DATE, input, MESSAGE_TYPE_DATE, undefined, options, this._options)]; 59 | } 60 | return this._applyOperations(input, output, options, null) as Result; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/shape/EnumShape.ts: -------------------------------------------------------------------------------- 1 | import { coerceToConst, getConstCoercibleInputs } from '../coerce/const.js'; 2 | import { NEVER } from '../coerce/never.js'; 3 | import { CODE_TYPE_ENUM, MESSAGE_TYPE_ENUM } from '../constants.js'; 4 | import { unique } from '../internal/arrays.js'; 5 | import { getCanonicalValue, isArray } from '../internal/lang.js'; 6 | import { ReadonlyDict } from '../internal/objects.js'; 7 | import { Type } from '../Type.js'; 8 | import { IssueOptions, Message, ParseOptions, Result } from '../types.js'; 9 | import { createIssue } from '../utils.js'; 10 | import { Shape } from './Shape.js'; 11 | 12 | /** 13 | * The shape of a value enumeration. 14 | * 15 | * @template Value The union of allowed enum values. 16 | * @group Shapes 17 | */ 18 | export class EnumShape extends Shape { 19 | /** 20 | * `true` if this shape coerces input values to the required type during parsing, or `false` otherwise. 21 | */ 22 | isCoercing = false; 23 | 24 | /** 25 | * The array of unique enum values. 26 | */ 27 | readonly values: readonly Value[]; 28 | 29 | /** 30 | * The type issue options or the type issue message. 31 | */ 32 | protected _options; 33 | 34 | /** 35 | * Creates a new {@link EnumShape} instance. 36 | * 37 | * @param source The array of allowed values, a const key-value mapping, or an enum object. 38 | * @param options The issue options or the issue message. 39 | * @template Value The union of allowed enum values. 40 | */ 41 | constructor( 42 | /** 43 | * The array of allowed values, a const key-value mapping, or an TypeScript enum object. 44 | */ 45 | readonly source: readonly Value[] | ReadonlyDict, 46 | options?: IssueOptions | Message 47 | ) { 48 | super(); 49 | 50 | this.values = getEnumValues(source); 51 | 52 | this._options = options; 53 | } 54 | 55 | /** 56 | * Enables an input value coercion. 57 | * 58 | * @returns The clone of the shape. 59 | */ 60 | coerce(): this { 61 | const shape = this._clone(); 62 | shape.isCoercing = true; 63 | return shape; 64 | } 65 | 66 | protected _getInputs(): readonly unknown[] { 67 | const inputs: unknown[] = this.values.slice(0); 68 | 69 | if (!this.isCoercing || inputs.length === 0) { 70 | return inputs; 71 | } 72 | if (!isArray(this.source)) { 73 | inputs.push(...Object.keys(this.source)); 74 | } 75 | for (const value of this.values) { 76 | inputs.push(...getConstCoercibleInputs(value)); 77 | } 78 | return unique(inputs.concat(Type.ARRAY)); 79 | } 80 | 81 | protected _apply(input: any, options: ParseOptions, _nonce: number): Result { 82 | let output = input; 83 | 84 | if ( 85 | !this.values.includes(output) && 86 | (!this.isCoercing || (output = coerceToEnum(this.source, this.values, input)) === NEVER) 87 | ) { 88 | return [createIssue(CODE_TYPE_ENUM, input, MESSAGE_TYPE_ENUM, this.values, options, this._options)]; 89 | } 90 | return this._applyOperations(input, output, options, null) as Result; 91 | } 92 | } 93 | 94 | /** 95 | * Returns unique values of the enum. Source must contain key-value and value-key mapping to be considered a native 96 | * enum. 97 | */ 98 | export function getEnumValues(source: ReadonlyDict): any[] { 99 | if (isArray(source)) { 100 | return unique(source); 101 | } 102 | 103 | const values: number[] = []; 104 | 105 | for (const key in source) { 106 | const a = source[key]; 107 | const b = source[a]; 108 | 109 | const aType = typeof a; 110 | const bType = typeof b; 111 | 112 | if (((aType !== 'string' || bType !== 'number') && (aType !== 'number' || bType !== 'string')) || b != key) { 113 | return unique(Object.values(source)); 114 | } 115 | if (typeof a === 'number' && values.indexOf(a) === -1) { 116 | values.push(a); 117 | } 118 | } 119 | return values; 120 | } 121 | 122 | function coerceToEnum( 123 | source: readonly Value[] | ReadonlyDict, 124 | values: readonly Value[], 125 | input: unknown 126 | ): Value { 127 | if (isArray(input) && input.length === 1 && values.includes((input = input[0]))) { 128 | return input as Value; 129 | } 130 | if (!isArray(source) && typeof (input = getCanonicalValue(input)) === 'string' && source.hasOwnProperty(input)) { 131 | return (source as ReadonlyDict)[input]; 132 | } 133 | 134 | for (const value of values) { 135 | if (coerceToConst(value, input) !== NEVER) { 136 | return value; 137 | } 138 | } 139 | return NEVER; 140 | } 141 | -------------------------------------------------------------------------------- /src/main/shape/InstanceShape.ts: -------------------------------------------------------------------------------- 1 | import { CODE_TYPE_INSTANCE_OF, MESSAGE_TYPE_INSTANCE_OF } from '../constants.js'; 2 | import { isEqualOrSubclass } from '../internal/lang.js'; 3 | import { Type } from '../Type.js'; 4 | import { IssueOptions, Message, ParseOptions, Result } from '../types.js'; 5 | import { createIssue } from '../utils.js'; 6 | import { Shape } from './Shape.js'; 7 | 8 | const arrayInputs = Object.freeze([Type.ARRAY]); 9 | const dateInputs = Object.freeze([Type.DATE]); 10 | const functionInputs = Object.freeze([Type.FUNCTION]); 11 | const mapInputs = Object.freeze([Type.MAP]); 12 | const objectInputs = Object.freeze([Type.OBJECT]); 13 | const promiseInputs = Object.freeze([Type.PROMISE]); 14 | const setInputs = Object.freeze([Type.SET]); 15 | 16 | /** 17 | * The shape of the class instance. 18 | * 19 | * @template Ctor The class constructor. 20 | * @group Shapes 21 | */ 22 | export class InstanceShape any> extends Shape> { 23 | /** 24 | * The type issue options or the type issue message. 25 | */ 26 | protected _options; 27 | 28 | /** 29 | * Creates a new {@link InstanceShape} instance. 30 | * 31 | * @param ctor The class constructor. 32 | * @param options The issue options or the issue message. 33 | * @template Ctor The class constructor. 34 | */ 35 | constructor( 36 | readonly ctor: Ctor, 37 | options?: IssueOptions | Message 38 | ) { 39 | super(); 40 | 41 | this._options = options; 42 | } 43 | 44 | protected _getInputs(): readonly unknown[] { 45 | const { ctor } = this; 46 | 47 | if (isEqualOrSubclass(ctor, Function)) { 48 | return functionInputs; 49 | } 50 | if (isEqualOrSubclass(ctor, Promise)) { 51 | return promiseInputs; 52 | } 53 | if (isEqualOrSubclass(ctor, Array)) { 54 | return arrayInputs; 55 | } 56 | if (isEqualOrSubclass(ctor, Date)) { 57 | return dateInputs; 58 | } 59 | if (isEqualOrSubclass(ctor, Set)) { 60 | return setInputs; 61 | } 62 | if (isEqualOrSubclass(ctor, Map)) { 63 | return mapInputs; 64 | } 65 | return objectInputs; 66 | } 67 | 68 | protected _apply(input: unknown, options: ParseOptions, _nonce: number): Result> { 69 | if (!(input instanceof this.ctor)) { 70 | return [createIssue(CODE_TYPE_INSTANCE_OF, input, MESSAGE_TYPE_INSTANCE_OF, this.ctor, options, this._options)]; 71 | } 72 | return this._applyOperations(input, input, options, null) as Result; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/shape/NeverShape.ts: -------------------------------------------------------------------------------- 1 | import { CODE_TYPE_NEVER, MESSAGE_TYPE_NEVER } from '../constants.js'; 2 | import { IssueOptions, Message, ParseOptions, Result } from '../types.js'; 3 | import { createIssue } from '../utils.js'; 4 | import { Shape } from './Shape.js'; 5 | 6 | const neverInputs = Object.freeze([]); 7 | 8 | /** 9 | * The shape that doesn't match any input. 10 | * 11 | * @group Shapes 12 | */ 13 | export class NeverShape extends Shape { 14 | /** 15 | * The type issue options or the type issue message. 16 | */ 17 | protected _options; 18 | 19 | /** 20 | * Creates a new {@link NeverShape} instance. 21 | * 22 | * @param options The issue options or the issue message. 23 | */ 24 | constructor(options?: IssueOptions | Message) { 25 | super(); 26 | 27 | this._options = options; 28 | } 29 | 30 | protected _getInputs(): readonly unknown[] { 31 | return neverInputs; 32 | } 33 | 34 | protected _apply(input: unknown, options: ParseOptions, _nonce: number): Result { 35 | return [createIssue(CODE_TYPE_NEVER, input, MESSAGE_TYPE_NEVER, undefined, options, this._options)]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/shape/NumberShape.ts: -------------------------------------------------------------------------------- 1 | import { NEVER } from '../coerce/never.js'; 2 | import { coerceToNumber, numberCoercibleInputs } from '../coerce/number.js'; 3 | import { CODE_TYPE_NUMBER, MESSAGE_TYPE_NUMBER } from '../constants.js'; 4 | import { Type } from '../Type.js'; 5 | import { Any, IssueOptions, Message, ParseOptions, Result } from '../types.js'; 6 | import { createIssue } from '../utils.js'; 7 | import { AllowShape, ReplaceShape, Shape } from './Shape.js'; 8 | 9 | const numberInputs = Object.freeze([Type.NUMBER]); 10 | 11 | /** 12 | * The shape of a number value. 13 | * 14 | * @group Shapes 15 | */ 16 | export class NumberShape extends Shape { 17 | /** 18 | * `true` if this shape coerces input values to the required type during parsing, or `false` otherwise. 19 | */ 20 | isCoercing = false; 21 | 22 | /** 23 | * The type issue options or the type issue message. 24 | */ 25 | protected _options; 26 | 27 | /** 28 | * Creates a new {@link NumberShape} instance. 29 | * 30 | * @param options The issue options or the issue message. 31 | */ 32 | constructor(options?: IssueOptions | Message) { 33 | super(); 34 | 35 | this._options = options; 36 | } 37 | 38 | /** 39 | * Allows `NaN` as an input and output value. 40 | */ 41 | nan(): AllowShape; 42 | 43 | /** 44 | * Replaces an input `NaN` value with a default output value. 45 | * 46 | * @param defaultValue The value that is used instead of `NaN` in the output. 47 | */ 48 | nan(defaultValue: T): ReplaceShape; 49 | 50 | nan(defaultValue?: any) { 51 | return this.replace(NaN, arguments.length === 0 ? NaN : defaultValue); 52 | } 53 | 54 | /** 55 | * Enables an input value coercion. 56 | * 57 | * @returns The clone of the shape. 58 | */ 59 | coerce(): this { 60 | const shape = this._clone(); 61 | shape.isCoercing = true; 62 | return shape; 63 | } 64 | 65 | protected _getInputs(): readonly unknown[] { 66 | return this.isCoercing ? numberCoercibleInputs : numberInputs; 67 | } 68 | 69 | protected _apply(input: any, options: ParseOptions, _nonce: number): Result { 70 | let output = input; 71 | 72 | if ( 73 | (typeof output !== 'number' || output !== output) && 74 | (!this.isCoercing || (output = coerceToNumber(input)) === NEVER) 75 | ) { 76 | return [createIssue(CODE_TYPE_NUMBER, input, MESSAGE_TYPE_NUMBER, undefined, options, this._options)]; 77 | } 78 | return this._applyOperations(input, output, options, null) as Result; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/shape/ReadonlyShape.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isPlainObject } from '../internal/lang.js'; 2 | import { cloneObject } from '../internal/objects.js'; 3 | import { toDeepPartialShape } from '../internal/shapes.js'; 4 | import { ParseOptions, Result } from '../types.js'; 5 | import { AnyShape, DeepPartialProtocol, DeepPartialShape, Input, Output, Shape } from './Shape.js'; 6 | 7 | // prettier-ignore 8 | type ToReadonly = 9 | T extends null | undefined ? T : 10 | T extends Array ? readonly V[] : 11 | T extends Set ? ReadonlySet : 12 | T extends Map ? ReadonlyMap : 13 | T extends object ? Readonly : 14 | T; 15 | 16 | /** 17 | * The shape that makes the output readonly. Only freezes plain objects and arrays at runtime, other object types are 18 | * left intact. 19 | * 20 | * @template BaseShape The shape that parses the input. 21 | * @group Shapes 22 | */ 23 | export class ReadonlyShape 24 | extends Shape, ToReadonly>> 25 | implements DeepPartialProtocol>> 26 | { 27 | /** 28 | * Creates the new {@link ReadonlyShape} instance. 29 | * 30 | * @param baseShape The shape that parses the input. 31 | * @template BaseShape The shape that parses the input. 32 | */ 33 | constructor( 34 | /** 35 | * The shape that parses the input. 36 | */ 37 | readonly baseShape: BaseShape 38 | ) { 39 | super(); 40 | } 41 | 42 | deepPartial(): ReadonlyShape> { 43 | return new ReadonlyShape(toDeepPartialShape(this.baseShape)); 44 | } 45 | 46 | protected _isAsync(): boolean { 47 | return this.baseShape.isAsync; 48 | } 49 | 50 | protected _getInputs(): readonly unknown[] { 51 | return this.baseShape.inputs; 52 | } 53 | 54 | protected _apply(input: unknown, options: ParseOptions, nonce: number): Result>> { 55 | let output = input; 56 | let result = this.baseShape['_apply'](input, options, nonce); 57 | 58 | if (result !== null) { 59 | if (isArray(result)) { 60 | return result; 61 | } 62 | output = result.value; 63 | } 64 | 65 | if (isPlainObject(output) || isArray(output)) { 66 | output = Object.freeze(output !== input ? output : isArray(output) ? output.slice(0) : cloneObject(output)); 67 | } 68 | 69 | return this._applyOperations(input, output, options, null) as Result; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/shape/StringShape.ts: -------------------------------------------------------------------------------- 1 | import { NEVER } from '../coerce/never.js'; 2 | import { coerceToString, stringCoercibleInputs } from '../coerce/string.js'; 3 | import { CODE_TYPE_STRING, MESSAGE_TYPE_STRING } from '../constants.js'; 4 | import { Type } from '../Type.js'; 5 | import { IssueOptions, Message, ParseOptions, Result } from '../types.js'; 6 | import { createIssue } from '../utils.js'; 7 | import { Shape } from './Shape.js'; 8 | 9 | const stringInputs = Object.freeze([Type.STRING]); 10 | 11 | /** 12 | * The shape of a string value. 13 | * 14 | * @group Shapes 15 | */ 16 | export class StringShape extends Shape { 17 | /** 18 | * `true` if this shape coerces input values to the required type during parsing, or `false` otherwise. 19 | */ 20 | isCoercing = false; 21 | 22 | /** 23 | * The type issue options or the type issue message. 24 | */ 25 | protected _options; 26 | 27 | /** 28 | * Creates a new {@link StringShape} instance. 29 | * 30 | * @param options The issue options or the issue message. 31 | */ 32 | constructor(options?: IssueOptions | Message) { 33 | super(); 34 | 35 | this._options = options; 36 | } 37 | 38 | /** 39 | * Enables an input value coercion. 40 | * 41 | * @returns The clone of the shape. 42 | */ 43 | coerce(): this { 44 | const shape = this._clone(); 45 | shape.isCoercing = true; 46 | return shape; 47 | } 48 | 49 | protected _getInputs(): readonly unknown[] { 50 | return this.isCoercing ? stringCoercibleInputs : stringInputs; 51 | } 52 | 53 | protected _apply(input: any, options: ParseOptions, _nonce: number): Result { 54 | let output = input; 55 | 56 | if (typeof output !== 'string' && (!this.isCoercing || (output = coerceToString(input)) === NEVER)) { 57 | return [createIssue(CODE_TYPE_STRING, input, MESSAGE_TYPE_STRING, undefined, options, this._options)]; 58 | } 59 | return this._applyOperations(input, output, options, null) as Result; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/shape/SymbolShape.ts: -------------------------------------------------------------------------------- 1 | import { CODE_TYPE_SYMBOL, MESSAGE_TYPE_SYMBOL } from '../constants.js'; 2 | import { Type } from '../Type.js'; 3 | import { IssueOptions, Message, ParseOptions, Result } from '../types.js'; 4 | import { createIssue } from '../utils.js'; 5 | import { Shape } from './Shape.js'; 6 | 7 | const symbolInputs = Object.freeze([Type.SYMBOL]); 8 | 9 | /** 10 | * The shape of an arbitrary symbol value. 11 | * 12 | * @group Shapes 13 | */ 14 | export class SymbolShape extends Shape { 15 | /** 16 | * The type issue options or the type issue message. 17 | */ 18 | protected _options; 19 | 20 | /** 21 | * Creates a new {@link SymbolShape} instance. 22 | * 23 | * @param options The issue options or the issue message. 24 | */ 25 | constructor(options?: IssueOptions | Message) { 26 | super(); 27 | 28 | this._options = options; 29 | } 30 | 31 | protected _getInputs(): readonly unknown[] { 32 | return symbolInputs; 33 | } 34 | 35 | protected _apply(input: unknown, options: ParseOptions, _nonce: number): Result { 36 | if (typeof input !== 'symbol') { 37 | return [createIssue(CODE_TYPE_SYMBOL, input, MESSAGE_TYPE_SYMBOL, undefined, options, this._options)]; 38 | } 39 | return this._applyOperations(input, input, options, null) as Result; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The module with the utility functions that can be used for plugin development. 3 | * 4 | * ```ts 5 | * import { createIssue } from 'doubter/utils'; 6 | * ``` 7 | * 8 | * @module utils 9 | */ 10 | 11 | import type { Issue, IssueOptions, Message, ParseOptions } from './core.js'; 12 | 13 | export { inspect } from './inspect.js'; 14 | 15 | export function toIssueOptions(options: T | Message | undefined): Partial { 16 | return options !== null && typeof options === 'object' ? options : ({ message: options } as T); 17 | } 18 | 19 | /** 20 | * Creates a new issue. 21 | * 22 | * @param code The issue code. 23 | * @param input The input value that caused an issue. 24 | * @param defaultMessage The default message that is used if there's no {@link IssueOptions.message} 25 | * @param param The issue param that is also passed to the message callback. 26 | * @param parseOptions The options passed to the {@link core!Shape._apply Shape._apply} or 27 | * {@link core!Shape._applyAsync Shape._applyAsync} 28 | * @param issueOptions The issue options or `undefined` if there's no specific issue options. 29 | */ 30 | export function createIssue( 31 | code: any, 32 | input: unknown, 33 | defaultMessage: Message, 34 | param: unknown, 35 | parseOptions: ParseOptions, 36 | issueOptions: IssueOptions | Message | undefined 37 | ): Issue { 38 | const issue: Issue = { code, path: undefined, input, message: undefined, param, meta: undefined }; 39 | 40 | let message; 41 | 42 | message = 43 | (issueOptions !== undefined && 44 | ((message = issueOptions), 45 | typeof issueOptions === 'function' || 46 | typeof issueOptions === 'string' || 47 | ((issue.meta = issueOptions.meta), (message = issueOptions.message)) !== undefined)) || 48 | (parseOptions !== undefined && 49 | parseOptions.messages !== undefined && 50 | (message = parseOptions.messages[code]) !== undefined) 51 | ? message 52 | : defaultMessage; 53 | 54 | if ( 55 | (typeof message === 'function' && ((message = message(issue, parseOptions)), issue.message === undefined)) || 56 | message !== undefined 57 | ) { 58 | issue.message = message; 59 | } 60 | return issue; 61 | } 62 | -------------------------------------------------------------------------------- /src/test/ValidationError.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { ValidationError } from '../main/index.js'; 3 | 4 | describe('ValidationError', () => { 5 | test('creates message from issues', () => { 6 | const error = new ValidationError([{ code: 'aaa' }, { message: 'bbb' }]); 7 | 8 | expect(error.toString()).toBe('ValidationError: [{ code: "aaa" }, { message: "bbb" }]'); 9 | }); 10 | 11 | test('uses custom message', () => { 12 | const error = new ValidationError([{}]); 13 | 14 | error.message = 'aaa'; 15 | 16 | expect(error.message).toBe('aaa'); 17 | 18 | error.message = 'bbb'; 19 | 20 | expect(error.message).toBe('bbb'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/test/coerce/bigint.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { NEVER } from '../../main/index.js'; 3 | import { coerceToBigInt } from '../../main/coerce/bigint.js'; 4 | 5 | describe('coerceToBigInt', () => { 6 | test('coerces a String object', () => { 7 | expect(coerceToBigInt(String('111'))).toBe(BigInt(111)); 8 | expect(coerceToBigInt([String('111')])).toBe(BigInt(111)); 9 | }); 10 | 11 | test('coerces a string', () => { 12 | expect(coerceToBigInt('111')).toBe(BigInt(111)); 13 | 14 | expect(coerceToBigInt('aaa')).toBe(NEVER); 15 | }); 16 | 17 | test('coerces a number', () => { 18 | expect(coerceToBigInt(111)).toBe(BigInt(111)); 19 | 20 | expect(coerceToBigInt(111.222)).toBe(NEVER); 21 | expect(coerceToBigInt(NaN)).toBe(NEVER); 22 | expect(coerceToBigInt(Infinity)).toBe(NEVER); 23 | expect(coerceToBigInt(-Infinity)).toBe(NEVER); 24 | }); 25 | 26 | test('coerces a boolean', () => { 27 | expect(coerceToBigInt(true)).toBe(BigInt(1)); 28 | expect(coerceToBigInt(false)).toBe(BigInt(0)); 29 | }); 30 | 31 | test('coerces null and undefined values', () => { 32 | expect(coerceToBigInt(null)).toBe(BigInt(0)); 33 | expect(coerceToBigInt(undefined)).toBe(BigInt(0)); 34 | }); 35 | 36 | test('coerces an array with a single bigint element', () => { 37 | expect(coerceToBigInt([BigInt(111)])).toBe(BigInt(111)); 38 | }); 39 | 40 | test('does not coerce unsuitable array', () => { 41 | expect(coerceToBigInt([BigInt(111), 'aaa'])).toBe(NEVER); 42 | expect(coerceToBigInt([BigInt(111), BigInt(111)])).toBe(NEVER); 43 | expect(coerceToBigInt(['aaa'])).toBe(NEVER); 44 | }); 45 | 46 | test('does not coerce objects and functions', () => { 47 | expect(coerceToBigInt({ key1: 111 })).toBe(NEVER); 48 | expect(coerceToBigInt(() => null)).toBe(NEVER); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/test/coerce/boolean.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { NEVER } from '../../main/index.js'; 3 | import { coerceToBoolean } from '../../main/coerce/boolean.js'; 4 | 5 | describe('coerceToBoolean', () => { 6 | test('coerces a Boolean object', () => { 7 | expect(coerceToBoolean(Boolean(true))).toBe(true); 8 | expect(coerceToBoolean([Boolean(false)])).toBe(false); 9 | }); 10 | 11 | test('coerces a String object', () => { 12 | expect(coerceToBoolean(String('true'))).toBe(true); 13 | expect(coerceToBoolean([String('false')])).toBe(false); 14 | }); 15 | 16 | test('coerces a string', () => { 17 | expect(coerceToBoolean('true')).toBe(true); 18 | 19 | expect(coerceToBoolean('aaa')).toBe(NEVER); 20 | }); 21 | 22 | test('coerces a number', () => { 23 | expect(coerceToBoolean(1)).toBe(true); 24 | expect(coerceToBoolean(0)).toBe(false); 25 | 26 | expect(coerceToBoolean(111)).toBe(NEVER); 27 | expect(coerceToBoolean(NaN)).toBe(NEVER); 28 | expect(coerceToBoolean(Infinity)).toBe(NEVER); 29 | expect(coerceToBoolean(-Infinity)).toBe(NEVER); 30 | }); 31 | 32 | test('coerces a boolean', () => { 33 | expect(coerceToBoolean(true)).toBe(true); 34 | expect(coerceToBoolean(false)).toBe(false); 35 | }); 36 | 37 | test('coerces null and undefined values', () => { 38 | expect(coerceToBoolean(null)).toBe(false); 39 | expect(coerceToBoolean(undefined)).toBe(false); 40 | }); 41 | 42 | test('coerces an array with a single boolean element', () => { 43 | expect(coerceToBoolean([true])).toBe(true); 44 | expect(coerceToBoolean([false])).toBe(false); 45 | 46 | expect(coerceToBoolean([[true]])).toBe(NEVER); 47 | expect(coerceToBoolean([BigInt(111), 'aaa'])).toBe(NEVER); 48 | expect(coerceToBoolean([BigInt(111), BigInt(111)])).toBe(NEVER); 49 | expect(coerceToBoolean(['aaa'])).toBe(NEVER); 50 | }); 51 | 52 | test('does not coerce objects and functions', () => { 53 | expect(coerceToBoolean({ key1: 111 })).toBe(NEVER); 54 | expect(coerceToBoolean(() => null)).toBe(NEVER); 55 | }); 56 | 57 | test('does not coerce a symbol', () => { 58 | expect(coerceToBoolean(Symbol())).toBe(NEVER); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/test/coerce/const.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { NEVER } from '../../main/index.js'; 3 | import { coerceToConst } from '../../main/coerce/const.js'; 4 | 5 | describe('coerceToConst', () => { 6 | test('coerces to a bigint', () => { 7 | expect(coerceToConst(BigInt(111), '111')).toBe(BigInt(111)); 8 | expect(coerceToConst(BigInt(111), ['111'])).toBe(BigInt(111)); 9 | }); 10 | 11 | test('coerces to a number', () => { 12 | expect(coerceToConst(111, '111')).toBe(111); 13 | expect(coerceToConst(111, ['111'])).toBe(111); 14 | expect(coerceToConst(111, new Number(111))).toBe(111); 15 | expect(coerceToConst(NaN, new Number(NaN))).toBe(NaN); 16 | expect(coerceToConst(NaN, NaN)).toBe(NaN); 17 | }); 18 | 19 | test('coerces to a string', () => { 20 | expect(coerceToConst('111', 111)).toBe('111'); 21 | expect(coerceToConst('111', [111])).toBe('111'); 22 | }); 23 | 24 | test('coerces to a boolean', () => { 25 | expect(coerceToConst(true, 'true')).toBe(true); 26 | expect(coerceToConst(true, ['true'])).toBe(true); 27 | expect(coerceToConst(true, 1)).toBe(true); 28 | }); 29 | 30 | test('coerces to a Date', () => { 31 | const value = new Date(1698059765298); 32 | 33 | expect(coerceToConst(value, value)).toBe(value); 34 | expect(coerceToConst(value, 1698059765298)).toBe(value); 35 | expect(coerceToConst(value, '2023-10-23T11:16:05.298Z')).toBe(value); 36 | expect(coerceToConst(value, [value])).toBe(value); 37 | expect(coerceToConst(value, [1698059765298])).toBe(value); 38 | expect(coerceToConst(value, ['2023-10-23T11:16:05.298Z'])).toBe(value); 39 | expect(coerceToConst(value, new Date())).toBe(NEVER); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/test/coerce/date.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { coerceToDate } from '../../main/coerce/date.js'; 3 | import { NEVER } from '../../main/coerce/never.js'; 4 | 5 | describe('coerceToDate', () => { 6 | test('coerces a String object', () => { 7 | expect(coerceToDate(new String('2020-02-02'))).toEqual(new Date('2020-02-02')); 8 | }); 9 | 10 | test('coerces a string', () => { 11 | expect(coerceToDate('2020-02-02')).toEqual(new Date('2020-02-02')); 12 | }); 13 | 14 | test('coerces a number', () => { 15 | expect(coerceToDate(111)).toEqual(new Date(111)); 16 | }); 17 | 18 | test('coerces a boolean', () => { 19 | expect(coerceToDate(true)).toBe(NEVER); 20 | expect(coerceToDate(false)).toBe(NEVER); 21 | }); 22 | 23 | test('coerces null and undefined values', () => { 24 | expect(coerceToDate(null)).toBe(NEVER); 25 | expect(coerceToDate(undefined)).toBe(NEVER); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/test/coerce/number.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { NEVER } from '../../main/index.js'; 3 | import { coerceToNumber } from '../../main/coerce/number.js'; 4 | 5 | describe('coerceToNumber', () => { 6 | test('coerces a Number object', () => { 7 | expect(coerceToNumber(Number(111))).toBe(111); 8 | expect(coerceToNumber([Number(111)])).toBe(111); 9 | }); 10 | 11 | test('coerces a String object', () => { 12 | expect(coerceToNumber(String('111'))).toBe(111); 13 | expect(coerceToNumber([String('111')])).toBe(111); 14 | }); 15 | 16 | test('coerces a string', () => { 17 | expect(coerceToNumber('111')).toBe(111); 18 | expect(coerceToNumber('111.222')).toBe(111.222); 19 | 20 | expect(coerceToNumber('aaa')).toBe(NEVER); 21 | }); 22 | 23 | test('does not coerce NaN', () => { 24 | expect(coerceToNumber(NaN)).toBe(NEVER); 25 | }); 26 | 27 | test('coerce Infinity only in an unconstrained number mode', () => { 28 | expect(coerceToNumber(Infinity)).toBe(Infinity); 29 | expect(coerceToNumber([-Infinity])).toBe(-Infinity); 30 | }); 31 | 32 | test('coerces a boolean', () => { 33 | expect(coerceToNumber(true)).toBe(1); 34 | expect(coerceToNumber(false)).toBe(0); 35 | }); 36 | 37 | test('coerces null and undefined values', () => { 38 | expect(coerceToNumber(null)).toBe(0); 39 | expect(coerceToNumber(undefined)).toBe(0); 40 | }); 41 | 42 | test('coerces an array with a single number element', () => { 43 | expect(coerceToNumber([111])).toBe(111); 44 | expect(coerceToNumber(['111'])).toBe(111); 45 | 46 | expect(coerceToNumber([[111]])).toBe(NEVER); 47 | expect(coerceToNumber([['111']])).toBe(NEVER); 48 | expect(coerceToNumber([BigInt(111), 'aaa'])).toBe(NEVER); 49 | expect(coerceToNumber([BigInt(111), BigInt(111)])).toBe(NEVER); 50 | expect(coerceToNumber(['aaa'])).toBe(NEVER); 51 | }); 52 | 53 | test('does not coerce objects and functions', () => { 54 | expect(coerceToNumber({ key1: 111 })).toBe(NEVER); 55 | expect(coerceToNumber(() => null)).toBe(NEVER); 56 | }); 57 | 58 | test('does not coerce a symbol', () => { 59 | expect(coerceToNumber(Symbol())).toBe(NEVER); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/test/coerce/string.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { NEVER } from '../../main/index.js'; 3 | import { coerceToString } from '../../main/coerce/string.js'; 4 | 5 | describe('coerceToString', () => { 6 | test('returns a string as is', () => { 7 | expect(coerceToString('aaa')).toBe('aaa'); 8 | }); 9 | 10 | test('coerces a String object', () => { 11 | expect(coerceToString(String('aaa'))).toBe('aaa'); 12 | expect(coerceToString([String('aaa')])).toBe('aaa'); 13 | }); 14 | 15 | test('coerces a number', () => { 16 | expect(coerceToString(111)).toBe('111'); 17 | expect(coerceToString(111.222)).toBe('111.222'); 18 | 19 | expect(coerceToString(NaN)).toBe(NEVER); 20 | expect(coerceToString(Infinity)).toBe(NEVER); 21 | expect(coerceToString(-Infinity)).toBe(NEVER); 22 | }); 23 | 24 | test('coerces a boolean', () => { 25 | expect(coerceToString(true)).toBe('true'); 26 | expect(coerceToString(false)).toBe('false'); 27 | }); 28 | 29 | test('coerces null and undefined values', () => { 30 | expect(coerceToString(null)).toBe(''); 31 | expect(coerceToString(undefined)).toBe(''); 32 | }); 33 | 34 | test('coerces an array with a single string element', () => { 35 | expect(coerceToString(['aaa'])).toBe('aaa'); 36 | expect(coerceToString([111])).toBe('111'); 37 | 38 | expect(coerceToString([])).toBe(NEVER); 39 | expect(coerceToString([['aaa']])).toBe(NEVER); 40 | expect(coerceToString([[111]])).toBe(NEVER); 41 | expect(coerceToString(['aaa', 'bbb'])).toBe(NEVER); 42 | expect(coerceToString(['aaa', 111])).toBe(NEVER); 43 | }); 44 | 45 | test('does not coerce objects and functions', () => { 46 | expect(coerceToString({ valueOf: () => 'aaa' })).toBe(NEVER); 47 | expect(coerceToString({ key1: 111 })).toBe(NEVER); 48 | expect(coerceToString(() => null)).toBe(NEVER); 49 | }); 50 | 51 | test('does not coerce a symbol', () => { 52 | expect(coerceToString(Symbol())).toBe(NEVER); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/test/dsl/and.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const OUTPUT: OUTPUT; 6 | 7 | expectType<{ key1: string } & { key2: number }>( 8 | d.and([d.object({ key1: d.string() }), d.object({ key2: d.number() })])[OUTPUT] 9 | ); 10 | 11 | expectType<{ aaa?: string } & { bbb?: number }>( 12 | d 13 | .and([ 14 | d.object({ 15 | aaa: d.string(), 16 | }), 17 | d.object({ 18 | bbb: d.number(), 19 | }), 20 | ]) 21 | .deepPartial()[OUTPUT] 22 | ); 23 | 24 | expectType<{ aaa?: Array } & { bbb?: number }>( 25 | d 26 | .and([ 27 | d.object({ 28 | aaa: d.array(d.string()), 29 | }), 30 | d.object({ 31 | bbb: d.number(), 32 | }), 33 | ]) 34 | .deepPartial()[OUTPUT] 35 | ); 36 | -------------------------------------------------------------------------------- /src/test/dsl/any.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | import { Type } from '../../main/Type.js'; 4 | 5 | describe('any', () => { 6 | test('returns a shape', () => { 7 | expect(d.any()).toBeInstanceOf(d.Shape); 8 | }); 9 | 10 | test('unknown is erased in an intersection', () => { 11 | expect(d.and([d.string(), d.any()]).inputs).toEqual([Type.STRING]); 12 | expect(d.and([d.never(), d.any()]).inputs).toEqual([]); 13 | }); 14 | 15 | test('returns a shape with a refinement', () => { 16 | const cb = () => true; 17 | 18 | expect(d.any(cb).operations[0]).toEqual({ 19 | type: cb, 20 | param: undefined, 21 | isAsync: false, 22 | tolerance: 'auto', 23 | callback: expect.any(Function), 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/test/dsl/array.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectNotAssignable, expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type INPUT, type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const INPUT: INPUT; 6 | declare const OUTPUT: OUTPUT; 7 | 8 | expectType(d.array()[INPUT]); 9 | 10 | expectType(d.array()[OUTPUT]); 11 | 12 | expectType<111[]>(d.array(d.const(111))[INPUT]); 13 | 14 | expectType<111[]>(d.array(d.const(111))[OUTPUT]); 15 | 16 | expectType>(d.array(d.number()).deepPartial()[OUTPUT]); 17 | 18 | expectType>(d.array(d.object({ aaa: d.number() })).deepPartial()[OUTPUT]); 19 | 20 | expectType(d.array(d.string()).readonly()[INPUT]); 21 | 22 | expectNotAssignable(d.array(d.string()).readonly()[OUTPUT]); 23 | 24 | expectType(d.array(d.string()).readonly()[OUTPUT]); 25 | -------------------------------------------------------------------------------- /src/test/dsl/array.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | import { Shape } from '../../main/index.js'; 4 | 5 | describe('array', () => { 6 | test('returns an unconstrained array shape', () => { 7 | const shape = d.array(); 8 | 9 | expect(shape).toBeInstanceOf(d.ArrayShape); 10 | expect(shape.headShapes.length).toBe(0); 11 | expect(shape.restShape).toEqual(new Shape()); 12 | }); 13 | 14 | test('returns an array shape with elements constrained by a rest shape', () => { 15 | const restShape = d.number(); 16 | const shape = d.array(restShape); 17 | 18 | expect(shape.headShapes.length).toBe(0); 19 | expect(shape.restShape).toBe(restShape); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/test/dsl/bigint.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('bigint', () => { 5 | test('returns a bigint shape', () => { 6 | expect(d.bigint()).toBeInstanceOf(d.BigIntShape); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/test/dsl/boolean.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('boolean', () => { 5 | test('returns a boolean shape', () => { 6 | expect(d.boolean()).toBeInstanceOf(d.BooleanShape); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/test/dsl/const.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const OUTPUT: OUTPUT; 6 | 7 | expectType<111>(d.const(111)[OUTPUT]); 8 | -------------------------------------------------------------------------------- /src/test/dsl/const.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('const', () => { 5 | test('returns a const shape', () => { 6 | const shape = d.const(111); 7 | 8 | expect(shape).toBeInstanceOf(d.ConstShape); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/test/dsl/convert.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type INPUT, type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const INPUT: INPUT; 6 | declare const OUTPUT: OUTPUT; 7 | 8 | expectType(d.convert(() => 'aaa')[INPUT]); 9 | 10 | expectType(d.convert(() => 'aaa')[OUTPUT]); 11 | 12 | const shape = d 13 | .object({ 14 | years: d.array(d.string()).convert(years => years.map(parseFloat)), 15 | }) 16 | .deepPartial(); 17 | 18 | expectType<{ years?: string[] }>(shape[INPUT]); 19 | 20 | expectType<{ years?: number[] }>(shape[OUTPUT]); 21 | -------------------------------------------------------------------------------- /src/test/dsl/convert.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('convert', () => { 5 | test('returns a shape', () => { 6 | expect(d.convert(() => 111)).toBeInstanceOf(d.ConvertShape); 7 | }); 8 | 9 | test('converts an input value', () => { 10 | expect(d.convert(input => input + 111).parse(222)).toBe(333); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/test/dsl/date.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type INPUT, type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const INPUT: INPUT; 6 | declare const OUTPUT: OUTPUT; 7 | 8 | expectType(d.date().toISOString()[INPUT]); 9 | 10 | expectType(d.date().toISOString()[OUTPUT]); 11 | -------------------------------------------------------------------------------- /src/test/dsl/date.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('date', () => { 5 | test('returns a shape', () => { 6 | expect(d.date()).toBeInstanceOf(d.DateShape); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/test/dsl/enum.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type INPUT, type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const INPUT: INPUT; 6 | declare const OUTPUT: OUTPUT; 7 | 8 | expectType<111 | 'aaa'>(d.enum([111, 'aaa'])[OUTPUT]); 9 | 10 | enum FooEnum { 11 | AAA, 12 | BBB, 13 | } 14 | 15 | expectType(d.enum(FooEnum)[OUTPUT]); 16 | 17 | expectType<'aaa' | 'bbb'>(d.enum({ AAA: 'aaa', BBB: 'bbb' } as const)[OUTPUT]); 18 | 19 | expectType<111 | 'aaa' | 333>(d.enum([111, 222, 333]).replace(222, 'aaa')[OUTPUT]); 20 | 21 | expectType(d.enum([33, 42]).replace(NaN, 0)[INPUT]); 22 | 23 | expectType<33 | 42 | 0>(d.enum([33, 42]).replace(NaN, 0)[OUTPUT]); 24 | 25 | expectType<111 | 333>(d.enum([111, 222, 333]).deny(222)[OUTPUT]); 26 | 27 | expectType<222>(d.enum([111, 222, 333]).refine((_value): _value is 222 => true)[OUTPUT]); 28 | 29 | const enumValues = [111, 'aaa'] as const; 30 | 31 | expectType<111 | 'aaa'>(d.enum(enumValues)[INPUT]); 32 | 33 | expectType<111 | 'aaa'>(d.enum(enumValues)[OUTPUT]); 34 | -------------------------------------------------------------------------------- /src/test/dsl/enum.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('enum', () => { 5 | test('returns an enum shape', () => { 6 | const shape = d.enum([111, 222]); 7 | 8 | expect(shape).toBeInstanceOf(d.EnumShape); 9 | expect(shape.inputs).toEqual([111, 222]); 10 | }); 11 | 12 | test('enums with no common values produce never when intersected', () => { 13 | expect(d.and([d.enum([111, 222]), d.enum([333])]).inputs).toEqual([]); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/test/dsl/function.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | import { ArrayShape, StringShape } from '../../main/index.js'; 4 | import { CODE_TYPE_FUNCTION } from '../../main/constants.js'; 5 | 6 | describe('function', () => { 7 | test('returns a function shape', () => { 8 | expect(d.fn()).toBeInstanceOf(d.FunctionShape); 9 | expect(d.function()).toBeInstanceOf(d.FunctionShape); 10 | }); 11 | 12 | test('unconstrained arguments by default', () => { 13 | const shape = d.fn(); 14 | 15 | expect(shape.argsShape).toBeInstanceOf(ArrayShape); 16 | expect(shape.argsShape.headShapes.length).toBe(0); 17 | }); 18 | 19 | test('wraps arguments into an array shape', () => { 20 | const shape = d.fn([d.string()]); 21 | 22 | expect(shape.argsShape.headShapes.length).toBe(1); 23 | expect(shape.argsShape.headShapes[0]).toBeInstanceOf(StringShape); 24 | }); 25 | 26 | test('recognizes options as the first argument', () => { 27 | const shape = d.fn({ message: 'aaa' }); 28 | 29 | expect(shape.argsShape).toBeInstanceOf(ArrayShape); 30 | expect(shape.argsShape.headShapes.length).toBe(0); 31 | 32 | expect(shape.try(111)).toEqual({ 33 | ok: false, 34 | issues: [{ code: CODE_TYPE_FUNCTION, input: 111, message: 'aaa' }], 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/test/dsl/instanceOf.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type INPUT, type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const INPUT: INPUT; 6 | declare const OUTPUT: OUTPUT; 7 | 8 | class TestClass { 9 | aaa = 111; 10 | } 11 | 12 | expectType(d.instanceOf(TestClass)[OUTPUT]); 13 | -------------------------------------------------------------------------------- /src/test/dsl/instanceOf.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('instanceOf', () => { 5 | test('returns an instance shape', () => { 6 | class TestClass {} 7 | 8 | expect(d.instanceOf(TestClass)).toBeInstanceOf(d.InstanceShape); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/test/dsl/intersection.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type INPUT, type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const INPUT: INPUT; 6 | declare const OUTPUT: OUTPUT; 7 | 8 | expectType<{ key1: string } & { key2: number }>( 9 | d.and([d.object({ key1: d.string() }), d.object({ key2: d.number() })])[OUTPUT] 10 | ); 11 | 12 | expectType(d.and([d.string(), d.string()])[OUTPUT]); 13 | 14 | expectType(d.and([d.or([d.string(), d.number()]), d.string()])[OUTPUT]); 15 | 16 | expectType( 17 | d.and([d.or([d.string(), d.number(), d.boolean()]), d.or([d.string(), d.number()])])[OUTPUT] 18 | ); 19 | 20 | expectType(d.and([d.or([d.string(), d.never()]), d.number()])[OUTPUT]); 21 | 22 | expectType(d.and([d.any(), d.string()])[OUTPUT]); 23 | 24 | expectType(d.and([d.never(), d.string()])[OUTPUT]); 25 | 26 | expectType(d.and([d.never(), d.any()])[OUTPUT]); 27 | 28 | expectType(d.and([d.never()])[INPUT]); 29 | 30 | expectType(d.and([d.never()])[OUTPUT]); 31 | -------------------------------------------------------------------------------- /src/test/dsl/intersection.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('intersection', () => { 5 | test('returns an intersection shape', () => { 6 | expect(d.intersection([d.string(), d.number()])).toBeInstanceOf(d.IntersectionShape); 7 | expect(d.and([d.string(), d.number()])).toBeInstanceOf(d.IntersectionShape); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/test/dsl/lazy.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type INPUT, type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const INPUT: INPUT; 6 | declare const OUTPUT: OUTPUT; 7 | 8 | expectType(d.lazy(() => d.string())[OUTPUT]); 9 | 10 | expectType(d.lazy(() => d.string().convert(parseFloat))[OUTPUT]); 11 | 12 | expectType<{ aaa?: string }>(d.lazy(() => d.object({ aaa: d.string().convert(parseFloat) })).deepPartial()[INPUT]); 13 | 14 | expectType<{ aaa?: string } | { aaa?: number }>( 15 | d.lazy(() => d.object({ aaa: d.string().convert(parseFloat) })).deepPartial()[OUTPUT] 16 | ); 17 | 18 | expectType(d.lazy(() => d.string()).circular(111)[OUTPUT]); 19 | 20 | expectType<{ aaa: number } | 111>( 21 | d.lazy(() => d.object({ aaa: d.string().convert(parseFloat) })).circular(111)[OUTPUT] 22 | ); 23 | 24 | expectType<{ aaa?: string } | { aaa?: number }>( 25 | d 26 | .lazy(() => d.object({ aaa: d.string().convert(parseFloat) })) 27 | .circular(111) 28 | .deepPartial()[OUTPUT] 29 | ); 30 | -------------------------------------------------------------------------------- /src/test/dsl/lazy.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | import { AsyncMockShape } from '../shape/mocks.js'; 4 | 5 | describe('lazy', () => { 6 | test('returns a lazy shape', () => { 7 | const providedShape = d.string(); 8 | const shape = d.lazy(() => providedShape); 9 | 10 | expect(shape).toBeInstanceOf(d.LazyShape); 11 | expect(shape.isAsync).toBe(false); 12 | expect(shape.providedShape).toBe(providedShape); 13 | }); 14 | 15 | test('returns an async shape', () => { 16 | const providedShape = new AsyncMockShape(); 17 | const shape = d.lazy(() => providedShape); 18 | 19 | expect(shape).toBeInstanceOf(d.LazyShape); 20 | expect(shape.isAsync).toBe(true); 21 | expect(shape.providedShape).toBe(providedShape); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/test/dsl/map.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectNotAssignable, expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type INPUT, type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const INPUT: INPUT; 6 | declare const OUTPUT: OUTPUT; 7 | 8 | expectType>(d.map(d.string(), d.number())[OUTPUT]); 9 | 10 | expectType>( 11 | d.map( 12 | d.string().convert((): 'bbb' => 'bbb'), 13 | d.number() 14 | )[OUTPUT] 15 | ); 16 | 17 | expectType>(d.map(d.string(), d.number()).deepPartial()[OUTPUT]); 18 | 19 | expectType>( 20 | d.map(d.object({ aaa: d.string() }), d.object({ bbb: d.number() })).deepPartial()[OUTPUT] 21 | ); 22 | 23 | expectType>(d.map(d.string(), d.string()).readonly()[INPUT]); 24 | 25 | expectNotAssignable>(d.map(d.string(), d.string()).readonly()[OUTPUT]); 26 | 27 | expectType>(d.map(d.string(), d.string()).readonly()[OUTPUT]); 28 | -------------------------------------------------------------------------------- /src/test/dsl/map.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('map', () => { 5 | test('returns a Map shape', () => { 6 | expect(d.map(d.string(), d.number())).toBeInstanceOf(d.MapShape); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/test/dsl/nan.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('nan', () => { 5 | test('returns a const shape', () => { 6 | const shape = d.nan(); 7 | 8 | expect(shape).toBeInstanceOf(d.ConstShape); 9 | expect(shape.value).toBe(NaN); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/test/dsl/never.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const OUTPUT: OUTPUT; 6 | 7 | expectType(d.never()[OUTPUT]); 8 | -------------------------------------------------------------------------------- /src/test/dsl/never.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | import { Type } from '../../main/Type.js'; 4 | 5 | describe('never', () => { 6 | test('returns a never shape', () => { 7 | expect(d.never()).toBeInstanceOf(d.NeverShape); 8 | }); 9 | 10 | test('never is erased in unions', () => { 11 | expect(d.or([d.string(), d.never()]).inputs).toEqual([Type.STRING]); 12 | }); 13 | 14 | test('never absorbs other types in intersections', () => { 15 | expect(d.and([d.string(), d.never()]).inputs).toEqual([]); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/test/dsl/not.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const OUTPUT: OUTPUT; 6 | 7 | expectType(d.not(d.string())[OUTPUT]); 8 | -------------------------------------------------------------------------------- /src/test/dsl/not.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('not', () => { 5 | test('returns an exclusion shape', () => { 6 | const excludedShape = d.string(); 7 | const shape = d.not(excludedShape); 8 | 9 | expect(shape).toBeInstanceOf(d.ExcludeShape); 10 | expect(shape.excludedShape).toBe(excludedShape); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/test/dsl/null.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('null', () => { 5 | test('returns a const shape', () => { 6 | const shape = d.null(); 7 | 8 | expect(shape).toBeInstanceOf(d.ConstShape); 9 | expect(shape.value).toBeNull(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/test/dsl/number.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type INPUT, type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const INPUT: INPUT; 6 | declare const OUTPUT: OUTPUT; 7 | 8 | expectType(d.number().replace('aaa', true)[INPUT]); 9 | 10 | expectType(d.number().replace('aaa', true)[OUTPUT]); 11 | 12 | expectType(d.number().replace(222, 'aaa')[INPUT]); 13 | 14 | expectType(d.number().replace(222, 'aaa')[OUTPUT]); 15 | 16 | expectType(d.number().replace(NaN, 0)[INPUT]); 17 | 18 | expectType(d.number().replace(NaN, 0)[OUTPUT]); 19 | 20 | expectType(d.number().nan()[INPUT]); 21 | 22 | expectType(d.number().nan()[OUTPUT]); 23 | 24 | expectType(d.number().nan(111)[INPUT]); 25 | 26 | expectType(d.number().nan(111)[OUTPUT]); 27 | 28 | expectType(d.number().nan('aaa')[INPUT]); 29 | 30 | expectType(d.number().nan('aaa')[OUTPUT]); 31 | 32 | expectType(d.number().allow(Infinity)[OUTPUT]); 33 | 34 | expectType(d.number().deny(111)[INPUT]); 35 | 36 | expectType(d.number().deny(111)[OUTPUT]); 37 | 38 | expectType(d.number().alter(Math.abs).alter(Math.pow, { param: 3 })[OUTPUT]); 39 | -------------------------------------------------------------------------------- /src/test/dsl/number.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | import { CODE_TYPE_NUMBER, MESSAGE_TYPE_NUMBER } from '../../main/constants.js'; 4 | 5 | describe('number', () => { 6 | test('returns a number shape', () => { 7 | expect(d.number()).toBeInstanceOf(d.NumberShape); 8 | }); 9 | 10 | test('raises an issue if value is not a number', () => { 11 | expect(d.number().try('aaa')).toEqual({ 12 | ok: false, 13 | issues: [{ code: CODE_TYPE_NUMBER, input: 'aaa', message: MESSAGE_TYPE_NUMBER }], 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/test/dsl/object.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type INPUT, type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const INPUT: INPUT; 6 | declare const OUTPUT: OUTPUT; 7 | 8 | expectType<{ aaa?: string }>( 9 | d.object({ 10 | aaa: d.string().optional(), 11 | })[OUTPUT] 12 | ); 13 | 14 | expectType<{ aaa?: any }>( 15 | d.object({ 16 | aaa: d.any(), 17 | })[OUTPUT] 18 | ); 19 | 20 | expectType<{ aaa: string; bbb: number }>( 21 | d.object({ 22 | aaa: d.string(), 23 | bbb: d.number(), 24 | })[OUTPUT] 25 | ); 26 | 27 | expectType<{ aaa: string }>( 28 | d 29 | .object({ 30 | aaa: d.string(), 31 | bbb: d.number(), 32 | }) 33 | .pick(['aaa'])[OUTPUT] 34 | ); 35 | 36 | expectType<{ bbb: number }>( 37 | d 38 | .object({ 39 | aaa: d.string(), 40 | bbb: d.number(), 41 | }) 42 | .omit(['aaa'])[OUTPUT] 43 | ); 44 | 45 | expectType<{ aaa: string; bbb: number }>(d.object({ aaa: d.string() }).extend({ bbb: d.number() })[OUTPUT]); 46 | 47 | expectType<{ aaa: string; bbb: number }>(d.object({ aaa: d.string() }).extend(d.object({ bbb: d.number() }))[OUTPUT]); 48 | 49 | expectType<{ aaa?: string; bbb?: number }>( 50 | d 51 | .object({ 52 | aaa: d.string(), 53 | bbb: d.number(), 54 | }) 55 | .partial()[OUTPUT] 56 | ); 57 | 58 | expectType<{ aaa?: string; bbb?: number }>( 59 | d 60 | .object({ 61 | aaa: d.string(), 62 | bbb: d.number(), 63 | }) 64 | .deepPartial()[OUTPUT] 65 | ); 66 | 67 | expectType<{ aaa?: string; bbb?: { ccc?: number } }>( 68 | d 69 | .object({ 70 | aaa: d.string(), 71 | bbb: d.object({ ccc: d.number() }), 72 | }) 73 | .deepPartial()[OUTPUT] 74 | ); 75 | 76 | expectType<{ aaa?: string; bbb?: Array }>( 77 | d 78 | .object({ 79 | aaa: d.string(), 80 | bbb: d.array(d.number()), 81 | }) 82 | .deepPartial()[OUTPUT] 83 | ); 84 | 85 | const keys = ['aaa'] as const; 86 | 87 | expectType<{ aaa: string }>(d.object({ aaa: d.string(), bbb: d.number() }).pick(keys)[OUTPUT]); 88 | 89 | expectType<{ bbb: number }>(d.object({ aaa: d.string(), bbb: d.number() }).omit(keys)[OUTPUT]); 90 | 91 | expectType<{ aaa?: string | undefined; bbb: number }>( 92 | d 93 | .object({ 94 | aaa: d.string(), 95 | bbb: d.number(), 96 | }) 97 | .partial(keys)[OUTPUT] 98 | ); 99 | 100 | expectType<{ aaa: string; bbb: number }>( 101 | d 102 | .object({ 103 | aaa: d.string().optional(), 104 | bbb: d.number(), 105 | }) 106 | .required(keys)[OUTPUT] 107 | ); 108 | 109 | d.object({ aaa: d.string(), bbb: d.number() }).notAllKeys(['bbb']); 110 | 111 | expectType<{ aaa: string; bbb: number }>(d.object({ aaa: d.string(), bbb: d.number() }).readonly()[INPUT]); 112 | 113 | expectType<{ readonly aaa: string; readonly bbb: number }>( 114 | d.object({ aaa: d.string(), bbb: d.number() }).readonly()[OUTPUT] 115 | ); 116 | -------------------------------------------------------------------------------- /src/test/dsl/object.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('object', () => { 5 | test('returns an object shape', () => { 6 | expect(d.object({ key1: d.number() })).toBeInstanceOf(d.ObjectShape); 7 | }); 8 | 9 | test('enhanced by a plugin', () => { 10 | expect(d.object({ key1: d.number() }).plain()).toBeInstanceOf(d.ObjectShape); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/test/dsl/or.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const OUTPUT: OUTPUT; 6 | 7 | expectType(d.or([d.number(), d.string()])[OUTPUT]); 8 | 9 | expectType<{ key1: string } | { key2: number }>( 10 | d.or([d.object({ key1: d.string() }), d.object({ key2: d.number() })])[OUTPUT] 11 | ); 12 | 13 | expectType<{ aaa?: string } | { bbb?: number }>( 14 | d 15 | .or([ 16 | d.object({ 17 | aaa: d.string(), 18 | }), 19 | d.object({ 20 | bbb: d.number(), 21 | }), 22 | ]) 23 | .deepPartial()[OUTPUT] 24 | ); 25 | 26 | expectType<{ aaa?: Array } | { bbb?: number }>( 27 | d 28 | .or([ 29 | d.object({ 30 | aaa: d.array(d.string()), 31 | }), 32 | d.object({ 33 | bbb: d.number(), 34 | }), 35 | ]) 36 | .deepPartial()[OUTPUT] 37 | ); 38 | -------------------------------------------------------------------------------- /src/test/dsl/or.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | import { Type } from '../../main/Type.js'; 4 | 5 | describe('or', () => { 6 | test('returns a union shape', () => { 7 | expect(d.or([d.number()])).toBeInstanceOf(d.UnionShape); 8 | }); 9 | 10 | test('unknown absorbs other types in a union', () => { 11 | expect(d.or([d.string(), d.any()]).inputs).toEqual([Type.UNKNOWN]); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/test/dsl/promise.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | 4 | expectType>(d.promise().parse(Promise.resolve('aaa'))); 5 | 6 | expectType>(d.promise(d.string()).parseAsync(Promise.resolve('aaa'))); 7 | -------------------------------------------------------------------------------- /src/test/dsl/promise.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('promise', () => { 5 | test('returns an unconstrained Promise shape', () => { 6 | const shape = d.promise(); 7 | 8 | expect(shape).toBeInstanceOf(d.PromiseShape); 9 | expect(shape.valueShape).toBeNull(); 10 | }); 11 | 12 | test('returns a Promise shape with constrained returned value', () => { 13 | const valueShape = d.string(); 14 | const shape = d.promise(valueShape); 15 | 16 | expect(shape).toBeInstanceOf(d.PromiseShape); 17 | expect(shape.valueShape).toBe(valueShape); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/test/dsl/record.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type INPUT, type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const INPUT: INPUT; 6 | declare const OUTPUT: OUTPUT; 7 | 8 | expectType>(d.record(d.number())[OUTPUT]); 9 | 10 | expectType<{ bbb: number }>( 11 | d.record( 12 | d.string().convert((): 'bbb' => 'bbb'), 13 | d.number() 14 | )[OUTPUT] 15 | ); 16 | 17 | expectType>(d.record(d.string(), d.boolean().optional())[OUTPUT]); 18 | 19 | d.record(d.number()).notAllKeys(['bbb']); 20 | 21 | d.record(d.enum(['aaa', 'bbb']), d.number()).notAllKeys(['bbb']); 22 | 23 | d.record( 24 | d.string().convert(x => x as 'aaa' | 'bbb'), 25 | d.number() 26 | ).notAllKeys(['bbb']); 27 | 28 | d.record(d.number()).notAllKeys(['bbb']); 29 | 30 | expectType<{ [key: string]: number }>(d.record(d.number()).readonly()[INPUT]); 31 | 32 | expectType<{ readonly [key: string]: number }>(d.record(d.number()).readonly()[OUTPUT]); 33 | -------------------------------------------------------------------------------- /src/test/dsl/record.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('record', () => { 5 | test('returns a record shape', () => { 6 | expect(d.record(d.string(), d.number())).toBeInstanceOf(d.RecordShape); 7 | }); 8 | 9 | test('enhanced by a plugin', () => { 10 | expect(d.record(d.string()).plain()).toBeInstanceOf(d.RecordShape); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/test/dsl/set.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectNotAssignable, expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type INPUT, type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const INPUT: INPUT; 6 | declare const OUTPUT: OUTPUT; 7 | 8 | expectType>(d.set(d.or([d.string(), d.number()]))[OUTPUT]); 9 | 10 | expectType>(d.set(d.const(111))[OUTPUT]); 11 | 12 | expectType>(d.set(d.string()).readonly()[INPUT]); 13 | 14 | expectNotAssignable>(d.set(d.string()).readonly()[OUTPUT]); 15 | 16 | expectType>(d.set(d.string()).readonly()[OUTPUT]); 17 | -------------------------------------------------------------------------------- /src/test/dsl/set.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('set', () => { 5 | test('returns a Set shape', () => { 6 | const valueShape = d.number(); 7 | const shape = d.set(valueShape); 8 | 9 | expect(shape.valueShape).toBe(valueShape); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/test/dsl/string.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type INPUT, type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const INPUT: INPUT; 6 | declare const OUTPUT: OUTPUT; 7 | 8 | expectType(d.string().alter((): 'aaa' => 'aaa')[OUTPUT]); 9 | 10 | const x = { param: 111 }; 11 | 12 | d.string().alter((value, _param) => (value === 'aaa' ? 'aaa' : 'bbb'), x); 13 | 14 | const stringShape = d.string().alter(value => (value === 'aaa' ? 'aaa' : 'bbb')); 15 | 16 | expectType(stringShape[INPUT]); 17 | 18 | expectType(stringShape[OUTPUT]); 19 | 20 | expectType<'bbb'>(stringShape.refine((_value): _value is 'bbb' => true)[OUTPUT]); 21 | 22 | expectType(stringShape.refine((_value): _value is 'bbb' => true).max(2)[OUTPUT]); 23 | 24 | expectType( 25 | d.string().alter( 26 | (value, param) => { 27 | expectType(param); 28 | return value; 29 | }, 30 | { param: 111 } 31 | )[OUTPUT] 32 | ); 33 | 34 | expectType<'aaa' | 'bbb'>(d.string().refine((_value): _value is 'aaa' | 'bbb' => true)[OUTPUT]); 35 | -------------------------------------------------------------------------------- /src/test/dsl/string.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | import { Type } from '../../main/Type.js'; 4 | 5 | describe('string', () => { 6 | test('returns a string shape', () => { 7 | expect(d.string()).toBeInstanceOf(d.StringShape); 8 | }); 9 | 10 | test('returns inputs for optional string', () => { 11 | expect(d.string().optional().inputs).toEqual([Type.STRING, undefined]); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/test/dsl/symbol.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('symbol', () => { 5 | test('returns a symbol shape', () => { 6 | expect(d.symbol()).toBeInstanceOf(d.SymbolShape); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/test/dsl/tuple.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectNotType, expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const OUTPUT: OUTPUT; 6 | 7 | expectType<[string, number]>(d.tuple([d.string(), d.number()])[OUTPUT]); 8 | 9 | expectNotType<[string, number, ...unknown[]]>(d.tuple([d.string(), d.number()])[OUTPUT]); 10 | 11 | expectType(d.tuple([d.string(), d.number()]).headShapes[1][OUTPUT]); 12 | 13 | expectType<[string, number, ...boolean[]]>(d.tuple([d.string(), d.number()], d.boolean())[OUTPUT]); 14 | -------------------------------------------------------------------------------- /src/test/dsl/tuple.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('tuple', () => { 5 | test('returns an array shape', () => { 6 | expect(d.tuple([d.number()])).toBeInstanceOf(d.ArrayShape); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/test/dsl/undefined.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('undefined', () => { 5 | test('returns a const shape', () => { 6 | const shape = d.undefined(); 7 | 8 | expect(shape).toBeInstanceOf(d.ConstShape); 9 | expect(shape.value).toBeUndefined(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/test/dsl/union.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd'; 2 | import * as d from '../../main/index.js'; 3 | import { type OUTPUT } from '../../main/shape/Shape.js'; 4 | 5 | declare const OUTPUT: OUTPUT; 6 | 7 | expectType(d.or([d.string(), d.number(), d.boolean()])[OUTPUT]); 8 | 9 | expectType(d.or([d.string(), d.never()])[OUTPUT]); 10 | 11 | expectType(d.or([d.string(), d.any()])[OUTPUT]); 12 | 13 | expectType(d.or([d.string(), d.unknown()])[OUTPUT]); 14 | -------------------------------------------------------------------------------- /src/test/dsl/union.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | import { Type } from '../../main/Type.js'; 4 | 5 | describe('union', () => { 6 | test('returns a union shape', () => { 7 | const shape = d.union([d.string(), d.number()]); 8 | 9 | expect(shape).toBeInstanceOf(d.UnionShape); 10 | expect(shape.inputs).toEqual([Type.STRING, Type.NUMBER]); 11 | 12 | expect(d.or([d.string()])).toBeInstanceOf(d.UnionShape); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/test/dsl/unknonwn.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('unknown', () => { 5 | test('returns a shape', () => { 6 | expect(d.unknown()).toBeInstanceOf(d.Shape); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/test/dsl/void.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import * as d from '../../main/index.js'; 3 | 4 | describe('void', () => { 5 | test('returns a const shape', () => { 6 | const shape = d.void(); 7 | 8 | expect(shape).toBeInstanceOf(d.ConstShape); 9 | expect(shape.value).toBeUndefined(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/test/internal/arrays.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { toArrayIndex, unique } from '../../main/internal/arrays.js'; 3 | 4 | describe('unique', () => { 5 | test('returns an array as is', () => { 6 | const arr1 = [NaN]; 7 | const arr2 = [1, 2, 3]; 8 | 9 | expect(unique(arr1)).toBe(arr1); 10 | expect(unique(arr2)).toBe(arr2); 11 | }); 12 | 13 | test('returns an array of unique values', () => { 14 | const arr = [1, 1, 1]; 15 | expect(unique(arr)).not.toBe(arr); 16 | expect(unique(arr)).toEqual([1]); 17 | }); 18 | 19 | test('preserves the first value entry', () => { 20 | const arr1 = [NaN, 1, NaN, 2, NaN]; 21 | const arr2 = [1, 2, 1]; 22 | 23 | expect(unique(arr1)).not.toBe(arr1); 24 | expect(unique(arr1)).toEqual([NaN, 1, 2]); 25 | 26 | expect(unique(arr2)).not.toBe(arr2); 27 | expect(unique(arr2)).toEqual([1, 2]); 28 | }); 29 | }); 30 | 31 | describe('toArrayIndex', () => { 32 | test('returns an array index', () => { 33 | expect(toArrayIndex('0')).toBe(0); 34 | expect(toArrayIndex('1')).toBe(1); 35 | expect(toArrayIndex('2')).toBe(2); 36 | 37 | expect(toArrayIndex(0)).toBe(0); 38 | expect(toArrayIndex(1)).toBe(1); 39 | expect(toArrayIndex(2)).toBe(2); 40 | }); 41 | 42 | test('returns -1 if value is not an array index', () => { 43 | expect(toArrayIndex('-5')).toBe(-1); 44 | expect(toArrayIndex('0xa')).toBe(-1); 45 | expect(toArrayIndex('016')).toBe(-1); 46 | expect(toArrayIndex('000')).toBe(-1); 47 | expect(toArrayIndex('1e+49')).toBe(-1); 48 | expect(toArrayIndex(-111)).toBe(-1); 49 | expect(toArrayIndex(111.222)).toBe(-1); 50 | expect(toArrayIndex('aaa')).toBe(-1); 51 | expect(toArrayIndex(NaN)).toBe(-1); 52 | expect(toArrayIndex(new Date())).toBe(-1); 53 | expect(toArrayIndex({ valueOf: () => 2 })).toBe(-1); 54 | expect(toArrayIndex({ toString: () => '2' })).toBe(-1); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/test/internal/bitmasks.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { getBit, toggleBit } from '../../main/internal/bitmasks.js'; 3 | 4 | describe('toggleBit', () => { 5 | test('sets bit', () => { 6 | expect(toggleBit(0b0, 5)).toBe(0b100000); 7 | expect(toggleBit(0b1, 5)).toBe(0b100001); 8 | expect(toggleBit(0b100, 5)).toBe(0b100100); 9 | expect(toggleBit(0, 31)).toBe(-2147483648); 10 | expect(toggleBit(0, 32)).toEqual([0, 1, 0]); 11 | expect(toggleBit(0, 35)).toEqual([0, 0b1000, 0]); 12 | }); 13 | 14 | test('removes bit', () => { 15 | expect(toggleBit(toggleBit(0b1, 5), 5)).toBe(0b1); 16 | }); 17 | }); 18 | 19 | describe('getBit', () => { 20 | test('get the bit status', () => { 21 | expect(getBit(toggleBit(toggleBit(0, 1), 2), 3)).toBe(0); 22 | expect(getBit(toggleBit(toggleBit(0, 1), 2), 2)).toBe(1); 23 | expect(getBit(toggleBit(toggleBit(0, 1), 2), 1)).toBe(1); 24 | expect(getBit(toggleBit(toggleBit(0, 1), 2), 1)).toBe(1); 25 | 26 | expect(getBit(toggleBit(toggleBit(0, 91), 92), 93)).toBe(0); 27 | expect(getBit(toggleBit(toggleBit(0, 91), 92), 92)).toBe(1); 28 | expect(getBit(toggleBit(toggleBit(0, 91), 92), 91)).toBe(1); 29 | }); 30 | 31 | test('edge cases', () => { 32 | expect(getBit(toggleBit(0b1, 31), 31)).toBe(1); 33 | expect(getBit(toggleBit(0b1, 32), 32)).toBe(1); 34 | 35 | expect(getBit(0, 2 ** 32)).toBe(0); 36 | 37 | expect(getBit(toggleBit(0, 2 ** 31), 2 ** 31 - 128)).toBe(0); 38 | 39 | expect(getBit(toggleBit(0, 2 ** 31 - 1), 2 ** 31 - 1)).toBe(1); 40 | 41 | expect(getBit([0], 2)).toBe(0); 42 | expect(getBit([0], 2 ** 31 - 1)).toBe(0); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/test/internal/lang.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { 3 | getCanonicalValue, 4 | isEqual, 5 | isEqualOrSubclass, 6 | isIterableObject, 7 | isValidDate, 8 | } from '../../main/internal/lang.js'; 9 | 10 | describe('isEqual', () => { 11 | test('checks equality', () => { 12 | expect(isEqual(NaN, NaN)).toBe(true); 13 | expect(isEqual(111, 111)).toBe(true); 14 | expect(isEqual(111, 222)).toBe(false); 15 | expect(isEqual({}, {})).toBe(false); 16 | }); 17 | 18 | test('0 is equal -0', () => { 19 | expect(isEqual(0, -0)).toBe(true); 20 | }); 21 | }); 22 | 23 | describe('isIterableObject', () => { 24 | test('returns value type', () => { 25 | expect(isIterableObject(new Map())).toBe(true); 26 | expect(isIterableObject(new Set())).toBe(true); 27 | expect(isIterableObject([])).toBe(true); 28 | expect(isIterableObject({ [Symbol.iterator]: 111 })).toBe(true); 29 | expect(isIterableObject({ [Symbol.iterator]: () => null })).toBe(true); 30 | expect(isIterableObject({ length: null })).toBe(true); 31 | expect(isIterableObject({ length: 111 })).toBe(true); 32 | expect(isIterableObject({ length: '111' })).toBe(true); 33 | expect(isIterableObject({ length: { valueOf: () => 111 } })).toBe(true); 34 | 35 | expect(isIterableObject({ length: undefined })).toBe(false); 36 | expect(isIterableObject({ length: 'aaa' })).toBe(false); 37 | expect(isIterableObject('')).toBe(false); 38 | }); 39 | }); 40 | 41 | describe('isEqualOrSubclass', () => { 42 | test('returns true if a descendant class', () => { 43 | expect(isEqualOrSubclass(Number, Number)).toBe(true); 44 | expect(isEqualOrSubclass(Number, Object)).toBe(true); 45 | expect(isEqualOrSubclass(Number, String)).toBe(false); 46 | }); 47 | }); 48 | 49 | describe('isValidDate', () => { 50 | test('returns true if date and time is not NaN', () => { 51 | expect(isValidDate(111)).toBe(false); 52 | expect(isValidDate(new Date(NaN))).toBe(false); 53 | expect(isValidDate(new Date(111))).toBe(true); 54 | }); 55 | }); 56 | 57 | describe('getCanonicalValue', () => { 58 | test('unwraps primitives', () => { 59 | expect(getCanonicalValue(new String('aaa'))).toBe('aaa'); 60 | expect(getCanonicalValue(new Number(111))).toBe(111); 61 | expect(getCanonicalValue(new Boolean(true))).toBe(true); 62 | expect(getCanonicalValue(Object(BigInt(111)))).toBe(BigInt(111)); 63 | expect(getCanonicalValue(Object(Symbol.for('aaa')))).toBe(Symbol.for('aaa')); 64 | }); 65 | 66 | test('preserves objects as is', () => { 67 | const obj = { valueOf: () => 111 }; 68 | 69 | expect(getCanonicalValue(obj)).toBe(obj); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/test/internal/objects.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { cloneObject, cloneRecord, pickKeys, setProperty } from '../../main/internal/objects.js'; 3 | 4 | describe('setProperty', () => { 5 | test('sets object value', () => { 6 | const obj: any = {}; 7 | 8 | setProperty(obj, 'aaa', 111); 9 | 10 | expect(obj.hasOwnProperty('aaa')).toBe(true); 11 | expect(obj.aaa).toBe(111); 12 | }); 13 | 14 | test('sets __proto__ value', () => { 15 | const obj: any = {}; 16 | 17 | setProperty(obj, '__proto__', 111); 18 | 19 | expect(obj.hasOwnProperty('__proto__')).toBe(true); 20 | expect(obj.__proto__).toBe(111); 21 | }); 22 | }); 23 | 24 | describe('cloneObject', () => { 25 | test('clones all keys', () => { 26 | const dict = { aaa: 111, bbb: 222 }; 27 | const obj = cloneObject(dict); 28 | 29 | expect(dict).not.toBe(obj); 30 | expect(obj).toEqual({ aaa: 111, bbb: 222 }); 31 | }); 32 | 33 | test('preserves prototype', () => { 34 | class TestClass { 35 | aaa = 111; 36 | } 37 | 38 | const dict = new TestClass(); 39 | dict.aaa = 222; 40 | 41 | const obj = cloneObject(dict); 42 | 43 | expect(dict).not.toBe(obj); 44 | expect(obj).toBeInstanceOf(TestClass); 45 | expect(obj.aaa).toBe(222); 46 | }); 47 | }); 48 | 49 | describe('cloneRecord', () => { 50 | test('clones limited number of keys', () => { 51 | const dict = { aaa: 111, bbb: 222 }; 52 | const obj = cloneRecord(dict, 1); 53 | 54 | expect(dict).not.toBe(obj); 55 | expect(obj).toEqual({ aaa: 111 }); 56 | }); 57 | 58 | test('does not copy keys', () => { 59 | const dict = { aaa: 111, bbb: 222 }; 60 | const obj = cloneRecord(dict, 0); 61 | 62 | expect(dict).not.toBe(obj); 63 | expect(obj).toEqual({}); 64 | }); 65 | 66 | test('preserves prototype', () => { 67 | class TestClass { 68 | aaa = 111; 69 | } 70 | 71 | const dict = new TestClass(); 72 | dict.aaa = 222; 73 | 74 | const obj = cloneRecord(dict, 1); 75 | 76 | expect(dict).not.toBe(obj); 77 | expect(obj).toBeInstanceOf(TestClass); 78 | expect(obj.aaa).toBe(222); 79 | }); 80 | }); 81 | 82 | describe('pickKeys', () => { 83 | test('clones existing keys', () => { 84 | const dict = { aaa: 111, bbb: 222 }; 85 | const obj = pickKeys(dict, ['bbb']); 86 | 87 | expect(dict).not.toBe(obj); 88 | expect(obj).toEqual({ bbb: 222 }); 89 | }); 90 | 91 | test('clones known keys', () => { 92 | const dict = { aaa: 111, bbb: 222 }; 93 | const obj = pickKeys(dict, ['aaa', 'ccc']); 94 | 95 | expect(dict).not.toBe(obj); 96 | expect(obj).toEqual({ aaa: 111 }); 97 | }); 98 | 99 | test('preserves prototype', () => { 100 | class TestClass { 101 | aaa = 111; 102 | bbb = 222; 103 | } 104 | 105 | const dict = new TestClass(); 106 | const obj = pickKeys(dict, ['aaa']); 107 | 108 | expect(dict).not.toBe(obj); 109 | expect(obj).toBeInstanceOf(TestClass); 110 | expect(obj.aaa).toBe(111); 111 | expect(obj.bbb).toBeUndefined(); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /src/test/internal/types.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest'; 2 | import { distributeTypes, unionTypes } from '../../main/internal/types.js'; 3 | import { Type } from '../../main/Type.js'; 4 | 5 | test('unionTypes', () => { 6 | expect(unionTypes([])).toEqual([]); 7 | expect(unionTypes([Type.UNKNOWN])).toEqual([Type.UNKNOWN]); 8 | expect(unionTypes([Type.UNKNOWN, Type.UNKNOWN])).toEqual([Type.UNKNOWN]); 9 | expect(unionTypes([111])).toEqual([111]); 10 | expect(unionTypes([111, 111])).toEqual([111]); 11 | expect(unionTypes([111, Type.NUMBER])).toEqual([Type.NUMBER]); 12 | expect(unionTypes([Type.NUMBER, 111])).toEqual([Type.NUMBER]); 13 | expect(unionTypes([Type.NUMBER, Type.STRING])).toEqual([Type.NUMBER, Type.STRING]); 14 | expect(unionTypes([Type.NUMBER, Type.NUMBER])).toEqual([Type.NUMBER]); 15 | expect(unionTypes([111, 'aaa'])).toEqual([111, 'aaa']); 16 | expect(unionTypes([111, 'aaa', Type.UNKNOWN])).toEqual([Type.UNKNOWN]); 17 | expect(unionTypes([Type.STRING, Type.UNKNOWN])).toEqual([Type.UNKNOWN]); 18 | }); 19 | 20 | test('distributeTypes', () => { 21 | expect(distributeTypes([])).toEqual([]); 22 | expect(distributeTypes([[], []])).toEqual([]); 23 | expect(distributeTypes([[Type.STRING], []])).toEqual([]); 24 | expect(distributeTypes([[], [Type.STRING]])).toEqual([]); 25 | expect(distributeTypes([[Type.STRING], [Type.STRING]])).toEqual([Type.STRING]); 26 | expect(distributeTypes([[Type.STRING], [Type.NUMBER]])).toEqual([]); 27 | expect(distributeTypes([[Type.STRING], ['aaa']])).toEqual(['aaa']); 28 | expect(distributeTypes([[Type.STRING], ['aaa', Type.NUMBER]])).toEqual(['aaa']); 29 | expect(distributeTypes([['aaa'], [Type.STRING, Type.NUMBER]])).toEqual(['aaa']); 30 | expect( 31 | distributeTypes([ 32 | [111, 'aaa'], 33 | ['aaa', 111], 34 | ]) 35 | ).toEqual([111, 'aaa']); 36 | expect(distributeTypes([[111, 'aaa'], ['aaa']])).toEqual(['aaa']); 37 | expect( 38 | distributeTypes([ 39 | [111, 'aaa'], 40 | [Type.NUMBER, Type.STRING], 41 | ]) 42 | ).toEqual([111, 'aaa']); 43 | }); 44 | -------------------------------------------------------------------------------- /src/test/perf/any.perf.js: -------------------------------------------------------------------------------- 1 | import { describe, test, measure } from 'toofast'; 2 | import * as valita from '@badrap/valita'; 3 | import * as doubter from '../../../lib/index.mjs'; 4 | 5 | describe('any().check(isNaN)', () => { 6 | test('valita', () => { 7 | const type = valita.unknown().assert(isNaN); 8 | 9 | measure(() => { 10 | type.parse(NaN); 11 | }); 12 | }); 13 | 14 | test('doubter', () => { 15 | const shape = doubter.any().check(v => (isNaN(v) ? null : [])); 16 | 17 | measure(() => { 18 | shape.parse(NaN); 19 | }); 20 | }); 21 | }); 22 | 23 | describe('any().refine(isNaN)', () => { 24 | test('valita', () => { 25 | const type = valita.unknown().assert(isNaN); 26 | 27 | measure(() => { 28 | type.parse(NaN); 29 | }); 30 | }); 31 | 32 | test('doubter', () => { 33 | const shape = doubter.any().refine(isNaN); 34 | 35 | measure(() => { 36 | shape.parse(NaN); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/test/perf/function.perf.js: -------------------------------------------------------------------------------- 1 | import { describe, measure, test } from 'toofast'; 2 | import * as zod from 'zod'; 3 | import * as doubter from '../../../lib/index.mjs'; 4 | 5 | describe('fn([number(), number()]).ensure(…)', () => { 6 | test('zod', () => { 7 | const fn = zod.function(zod.tuple([zod.number(), zod.number()])).implement((a, b) => a + b); 8 | 9 | measure(() => { 10 | fn(1, 2); 11 | }); 12 | }); 13 | 14 | test('doubter', () => { 15 | const fn = doubter.fn([doubter.number(), doubter.number()]).ensure((a, b) => a + b); 16 | 17 | measure(() => { 18 | fn(1, 2); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/test/perf/inspect.perf.js: -------------------------------------------------------------------------------- 1 | import { describe, measure, test } from 'toofast'; 2 | import { inspect } from '../../../lib/utils.mjs'; 3 | 4 | describe('inspect', () => { 5 | const value = { 6 | a1: [1, 2, 3], 7 | a2: 'foo', 8 | a3: false, 9 | a4: { 10 | a41: 'bar', 11 | a42: 3.1415, 12 | }, 13 | }; 14 | 15 | test('inspect(…)', () => { 16 | measure(() => { 17 | inspect(value); 18 | }); 19 | }); 20 | 21 | test('JSON.stringify(…)', () => { 22 | measure(() => { 23 | JSON.stringify(value); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/test/perf/intersection.perf.js: -------------------------------------------------------------------------------- 1 | import { describe, measure, test } from 'toofast'; 2 | import * as myzod from 'myzod'; 3 | import * as zod from 'zod'; 4 | import * as doubter from '../../../lib/index.mjs'; 5 | 6 | describe('and([object({ foo: string() }), object({ bar: number() })])', () => { 7 | const value = { foo: 'aaa', bar: 123 }; 8 | 9 | test('zod', () => { 10 | const type = zod.intersection( 11 | zod.object({ foo: zod.string() }).passthrough(), 12 | zod.object({ bar: zod.number() }).passthrough() 13 | ); 14 | 15 | measure(() => { 16 | type.parse(value); 17 | }); 18 | }); 19 | 20 | test('myzod', () => { 21 | const type = myzod.intersection( 22 | myzod.object({ foo: myzod.string() }, { allowUnknown: true }), 23 | myzod.object({ bar: myzod.number() }, { allowUnknown: true }) 24 | ); 25 | 26 | measure(() => { 27 | type.parse(value); 28 | }); 29 | }); 30 | 31 | test('doubter', () => { 32 | const shape = doubter.and([doubter.object({ foo: doubter.string() }), doubter.object({ bar: doubter.number() })]); 33 | 34 | measure(() => { 35 | shape.parse(value); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/test/perf/lazy.perf.js: -------------------------------------------------------------------------------- 1 | import { describe, measure, test } from 'toofast'; 2 | import * as doubter from '../../../lib/index.mjs'; 3 | 4 | describe('lazy(() => object(…))', () => { 5 | const value = {}; 6 | value.value = value; 7 | 8 | test('doubter', () => { 9 | const shape = doubter.lazy(() => doubter.object({ value: shape })); 10 | 11 | measure(() => { 12 | shape.parse(value); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/test/perf/number.perf.js: -------------------------------------------------------------------------------- 1 | import { describe, measure, test } from 'toofast'; 2 | import * as valita from '@badrap/valita'; 3 | import { Ajv } from 'ajv'; 4 | import * as myzod from 'myzod'; 5 | import * as zod from 'zod'; 6 | import * as doubter from '../../../lib/index.mjs'; 7 | 8 | describe('number()', () => { 9 | const value = 4; 10 | 11 | test('Ajv', () => { 12 | const validate = new Ajv().compile({ 13 | $schema: 'http://json-schema.org/draft-07/schema#', 14 | type: 'number', 15 | }); 16 | 17 | measure(() => { 18 | validate(value); 19 | }); 20 | }); 21 | 22 | test('zod', () => { 23 | const type = zod.number(); 24 | 25 | measure(() => { 26 | type.parse(value); 27 | }); 28 | }); 29 | 30 | test('myzod', () => { 31 | const type = myzod.number(); 32 | 33 | measure(() => { 34 | type.parse(value); 35 | }); 36 | }); 37 | 38 | test('valita', () => { 39 | const type = valita.number(); 40 | 41 | measure(() => { 42 | type.parse(value); 43 | }); 44 | }); 45 | 46 | test('doubter', () => { 47 | const shape = doubter.number(); 48 | 49 | measure(() => { 50 | shape.parse(value); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('number().multipleOf(1)', () => { 56 | const value = 49; 57 | 58 | test('Ajv', () => { 59 | const validate = new Ajv().compile({ 60 | $schema: 'http://json-schema.org/draft-07/schema#', 61 | type: 'number', 62 | multipleOf: 1, 63 | }); 64 | 65 | measure(() => { 66 | validate(value); 67 | }); 68 | }); 69 | 70 | test('zod', () => { 71 | const type = zod.number().multipleOf(1); 72 | 73 | measure(() => { 74 | type.parse(value); 75 | }); 76 | }); 77 | 78 | test('doubter', () => { 79 | const shape = doubter.number().multipleOf(1); 80 | 81 | measure(() => { 82 | shape.parse(value); 83 | }); 84 | }); 85 | }); 86 | 87 | describe('number().gte(1).lte(5)', () => { 88 | const value = 4; 89 | 90 | test('Ajv', () => { 91 | const validate = new Ajv().compile({ 92 | $schema: 'http://json-schema.org/draft-07/schema#', 93 | type: 'number', 94 | minimum: 1, 95 | maximum: 5, 96 | }); 97 | 98 | measure(() => { 99 | validate(value); 100 | }); 101 | }); 102 | 103 | test('zod', () => { 104 | const type = zod.number().min(1).max(5); 105 | 106 | measure(() => { 107 | type.parse(value); 108 | }); 109 | }); 110 | 111 | test('myzod', () => { 112 | const type = myzod.number().min(1).max(5); 113 | 114 | measure(() => { 115 | type.parse(value); 116 | }); 117 | }); 118 | 119 | test('doubter', () => { 120 | const shape = doubter.number().gte(1).lte(5); 121 | 122 | measure(() => { 123 | shape.parse(value); 124 | }); 125 | }); 126 | }); 127 | 128 | describe('number().int()', () => { 129 | const value = 4; 130 | 131 | test('Ajv', () => { 132 | const validate = new Ajv().compile({ 133 | $schema: 'http://json-schema.org/draft-07/schema#', 134 | type: 'integer', 135 | }); 136 | 137 | measure(() => { 138 | validate(value); 139 | }); 140 | }); 141 | 142 | test('zod', () => { 143 | const type = zod.number().int(); 144 | 145 | measure(() => { 146 | type.parse(value); 147 | }); 148 | }); 149 | 150 | test('doubter', () => { 151 | const shape = doubter.number().int(); 152 | 153 | measure(() => { 154 | shape.parse(value); 155 | }); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /src/test/perf/record.perf.js: -------------------------------------------------------------------------------- 1 | import { describe, measure, test } from 'toofast'; 2 | import * as valita from '@badrap/valita'; 3 | import { Ajv } from 'ajv'; 4 | import * as myzod from 'myzod'; 5 | import * as zod from 'zod'; 6 | import * as doubter from '../../../lib/index.mjs'; 7 | 8 | describe('record(string(), number())', () => { 9 | const value = { a: 1, b: 2, c: 3 }; 10 | 11 | test('Ajv', () => { 12 | const validate = new Ajv().compile({ 13 | $schema: 'http://json-schema.org/draft-07/schema#', 14 | type: 'object', 15 | propertyNames: { type: 'string' }, 16 | additionalProperties: { type: 'number' }, 17 | }); 18 | 19 | measure(() => { 20 | validate(value); 21 | }); 22 | }); 23 | 24 | test('zod', () => { 25 | const type = zod.record(zod.string(), zod.number()); 26 | 27 | measure(() => { 28 | type.parse(value); 29 | }); 30 | }); 31 | 32 | test('myzod', () => { 33 | const type = myzod.record(myzod.number()); 34 | 35 | measure(() => { 36 | type.parse(value); 37 | }); 38 | }); 39 | 40 | test('valita', () => { 41 | const type = valita.record(valita.number()); 42 | 43 | measure(() => { 44 | type.parse(value); 45 | }); 46 | }); 47 | 48 | test('doubter', () => { 49 | const shape = doubter.record(doubter.string(), doubter.number()); 50 | 51 | measure(() => { 52 | shape.parse(value); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/test/perf/set.perf.js: -------------------------------------------------------------------------------- 1 | import { describe, measure, test } from 'toofast'; 2 | import { Ajv } from 'ajv'; 3 | import * as zod from 'zod'; 4 | import * as doubter from '../../../lib/index.mjs'; 5 | 6 | describe('set(number())', () => { 7 | const value = new Set([111, 222]); 8 | const ajvValue = Array.from(value); 9 | 10 | test('Ajv', () => { 11 | const ajv = new Ajv(); 12 | 13 | const schema = { 14 | $id: 'test', 15 | $schema: 'http://json-schema.org/draft-07/schema#', 16 | type: 'array', 17 | items: { 18 | type: 'number', 19 | }, 20 | uniqueItems: true, 21 | }; 22 | 23 | const validate = ajv.compile(schema); 24 | 25 | measure(() => { 26 | validate(ajvValue); 27 | }); 28 | }); 29 | 30 | test('zod', () => { 31 | const type = zod.set(zod.number()); 32 | 33 | measure(() => { 34 | type.parse(value); 35 | }); 36 | }); 37 | 38 | test('doubter', () => { 39 | const shape = doubter.set(doubter.number()); 40 | 41 | measure(() => { 42 | shape.parse(value); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/test/plugin/array-essentials.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { ArrayShape, ConstShape, Shape } from '../../main/index.js'; 3 | import { CODE_ARRAY_INCLUDES, CODE_ARRAY_MAX, CODE_ARRAY_MIN, MESSAGE_ARRAY_INCLUDES } from '../../main/constants.js'; 4 | 5 | describe('length', () => { 6 | test('checks length', () => { 7 | const shape = new ArrayShape([], new Shape()).length(2); 8 | 9 | expect(shape.try([111, 222])).toEqual({ ok: true, value: [111, 222] }); 10 | expect(shape.try([111])).toEqual({ ok: false, issues: expect.any(Array) }); 11 | expect(shape.try([111, 222, 333])).toEqual({ ok: false, issues: expect.any(Array) }); 12 | }); 13 | }); 14 | 15 | describe('min', () => { 16 | test('checks min length', () => { 17 | const shape = new ArrayShape([], new Shape()).min(2); 18 | 19 | expect(shape.try([111, 222])).toEqual({ ok: true, value: [111, 222] }); 20 | expect(shape.try([111])).toEqual({ 21 | ok: false, 22 | issues: [{ code: CODE_ARRAY_MIN, input: [111], message: 'Must have the minimum length of 2', param: 2 }], 23 | }); 24 | }); 25 | }); 26 | 27 | describe('max', () => { 28 | test('checks max length', () => { 29 | const shape = new ArrayShape([], new Shape()).max(2); 30 | 31 | expect(shape.try([111, 222])).toEqual({ ok: true, value: [111, 222] }); 32 | expect(shape.try([111, 222, 333])).toEqual({ 33 | ok: false, 34 | issues: [ 35 | { code: CODE_ARRAY_MAX, input: [111, 222, 333], message: 'Must have the maximum length of 2', param: 2 }, 36 | ], 37 | }); 38 | }); 39 | }); 40 | 41 | describe('nonEmpty', () => { 42 | test('checks min length', () => { 43 | const shape = new ArrayShape([], new Shape()).nonEmpty(); 44 | 45 | expect(shape.try([111])).toEqual({ ok: true, value: [111] }); 46 | expect(shape.try([])).toEqual({ 47 | ok: false, 48 | issues: [{ code: CODE_ARRAY_MIN, input: [], message: 'Must have the minimum length of 1', param: 1 }], 49 | }); 50 | }); 51 | }); 52 | 53 | describe('includes', () => { 54 | test('checks that an array includes a literal value', () => { 55 | const shape = new ArrayShape([], new Shape()).includes(111); 56 | 57 | expect(shape.try([111])).toEqual({ ok: true, value: [111] }); 58 | expect(shape.try([222])).toEqual({ 59 | ok: false, 60 | issues: [{ code: CODE_ARRAY_INCLUDES, input: [222], message: MESSAGE_ARRAY_INCLUDES, param: 111 }], 61 | }); 62 | }); 63 | 64 | test('checks that an array includes an element that conforms the shape', () => { 65 | const shape = new ArrayShape([], new Shape()).includes(new ConstShape(111)); 66 | 67 | expect(shape.isAsync).toBe(false); 68 | expect(shape.try([111])).toEqual({ ok: true, value: [111] }); 69 | expect(shape.try([222])).toEqual({ 70 | ok: false, 71 | issues: [ 72 | { 73 | code: CODE_ARRAY_INCLUDES, 74 | input: [222], 75 | message: MESSAGE_ARRAY_INCLUDES, 76 | param: expect.any(ConstShape), 77 | }, 78 | ], 79 | }); 80 | }); 81 | 82 | test('checks that an array includes an element that conforms the async shape', async () => { 83 | const shape = new ArrayShape([], new Shape()).includes(new ConstShape(222).checkAsync(() => Promise.resolve())); 84 | 85 | expect(shape.isAsync).toBe(true); 86 | 87 | await expect(shape.tryAsync([111, 222, 333])).resolves.toEqual({ ok: true, value: [111, 222, 333] }); 88 | await expect(shape.tryAsync([111, 333])).resolves.toEqual({ 89 | ok: false, 90 | issues: [ 91 | { 92 | code: CODE_ARRAY_INCLUDES, 93 | input: [111, 333], 94 | message: MESSAGE_ARRAY_INCLUDES, 95 | param: expect.any(ConstShape), 96 | }, 97 | ], 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/test/plugin/bigint-essentials.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { BigIntShape } from '../../main/index.js'; 3 | import { CODE_BIGINT_MAX, CODE_BIGINT_MIN } from '../../main/constants.js'; 4 | 5 | describe('min', () => { 6 | test('raises if value is not greater than or equal', () => { 7 | expect(new BigIntShape().min(2).try(BigInt(1))).toEqual({ 8 | ok: false, 9 | issues: [ 10 | { code: CODE_BIGINT_MIN, input: BigInt(1), param: BigInt(2), message: 'Must be greater than or equal to 2n' }, 11 | ], 12 | }); 13 | 14 | expect(new BigIntShape().min(2).parse(BigInt(2))).toBe(BigInt(2)); 15 | }); 16 | 17 | test('overrides message', () => { 18 | expect(new BigIntShape().min(2, { message: 'xxx', meta: 'yyy' }).try(BigInt(0))).toEqual({ 19 | ok: false, 20 | issues: [{ code: CODE_BIGINT_MIN, input: BigInt(0), param: BigInt(2), message: 'xxx', meta: 'yyy' }], 21 | }); 22 | }); 23 | }); 24 | 25 | describe('max', () => { 26 | test('raises if value is not less than or equal', () => { 27 | expect(new BigIntShape().max(2).try(BigInt(3))).toEqual({ 28 | ok: false, 29 | issues: [ 30 | { code: CODE_BIGINT_MAX, input: BigInt(3), param: BigInt(2), message: 'Must be less than or equal to 2n' }, 31 | ], 32 | }); 33 | 34 | expect(new BigIntShape().max(2).try(BigInt(3))).toEqual({ 35 | ok: false, 36 | issues: [ 37 | { code: CODE_BIGINT_MAX, input: BigInt(3), param: BigInt(2), message: 'Must be less than or equal to 2n' }, 38 | ], 39 | }); 40 | 41 | expect(new BigIntShape().max(2).parse(BigInt(2))).toBe(BigInt(2)); 42 | }); 43 | 44 | test('overrides message', () => { 45 | expect(new BigIntShape().max(2, { message: 'xxx', meta: 'yyy' }).try(BigInt(3))).toEqual({ 46 | ok: false, 47 | issues: [{ code: CODE_BIGINT_MAX, input: BigInt(3), param: BigInt(2), message: 'xxx', meta: 'yyy' }], 48 | }); 49 | }); 50 | }); 51 | 52 | describe('positive/nonNegative', () => { 53 | test('raises if value is not positive', () => { 54 | expect(new BigIntShape().positive().try(BigInt(-111)).ok).toBe(false); 55 | expect(new BigIntShape().nonNegative().try(BigInt(-111)).ok).toBe(false); 56 | 57 | expect(new BigIntShape().positive().try(BigInt(222)).ok).toBe(true); 58 | expect(new BigIntShape().nonNegative().try(BigInt(222)).ok).toBe(true); 59 | }); 60 | }); 61 | 62 | describe('negative/nonPositive', () => { 63 | test('raises if value is not negative', () => { 64 | expect(new BigIntShape().negative().try(BigInt(111)).ok).toBe(false); 65 | expect(new BigIntShape().nonPositive().try(BigInt(111)).ok).toBe(false); 66 | 67 | expect(new BigIntShape().negative().try(BigInt(-222)).ok).toBe(true); 68 | expect(new BigIntShape().nonPositive().try(BigInt(-222)).ok).toBe(true); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/test/plugin/date-essentials.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { DateShape } from '../../main/index.js'; 3 | import { CODE_DATE_MAX, CODE_DATE_MIN } from '../../main/constants.js'; 4 | 5 | const currDate = new Date(Date.UTC(2024, 8, 2, 16, 15, 53, 299)); 6 | const prevDate = new Date(currDate.getTime() - 1); 7 | const nextDate = new Date(currDate.getTime() + 1); 8 | 9 | describe('min', () => { 10 | test('raises if value is not greater than or equal', () => { 11 | expect(new DateShape().min(currDate).try(prevDate)).toEqual({ 12 | ok: false, 13 | issues: [ 14 | { code: CODE_DATE_MIN, input: prevDate, param: currDate, message: 'Must be after 2024-09-02T16:15:53.299Z' }, 15 | ], 16 | }); 17 | 18 | expect(new DateShape().min(currDate).parse(currDate)).toBe(currDate); 19 | expect(new DateShape().min(currDate).parse(nextDate)).toBe(nextDate); 20 | }); 21 | 22 | test('overrides message', () => { 23 | expect(new DateShape().min(currDate, { message: 'xxx', meta: 'yyy' }).try(prevDate)).toEqual({ 24 | ok: false, 25 | issues: [{ code: CODE_DATE_MIN, input: prevDate, param: currDate, message: 'xxx', meta: 'yyy' }], 26 | }); 27 | }); 28 | }); 29 | 30 | describe('max', () => { 31 | test('raises if value is not greater than or equal', () => { 32 | expect(new DateShape().max(currDate).try(nextDate)).toEqual({ 33 | ok: false, 34 | issues: [ 35 | { code: CODE_DATE_MAX, input: nextDate, param: currDate, message: 'Must be before 2024-09-02T16:15:53.299Z' }, 36 | ], 37 | }); 38 | 39 | expect(new DateShape().max(currDate).parse(currDate)).toBe(currDate); 40 | expect(new DateShape().max(currDate).parse(prevDate)).toBe(prevDate); 41 | }); 42 | 43 | test('overrides message', () => { 44 | expect(new DateShape().max(currDate, { message: 'xxx', meta: 'yyy' }).try(nextDate)).toEqual({ 45 | ok: false, 46 | issues: [{ code: CODE_DATE_MAX, input: nextDate, param: currDate, message: 'xxx', meta: 'yyy' }], 47 | }); 48 | }); 49 | }); 50 | 51 | describe('toISOString', () => { 52 | test('converts date to ISO string', () => { 53 | expect(new DateShape().toISOString().parse(currDate)).toBe(currDate.toISOString()); 54 | }); 55 | }); 56 | 57 | describe('toTimestamp', () => { 58 | test('converts date to timestamp', () => { 59 | expect(new DateShape().toTimestamp().parse(currDate)).toBe(currDate.getTime()); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/test/plugin/object-eval.test.ts: -------------------------------------------------------------------------------- 1 | import '../../main/plugin/object-eval'; 2 | import '../shape/ObjectShape.test'; 3 | 4 | // Runs ObjectShape tests with runtime code evaluation enabled 5 | -------------------------------------------------------------------------------- /src/test/plugin/set-essentials.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { SetShape, Shape } from '../../main/index.js'; 3 | import { CODE_SET_MAX, CODE_SET_MIN } from '../../main/constants.js'; 4 | 5 | describe('size', () => { 6 | test('checks size', () => { 7 | const shape = new SetShape(new Shape()).size(2); 8 | 9 | expect(shape.try(new Set([111, 222]))).toEqual({ ok: true, value: new Set([111, 222]) }); 10 | expect(shape.try(new Set([111]))).toEqual({ ok: false, issues: expect.any(Array) }); 11 | expect(shape.try(new Set([111, 222, 333]))).toEqual({ ok: false, issues: expect.any(Array) }); 12 | }); 13 | }); 14 | 15 | describe('min', () => { 16 | test('checks min size', () => { 17 | const shape = new SetShape(new Shape()).min(2); 18 | 19 | expect(shape.try(new Set([111, 222]))).toEqual({ ok: true, value: new Set([111, 222]) }); 20 | expect(shape.try(new Set([111]))).toEqual({ 21 | ok: false, 22 | issues: [{ code: CODE_SET_MIN, input: new Set([111]), message: 'Must have the minimum size of 2', param: 2 }], 23 | }); 24 | }); 25 | }); 26 | 27 | describe('max', () => { 28 | test('checks max size', () => { 29 | const shape = new SetShape(new Shape()).max(2); 30 | 31 | expect(shape.try(new Set([111, 222]))).toEqual({ ok: true, value: new Set([111, 222]) }); 32 | expect(shape.try(new Set([111, 222, 333]))).toEqual({ 33 | ok: false, 34 | issues: [ 35 | { code: CODE_SET_MAX, input: new Set([111, 222, 333]), message: 'Must have the maximum size of 2', param: 2 }, 36 | ], 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/test/shape/BigIntShape.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { BigIntShape } from '../../main/index.js'; 3 | import { bigintCoercibleInputs } from '../../main/coerce/bigint.js'; 4 | import { CODE_TYPE_BIGINT, MESSAGE_TYPE_BIGINT } from '../../main/constants.js'; 5 | import { Type } from '../../main/Type.js'; 6 | 7 | describe('BigIntShape', () => { 8 | test('creates a BigIntShape', () => { 9 | const shape = new BigIntShape(); 10 | 11 | expect(shape.isAsync).toBe(false); 12 | expect(shape.inputs).toEqual([Type.BIGINT]); 13 | }); 14 | 15 | test('parses bigint values', () => { 16 | const input = BigInt(111); 17 | 18 | expect(new BigIntShape().parse(input)).toBe(input); 19 | }); 20 | 21 | test('raises an issue if an input is not a bigint', () => { 22 | expect(new BigIntShape().try('aaa')).toEqual({ 23 | ok: false, 24 | issues: [{ code: CODE_TYPE_BIGINT, input: 'aaa', message: 'Must be a bigint' }], 25 | }); 26 | }); 27 | 28 | test('overrides a message for a type issue', () => { 29 | expect(new BigIntShape({ message: 'aaa', meta: 'bbb' }).try(111)).toEqual({ 30 | ok: false, 31 | issues: [{ code: CODE_TYPE_BIGINT, input: 111, message: 'aaa', meta: 'bbb' }], 32 | }); 33 | }); 34 | 35 | describe('coerce', () => { 36 | test('extends shape inputs', () => { 37 | expect(new BigIntShape().coerce().inputs).toBe(bigintCoercibleInputs); 38 | }); 39 | 40 | test('coerces an input', () => { 41 | expect(new BigIntShape().coerce().parse(111)).toBe(BigInt(111)); 42 | expect(new BigIntShape().coerce().parse(new Number(111))).toBe(BigInt(111)); 43 | expect(new BigIntShape().coerce().parse([new Number(111)])).toBe(BigInt(111)); 44 | expect(new BigIntShape().coerce().parse(true)).toBe(BigInt(1)); 45 | }); 46 | 47 | test('raises an issue if coercion fails', () => { 48 | expect(new BigIntShape().coerce().try(['aaa'])).toEqual({ 49 | ok: false, 50 | issues: [{ code: CODE_TYPE_BIGINT, input: ['aaa'], message: MESSAGE_TYPE_BIGINT }], 51 | }); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/test/shape/BooleanShape.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from 'vitest'; 2 | import { BooleanShape } from '../../main/index.js'; 3 | import { booleanCoercibleInputs } from '../../main/coerce/boolean.js'; 4 | import { CODE_TYPE_BOOLEAN, MESSAGE_TYPE_BOOLEAN } from '../../main/constants.js'; 5 | import { Type } from '../../main/Type.js'; 6 | 7 | describe('BooleanShape', () => { 8 | test('creates a BooleanShape', () => { 9 | const shape = new BooleanShape(); 10 | 11 | expect(shape.isAsync).toBe(false); 12 | expect(shape.inputs).toEqual([Type.BOOLEAN]); 13 | }); 14 | 15 | test('parses boolean values', () => { 16 | expect(new BooleanShape().parse(true)).toBe(true); 17 | }); 18 | 19 | test('raises an issue if an input is not a boolean', () => { 20 | expect(new BooleanShape().try('aaa')).toEqual({ 21 | ok: false, 22 | issues: [{ code: CODE_TYPE_BOOLEAN, input: 'aaa', message: MESSAGE_TYPE_BOOLEAN }], 23 | }); 24 | }); 25 | 26 | test('overrides a message for a type issue', () => { 27 | expect(new BooleanShape({ message: 'aaa', meta: 'bbb' }).try(111)).toEqual({ 28 | ok: false, 29 | issues: [{ code: CODE_TYPE_BOOLEAN, input: 111, message: 'aaa', meta: 'bbb' }], 30 | }); 31 | }); 32 | 33 | describe('coerce', () => { 34 | test('extends shape inputs', () => { 35 | expect(new BooleanShape().coerce().inputs).toBe(booleanCoercibleInputs); 36 | }); 37 | 38 | test('coerces an input', () => { 39 | expect(new BooleanShape().coerce().parse(1)).toBe(true); 40 | expect(new BooleanShape().coerce().parse(new Boolean(true))).toBe(true); 41 | expect(new BooleanShape().coerce().parse([new Boolean(true)])).toBe(true); 42 | expect(new BooleanShape().coerce().parse('true')).toBe(true); 43 | }); 44 | 45 | test('raises an issue if coercion fails', () => { 46 | expect(new BooleanShape().coerce().try(222)).toEqual({ 47 | ok: false, 48 | issues: [{ code: CODE_TYPE_BOOLEAN, input: 222, message: MESSAGE_TYPE_BOOLEAN }], 49 | }); 50 | }); 51 | }); 52 | 53 | describe('async', () => { 54 | test('invokes async check', async () => { 55 | const checkMock = vi.fn(() => Promise.resolve([{ code: 'xxx' }])); 56 | 57 | const shape = new BooleanShape().checkAsync(checkMock); 58 | 59 | expect(shape.isAsync).toBe(true); 60 | 61 | await expect(shape.tryAsync(true)).resolves.toEqual({ 62 | ok: false, 63 | issues: [{ code: 'xxx' }], 64 | }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/test/shape/ConstShape.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { ConstShape } from '../../main/index.js'; 3 | import { CODE_TYPE_CONST } from '../../main/constants.js'; 4 | 5 | describe('ConstShape', () => { 6 | test('parses exact value', () => { 7 | const shape = new ConstShape('aaa'); 8 | 9 | expect(shape.value).toBe('aaa'); 10 | expect(shape.parse('aaa')); 11 | expect(shape.inputs).toEqual(['aaa']); 12 | }); 13 | 14 | test('supports NaN', () => { 15 | expect(new ConstShape(NaN).parse(NaN)).toBe(NaN); 16 | }); 17 | 18 | test('raises an issue if an input does not equal to the value', () => { 19 | expect(new ConstShape('aaa').try('bbb')).toEqual({ 20 | ok: false, 21 | issues: [{ code: CODE_TYPE_CONST, input: 'bbb', message: 'Must be equal to "aaa"', param: 'aaa' }], 22 | }); 23 | }); 24 | 25 | test('applies operations', () => { 26 | const shape = new ConstShape('aaa').check(() => [{ code: 'xxx' }]); 27 | 28 | expect(shape.try('aaa')).toEqual({ 29 | ok: false, 30 | issues: [{ code: 'xxx' }], 31 | }); 32 | }); 33 | 34 | describe('coerce', () => { 35 | test('coerces an input', () => { 36 | expect(new ConstShape('111').coerce().parse(111)).toBe('111'); 37 | expect(new ConstShape('111').coerce().parse(new Number(111))).toBe('111'); 38 | expect(new ConstShape('111').coerce().parse([new Number(111)])).toBe('111'); 39 | expect(new ConstShape(1).coerce().parse(true)).toBe(1); 40 | }); 41 | 42 | test('raises an issue if coercion fails', () => { 43 | expect(new ConstShape(111).coerce().try('aaa')).toEqual({ 44 | ok: false, 45 | issues: [{ code: CODE_TYPE_CONST, input: 'aaa', message: 'Must be equal to 111', param: 111 }], 46 | }); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/test/shape/DateShape.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { DateShape } from '../../main/index.js'; 3 | import { dateCoercibleInputs } from '../../main/coerce/date.js'; 4 | import { CODE_TYPE_DATE, MESSAGE_TYPE_DATE } from '../../main/constants.js'; 5 | import { Type } from '../../main/Type.js'; 6 | 7 | describe('DateShape', () => { 8 | test('creates a DateShape', () => { 9 | const shape = new DateShape(); 10 | 11 | expect(shape.isAsync).toBe(false); 12 | expect(shape.inputs).toEqual([Type.DATE]); 13 | }); 14 | 15 | test('parses date values', () => { 16 | const input = new Date(); 17 | 18 | expect(new DateShape().parse(input)).toBe(input); 19 | }); 20 | 21 | test('raises an issue if an input is not a Date instance', () => { 22 | expect(new DateShape().try('aaa')).toEqual({ 23 | ok: false, 24 | issues: [{ code: CODE_TYPE_DATE, input: 'aaa', message: MESSAGE_TYPE_DATE }], 25 | }); 26 | }); 27 | 28 | test('overrides a message for a type issue', () => { 29 | expect(new DateShape({ message: 'aaa', meta: 'bbb' }).try(111)).toEqual({ 30 | ok: false, 31 | issues: [{ code: CODE_TYPE_DATE, input: 111, message: 'aaa', meta: 'bbb' }], 32 | }); 33 | }); 34 | 35 | describe('coerce', () => { 36 | test('extends shape inputs', () => { 37 | expect(new DateShape().coerce().inputs).toBe(dateCoercibleInputs); 38 | }); 39 | 40 | test('coerces an input', () => { 41 | expect(new DateShape().coerce().parse(111)).toEqual(new Date(111)); 42 | expect(new DateShape().coerce().parse(new Number(111))).toEqual(new Date(111)); 43 | expect(new DateShape().coerce().parse([new Number(111)])).toEqual(new Date(111)); 44 | expect(new DateShape().coerce().parse('2020-02-02')).toEqual(new Date('2020-02-02')); 45 | }); 46 | 47 | test('raises an issue if coercion fails', () => { 48 | expect(new DateShape().coerce().try('aaa')).toEqual({ 49 | ok: false, 50 | issues: [{ code: CODE_TYPE_DATE, input: 'aaa', message: MESSAGE_TYPE_DATE }], 51 | }); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/test/shape/InstanceShape.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { InstanceShape } from '../../main/index.js'; 3 | import { CODE_TYPE_INSTANCE_OF } from '../../main/constants.js'; 4 | import { Type } from '../../main/Type.js'; 5 | 6 | describe('InstanceShape', () => { 7 | class TestClass {} 8 | 9 | test('creates an InstanceShape', () => { 10 | const shape = new InstanceShape(TestClass); 11 | 12 | expect(shape.ctor).toBe(TestClass); 13 | expect(shape.inputs).toEqual([Type.OBJECT]); 14 | }); 15 | 16 | test('parses an instance of a class', () => { 17 | const input = new TestClass(); 18 | 19 | expect(new InstanceShape(TestClass).parse(input)).toBe(input); 20 | }); 21 | 22 | test('raises an issue if an input is not an instance of the class', () => { 23 | expect(new InstanceShape(TestClass).try({})).toEqual({ 24 | ok: false, 25 | issues: [{ code: CODE_TYPE_INSTANCE_OF, input: {}, param: TestClass, message: 'Must be a class instance' }], 26 | }); 27 | }); 28 | 29 | test('overrides a message for a type issue', () => { 30 | expect(new InstanceShape(TestClass, { message: 'aaa', meta: 'bbb' }).try({})).toEqual({ 31 | ok: false, 32 | issues: [{ code: CODE_TYPE_INSTANCE_OF, input: {}, param: TestClass, message: 'aaa', meta: 'bbb' }], 33 | }); 34 | }); 35 | 36 | describe('inputs', () => { 37 | test('uses function input type for an Function and its subclasses', () => { 38 | expect(new InstanceShape(Function).inputs).toEqual([Type.FUNCTION]); 39 | expect(new InstanceShape(class extends Function {}).inputs).toEqual([Type.FUNCTION]); 40 | }); 41 | 42 | test('uses Promise input type for an Function and its subclasses', () => { 43 | expect(new InstanceShape(Promise).inputs).toEqual([Type.PROMISE]); 44 | }); 45 | 46 | test('uses array input type for an Array and its subclasses', () => { 47 | expect(new InstanceShape(Array).inputs).toEqual([Type.ARRAY]); 48 | expect(new InstanceShape(class extends Array {}).inputs).toEqual([Type.ARRAY]); 49 | }); 50 | 51 | test('uses date input type for an Date and its subclasses', () => { 52 | expect(new InstanceShape(Date).inputs).toEqual([Type.DATE]); 53 | expect(new InstanceShape(class extends Date {}).inputs).toEqual([Type.DATE]); 54 | }); 55 | 56 | test('uses Set input type for an Date and its subclasses', () => { 57 | expect(new InstanceShape(Set).inputs).toEqual([Type.SET]); 58 | expect(new InstanceShape(class extends Set {}).inputs).toEqual([Type.SET]); 59 | }); 60 | 61 | test('uses Map input type for an Date and its subclasses', () => { 62 | expect(new InstanceShape(Map).inputs).toEqual([Type.MAP]); 63 | expect(new InstanceShape(class extends Map {}).inputs).toEqual([Type.MAP]); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/test/shape/NeverShape.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { NeverShape } from '../../main/index.js'; 3 | import { CODE_TYPE_NEVER, MESSAGE_TYPE_NEVER } from '../../main/constants.js'; 4 | 5 | describe('NeverShape', () => { 6 | test('has empty inputs', () => { 7 | expect(new NeverShape().inputs).toEqual([]); 8 | }); 9 | 10 | test('always raises an issue', () => { 11 | expect(new NeverShape().try(111)).toEqual({ 12 | ok: false, 13 | issues: [{ code: CODE_TYPE_NEVER, input: 111, message: MESSAGE_TYPE_NEVER, param: undefined }], 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/test/shape/NumberShape.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { NumberShape } from '../../main/index.js'; 3 | import { numberCoercibleInputs } from '../../main/coerce/number.js'; 4 | import { 5 | CODE_NUMBER_GT, 6 | CODE_NUMBER_MULTIPLE_OF, 7 | CODE_TYPE_NUMBER, 8 | MESSAGE_TYPE_NUMBER, 9 | } from '../../main/constants.js'; 10 | import { Type } from '../../main/Type.js'; 11 | 12 | describe('NumberShape', () => { 13 | test('creates a NumberShape', () => { 14 | const shape = new NumberShape(); 15 | 16 | expect(shape.isAsync).toBe(false); 17 | expect(shape.inputs).toEqual([Type.NUMBER]); 18 | }); 19 | 20 | test('parses a number', () => { 21 | expect(new NumberShape().parse(111)).toBe(111); 22 | }); 23 | 24 | test('raises if value is not a number', () => { 25 | expect(new NumberShape().try('111')).toEqual({ 26 | ok: false, 27 | issues: [{ code: CODE_TYPE_NUMBER, input: '111', message: MESSAGE_TYPE_NUMBER }], 28 | }); 29 | 30 | expect(new NumberShape().try(NaN)).toEqual({ 31 | ok: false, 32 | issues: [{ code: CODE_TYPE_NUMBER, input: NaN, message: expect.any(String) }], 33 | }); 34 | 35 | expect(new NumberShape().gt(2).parse(3)).toBe(3); 36 | }); 37 | 38 | test('allows infinity', () => { 39 | expect(new NumberShape().parse(Infinity)).toBe(Infinity); 40 | }); 41 | 42 | test('overrides message for type issue', () => { 43 | expect(new NumberShape({ message: 'aaa', meta: 'bbb' }).try('ccc')).toEqual({ 44 | ok: false, 45 | issues: [{ code: CODE_TYPE_NUMBER, input: 'ccc', message: 'aaa', meta: 'bbb' }], 46 | }); 47 | }); 48 | 49 | test('raises a single issue in an early-return mode', () => { 50 | expect(new NumberShape().gt(2).multipleOf(3).try(1, { earlyReturn: true })).toEqual({ 51 | ok: false, 52 | issues: [{ code: CODE_NUMBER_GT, input: 1, param: 2, message: 'Must be greater than 2' }], 53 | }); 54 | }); 55 | 56 | test('raises multiple issues', () => { 57 | expect(new NumberShape().gt(2).multipleOf(3).try(1)).toEqual({ 58 | ok: false, 59 | issues: [ 60 | { code: CODE_NUMBER_GT, input: 1, param: 2, message: 'Must be greater than 2' }, 61 | { code: CODE_NUMBER_MULTIPLE_OF, input: 1, param: 3, message: 'Must be a multiple of 3' }, 62 | ], 63 | }); 64 | }); 65 | 66 | test('applies operations', () => { 67 | expect(new NumberShape().check(() => [{ code: 'xxx' }]).try(111)).toEqual({ 68 | ok: false, 69 | issues: [{ code: 'xxx' }], 70 | }); 71 | }); 72 | 73 | test('supports async validation', async () => { 74 | await expect(new NumberShape().gt(3).tryAsync(2)).resolves.toEqual({ 75 | ok: false, 76 | issues: [{ code: CODE_NUMBER_GT, input: 2, param: 3, message: 'Must be greater than 3' }], 77 | }); 78 | }); 79 | 80 | describe('nan', () => { 81 | test('allows NaN', () => { 82 | expect(new NumberShape().nan().try(NaN)).toEqual({ ok: true, value: NaN }); 83 | }); 84 | 85 | test('undefined can be used as a default value', () => { 86 | expect(new NumberShape().nan(undefined).try(NaN)).toEqual({ ok: true, value: undefined }); 87 | }); 88 | }); 89 | 90 | describe('coerce', () => { 91 | test('extends shape inputs', () => { 92 | expect(new NumberShape().coerce().inputs).toEqual(numberCoercibleInputs); 93 | }); 94 | 95 | test('coerces an input', () => { 96 | expect(new NumberShape().coerce().parse('111')).toBe(111); 97 | expect(new NumberShape().coerce().parse(true)).toBe(1); 98 | expect(new NumberShape().coerce().parse([111])).toBe(111); 99 | }); 100 | 101 | test('raises an issue if coercion fails', () => { 102 | expect(new NumberShape().coerce().try(['aaa'])).toEqual({ 103 | ok: false, 104 | issues: [{ code: CODE_TYPE_NUMBER, input: ['aaa'], message: MESSAGE_TYPE_NUMBER }], 105 | }); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /src/test/shape/ReadonlyShape.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { ReadonlyShape, Shape, StringShape } from '../../main/index.js'; 3 | import { CODE_TYPE_STRING, MESSAGE_TYPE_STRING } from '../../main/constants.js'; 4 | 5 | describe('ReadonlyShape', () => { 6 | test('returns the value from the base shape if parsing succeeds', () => { 7 | expect(new ReadonlyShape(new StringShape()).parse('bbb')).toBe('bbb'); 8 | }); 9 | 10 | test('returns an error from the base shape', () => { 11 | expect(new ReadonlyShape(new StringShape()).try(111)).toEqual({ 12 | ok: false, 13 | issues: [{ code: CODE_TYPE_STRING, input: 111, message: MESSAGE_TYPE_STRING }], 14 | }); 15 | }); 16 | 17 | test('returns primitives as is', () => { 18 | expect(new ReadonlyShape(new Shape()).parse(null)).toBe(null); 19 | expect(new ReadonlyShape(new Shape()).parse(undefined)).toBe(undefined); 20 | expect(new ReadonlyShape(new Shape()).parse(111)).toBe(111); 21 | expect(new ReadonlyShape(new Shape()).parse('aaa')).toBe('aaa'); 22 | }); 23 | 24 | test('freezes a plain object', () => { 25 | const input = { key: 'aaa' }; 26 | const output = new ReadonlyShape(new Shape()).parse(input); 27 | 28 | expect(output).not.toBe(input); 29 | expect(output).toEqual(input); 30 | expect(Object.isFrozen(input)).toBe(false); 31 | expect(Object.isFrozen(output)).toBe(true); 32 | }); 33 | 34 | test('freezes an object with null prototype', () => { 35 | const input = Object.create(null); 36 | input.key = 'aaa'; 37 | 38 | const output = new ReadonlyShape(new Shape()).parse(input); 39 | 40 | expect(output).not.toBe(input); 41 | expect(output).toEqual(input); 42 | expect(Object.getPrototypeOf(output)).toBe(null); 43 | expect(Object.isFrozen(input)).toBe(false); 44 | expect(Object.isFrozen(output)).toBe(true); 45 | }); 46 | 47 | test('freezes an array', () => { 48 | const input = [111, 222]; 49 | const output = new ReadonlyShape(new Shape()).parse(input); 50 | 51 | expect(output).not.toBe(input); 52 | expect(output).toEqual(input); 53 | expect(Object.isFrozen(input)).toBe(false); 54 | expect(Object.isFrozen(output)).toBe(true); 55 | }); 56 | 57 | test('does not freeze Map', () => { 58 | const input = new Map(); 59 | const output = new ReadonlyShape(new Shape()).parse(input); 60 | 61 | expect(output).toBe(input); 62 | expect(Object.isFrozen(output)).toBe(false); 63 | }); 64 | 65 | test('does not freeze Set', () => { 66 | const input = new Set(); 67 | const output = new ReadonlyShape(new Shape()).parse(input); 68 | 69 | expect(output).toBe(input); 70 | expect(Object.isFrozen(output)).toBe(false); 71 | }); 72 | 73 | test('does not freeze a class instance', () => { 74 | const input = new (class {})(); 75 | const output = new ReadonlyShape(new Shape()).parse(input); 76 | 77 | expect(output).toBe(input); 78 | expect(Object.isFrozen(output)).toBe(false); 79 | }); 80 | 81 | test('freezes an object returned from the base shape', () => { 82 | const value = { key: 'aaa' }; 83 | const output = new ReadonlyShape(new Shape().convert(() => value)).parse(111); 84 | 85 | expect(output).toBe(value); 86 | expect(Object.isFrozen(output)).toBe(true); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/test/shape/StringShape.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { StringShape } from '../../main/index.js'; 3 | import { stringCoercibleInputs } from '../../main/coerce/string.js'; 4 | import { CODE_STRING_MIN, CODE_STRING_REGEX, CODE_TYPE_STRING, MESSAGE_TYPE_STRING } from '../../main/constants.js'; 5 | import { Type } from '../../main/Type.js'; 6 | 7 | describe('StringShape', () => { 8 | test('creates a string shape', () => { 9 | const shape = new StringShape(); 10 | 11 | expect(shape.isAsync).toBe(false); 12 | expect(shape.inputs).toEqual([Type.STRING]); 13 | }); 14 | 15 | test('allows a string', () => { 16 | expect(new StringShape().parse('aaa')).toBe('aaa'); 17 | }); 18 | 19 | test('raises if value is not a string', () => { 20 | expect(new StringShape().try(111)).toEqual({ 21 | ok: false, 22 | issues: [{ code: CODE_TYPE_STRING, input: 111, message: MESSAGE_TYPE_STRING }], 23 | }); 24 | 25 | expect(new StringShape().parse('aaa')).toBe('aaa'); 26 | }); 27 | 28 | test('overrides message for type issue', () => { 29 | expect(new StringShape({ message: 'xxx', meta: 'yyy' }).try(111)).toEqual({ 30 | ok: false, 31 | issues: [{ code: CODE_TYPE_STRING, input: 111, message: 'xxx', meta: 'yyy' }], 32 | }); 33 | }); 34 | 35 | test('raises multiple issues', () => { 36 | expect(new StringShape({}).min(3).regex(/aaaa/).try('aa')).toEqual({ 37 | ok: false, 38 | issues: [ 39 | { code: CODE_STRING_MIN, input: 'aa', param: 3, message: 'Must have the minimum length of 3' }, 40 | { code: CODE_STRING_REGEX, input: 'aa', param: /aaaa/, message: 'Must match the pattern /aaaa/' }, 41 | ], 42 | }); 43 | }); 44 | 45 | test('raises a single issue in an early-return mode', () => { 46 | expect(new StringShape().min(3).regex(/aaaa/).try('aa', { earlyReturn: true })).toEqual({ 47 | ok: false, 48 | issues: [{ code: CODE_STRING_MIN, input: 'aa', param: 3, message: 'Must have the minimum length of 3' }], 49 | }); 50 | }); 51 | 52 | test('applies operations', () => { 53 | const shape = new StringShape().check(() => [{ code: 'xxx' }]); 54 | 55 | expect(shape.try('')).toEqual({ 56 | ok: false, 57 | issues: [{ code: 'xxx' }], 58 | }); 59 | }); 60 | 61 | describe('coerce', () => { 62 | test('extends shape inputs', () => { 63 | expect(new StringShape().coerce().inputs).toBe(stringCoercibleInputs); 64 | }); 65 | 66 | test('coerces an input', () => { 67 | expect(new StringShape().coerce().parse(111)).toBe('111'); 68 | expect(new StringShape().coerce().parse(true)).toBe('true'); 69 | expect(new StringShape().coerce().parse(['aaa'])).toBe('aaa'); 70 | }); 71 | 72 | test('raises an issue if coercion fails', () => { 73 | expect(new StringShape().coerce().try([111, 222])).toEqual({ 74 | ok: false, 75 | issues: [{ code: CODE_TYPE_STRING, input: [111, 222], message: MESSAGE_TYPE_STRING }], 76 | }); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/test/shape/SymbolShape.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { SymbolShape } from '../../main/index.js'; 3 | import { CODE_TYPE_SYMBOL, MESSAGE_TYPE_SYMBOL } from '../../main/constants.js'; 4 | import { Type } from '../../main/Type.js'; 5 | 6 | describe('SymbolShape', () => { 7 | test('creates a SymbolShape', () => { 8 | const shape = new SymbolShape(); 9 | 10 | expect(shape.isAsync).toBe(false); 11 | expect(shape.inputs).toEqual([Type.SYMBOL]); 12 | }); 13 | 14 | test('parses symbol values', () => { 15 | const input = Symbol(); 16 | 17 | expect(new SymbolShape().parse(input)).toBe(input); 18 | }); 19 | 20 | test('raises an issue if an input is not a symbol', () => { 21 | expect(new SymbolShape().try('aaa')).toEqual({ 22 | ok: false, 23 | issues: [{ code: CODE_TYPE_SYMBOL, input: 'aaa', message: MESSAGE_TYPE_SYMBOL }], 24 | }); 25 | }); 26 | 27 | test('overrides a message for a type issue', () => { 28 | expect(new SymbolShape({ message: 'aaa', meta: 'bbb' }).try(111)).toEqual({ 29 | ok: false, 30 | issues: [{ code: CODE_TYPE_SYMBOL, input: 111, message: 'aaa', meta: 'bbb' }], 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/test/shape/mocks.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | import { ParseOptions, Result, Shape } from '../../main/core.js'; 3 | 4 | export class MockShape extends Shape { 5 | constructor() { 6 | super(); 7 | 8 | spyOnShape(this); 9 | } 10 | } 11 | 12 | export interface MockShape { 13 | _apply(input: unknown, options: ParseOptions, nonce: number): Result; 14 | 15 | _applyAsync(input: unknown, options: ParseOptions, nonce: number): Promise; 16 | } 17 | 18 | export class AsyncMockShape extends MockShape { 19 | _applyAsync(input: unknown, options: ParseOptions, nonce: number) { 20 | return new Promise(resolve => { 21 | resolve(Shape.prototype['_apply'].call(this, input, options, nonce)); 22 | }); 23 | } 24 | 25 | protected _isAsync(): boolean { 26 | return true; 27 | } 28 | } 29 | 30 | export function spyOnShape(shape: Shape): MockShape { 31 | shape['_apply'] = vi.fn(shape['_apply']); 32 | shape['_applyAsync'] = vi.fn(shape['_applyAsync']); 33 | 34 | return shape as MockShape; 35 | } 36 | -------------------------------------------------------------------------------- /src/test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { createIssue } from '../main/utils.js'; 3 | 4 | describe('createIssue', () => { 5 | test('uses the default message', () => { 6 | expect(createIssue('xxx', 111, 'aaa', 222, {}, undefined)).toEqual({ 7 | code: 'xxx', 8 | input: 111, 9 | message: 'aaa', 10 | meta: undefined, 11 | param: 222, 12 | path: undefined, 13 | }); 14 | }); 15 | 16 | test('uses the string message from apply options', () => { 17 | expect(createIssue('xxx', 111, 'aaa', 222, { messages: { xxx: 'bbb' } }, undefined)).toEqual({ 18 | code: 'xxx', 19 | input: 111, 20 | message: 'bbb', 21 | meta: undefined, 22 | param: 222, 23 | path: undefined, 24 | }); 25 | }); 26 | 27 | test('uses the function message from apply options', () => { 28 | expect(createIssue('xxx', 111, 'aaa', 222, { messages: { xxx: () => 'bbb' } }, undefined)).toEqual({ 29 | code: 'xxx', 30 | input: 111, 31 | message: 'bbb', 32 | meta: undefined, 33 | param: 222, 34 | path: undefined, 35 | }); 36 | }); 37 | 38 | test('uses string issue options as a message', () => { 39 | expect(createIssue('xxx', 111, 'aaa', 222, { messages: { xxx: 'bbb' } }, 'ccc')).toEqual({ 40 | code: 'xxx', 41 | input: 111, 42 | message: 'ccc', 43 | meta: undefined, 44 | param: 222, 45 | path: undefined, 46 | }); 47 | }); 48 | 49 | test('uses function issue options as a message', () => { 50 | expect(createIssue('xxx', 111, 'aaa', 222, { messages: { xxx: 'bbb' } }, () => 'ccc')).toEqual({ 51 | code: 'xxx', 52 | input: 111, 53 | message: 'ccc', 54 | meta: undefined, 55 | param: 222, 56 | path: undefined, 57 | }); 58 | }); 59 | 60 | test('uses message and meta from issue options', () => { 61 | expect(createIssue('xxx', 111, 'aaa', 222, { messages: { xxx: 'bbb' } }, { message: 'ccc', meta: 'ddd' })).toEqual({ 62 | code: 'xxx', 63 | input: 111, 64 | message: 'ccc', 65 | meta: 'ddd', 66 | param: 222, 67 | path: undefined, 68 | }); 69 | }); 70 | 71 | test('uses the default message if issue options do not have a message', () => { 72 | expect(createIssue('xxx', 111, 'aaa', 222, {}, { meta: 'ddd' })).toEqual({ 73 | code: 'xxx', 74 | input: 111, 75 | message: 'aaa', 76 | meta: 'ddd', 77 | param: 222, 78 | path: undefined, 79 | }); 80 | }); 81 | 82 | test('uses the message from apply options if issue options do not have a message', () => { 83 | expect(createIssue('xxx', 111, 'aaa', 222, { messages: { xxx: 'bbb' } }, { meta: 'ddd' })).toEqual({ 84 | code: 'xxx', 85 | input: 111, 86 | message: 'bbb', 87 | meta: 'ddd', 88 | param: 222, 89 | path: undefined, 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /toofast.json: -------------------------------------------------------------------------------- 1 | { 2 | "testOptions": { 3 | "warmupIterationCount": 100, 4 | "targetRme": 0.001 5 | }, 6 | "include": ["./src/test/**/*.perf.js"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./src/main/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "isolatedModules": true, 5 | "strict": true, 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "skipLibCheck": true, 9 | "target": "ES2020", 10 | "module": "NodeNext", 11 | "lib": ["ES2020"], 12 | "outDir": "./lib" 13 | }, 14 | "include": ["./src/**/*"] 15 | } 16 | -------------------------------------------------------------------------------- /tsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/tsdoc/v0/tsdoc.schema.json", 3 | "extends": ["typedoc/tsdoc.json"], 4 | "noStandardTags": false, 5 | "tagDefinitions": [ 6 | { 7 | "tagName": "@plugin", 8 | "syntaxKind": "block" 9 | }, 10 | { 11 | "tagName": "@alias", 12 | "syntaxKind": "block" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Doubter", 3 | "includeVersion": true, 4 | "excludePrivate": true, 5 | "excludeExternals": true, 6 | "excludeInternal": true, 7 | "disableSources": true, 8 | "hideGenerator": true, 9 | "readme": "none", 10 | "tsconfig": "./tsconfig.json", 11 | "navigation": { 12 | "includeFolders": false 13 | }, 14 | "entryPoints": ["./src/main/index.ts", "./src/main/utils.ts", "./src/main/plugin/*.ts"], 15 | "groupOrder": [ 16 | "DSL", 17 | "Shapes", 18 | "Type Inference", 19 | "Issues", 20 | "Operations", 21 | "Other", 22 | "Errors", 23 | "Functions", 24 | "References", 25 | "Constructors", 26 | "Properties", 27 | "Methods", 28 | "Plugin Properties", 29 | "Plugin Methods" 30 | ], 31 | "kindSortOrder": [ 32 | "Function", 33 | "Reference", 34 | "Project", 35 | "Module", 36 | "Namespace", 37 | "Enum", 38 | "EnumMember", 39 | "Class", 40 | "Interface", 41 | "TypeAlias", 42 | "Constructor", 43 | "Property", 44 | "Variable", 45 | "Accessor", 46 | "Method", 47 | "Parameter", 48 | "TypeParameter", 49 | "TypeLiteral", 50 | "CallSignature", 51 | "ConstructorSignature", 52 | "IndexSignature", 53 | "GetSignature", 54 | "SetSignature" 55 | ], 56 | "intentionallyNotExported": [ 57 | "Awaitable", 58 | "DeepPartialArrayShape", 59 | "DeepPartialIntersectionShape", 60 | "DeepPartialObjectShape", 61 | "DeepPartialPromiseShape", 62 | "DeepPartialUnionShape", 63 | "Dict", 64 | "ExcludeLiteral", 65 | "InferPromise", 66 | "Intersect", 67 | "OptionalDeepPartialShape", 68 | "OptionalKeys", 69 | "OptionalPropShapes", 70 | "Promisify", 71 | "ReadonlyDict", 72 | "RequiredPropShapes", 73 | "InferOrDefault", 74 | "ThisType", 75 | "ToReadonly", 76 | "INPUT", 77 | "OUTPUT" 78 | ], 79 | "plugin": ["typedoc-plugin-mdn-links"] 80 | } 81 | --------------------------------------------------------------------------------