├── .github └── workflows │ └── coverage.yml ├── .gitignore ├── .prettierrc.txt ├── README.md ├── eslint.config.mjs ├── examples └── javascript │ ├── 01_simple_validators.js │ ├── 02_variant_validation.js │ ├── 03_object_validation.js │ ├── 04_and_validator.js │ ├── 05_arrays_validation.js │ ├── 06_primitive_values_validation.js │ ├── 10_full_flow.js │ ├── package-lock.json │ └── package.json ├── package-lock.json ├── package.json ├── src ├── Compiler.ts ├── IQuartetInstance.ts ├── IRawSchema.ts ├── IfAny.ts ├── __tests__ │ ├── beautifyStatements.test.ts │ ├── e.and.test.ts │ ├── e.any.test.ts │ ├── e.array.test.ts │ ├── e.arrayOf.test.ts │ ├── e.custom.test.ts │ ├── e.inlining.test.ts │ ├── e.minmax.test.ts │ ├── e.never.test.ts │ ├── e.not.test.ts │ ├── e.object.test.ts │ ├── e.pair.test.ts │ ├── e.primitives.test.ts │ ├── e.standard.test.ts │ ├── e.testMethod.test.ts │ ├── e.types.test.ts │ ├── e.variants.test.ts │ ├── getAccessor.test.ts │ ├── getAccessorWithAlloc.test.ts │ ├── getAlloc.test.ts │ ├── getExplanations.ts │ ├── has.test.ts │ ├── ifInvalidReturnFalse.test.ts │ ├── testValidator.ts │ ├── testValidatorImpure.ts │ ├── testValidatorWithExplanations.ts │ ├── v.and.test.ts │ ├── v.any.test.ts │ ├── v.array.test.ts │ ├── v.arrayOf.test.ts │ ├── v.email.test.ts │ ├── v.invalid.test.ts │ ├── v.minmax.test.ts │ ├── v.never.test.ts │ ├── v.not.test.ts │ ├── v.object.test.ts │ ├── v.pair.test.ts │ ├── v.primitives.test.ts │ ├── v.standard.test.ts │ ├── v.testMethod.test.ts │ ├── v.types.test.ts │ └── v.variants.test.ts ├── compilers │ ├── beautifyStatements.ts │ ├── eCompiler │ │ ├── eCompileSchema.ts │ │ ├── eCompiler.ts │ │ ├── explanation.ts │ │ ├── getExplanator.ts │ │ ├── index.ts │ │ ├── returnExplanations.test.ts │ │ └── returnExplanations.ts │ ├── implStandard.ts │ └── vCompiler │ │ ├── getValidatorFromSchema.ts │ │ ├── ifInvalidReturnFalse.ts │ │ ├── index.ts │ │ ├── vCompileSchema.ts │ │ └── vCompiler.ts ├── e.ts ├── empty.ts ├── explanations │ ├── index.ts │ ├── schemaToExplanationSchema.test.ts │ ├── schemaToExplanationSchema.ts │ └── types.ts ├── getAlloc.ts ├── index.ts ├── infer.ts ├── methods.ts ├── rawSchemaToSchema.ts ├── schemas │ ├── SchemaType.ts │ ├── SpecialProp.ts │ ├── and.ts │ ├── anySchema.ts │ ├── array.ts │ ├── arrayOf.ts │ ├── boolean.ts │ ├── custom.ts │ ├── finite.ts │ ├── functionSchema.ts │ ├── index.ts │ ├── max.ts │ ├── maxLength.ts │ ├── min.ts │ ├── minLength.ts │ ├── negative.ts │ ├── neverSchema.ts │ ├── not.ts │ ├── notANumber.ts │ ├── number.ts │ ├── objectSchema.ts │ ├── pair.ts │ ├── positive.ts │ ├── safeInteger.ts │ ├── string.ts │ ├── symbol.ts │ ├── testSchema.ts │ └── variant.ts ├── strnum.ts ├── types.ts ├── utils.ts └── v.ts ├── tsconfig.json ├── tslint.json └── vitest.config.ts /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | on: ["push", "pull_request"] 2 | 3 | name: Test Coveralls 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Use Node.js 22.x 12 | uses: actions/setup-node@v3 13 | with: 14 | node-version: 22.x 15 | - name: npm install, make test-coverage 16 | run: | 17 | npm install 18 | npm run test:coverage 19 | - name: Coveralls 20 | uses: coverallsapp/github-action@v2 21 | with: 22 | format: lcov 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /lib 3 | coverage 4 | **/.DS_Store 5 | src/playground.test.ts -------------------------------------------------------------------------------- /.prettierrc.txt: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "singleQuote": true 5 | } -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { includeIgnoreFile } from "@eslint/compat"; 2 | import js from "@eslint/js"; 3 | import { defineConfig } from "eslint/config"; 4 | import globals from "globals"; 5 | import { fileURLToPath } from "node:url"; 6 | import tseslint from "typescript-eslint"; 7 | 8 | const gitignorePath = fileURLToPath(new URL(".gitignore", import.meta.url)); 9 | 10 | export default defineConfig([ 11 | includeIgnoreFile(gitignorePath), 12 | { 13 | files: ["**/*.{js,mjs,cjs,ts}"], 14 | plugins: { js }, 15 | ignores: ["lib/**"], 16 | extends: ["js/recommended"], 17 | }, 18 | { 19 | files: ["**/*.{js,mjs,cjs,ts}"], 20 | ignores: ["lib/**"], 21 | languageOptions: { globals: { ...globals.browser, ...globals.node } }, 22 | }, 23 | { 24 | ignores: ["lib/**"], 25 | extends: tseslint.configs.recommended, 26 | }, 27 | ]); 28 | -------------------------------------------------------------------------------- /examples/javascript/01_simple_validators.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 01_simple_validators.js 3 | * 4 | * Contains examples of simplest validations. 5 | * 6 | * They are used within other schemas of validation 7 | */ 8 | 9 | // import v - pre-created instance of quartet. 10 | import { v } from "quartet"; // const { v } = require('quartet') 11 | 12 | // v.number - schema to check if value is a number 13 | 14 | const isNumber = v(v.number); 15 | // const isNumber = x => typeof x === 'number' 16 | 17 | isNumber(1); // true 18 | isNumber("1"); // false 19 | 20 | // v.string - schema to check if value is a string 21 | const isString = v(v.string); 22 | // const isString = x => typeof x === 'string' 23 | isString(1); // false 24 | isString("1"); // true 25 | 26 | // v.boolean - schema to check if value is a boolean value 27 | const isBoolean = v(v.boolean); 28 | // const isBoolean = x => typeof x === 'boolean' 29 | isBoolean(1); // false 30 | isBoolean(true); // true 31 | isBoolean(false); // true 32 | 33 | // v.positive is the same as 34 | const isPositiveNumber = v(v.positive); 35 | // equivalents: 36 | // const isPositiveNumber = n => n > 0 37 | // const isPositiveNumber = v(v.min(0, true)) 38 | 39 | isPositiveNumber(1); // true 40 | isPositiveNumber(0); // false 41 | isPositiveNumber(-1); // false 42 | 43 | // v.negative is the same as 44 | // value => typeof value === 'number' && value < 0 45 | 46 | const isNegativeNumber = v(v.negative); 47 | // equivalents: 48 | // const isNegativeNumber = n => n < 0 49 | // const isNegativeNumber = v(v.max(0, true)) 50 | isNegativeNumber(1); // false 51 | isNegativeNumber(0); // false 52 | isNegativeNumber(-1); // true 53 | 54 | // v.safeInteger is the same as 55 | const isSafeInteger = v(v.safeInteger); 56 | // const isSafeInteger x => Number.isSafeInteger(x) 57 | isSafeInteger(1); // true 58 | isSafeInteger(1.5); // false 59 | isSafeInteger(NaN); // false 60 | isSafeInteger(Infinity); // false 61 | isSafeInteger(-Infinity); // false 62 | -------------------------------------------------------------------------------- /examples/javascript/02_variant_validation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 02_variant_validation.js 3 | * 4 | * Contains examples of idea of variant validations. 5 | * 6 | * VariantSchema = [schema1, schema2, schema3, ...] 7 | * 8 | * When instance of quartet(means `v`) takes the array it will create validator that will 9 | * return true if value is valid by **one of** these schemas. 10 | * 11 | * See examples below. 12 | */ 13 | 14 | import { v } from "quartet"; // const { v } = require('quartet') 15 | 16 | const checkNumberOrString = v([v.number, v.string]); // validator of (string | number) 17 | 18 | checkNumberOrString(1); // true 19 | checkNumberOrString("1"); // true 20 | checkNumberOrString(true); // false 21 | 22 | const checkNumberOrStringOrBoolean = v([v.number, v.string, v.boolean]); // validator of (string | number | boolean) 23 | 24 | checkNumberOrStringOrBoolean(1); // true 25 | checkNumberOrStringOrBoolean("1"); // true 26 | checkNumberOrStringOrBoolean(true); // true 27 | checkNumberOrStringOrBoolean([]); // false 28 | 29 | // Example for empty array schema: there is no any schema that will show that value is valid 30 | const checkNothing = v([]); // The same as () => false, it means that nothing is valid 31 | checkNothing(1); // false 32 | checkNothing("1"); // false 33 | checkNothing(true); // false 34 | 35 | // Example of array schema with one element 36 | const checkNumber = v([v.number]); // The same as v.number (as well as value => typeof value === 'number') 37 | 38 | checkNumber(1); // true 39 | checkNumber("1"); // false 40 | -------------------------------------------------------------------------------- /examples/javascript/03_object_validation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 03_object_validation.js 3 | * 4 | * Contains examples of idea of object validators. 5 | * 6 | * One of the main ideas of quartet was that schema of the object validation must be an object. 7 | * 8 | * ObjectSchema = { 9 | * prop: propSchema, 10 | * prop2: propSchema2, 11 | * ..., 12 | * [v.rest]?: otherPropsSchema 13 | * } 14 | */ 15 | 16 | import { v } from "quartet"; // const { v } = require('quartet') 17 | 18 | // One prop 19 | 20 | const checkUser = v({ 21 | id: v.string, 22 | }); 23 | 24 | checkUser(null); // false 25 | checkUser({}); // false 26 | checkUser({ id: 42 }); // false 27 | checkUser({ id: "42" }); // true 28 | 29 | // Many props 30 | 31 | const checkAgedUser = v({ 32 | id: v.string, 33 | age: v.number, 34 | isMale: v.boolean, 35 | }); 36 | 37 | checkAgedUser(undefined); // false, undefined cannot have properties 38 | checkAgedUser({}); // false, id, age, isMale are undefined 39 | checkAgedUser({ id: "1" }); // false, age and isMale are undefined 40 | checkAgedUser({ id: 1, age: 22, isMale: true }); // false, id is not a string 41 | checkAgedUser({ id: "1", age: "22", isMale: true }); // false, age is not a number 42 | checkAgedUser({ id: "1", age: 22, isMale: "true" }); // false, isMale is not a boolean 43 | checkAgedUser({ id: "1", age: 22, isMale: true }); // true 44 | 45 | // Other props 46 | 47 | const checkPhoneBook = v({ 48 | phoneBookAuthorId: v.number, 49 | [v.rest]: v.string, // all others fields must be strings 50 | }); 51 | 52 | const validPhoneBook = { 53 | phoneBookAuthorId: 42, 54 | andrew: "097-500-7475", 55 | bohdan: "063-300-1010", 56 | taras: "044-332-55", 57 | }; 58 | 59 | checkPhoneBook(validPhoneBook); // true 60 | 61 | checkPhoneBook({ 62 | phoneBookAuthorId: "123", 63 | andrew: "097-500-7475", 64 | bohdan: "063-300-1010", 65 | taras: "044-332-55", 66 | }); // false, because phoneBookAuthorId is not a number 67 | 68 | checkPhoneBook({ 69 | phoneBookAuthorId: 123, 70 | andrew: null, 71 | bohdan: "063-300-1010", 72 | taras: "044-332-55", 73 | }); // false, because andrew: null 74 | -------------------------------------------------------------------------------- /examples/javascript/04_and_validator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 04_and_validator.js 3 | * 4 | * Contains examples of and-method usage. 5 | * 6 | * This method has such type: v.and(schema, schema2, ...) => Validator 7 | */ 8 | 9 | import { v } from "quartet"; // const { v } = require('quartet') 10 | 11 | const checkNotEmptyArray = v(v.and(v.array, v.minLength(1))); 12 | 13 | checkNotEmptyArray("1"); // false 14 | checkNotEmptyArray(1); // false 15 | checkNotEmptyArray([]); // false 16 | checkNotEmptyArray([1]); // true 17 | -------------------------------------------------------------------------------- /examples/javascript/05_arrays_validation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 05_arrays_validation.js 3 | * 4 | * Contains examples of arrays-validation. 5 | * 6 | * Almost most usable method is v.arrayOf(elementSchema) => Validator of Array 7 | */ 8 | 9 | import { v } from "quartet"; // const { v } = require('quartet') 10 | 11 | const checkArrayOfNumber = v(v.arrayOf(v.number)); // same as v.compileArrayOf(v.number) 12 | 13 | checkArrayOfNumber(1); // false, because 1 is not an array 14 | checkArrayOfNumber("1"); // false, because '1' is not an array 15 | checkArrayOfNumber([]); // true 16 | checkArrayOfNumber([1, 2, 3]); // true 17 | checkArrayOfNumber([1, 2, "3"]); // false, because '3' is not a number 18 | 19 | // same as v(v.arrayOf({...})) 20 | const checkArrayOfPersons = v.compileArrayOf({ 21 | id: v.string, 22 | age: v.and(v.positive, v.safeInteger), 23 | isMale: v.boolean, 24 | }); 25 | 26 | checkArrayOfPersons([]); // true 27 | checkArrayOfPersons([ 28 | // true 29 | { id: "1", age: 22, isMale: true }, 30 | { id: "2", age: 12, isMale: false }, 31 | { id: "3", age: 32, isMale: true }, 32 | { id: "4", age: 15, isMale: true }, 33 | { id: "5", age: 13, isMale: false }, 34 | { id: "6", age: 18, isMale: false }, 35 | ]); 36 | 37 | checkArrayOfPersons([ 38 | // false, because second person age is 12.5 - is not integer 39 | { id: "1", age: 22, isMale: true }, 40 | { id: "2", age: 12.5, isMale: false }, 41 | { id: "3", age: 32, isMale: true }, 42 | { id: "4", age: 15, isMale: true }, 43 | { id: "5", age: 13, isMale: false }, 44 | { id: "6", age: 18, isMale: false }, 45 | ]); 46 | -------------------------------------------------------------------------------- /examples/javascript/06_primitive_values_validation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 06_primitive_values_validation.js 3 | * 4 | * Sometimes we know all the valid values of primitive types(number, string, boolean, symbol, undefined, null) 5 | * 6 | * When we want to validate them we can use them in the schema 7 | */ 8 | 9 | import { v } from "quartet"; // const { v } = require('quartet') 10 | 11 | const check42 = v(42); 12 | check42(1); // false 13 | check42("42"); // false 14 | check42(42); // true 15 | 16 | const checkIsGoodNumber = v([3, 7, 12]); 17 | 18 | checkIsGoodNumber(3); // true 19 | checkIsGoodNumber(7); // true 20 | checkIsGoodNumber(12); // true 21 | checkIsGoodNumber(42); // false 22 | 23 | const checkSex = v(["male", "female"]); 24 | 25 | checkSex("male"); // true 26 | checkSex("female"); // true 27 | checkSex(null); // false 28 | 29 | const checkTrue = v(true); 30 | 31 | checkTrue(true); // true 32 | checkTrue("true"); // false 33 | checkTrue(false); // false 34 | 35 | const USER_STATUSES = { 36 | VERIFIED: "VERIFIED", 37 | NON_VERIFIED: "NON_VERIFIED", 38 | ADMIN: "ADMIN", 39 | }; 40 | 41 | const ENGLISH_SKILL_LEVEL = { 42 | BEGINNER: 0, 43 | INTERMEDIATE: 1, 44 | ADVANCED: 2, 45 | }; 46 | 47 | const checkUser = v({ 48 | name: v.string, 49 | age: v.and(v.positive, v.safeInteger), 50 | sex: ["male", "female"], 51 | status: Object.values(USER_STATUSES), 52 | englishSkillLevel: Object.values(ENGLISH_SKILL_LEVEL), 53 | house: [null, { address: v.string }], // null if user has not house, or object with house info 54 | }); 55 | 56 | checkUser({ 57 | name: "Andrew", // is string 58 | age: 22, // is positive integer 59 | sex: "male", // is one of ['male', 'female'] 60 | status: "ADMIN", // is one of ['VERIFIED', 'NON_VERIFIED', 'ADMIN'] 61 | englishSkillLevel: 1, // is one of [1,2,3] 62 | house: null, // house absent, but valid 63 | }); // true 64 | 65 | checkUser({ 66 | name: "Andrew", 67 | age: 22, 68 | sex: "male", 69 | status: "ADMIN", 70 | englishSkillLevel: 1, 71 | house: { 72 | // house present and valid 73 | address: "st. Yanhelya, 5", 74 | }, 75 | }); // true 76 | 77 | checkUser({ 78 | name: "Andrew", 79 | age: 22, 80 | sex: "male", 81 | status: "ADMIN", 82 | englishSkillLevel: 1, 83 | house: { 84 | address: 123, // invalid 85 | }, 86 | }); // false 87 | 88 | checkUser({ 89 | name: "Andrew", 90 | age: 22.5, // is not an integer 91 | sex: "male", 92 | status: "NON_VERIFIED", 93 | englishSkillLevel: 1, 94 | house: null, 95 | }); // => false 96 | 97 | checkUser({ 98 | name: "Andrew", 99 | age: 22, 100 | sex: "male", 101 | status: "NON_VERIFIED", 102 | englishSkillLevel: 3, // invalid english skill level 103 | }); // => false 104 | 105 | checkUser({ 106 | name: "Andrew", 107 | age: 22, 108 | sex: "male", 109 | status: "NON_VERIFIED", 110 | englishSkillLevel: 3, // invalid english skill level 111 | }); // => false 112 | -------------------------------------------------------------------------------- /examples/javascript/10_full_flow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 10_full_flow.js 3 | * 4 | * This file contains little example of full flow of api request with validation 5 | */ 6 | 7 | import { v } from "quartet"; // const { v } = require("quartet") 8 | 9 | const checkUser = v({ 10 | id: v.number, 11 | name: v.string, 12 | age: v.and(v.positive, v.safeInteger), 13 | sex: ["male", "female"], 14 | house: [null, { address: v.string }], 15 | hasWord: v.boolean, 16 | }); 17 | 18 | // Function that will validate response from fetcher and returns valid response 19 | const isValidUserId = v(v.and(v.safeInteger, v.positive)); 20 | const safeFetchUser = async (fetcher, userId) => { 21 | // Validation of input value 22 | if (!isValidUserId(userId)) { 23 | throw new TypeError("userId is invalid"); 24 | } 25 | 26 | const response = await fetcher(userId); 27 | 28 | // Validation of response 29 | if (!checkUser(response)) { 30 | // get validation with 31 | throw new TypeError("wrong user response data"); 32 | } 33 | return response; 34 | }; 35 | 36 | // Mock async function that returns valid user data 37 | const validFetcher = async (userId) => ({ 38 | id: userId, 39 | name: "Andrew", 40 | age: 22, 41 | sex: "male", 42 | house: { address: "st. Yanhelya, 5" }, 43 | hasWord: true, 44 | }); 45 | 46 | // Mock async function that returns invalid user data 47 | const invalidFetcher = async (userId) => ({ 48 | id: userId, 49 | name: "Andrew", 50 | age: 22.5, 51 | sex: null, 52 | }); 53 | 54 | // Demo function 55 | async function demo() { 56 | try { 57 | const validUser = await safeFetchUser(validFetcher, 1); 58 | console.log(validUser); 59 | /* 60 | console: 61 | { 62 | "id": 1, 63 | "name": "Andrew", 64 | "age": 22, 65 | "sex": "male", 66 | "house": { 67 | "address": "st. Yanhelya, 5" 68 | }, 69 | "hasWord": true 70 | } 71 | */ 72 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 73 | const invalidUser = await safeFetchUser(invalidFetcher, 2); // throws Error with explanations 74 | } catch { 75 | // Handle Error 76 | } 77 | } 78 | 79 | demo(); 80 | -------------------------------------------------------------------------------- /examples/javascript/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "quartet": { 8 | "version": "8.2.2", 9 | "resolved": "https://registry.npmjs.org/quartet/-/quartet-8.2.2.tgz", 10 | "integrity": "sha512-ZgGxjkelYJRXUQWWZ8mzY80OZ2Pskg+vsANMtaEsvIKI1odFMQoh8rwrlz5R4W5lRai7BYeCijVRRiHK2Tq1dg==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "01_SimpleValidation.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "AndrewBeletskiy ", 11 | "license": "ISC", 12 | "dependencies": { 13 | "quartet": "^8.2.2" 14 | } 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quartet", 3 | "version": "11.0.3", 4 | "description": "functional and convenient validation library", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "test": "vitest --run", 9 | "test:watch": "vitest", 10 | "test:coverage": "vitest run --coverage", 11 | "build": "tsc", 12 | "format": "prettier --write \"src/**/*.ts\" \"examples/**/*.js\"", 13 | "prepare": "npm run build", 14 | "prepublishOnly": "npm test && npm run lint", 15 | "preversion": "npm run lint", 16 | "lint": "eslint .", 17 | "version": "npm run format && git add -A src", 18 | "postversion": "git push && git push --tags", 19 | "size": "size-limit" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/whiteand/ts-quartet.git" 24 | }, 25 | "size-limit": [ 26 | { 27 | "path": "lib/index.js" 28 | } 29 | ], 30 | "keywords": [ 31 | "validation", 32 | "library", 33 | "pure", 34 | "functional" 35 | ], 36 | "author": "andrewbeletskiy", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/whiteand/ts-quartet/issues" 40 | }, 41 | "homepage": "https://github.com/whiteand/ts-quartet#readme", 42 | "devDependencies": { 43 | "@eslint/js": "^9.24.0", 44 | "@size-limit/preset-small-lib": "^11.2.0", 45 | "@vitest/coverage-v8": "^3.1.1", 46 | "eslint": "^9.24.0", 47 | "globals": "^16.0.0", 48 | "prettier": "^3.5.3", 49 | "size-limit": "^11.2.0", 50 | "typescript": "^5.8.3", 51 | "typescript-eslint": "^8.29.1", 52 | "vitest": "^3.1.1", 53 | "@eslint/compat": "^1.2.8" 54 | }, 55 | "files": [ 56 | "lib/**/*" 57 | ], 58 | "dependencies": { 59 | "@standard-schema/spec": "^1.0.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Compiler.ts: -------------------------------------------------------------------------------- 1 | import { ValidateBySchema } from "./infer"; 2 | import { RawSchema } from "./IRawSchema"; 3 | import { CompilationResult, Z } from "./types"; 4 | 5 | export type Compiler = ( 6 | rawSchema: R, 7 | ) => CompilationResult, E>; 8 | -------------------------------------------------------------------------------- /src/IQuartetInstance.ts: -------------------------------------------------------------------------------- 1 | import { Compiler } from "./Compiler"; 2 | import { IMethods } from "./methods"; 3 | 4 | export interface IQuartetInstance 5 | extends Compiler, 6 | IMethods {} 7 | -------------------------------------------------------------------------------- /src/IRawSchema.ts: -------------------------------------------------------------------------------- 1 | import { SpecialProp } from "./schemas/SpecialProp"; 2 | import { 3 | CompilationResult, 4 | IAndSchema, 5 | IAnySchema, 6 | IArrayOfSchema, 7 | IArraySchema, 8 | IBooleanSchema, 9 | ICustomSchema, 10 | IFiniteSchema, 11 | IFunctionSchema, 12 | IMaxLengthSchema, 13 | IMaxSchema, 14 | IMinLengthSchema, 15 | IMinSchema, 16 | INegativeSchema, 17 | INeverSchema, 18 | INotANumberSchema, 19 | INotSchema, 20 | INumberSchema, 21 | IPairSchema, 22 | IPositiveSchema, 23 | ISafeIntegerSchema, 24 | IStringSchema, 25 | ISymbolSchema, 26 | ITestSchema, 27 | KeyType, 28 | TPrimitiveSchema, 29 | } from "./types"; 30 | 31 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 32 | export interface IRawSchemaArr extends ReadonlyArray {} 33 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 34 | export interface IRawSchemaDict extends Readonly> {} 35 | 36 | export type RawSchema = 37 | | IAndSchema 38 | | IAnySchema 39 | | IArraySchema 40 | | IArrayOfSchema 41 | | IBooleanSchema 42 | | IFiniteSchema 43 | | IFunctionSchema 44 | | IMaxSchema 45 | | IMaxLengthSchema 46 | | IMinSchema 47 | | IMinLengthSchema 48 | | INegativeSchema 49 | | INeverSchema 50 | | INotSchema 51 | | INotANumberSchema 52 | | INumberSchema 53 | | IPairSchema 54 | | IPositiveSchema 55 | | ISafeIntegerSchema 56 | | IStringSchema 57 | | ISymbolSchema 58 | | ITestSchema 59 | | ICustomSchema 60 | | TPrimitiveSchema 61 | | CompilationResult 62 | | IRawSchemaArr 63 | | (IRawSchemaDict & { 64 | readonly [SpecialProp.Rest]?: RawSchema; 65 | readonly [SpecialProp.RestOmit]?: KeyType[]; 66 | }); 67 | -------------------------------------------------------------------------------- /src/IfAny.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-object-type */ 2 | 3 | export type IfAny = true extends T 4 | ? "1" extends T 5 | ? 1 extends T 6 | ? {} extends T 7 | ? (() => void) extends T 8 | ? null extends T 9 | ? Any 10 | : NotAny 11 | : NotAny 12 | : NotAny 13 | : NotAny 14 | : NotAny 15 | : NotAny; 16 | -------------------------------------------------------------------------------- /src/__tests__/beautifyStatements.test.ts: -------------------------------------------------------------------------------- 1 | import { beautifyStatements } from "../compilers/beautifyStatements"; 2 | import { describe, expect } from "vitest"; 3 | 4 | describe("beautifyStatements", (test) => { 5 | test("simple with default tab size", () => { 6 | expect( 7 | beautifyStatements(["if (true) {", 'console.log("something")', "}"]).join( 8 | "\n", 9 | ), 10 | ).toEqual(` if (true) {\n console.log("something")\n }`); 11 | }); 12 | test("simple with non default tab size", () => { 13 | expect( 14 | beautifyStatements( 15 | ["if (true) {", 'console.log("something")', "}"], 16 | 0, 17 | ).join("\n"), 18 | ).toEqual(`if (true) {\n console.log("something")\n}`); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/__tests__/e.and.test.ts: -------------------------------------------------------------------------------- 1 | import { e as v } from ".."; 2 | import { testValidatorImpure } from "./testValidatorImpure"; 3 | import { describe, expect } from "vitest"; 4 | 5 | describe("v.and(...)", (test) => { 6 | test("v.and()", () => { 7 | testValidatorImpure( 8 | v(v.and()), 9 | [null, false, [], {}, 1, 0, NaN, undefined, true], 10 | [], 11 | ); 12 | }); 13 | test("v.and(v.safeInteger, v.min(1), v.max(5))", () => { 14 | const checkRating = v(v.and(v.safeInteger, v.min(1), v.max(5))); 15 | testValidatorImpure( 16 | checkRating, 17 | [1, 2, 3, 4, 5], 18 | [ 19 | "1", 20 | "2", 21 | "3", 22 | "4", 23 | "5", 24 | [1], 25 | ["1"], 26 | [2], 27 | ["2"], 28 | [5], 29 | ["5"], 30 | 10, 31 | 0, 32 | NaN, 33 | Infinity, 34 | -Infinity, 35 | 1.5, 36 | ], 37 | ); 38 | }); 39 | test("v.and(v.safeInteger, v.and(v.min(1), v.max(5)))", () => { 40 | const checkRating = v(v.and(v.safeInteger, v.and(v.min(1), v.max(5)))); 41 | testValidatorImpure( 42 | checkRating, 43 | [1, 2, 3, 4, 5], 44 | [ 45 | "1", 46 | "2", 47 | "3", 48 | "4", 49 | "5", 50 | [1], 51 | ["1"], 52 | [2], 53 | ["2"], 54 | [5], 55 | ["5"], 56 | 10, 57 | 0, 58 | NaN, 59 | Infinity, 60 | -Infinity, 61 | 1.5, 62 | ], 63 | ); 64 | }); 65 | test("v.and fast path", () => { 66 | let flag = true; 67 | const validator = v( 68 | v.and( 69 | v.never, 70 | v.custom(() => { 71 | flag = false; 72 | return true; 73 | }), 74 | ), 75 | ); 76 | validator(1); 77 | expect(flag).toBe(true); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/__tests__/e.any.test.ts: -------------------------------------------------------------------------------- 1 | import { describe } from "vitest"; 2 | import { e } from "../e"; 3 | import { ExplanationSchemaType } from "../explanations/types"; 4 | import { testValidatorWithExplanations } from "./testValidatorWithExplanations"; 5 | 6 | describe("e.any", (test) => { 7 | test("e.any", () => { 8 | testValidatorWithExplanations( 9 | e(e.any), 10 | [null, false, [], {}, 1, 0, NaN, undefined, true], 11 | [], 12 | ); 13 | }); 14 | test("{ a: e.any }", () => { 15 | testValidatorWithExplanations( 16 | e({ a: e.any }), 17 | [null, false, [], {}, 1, 0, NaN, undefined, true].map((a) => ({ a })), 18 | [ 19 | [ 20 | null, 21 | [ 22 | { 23 | innerExplanations: [], 24 | path: [], 25 | schema: { 26 | propsSchemas: { 27 | a: { 28 | type: ExplanationSchemaType.Any, 29 | }, 30 | }, 31 | type: ExplanationSchemaType.Object, 32 | }, 33 | value: null, 34 | }, 35 | ], 36 | ], 37 | [ 38 | undefined, 39 | [ 40 | { 41 | innerExplanations: [], 42 | path: [], 43 | schema: { 44 | propsSchemas: { 45 | a: { 46 | type: ExplanationSchemaType.Any, 47 | }, 48 | }, 49 | type: ExplanationSchemaType.Object, 50 | }, 51 | value: undefined, 52 | }, 53 | ], 54 | ], 55 | ], 56 | ); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/__tests__/e.array.test.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:object-literal-sort-keys */ 2 | import { describe } from "vitest"; 3 | import { e as v } from ".."; 4 | import { ExplanationSchemaType } from "../explanations"; 5 | import { testValidatorWithExplanations } from "./testValidatorWithExplanations"; 6 | 7 | describe("v.array", (test) => { 8 | test("v.array", () => { 9 | testValidatorWithExplanations( 10 | v(v.array), 11 | [[], [1, 2, "3"]], 12 | [ 13 | [ 14 | {}, 15 | [ 16 | { 17 | value: {}, 18 | path: [], 19 | innerExplanations: [], 20 | schema: { 21 | type: ExplanationSchemaType.Array, 22 | }, 23 | }, 24 | ], 25 | ], 26 | [ 27 | { length: 10 }, 28 | [ 29 | { 30 | value: { length: 10 }, 31 | path: [], 32 | innerExplanations: [], 33 | schema: { 34 | type: ExplanationSchemaType.Array, 35 | }, 36 | }, 37 | ], 38 | ], 39 | [ 40 | "Andrew", 41 | [ 42 | { 43 | value: "Andrew", 44 | path: [], 45 | innerExplanations: [], 46 | schema: { 47 | type: ExplanationSchemaType.Array, 48 | }, 49 | }, 50 | ], 51 | ], 52 | ], 53 | ); 54 | }); 55 | test("{ a: v.array }", () => { 56 | testValidatorWithExplanations( 57 | v({ a: v.array }), 58 | [[], [1, 2, "3"]].map((a) => ({ a })), 59 | [ 60 | [ 61 | { a: {} }, 62 | [ 63 | { 64 | value: {}, 65 | path: ["a"], 66 | innerExplanations: [], 67 | schema: { 68 | type: ExplanationSchemaType.Array, 69 | }, 70 | }, 71 | ], 72 | ], 73 | [ 74 | { a: { length: 10 } }, 75 | [ 76 | { 77 | value: { length: 10 }, 78 | path: ["a"], 79 | innerExplanations: [], 80 | schema: { 81 | type: ExplanationSchemaType.Array, 82 | }, 83 | }, 84 | ], 85 | ], 86 | [ 87 | { a: "Andrew" }, 88 | [ 89 | { 90 | value: "Andrew", 91 | path: ["a"], 92 | innerExplanations: [], 93 | schema: { 94 | type: ExplanationSchemaType.Array, 95 | }, 96 | }, 97 | ], 98 | ], 99 | ], 100 | ); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /src/__tests__/e.arrayOf.test.ts: -------------------------------------------------------------------------------- 1 | import { e as v, Z } from ".."; 2 | import { ExplanationSchemaType } from "../explanations"; 3 | import { getExplanations } from "./getExplanations"; 4 | import { testValidatorImpure } from "./testValidatorImpure"; 5 | import { testValidatorWithExplanations } from "./testValidatorWithExplanations"; 6 | import { describe, expect } from "vitest"; 7 | 8 | describe("v.arrayOf", (test) => { 9 | test("v.arrayOf(v.number)", () => { 10 | const validator = v(v.arrayOf(v.number)); 11 | testValidatorWithExplanations(validator, [[], [1, NaN, 2]], []); 12 | expect(getExplanations(validator, [1, 2, "3"])).toEqual([ 13 | { 14 | path: [2], 15 | innerExplanations: [], 16 | schema: { 17 | type: ExplanationSchemaType.Number, 18 | }, 19 | value: "3", 20 | }, 21 | ]); 22 | expect(getExplanations(validator, ["3"])).toEqual([ 23 | { 24 | path: [0], 25 | innerExplanations: [], 26 | schema: { 27 | type: ExplanationSchemaType.Number, 28 | }, 29 | value: "3", 30 | }, 31 | ]); 32 | expect(getExplanations(validator, [[1]])).toEqual([ 33 | { 34 | path: [0], 35 | innerExplanations: [], 36 | schema: { 37 | type: ExplanationSchemaType.Number, 38 | }, 39 | value: [1], 40 | }, 41 | ]); 42 | expect(getExplanations(validator, {})).toEqual([ 43 | { 44 | path: [], 45 | innerExplanations: [], 46 | schema: { 47 | elementSchema: { 48 | type: ExplanationSchemaType.Number, 49 | }, 50 | type: ExplanationSchemaType.ArrayOf, 51 | }, 52 | value: {}, 53 | }, 54 | ]); 55 | expect(getExplanations(validator, { length: 10 })).toEqual([ 56 | { 57 | path: [], 58 | innerExplanations: [], 59 | schema: { 60 | elementSchema: { 61 | type: ExplanationSchemaType.Number, 62 | }, 63 | type: ExplanationSchemaType.ArrayOf, 64 | }, 65 | value: { 66 | length: 10, 67 | }, 68 | }, 69 | ]); 70 | expect(getExplanations(validator, "Andrew")).toEqual([ 71 | { 72 | path: [], 73 | innerExplanations: [], 74 | schema: { 75 | elementSchema: { 76 | type: ExplanationSchemaType.Number, 77 | }, 78 | type: ExplanationSchemaType.ArrayOf, 79 | }, 80 | value: "Andrew", 81 | }, 82 | ]); 83 | }); 84 | test("{ a: v.arrayOf(v.number) }", () => { 85 | const validator = v({ a: v.arrayOf(v.number) }); 86 | testValidatorWithExplanations( 87 | validator, 88 | [[], [1, NaN, 2]].map((a) => ({ a })), 89 | [], 90 | ); 91 | expect(getExplanations(validator, { a: [1, 2, "3"] })).toEqual([ 92 | { 93 | path: ["a", 2], 94 | innerExplanations: [], 95 | schema: { 96 | type: ExplanationSchemaType.Number, 97 | }, 98 | value: "3", 99 | }, 100 | ]); 101 | expect(getExplanations(validator, { a: ["3"] })).toEqual([ 102 | { 103 | path: ["a", 0], 104 | innerExplanations: [], 105 | schema: { 106 | type: ExplanationSchemaType.Number, 107 | }, 108 | value: "3", 109 | }, 110 | ]); 111 | expect(getExplanations(validator, { a: [[1]] })).toEqual([ 112 | { 113 | path: ["a", 0], 114 | innerExplanations: [], 115 | schema: { 116 | type: ExplanationSchemaType.Number, 117 | }, 118 | value: [1], 119 | }, 120 | ]); 121 | expect(getExplanations(validator, { a: {} })).toEqual([ 122 | { 123 | path: ["a"], 124 | innerExplanations: [], 125 | schema: { 126 | elementSchema: { 127 | type: ExplanationSchemaType.Number, 128 | }, 129 | type: ExplanationSchemaType.ArrayOf, 130 | }, 131 | value: {}, 132 | }, 133 | ]); 134 | expect(getExplanations(validator, { a: { length: 10 } })).toEqual([ 135 | { 136 | path: ["a"], 137 | innerExplanations: [], 138 | schema: { 139 | elementSchema: { 140 | type: ExplanationSchemaType.Number, 141 | }, 142 | type: ExplanationSchemaType.ArrayOf, 143 | }, 144 | value: { 145 | length: 10, 146 | }, 147 | }, 148 | ]); 149 | expect(getExplanations(validator, { a: "Andrew" })).toEqual([ 150 | { 151 | path: ["a"], 152 | innerExplanations: [], 153 | schema: { 154 | elementSchema: { 155 | type: ExplanationSchemaType.Number, 156 | }, 157 | type: ExplanationSchemaType.ArrayOf, 158 | }, 159 | value: "Andrew", 160 | }, 161 | ]); 162 | }); 163 | test("v.arrayOf(v.pair)", () => { 164 | const customValidator = ({ key, value }: { key: number; value: Z }) => { 165 | return value === key * key; 166 | }; 167 | const checkSquares = v(v.arrayOf(v.pair(v.custom(customValidator)))); 168 | testValidatorImpure( 169 | checkSquares, 170 | [[], [0, 1], [0, 1, 4]], 171 | [[1], [0, 2], [1, 2, "3"], ["3"], [[1]], {}, { length: 10 }, "Andrew"], 172 | ); 173 | expect(getExplanations(checkSquares, [1])).toEqual([ 174 | { 175 | path: [0], 176 | innerExplanations: [], 177 | schema: { 178 | description: "customValidator", 179 | innerExplanations: [], 180 | type: ExplanationSchemaType.Custom, 181 | }, 182 | value: { 183 | key: 0, 184 | value: 1, 185 | }, 186 | }, 187 | ]); 188 | expect(getExplanations(checkSquares, [0, 2])).toEqual([ 189 | { 190 | path: [1], 191 | innerExplanations: [], 192 | schema: { 193 | description: "customValidator", 194 | innerExplanations: [], 195 | type: ExplanationSchemaType.Custom, 196 | }, 197 | value: { 198 | key: 1, 199 | value: 2, 200 | }, 201 | }, 202 | ]); 203 | expect(getExplanations(checkSquares, [1, 2, "3"])).toEqual([ 204 | { 205 | path: [0], 206 | innerExplanations: [], 207 | schema: { 208 | description: "customValidator", 209 | innerExplanations: [], 210 | type: ExplanationSchemaType.Custom, 211 | }, 212 | value: { 213 | key: 0, 214 | value: 1, 215 | }, 216 | }, 217 | ]); 218 | expect(getExplanations(checkSquares, ["3"])).toEqual([ 219 | { 220 | path: [0], 221 | innerExplanations: [], 222 | schema: { 223 | description: "customValidator", 224 | innerExplanations: [], 225 | type: ExplanationSchemaType.Custom, 226 | }, 227 | value: { 228 | key: 0, 229 | value: "3", 230 | }, 231 | }, 232 | ]); 233 | expect(getExplanations(checkSquares, [[1]])).toEqual([ 234 | { 235 | path: [0], 236 | innerExplanations: [], 237 | schema: { 238 | description: "customValidator", 239 | innerExplanations: [], 240 | type: ExplanationSchemaType.Custom, 241 | }, 242 | value: { 243 | key: 0, 244 | value: [1], 245 | }, 246 | }, 247 | ]); 248 | expect(getExplanations(checkSquares, {})).toEqual([ 249 | { 250 | path: [], 251 | innerExplanations: [], 252 | schema: { 253 | elementSchema: { 254 | keyValueSchema: { 255 | description: "customValidator", 256 | innerExplanations: [], 257 | type: ExplanationSchemaType.Custom, 258 | }, 259 | type: ExplanationSchemaType.Pair, 260 | }, 261 | type: ExplanationSchemaType.ArrayOf, 262 | }, 263 | value: {}, 264 | }, 265 | ]); 266 | expect(getExplanations(checkSquares, { length: 10 })).toEqual([ 267 | { 268 | path: [], 269 | innerExplanations: [], 270 | schema: { 271 | elementSchema: { 272 | keyValueSchema: { 273 | description: "customValidator", 274 | innerExplanations: [], 275 | type: ExplanationSchemaType.Custom, 276 | }, 277 | type: ExplanationSchemaType.Pair, 278 | }, 279 | type: ExplanationSchemaType.ArrayOf, 280 | }, 281 | value: { 282 | length: 10, 283 | }, 284 | }, 285 | ]); 286 | expect(getExplanations(checkSquares, "Andrew")).toEqual([ 287 | { 288 | path: [], 289 | innerExplanations: [], 290 | schema: { 291 | elementSchema: { 292 | keyValueSchema: { 293 | description: "customValidator", 294 | innerExplanations: [], 295 | type: ExplanationSchemaType.Custom, 296 | }, 297 | type: ExplanationSchemaType.Pair, 298 | }, 299 | type: ExplanationSchemaType.ArrayOf, 300 | }, 301 | value: "Andrew", 302 | }, 303 | ]); 304 | }); 305 | test("stops on first invalid", () => { 306 | const arr: Z[] = [1, 2, 3, "4", 5, 6]; 307 | const checked: Z[] = []; 308 | const checkArr = v( 309 | v.arrayOf( 310 | v.and( 311 | v.custom((value) => { 312 | checked.push(value); 313 | return true; 314 | }), 315 | v.number, 316 | ), 317 | ), 318 | ); 319 | expect(checkArr(arr)).toBe(false); 320 | expect(checked).toEqual([1, 2, 3, "4"]); 321 | }); 322 | }); 323 | -------------------------------------------------------------------------------- /src/__tests__/e.custom.test.ts: -------------------------------------------------------------------------------- 1 | import { e, ExplanationSchemaType, Z } from ".."; 2 | import { getExplanations } from "./getExplanations"; 3 | import { testValidatorImpure } from "./testValidatorImpure"; 4 | import { describe, expect } from "vitest"; 5 | 6 | describe("e.custom", (test) => { 7 | test("e(e.arrayOf(e.custom(e(e.number))))", () => { 8 | const checkNumber = e(e.number); 9 | const checkArrNumber = e( 10 | e.arrayOf(e.custom(checkNumber, "should be number")), 11 | ); 12 | testValidatorImpure( 13 | checkArrNumber, 14 | [[], [1], [1, 2, 3]], 15 | [null, false, { length: 1, 0: 1 }, ["1"]], 16 | ); 17 | expect(getExplanations(checkArrNumber, null)).toEqual([ 18 | { 19 | path: [], 20 | innerExplanations: [], 21 | schema: { 22 | elementSchema: { 23 | description: "should be number", 24 | innerExplanations: [ 25 | { 26 | path: [], 27 | innerExplanations: [], 28 | schema: { 29 | type: ExplanationSchemaType.Number, 30 | }, 31 | value: "1", 32 | }, 33 | ], 34 | type: ExplanationSchemaType.Custom, 35 | }, 36 | type: ExplanationSchemaType.ArrayOf, 37 | }, 38 | value: null, 39 | }, 40 | ]); 41 | expect(getExplanations(checkArrNumber, false)).toEqual([ 42 | { 43 | path: [], 44 | innerExplanations: [], 45 | schema: { 46 | elementSchema: { 47 | description: `should be number`, 48 | innerExplanations: [ 49 | { 50 | path: [], 51 | innerExplanations: [], 52 | schema: { 53 | type: ExplanationSchemaType.Number, 54 | }, 55 | value: "1", 56 | }, 57 | ], 58 | type: ExplanationSchemaType.Custom, 59 | }, 60 | type: ExplanationSchemaType.ArrayOf, 61 | }, 62 | value: false, 63 | }, 64 | ]); 65 | expect(getExplanations(checkArrNumber, { length: 1, 0: 1 })).toEqual([ 66 | { 67 | path: [], 68 | innerExplanations: [], 69 | schema: { 70 | elementSchema: { 71 | description: "should be number", 72 | innerExplanations: [ 73 | { 74 | path: [], 75 | innerExplanations: [], 76 | schema: { 77 | type: ExplanationSchemaType.Number, 78 | }, 79 | value: "1", 80 | }, 81 | ], 82 | type: ExplanationSchemaType.Custom, 83 | }, 84 | type: ExplanationSchemaType.ArrayOf, 85 | }, 86 | value: { 87 | "0": 1, 88 | length: 1, 89 | }, 90 | }, 91 | ]); 92 | expect(getExplanations(checkArrNumber, ["1"])).toEqual([ 93 | { 94 | path: [0], 95 | innerExplanations: [], 96 | schema: { 97 | description: "should be number", 98 | innerExplanations: [ 99 | { 100 | path: [], 101 | innerExplanations: [], 102 | schema: { 103 | type: ExplanationSchemaType.Number, 104 | }, 105 | value: "1", 106 | }, 107 | ], 108 | type: ExplanationSchemaType.Custom, 109 | }, 110 | value: "1", 111 | }, 112 | ]); 113 | }); 114 | test('e(e.arrayOf(e.custom(x => typeof x === "number")))', () => { 115 | const checkNumber = (x: Z) => typeof x === "number"; 116 | const checkArrNumber = e(e.arrayOf(e.custom(checkNumber))); 117 | testValidatorImpure( 118 | checkArrNumber, 119 | [[], [1], [1, 2, 3]], 120 | [null, false, { length: 1, 0: 1 }, ["1"]], 121 | ); 122 | expect(getExplanations(checkArrNumber, null)).toEqual([ 123 | { 124 | path: [], 125 | innerExplanations: [], 126 | schema: { 127 | elementSchema: { 128 | description: "checkNumber", 129 | innerExplanations: [], 130 | type: ExplanationSchemaType.Custom, 131 | }, 132 | type: ExplanationSchemaType.ArrayOf, 133 | }, 134 | value: null, 135 | }, 136 | ]); 137 | expect(getExplanations(checkArrNumber, false)).toEqual([ 138 | { 139 | path: [], 140 | innerExplanations: [], 141 | schema: { 142 | elementSchema: { 143 | description: "checkNumber", 144 | innerExplanations: [], 145 | type: ExplanationSchemaType.Custom, 146 | }, 147 | type: ExplanationSchemaType.ArrayOf, 148 | }, 149 | value: false, 150 | }, 151 | ]); 152 | expect(getExplanations(checkArrNumber, { length: 1, 0: 1 })).toEqual([ 153 | { 154 | path: [], 155 | innerExplanations: [], 156 | schema: { 157 | elementSchema: { 158 | description: "checkNumber", 159 | innerExplanations: [], 160 | type: ExplanationSchemaType.Custom, 161 | }, 162 | type: ExplanationSchemaType.ArrayOf, 163 | }, 164 | value: { 165 | "0": 1, 166 | length: 1, 167 | }, 168 | }, 169 | ]); 170 | expect(getExplanations(checkArrNumber, ["1"])).toEqual([ 171 | { 172 | path: [0], 173 | innerExplanations: [], 174 | schema: { 175 | description: "checkNumber", 176 | innerExplanations: [], 177 | type: ExplanationSchemaType.Custom, 178 | }, 179 | value: "1", 180 | }, 181 | ]); 182 | }); 183 | test("it has proper description", () => { 184 | const check = e(e.custom((x) => x === 42, "not 42")); 185 | expect(check(42)).toBe(true); 186 | expect(check(41)).toBe(false); 187 | expect(check.explanations).toMatchInlineSnapshot(` 188 | [ 189 | { 190 | "innerExplanations": [], 191 | "path": [], 192 | "schema": { 193 | "description": "not 42", 194 | "innerExplanations": [], 195 | "type": "Custom", 196 | }, 197 | "value": 41, 198 | }, 199 | ] 200 | `); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /src/__tests__/e.inlining.test.ts: -------------------------------------------------------------------------------- 1 | import { e, ExplanationSchemaType } from ".."; 2 | import { getExplanations } from "./getExplanations"; 3 | import { testValidatorImpure } from "./testValidatorImpure"; 4 | import { describe, expect } from "vitest"; 5 | 6 | describe("e.inline", (test) => { 7 | test("e(e.arrayOf(e(e.number)))", () => { 8 | const checkNumber = e(e.number); 9 | const checkArrNumber = e(e.arrayOf(checkNumber)); 10 | testValidatorImpure( 11 | checkArrNumber, 12 | [[], [1], [1, 2, 3]], 13 | [null, false, { length: 1, 0: 1 }, ["1"]], 14 | ); 15 | expect(getExplanations(checkArrNumber, null)).toEqual([ 16 | { 17 | path: [], 18 | innerExplanations: [], 19 | schema: { 20 | elementSchema: { 21 | type: ExplanationSchemaType.Number, 22 | }, 23 | type: ExplanationSchemaType.ArrayOf, 24 | }, 25 | value: null, 26 | }, 27 | ]); 28 | expect(getExplanations(checkArrNumber, false)).toEqual([ 29 | { 30 | path: [], 31 | innerExplanations: [], 32 | schema: { 33 | elementSchema: { 34 | type: ExplanationSchemaType.Number, 35 | }, 36 | type: ExplanationSchemaType.ArrayOf, 37 | }, 38 | value: false, 39 | }, 40 | ]); 41 | expect(getExplanations(checkArrNumber, { length: 1, 0: 1 })).toEqual([ 42 | { 43 | path: [], 44 | innerExplanations: [], 45 | schema: { 46 | elementSchema: { 47 | type: ExplanationSchemaType.Number, 48 | }, 49 | type: ExplanationSchemaType.ArrayOf, 50 | }, 51 | value: { 52 | "0": 1, 53 | length: 1, 54 | }, 55 | }, 56 | ]); 57 | expect(getExplanations(checkArrNumber, ["1"])).toEqual([ 58 | { 59 | path: [0], 60 | innerExplanations: [], 61 | schema: { 62 | type: ExplanationSchemaType.Number, 63 | }, 64 | value: "1", 65 | }, 66 | ]); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/__tests__/e.never.test.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:object-literal-sort-keys */ 2 | import { e as v } from ".."; 3 | import { ExplanationSchemaType } from "../explanations"; 4 | import { testValidatorWithExplanations } from "./testValidatorWithExplanations"; 5 | import { describe } from "vitest"; 6 | 7 | describe("v.never", (test) => { 8 | test("v.never", () => { 9 | testValidatorWithExplanations( 10 | v(v.never), 11 | [], 12 | [ 13 | [ 14 | null, 15 | [ 16 | { 17 | value: null, 18 | schema: { type: ExplanationSchemaType.Never }, 19 | path: [], 20 | innerExplanations: [], 21 | }, 22 | ], 23 | ], 24 | [ 25 | false, 26 | [ 27 | { 28 | value: false, 29 | schema: { type: ExplanationSchemaType.Never }, 30 | path: [], 31 | innerExplanations: [], 32 | }, 33 | ], 34 | ], 35 | [ 36 | [], 37 | [ 38 | { 39 | value: [], 40 | schema: { type: ExplanationSchemaType.Never }, 41 | path: [], 42 | innerExplanations: [], 43 | }, 44 | ], 45 | ], 46 | [ 47 | {}, 48 | [ 49 | { 50 | value: {}, 51 | schema: { type: ExplanationSchemaType.Never }, 52 | path: [], 53 | innerExplanations: [], 54 | }, 55 | ], 56 | ], 57 | [ 58 | 1, 59 | [ 60 | { 61 | value: 1, 62 | schema: { type: ExplanationSchemaType.Never }, 63 | path: [], 64 | innerExplanations: [], 65 | }, 66 | ], 67 | ], 68 | [ 69 | 0, 70 | [ 71 | { 72 | value: 0, 73 | schema: { type: ExplanationSchemaType.Never }, 74 | path: [], 75 | innerExplanations: [], 76 | }, 77 | ], 78 | ], 79 | [ 80 | NaN, 81 | [ 82 | { 83 | value: NaN, 84 | schema: { type: ExplanationSchemaType.Never }, 85 | path: [], 86 | innerExplanations: [], 87 | }, 88 | ], 89 | ], 90 | [ 91 | undefined, 92 | [ 93 | { 94 | value: undefined, 95 | schema: { type: ExplanationSchemaType.Never }, 96 | path: [], 97 | innerExplanations: [], 98 | }, 99 | ], 100 | ], 101 | ], 102 | ); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /src/__tests__/e.not.test.ts: -------------------------------------------------------------------------------- 1 | import { e as v, ExplanationSchemaType } from ".."; 2 | import { getExplanations } from "./getExplanations"; 3 | import { testValidatorImpure } from "./testValidatorImpure"; 4 | import { describe, expect } from "vitest"; 5 | 6 | describe("v.not", (test) => { 7 | test("v.not", () => { 8 | const validator = v(v.not(false)); 9 | testValidatorImpure( 10 | validator, 11 | [null, [], {}, 1, 0, NaN, undefined, true], 12 | [false], 13 | ); 14 | 15 | expect(getExplanations(validator, false)).toEqual([ 16 | { 17 | path: [], 18 | schema: { 19 | schema: false, 20 | type: ExplanationSchemaType.Not, 21 | }, 22 | value: false, 23 | innerExplanations: [], 24 | }, 25 | ]); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/__tests__/e.object.test.ts: -------------------------------------------------------------------------------- 1 | import { e as v, ExplanationSchemaType, Z } from ".."; 2 | import { getExplanations } from "./getExplanations"; 3 | import { testValidatorImpure } from "./testValidatorImpure"; 4 | import { describe, expect } from "vitest"; 5 | 6 | describe("v({ ... })", (test) => { 7 | test("v({})", () => { 8 | const notNull = v({}); 9 | testValidatorImpure( 10 | notNull, 11 | [{}, 1, 0, false, true, "", []], 12 | [null, undefined], 13 | ); 14 | expect(getExplanations(notNull, null)).toEqual([ 15 | { 16 | path: [], 17 | innerExplanations: [], 18 | schema: { 19 | propsSchemas: {}, 20 | type: ExplanationSchemaType.Object, 21 | }, 22 | value: null, 23 | }, 24 | ]); 25 | expect(getExplanations(notNull, undefined)).toEqual([ 26 | { 27 | path: [], 28 | innerExplanations: [], 29 | schema: { 30 | propsSchemas: {}, 31 | type: ExplanationSchemaType.Object, 32 | }, 33 | value: undefined, 34 | }, 35 | ]); 36 | }); 37 | test("v({ a: number })", () => { 38 | const notNull = v({ a: v.number }); 39 | testValidatorImpure( 40 | notNull, 41 | [{ a: NaN }, { a: Infinity }, { a: 1, b: "123" }], 42 | [null, undefined, {}, 1, 0, false, true, "", []], 43 | ); 44 | expect(getExplanations(notNull, null)).toEqual([ 45 | { 46 | path: [], 47 | innerExplanations: [], 48 | schema: { 49 | propsSchemas: { 50 | a: { 51 | type: ExplanationSchemaType.Number, 52 | }, 53 | }, 54 | type: ExplanationSchemaType.Object, 55 | }, 56 | value: null, 57 | }, 58 | ]); 59 | expect(getExplanations(notNull, undefined)).toEqual([ 60 | { 61 | path: [], 62 | innerExplanations: [], 63 | schema: { 64 | propsSchemas: { 65 | a: { 66 | type: ExplanationSchemaType.Number, 67 | }, 68 | }, 69 | type: ExplanationSchemaType.Object, 70 | }, 71 | value: undefined, 72 | }, 73 | ]); 74 | expect(getExplanations(notNull, {})).toEqual([ 75 | { 76 | path: ["a"], 77 | innerExplanations: [], 78 | schema: { 79 | type: ExplanationSchemaType.Number, 80 | }, 81 | value: undefined, 82 | }, 83 | ]); 84 | expect(getExplanations(notNull, 1)).toEqual([ 85 | { 86 | path: ["a"], 87 | innerExplanations: [], 88 | schema: { 89 | type: ExplanationSchemaType.Number, 90 | }, 91 | value: undefined, 92 | }, 93 | ]); 94 | expect(getExplanations(notNull, 0)).toEqual([ 95 | { 96 | path: ["a"], 97 | innerExplanations: [], 98 | schema: { 99 | type: ExplanationSchemaType.Number, 100 | }, 101 | value: undefined, 102 | }, 103 | ]); 104 | expect(getExplanations(notNull, false)).toEqual([ 105 | { 106 | path: ["a"], 107 | innerExplanations: [], 108 | schema: { 109 | type: ExplanationSchemaType.Number, 110 | }, 111 | value: undefined, 112 | }, 113 | ]); 114 | expect(getExplanations(notNull, true)).toEqual([ 115 | { 116 | path: ["a"], 117 | innerExplanations: [], 118 | schema: { 119 | type: ExplanationSchemaType.Number, 120 | }, 121 | value: undefined, 122 | }, 123 | ]); 124 | expect(getExplanations(notNull, "")).toEqual([ 125 | { 126 | path: ["a"], 127 | innerExplanations: [], 128 | schema: { 129 | type: ExplanationSchemaType.Number, 130 | }, 131 | value: undefined, 132 | }, 133 | ]); 134 | expect(getExplanations(notNull, [])).toEqual([ 135 | { 136 | path: ["a"], 137 | innerExplanations: [], 138 | schema: { 139 | type: ExplanationSchemaType.Number, 140 | }, 141 | value: undefined, 142 | }, 143 | ]); 144 | }); 145 | test('v({ a: number, [restOmit]: ["valid"] })', () => { 146 | const notNull = v({ a: v.number, [v.restOmit]: ["valid"] }); 147 | testValidatorImpure( 148 | notNull, 149 | [{ a: NaN }, { a: Infinity }, { a: 1 }], 150 | [null, undefined, {}, 1, 0, false, true, "", []], 151 | ); 152 | expect(getExplanations(notNull, null)).toEqual([ 153 | { 154 | path: [], 155 | innerExplanations: [], 156 | schema: { 157 | propsSchemas: { 158 | a: { 159 | type: ExplanationSchemaType.Number, 160 | }, 161 | }, 162 | type: ExplanationSchemaType.Object, 163 | }, 164 | value: null, 165 | }, 166 | ]); 167 | expect(getExplanations(notNull, undefined)).toEqual([ 168 | { 169 | path: [], 170 | innerExplanations: [], 171 | schema: { 172 | propsSchemas: { 173 | a: { 174 | type: ExplanationSchemaType.Number, 175 | }, 176 | }, 177 | type: ExplanationSchemaType.Object, 178 | }, 179 | value: undefined, 180 | }, 181 | ]); 182 | expect(getExplanations(notNull, {})).toEqual([ 183 | { 184 | path: ["a"], 185 | innerExplanations: [], 186 | schema: { 187 | type: ExplanationSchemaType.Number, 188 | }, 189 | value: undefined, 190 | }, 191 | ]); 192 | expect(getExplanations(notNull, 1)).toEqual([ 193 | { 194 | path: ["a"], 195 | innerExplanations: [], 196 | schema: { 197 | type: ExplanationSchemaType.Number, 198 | }, 199 | value: undefined, 200 | }, 201 | ]); 202 | expect(getExplanations(notNull, 0)).toEqual([ 203 | { 204 | path: ["a"], 205 | innerExplanations: [], 206 | schema: { 207 | type: ExplanationSchemaType.Number, 208 | }, 209 | value: undefined, 210 | }, 211 | ]); 212 | expect(getExplanations(notNull, false)).toEqual([ 213 | { 214 | path: ["a"], 215 | innerExplanations: [], 216 | schema: { 217 | type: ExplanationSchemaType.Number, 218 | }, 219 | value: undefined, 220 | }, 221 | ]); 222 | expect(getExplanations(notNull, true)).toEqual([ 223 | { 224 | path: ["a"], 225 | innerExplanations: [], 226 | schema: { 227 | type: ExplanationSchemaType.Number, 228 | }, 229 | value: undefined, 230 | }, 231 | ]); 232 | expect(getExplanations(notNull, "")).toEqual([ 233 | { 234 | path: ["a"], 235 | innerExplanations: [], 236 | schema: { 237 | type: ExplanationSchemaType.Number, 238 | }, 239 | value: undefined, 240 | }, 241 | ]); 242 | expect(getExplanations(notNull, [])).toEqual([ 243 | { 244 | path: ["a"], 245 | innerExplanations: [], 246 | schema: { 247 | type: ExplanationSchemaType.Number, 248 | }, 249 | value: undefined, 250 | }, 251 | ]); 252 | }); 253 | test("v({ [restOmit]: something })", () => { 254 | const notNull = v({ 255 | [v.restOmit]: ["andrew"], 256 | }); 257 | testValidatorImpure( 258 | notNull, 259 | [{}, 1, 0, false, true, "", []], 260 | [null, undefined], 261 | ); 262 | expect(getExplanations(notNull, null)).toEqual([ 263 | { 264 | path: [], 265 | innerExplanations: [], 266 | schema: { 267 | propsSchemas: {}, 268 | type: ExplanationSchemaType.Object, 269 | }, 270 | value: null, 271 | }, 272 | ]); 273 | expect(getExplanations(notNull, undefined)).toEqual([ 274 | { 275 | path: [], 276 | innerExplanations: [], 277 | schema: { 278 | propsSchemas: {}, 279 | type: ExplanationSchemaType.Object, 280 | }, 281 | value: undefined, 282 | }, 283 | ]); 284 | }); 285 | test("v({ [rest]: number })", () => { 286 | const notNull = v({ 287 | [v.rest]: v.number, 288 | }); 289 | testValidatorImpure( 290 | notNull, 291 | [{}, 1, 0, false, true, "", [], { a: 1 }], 292 | [{ b: "string" }, null, undefined], 293 | ); 294 | expect(getExplanations(notNull, { b: "string" })).toEqual([ 295 | { 296 | path: ["b"], 297 | innerExplanations: [], 298 | schema: { 299 | type: ExplanationSchemaType.Number, 300 | }, 301 | value: "string", 302 | }, 303 | ]); 304 | expect(getExplanations(notNull, null)).toEqual([ 305 | { 306 | path: [], 307 | innerExplanations: [], 308 | schema: { 309 | "[v.restOmit]": [], 310 | "[v.rest]": { 311 | type: ExplanationSchemaType.Number, 312 | }, 313 | propsSchemas: {}, 314 | type: ExplanationSchemaType.Object, 315 | }, 316 | value: null, 317 | }, 318 | ]); 319 | expect(getExplanations(notNull, undefined)).toEqual([ 320 | { 321 | path: [], 322 | innerExplanations: [], 323 | schema: { 324 | "[v.restOmit]": [], 325 | "[v.rest]": { 326 | type: ExplanationSchemaType.Number, 327 | }, 328 | propsSchemas: {}, 329 | type: ExplanationSchemaType.Object, 330 | }, 331 | value: undefined, 332 | }, 333 | ]); 334 | }); 335 | 336 | test('v({ [rest]: number, [restOmit]: ["valid"] })', () => { 337 | const notNull = v({ 338 | [v.rest]: v.number, 339 | [v.restOmit]: ["valid"], 340 | }); 341 | testValidatorImpure( 342 | notNull, 343 | [ 344 | {}, 345 | 1, 346 | 0, 347 | false, 348 | true, 349 | "", 350 | [], 351 | { a: 1 }, 352 | { a: 1, valid: null }, 353 | { valid: null }, 354 | ], 355 | [{ b: "string" }, null, undefined], 356 | ); 357 | expect(getExplanations(notNull, { b: "string" })).toEqual([ 358 | { 359 | path: ["b"], 360 | innerExplanations: [], 361 | schema: { 362 | type: ExplanationSchemaType.Number, 363 | }, 364 | value: "string", 365 | }, 366 | ]); 367 | expect(getExplanations(notNull, null)).toEqual([ 368 | { 369 | path: [], 370 | innerExplanations: [], 371 | schema: { 372 | "[v.restOmit]": ["valid"], 373 | "[v.rest]": { 374 | type: ExplanationSchemaType.Number, 375 | }, 376 | propsSchemas: {}, 377 | type: ExplanationSchemaType.Object, 378 | }, 379 | value: null, 380 | }, 381 | ]); 382 | expect(getExplanations(notNull, undefined)).toEqual([ 383 | { 384 | path: [], 385 | innerExplanations: [], 386 | schema: { 387 | "[v.restOmit]": ["valid"], 388 | "[v.rest]": { 389 | type: ExplanationSchemaType.Number, 390 | }, 391 | propsSchemas: {}, 392 | type: ExplanationSchemaType.Object, 393 | }, 394 | value: undefined, 395 | }, 396 | ]); 397 | }); 398 | test("v({ a: string, [rest]: number })", () => { 399 | const notNull = v({ 400 | a: v.string, 401 | [v.rest]: v.number, 402 | }); 403 | testValidatorImpure( 404 | notNull, 405 | [{ a: "" }, { a: "1" }, { a: "1", b: 1 }], 406 | [ 407 | { b: "string" }, 408 | null, 409 | undefined, 410 | { a: 1 }, 411 | {}, 412 | [], 413 | { a: null }, 414 | { a: "1", b: "1" }, 415 | ], 416 | ); 417 | expect(getExplanations(notNull, { b: "string" })).toEqual([ 418 | { 419 | path: ["a"], 420 | innerExplanations: [], 421 | schema: { 422 | type: ExplanationSchemaType.String, 423 | }, 424 | value: undefined, 425 | }, 426 | ]); 427 | expect(getExplanations(notNull, null)).toEqual([ 428 | { 429 | path: [], 430 | innerExplanations: [], 431 | schema: { 432 | "[v.restOmit]": [], 433 | "[v.rest]": { 434 | type: ExplanationSchemaType.Number, 435 | }, 436 | propsSchemas: { 437 | a: { 438 | type: ExplanationSchemaType.String, 439 | }, 440 | }, 441 | type: ExplanationSchemaType.Object, 442 | }, 443 | value: null, 444 | }, 445 | ]); 446 | expect(getExplanations(notNull, undefined)).toEqual([ 447 | { 448 | path: [], 449 | innerExplanations: [], 450 | schema: { 451 | "[v.restOmit]": [], 452 | "[v.rest]": { 453 | type: ExplanationSchemaType.Number, 454 | }, 455 | propsSchemas: { 456 | a: { 457 | type: ExplanationSchemaType.String, 458 | }, 459 | }, 460 | type: ExplanationSchemaType.Object, 461 | }, 462 | value: undefined, 463 | }, 464 | ]); 465 | expect(getExplanations(notNull, { a: 1 })).toEqual([ 466 | { 467 | path: ["a"], 468 | innerExplanations: [], 469 | schema: { 470 | type: ExplanationSchemaType.String, 471 | }, 472 | value: 1, 473 | }, 474 | ]); 475 | expect(getExplanations(notNull, {})).toEqual([ 476 | { 477 | path: ["a"], 478 | innerExplanations: [], 479 | schema: { 480 | type: ExplanationSchemaType.String, 481 | }, 482 | value: undefined, 483 | }, 484 | ]); 485 | expect(getExplanations(notNull, [])).toEqual([ 486 | { 487 | path: ["a"], 488 | innerExplanations: [], 489 | schema: { 490 | type: ExplanationSchemaType.String, 491 | }, 492 | value: undefined, 493 | }, 494 | ]); 495 | expect(getExplanations(notNull, { a: null })).toEqual([ 496 | { 497 | path: ["a"], 498 | innerExplanations: [], 499 | schema: { 500 | type: ExplanationSchemaType.String, 501 | }, 502 | value: null, 503 | }, 504 | ]); 505 | expect(getExplanations(notNull, { a: "1", b: "1" })).toEqual([ 506 | { 507 | path: ["b"], 508 | innerExplanations: [], 509 | schema: { 510 | type: ExplanationSchemaType.Number, 511 | }, 512 | value: "1", 513 | }, 514 | ]); 515 | }); 516 | test('v({ [rest]: number, a: string, [restOmit]: ["b"] })', () => { 517 | const notNull = v({ 518 | a: v.string, 519 | [v.rest]: v.number, 520 | [v.restOmit]: ["b"], 521 | }); 522 | testValidatorImpure( 523 | notNull, 524 | [ 525 | { a: "" }, 526 | { a: "1" }, 527 | { a: "1", b: 1 }, 528 | { a: "1", b: 1, c: 2 }, 529 | { a: "1", b: null }, 530 | { a: "1", b: 2 }, 531 | ], 532 | [{ b: "string" }, null, undefined, { a: 1 }, {}, [], { a: null }], 533 | ); 534 | expect(getExplanations(notNull, { b: "string" })).toEqual([ 535 | { 536 | path: ["a"], 537 | innerExplanations: [], 538 | schema: { 539 | type: ExplanationSchemaType.String, 540 | }, 541 | value: undefined, 542 | }, 543 | ]); 544 | expect(getExplanations(notNull, null)).toEqual([ 545 | { 546 | path: [], 547 | innerExplanations: [], 548 | schema: { 549 | "[v.restOmit]": ["b"], 550 | "[v.rest]": { 551 | type: ExplanationSchemaType.Number, 552 | }, 553 | propsSchemas: { 554 | a: { 555 | type: ExplanationSchemaType.String, 556 | }, 557 | }, 558 | type: ExplanationSchemaType.Object, 559 | }, 560 | value: null, 561 | }, 562 | ]); 563 | expect(getExplanations(notNull, undefined)).toEqual([ 564 | { 565 | path: [], 566 | innerExplanations: [], 567 | schema: { 568 | "[v.restOmit]": ["b"], 569 | "[v.rest]": { 570 | type: ExplanationSchemaType.Number, 571 | }, 572 | propsSchemas: { 573 | a: { 574 | type: ExplanationSchemaType.String, 575 | }, 576 | }, 577 | type: ExplanationSchemaType.Object, 578 | }, 579 | value: undefined, 580 | }, 581 | ]); 582 | expect(getExplanations(notNull, { a: 1 })).toEqual([ 583 | { 584 | path: ["a"], 585 | innerExplanations: [], 586 | schema: { 587 | type: ExplanationSchemaType.String, 588 | }, 589 | value: 1, 590 | }, 591 | ]); 592 | expect(getExplanations(notNull, {})).toEqual([ 593 | { 594 | path: ["a"], 595 | innerExplanations: [], 596 | schema: { 597 | type: ExplanationSchemaType.String, 598 | }, 599 | value: undefined, 600 | }, 601 | ]); 602 | expect(getExplanations(notNull, [])).toEqual([ 603 | { 604 | path: ["a"], 605 | innerExplanations: [], 606 | schema: { 607 | type: ExplanationSchemaType.String, 608 | }, 609 | value: undefined, 610 | }, 611 | ]); 612 | expect(getExplanations(notNull, { a: null })).toEqual([ 613 | { 614 | path: ["a"], 615 | innerExplanations: [], 616 | schema: { 617 | type: ExplanationSchemaType.String, 618 | }, 619 | value: null, 620 | }, 621 | ]); 622 | }); 623 | test("{ v.pair }", () => { 624 | let pairFromValidator: Z | null = null; 625 | const checkDict = v({ 626 | [v.rest]: v.pair( 627 | v.custom((pair) => { 628 | pairFromValidator = pair; 629 | return true; 630 | }), 631 | ), 632 | }); 633 | expect( 634 | checkDict({ 635 | a: 123, 636 | }), 637 | ).toBe(true); 638 | expect(pairFromValidator).toEqual({ key: "a", value: 123 }); 639 | }); 640 | test("{ v.pair }", () => { 641 | let pairFromValidator: Z | null = null; 642 | const checkDict = v({ 643 | a: v.pair( 644 | v.custom((pair) => { 645 | pairFromValidator = pair; 646 | return true; 647 | }), 648 | ), 649 | }); 650 | expect( 651 | checkDict({ 652 | a: 123, 653 | }), 654 | ).toBe(true); 655 | expect(pairFromValidator).toEqual({ key: "a", value: 123 }); 656 | }); 657 | test("symbol prop", () => { 658 | const checkQuartet = v({ 659 | [Symbol.for("quartet")]: v.number, 660 | }); 661 | testValidatorImpure( 662 | checkQuartet, 663 | [ 664 | { [Symbol.for("quartet")]: 1 }, 665 | { [Symbol.for("quartet")]: "1" }, 666 | {}, 667 | 0, 668 | 1, 669 | false, 670 | true, 671 | ], 672 | [null, undefined], 673 | ); 674 | expect(getExplanations(checkQuartet, null)).toEqual([ 675 | { 676 | path: [], 677 | innerExplanations: [], 678 | schema: { 679 | propsSchemas: {}, 680 | type: ExplanationSchemaType.Object, 681 | }, 682 | value: null, 683 | }, 684 | ]); 685 | expect(getExplanations(checkQuartet, undefined)).toEqual([ 686 | { 687 | path: [], 688 | innerExplanations: [], 689 | schema: { 690 | propsSchemas: {}, 691 | type: ExplanationSchemaType.Object, 692 | }, 693 | value: undefined, 694 | }, 695 | ]); 696 | }); 697 | test("for readme", () => { 698 | const checkPerson = v({ 699 | name: v.string, 700 | }); 701 | expect(checkPerson({ name: 1 })).toBe(false); // false 702 | expect(checkPerson.explanations).toEqual([ 703 | { 704 | path: ["name"], 705 | innerExplanations: [], 706 | schema: { 707 | type: "String", 708 | }, 709 | value: 1, 710 | }, 711 | ]); 712 | }); 713 | test("nested objects", () => { 714 | const validator = v({ 715 | p: { 716 | name: v.string, 717 | }, 718 | }); 719 | expect(validator({ p: { name: 1 } })).toBe(false); // false 720 | expect(validator.explanations).toEqual([ 721 | { 722 | path: ["p", "name"], 723 | innerExplanations: [], 724 | schema: { 725 | type: "String", 726 | }, 727 | value: 1, 728 | }, 729 | ]); 730 | }); 731 | }); 732 | -------------------------------------------------------------------------------- /src/__tests__/e.pair.test.ts: -------------------------------------------------------------------------------- 1 | import { describe } from "vitest"; 2 | import { e as v } from "../e"; 3 | import { testValidatorImpure } from "./testValidatorImpure"; 4 | 5 | describe("v.pair", (test) => { 6 | test("v.pair", () => { 7 | testValidatorImpure( 8 | v(v.pair({ key: undefined, value: 10 })), 9 | [10], 10 | [null, false, undefined, "10", { valueOf: () => 10 }], 11 | ); 12 | }); 13 | test("{ a: v.pair }", () => { 14 | testValidatorImpure( 15 | v({ a: v.pair({ key: "a", value: 10 }) }), 16 | [{ a: 10 }], 17 | [null, false, undefined, "10", { valueOf: () => 10 }].map((a) => ({ a })), 18 | ); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/__tests__/e.standard.test.ts: -------------------------------------------------------------------------------- 1 | import type { StandardSchemaV1 } from "@standard-schema/spec"; 2 | import { describe, expect } from "vitest"; 3 | import { e } from "../e"; 4 | 5 | const util = { 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | equal: (_value: a extends b ? (b extends a ? true : false) : false) => { 8 | // just for typechecking 9 | }, 10 | }; 11 | 12 | describe("standard schema compat", (test) => { 13 | test("assignability", () => { 14 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 15 | const _s1: StandardSchemaV1 = e(e.string); 16 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 17 | const _s4: StandardSchemaV1 = e(e.string); 18 | }); 19 | 20 | test("type inference", () => { 21 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 22 | const stringToNumber = e(e.safeInteger); 23 | type input = StandardSchemaV1.InferInput; 24 | util.equal(true); 25 | type output = StandardSchemaV1.InferOutput; 26 | util.equal(true); 27 | }); 28 | 29 | test("valid parse", () => { 30 | const schema = e(e.string); 31 | const result = schema["~standard"]["validate"]("hello"); 32 | if (result instanceof Promise) { 33 | throw new Error("Expected sync result"); 34 | } 35 | expect(result.issues).toEqual(undefined); 36 | if (result.issues) { 37 | throw new Error("Expected no issues"); 38 | } else { 39 | expect(result.value).toEqual("hello"); 40 | } 41 | }); 42 | 43 | test("invalid parse", () => { 44 | const schema = e(e.string); 45 | const result = schema["~standard"]["validate"](1234); 46 | if (result instanceof Promise) { 47 | throw new Error("Expected sync result"); 48 | } 49 | expect(result.issues).toBeDefined(); 50 | if (!result.issues) { 51 | throw new Error("Expected issues"); 52 | } 53 | expect(result.issues.length).toEqual(1); 54 | expect(result.issues[0].path).toEqual([]); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/__tests__/e.testMethod.test.ts: -------------------------------------------------------------------------------- 1 | import { e as v, ExplanationSchemaType } from ".."; 2 | import { getExplanations } from "./getExplanations"; 3 | import { testValidatorImpure } from "./testValidatorImpure"; 4 | import { describe, expect } from "vitest"; 5 | 6 | describe("v.test", (test) => { 7 | test("v.test", () => { 8 | const tester = /^a/; 9 | const validator = v(v.test(tester)); 10 | testValidatorImpure( 11 | validator, 12 | ["a", "andrew"], 13 | ["A", null, false, [], {}, 1, 0, NaN, undefined, true], 14 | ); 15 | expect(getExplanations(validator, "A")).toEqual([ 16 | { 17 | path: [], 18 | innerExplanations: [], 19 | schema: { 20 | description: "/^a/", 21 | type: ExplanationSchemaType.Test, 22 | }, 23 | value: "A", 24 | }, 25 | ]); 26 | expect(getExplanations(validator, null)).toEqual([ 27 | { 28 | path: [], 29 | innerExplanations: [], 30 | schema: { 31 | description: "/^a/", 32 | type: ExplanationSchemaType.Test, 33 | }, 34 | value: null, 35 | }, 36 | ]); 37 | expect(getExplanations(validator, false)).toEqual([ 38 | { 39 | path: [], 40 | innerExplanations: [], 41 | schema: { 42 | description: "/^a/", 43 | type: ExplanationSchemaType.Test, 44 | }, 45 | value: false, 46 | }, 47 | ]); 48 | expect(getExplanations(validator, [])).toEqual([ 49 | { 50 | path: [], 51 | innerExplanations: [], 52 | schema: { 53 | description: "/^a/", 54 | type: ExplanationSchemaType.Test, 55 | }, 56 | value: [], 57 | }, 58 | ]); 59 | expect(getExplanations(validator, {})).toEqual([ 60 | { 61 | path: [], 62 | innerExplanations: [], 63 | schema: { 64 | description: "/^a/", 65 | type: ExplanationSchemaType.Test, 66 | }, 67 | value: {}, 68 | }, 69 | ]); 70 | expect(getExplanations(validator, 1)).toEqual([ 71 | { 72 | path: [], 73 | innerExplanations: [], 74 | schema: { 75 | description: "/^a/", 76 | type: ExplanationSchemaType.Test, 77 | }, 78 | value: 1, 79 | }, 80 | ]); 81 | expect(getExplanations(validator, 0)).toEqual([ 82 | { 83 | path: [], 84 | innerExplanations: [], 85 | schema: { 86 | description: "/^a/", 87 | type: ExplanationSchemaType.Test, 88 | }, 89 | value: 0, 90 | }, 91 | ]); 92 | expect(getExplanations(validator, NaN)).toEqual([ 93 | { 94 | path: [], 95 | innerExplanations: [], 96 | schema: { 97 | description: "/^a/", 98 | type: ExplanationSchemaType.Test, 99 | }, 100 | value: NaN, 101 | }, 102 | ]); 103 | expect(getExplanations(validator, undefined)).toEqual([ 104 | { 105 | path: [], 106 | innerExplanations: [], 107 | schema: { 108 | description: "/^a/", 109 | type: ExplanationSchemaType.Test, 110 | }, 111 | value: undefined, 112 | }, 113 | ]); 114 | expect(getExplanations(validator, true)).toEqual([ 115 | { 116 | path: [], 117 | innerExplanations: [], 118 | schema: { 119 | description: "/^a/", 120 | type: ExplanationSchemaType.Test, 121 | }, 122 | value: true, 123 | }, 124 | ]); 125 | }); 126 | test("{ a: v.test }", () => { 127 | const tester = /^a/; 128 | const validator = v({ a: v.test(tester) }); 129 | testValidatorImpure( 130 | validator, 131 | ["a", "andrew"].map((a) => ({ a })), 132 | ["A", null, false, [], {}, 1, 0, NaN, undefined, true].map((a) => ({ 133 | a, 134 | })), 135 | ); 136 | expect(getExplanations(validator, { a: "A" })).toEqual([ 137 | { 138 | path: ["a"], 139 | innerExplanations: [], 140 | schema: { 141 | description: "/^a/", 142 | type: ExplanationSchemaType.Test, 143 | }, 144 | value: "A", 145 | }, 146 | ]); 147 | expect(getExplanations(validator, { a: false })).toEqual([ 148 | { 149 | path: ["a"], 150 | innerExplanations: [], 151 | schema: { 152 | description: "/^a/", 153 | type: ExplanationSchemaType.Test, 154 | }, 155 | value: false, 156 | }, 157 | ]); 158 | expect(getExplanations(validator, { a: [] })).toEqual([ 159 | { 160 | path: ["a"], 161 | innerExplanations: [], 162 | schema: { 163 | description: "/^a/", 164 | type: ExplanationSchemaType.Test, 165 | }, 166 | value: [], 167 | }, 168 | ]); 169 | expect(getExplanations(validator, { a: {} })).toEqual([ 170 | { 171 | path: ["a"], 172 | innerExplanations: [], 173 | schema: { 174 | description: "/^a/", 175 | type: ExplanationSchemaType.Test, 176 | }, 177 | value: {}, 178 | }, 179 | ]); 180 | expect(getExplanations(validator, { a: 1 })).toEqual([ 181 | { 182 | path: ["a"], 183 | innerExplanations: [], 184 | schema: { 185 | description: "/^a/", 186 | type: ExplanationSchemaType.Test, 187 | }, 188 | value: 1, 189 | }, 190 | ]); 191 | expect(getExplanations(validator, { a: 0 })).toEqual([ 192 | { 193 | path: ["a"], 194 | innerExplanations: [], 195 | schema: { 196 | description: "/^a/", 197 | type: ExplanationSchemaType.Test, 198 | }, 199 | value: 0, 200 | }, 201 | ]); 202 | expect(getExplanations(validator, { a: NaN })).toEqual([ 203 | { 204 | path: ["a"], 205 | innerExplanations: [], 206 | schema: { 207 | description: "/^a/", 208 | type: ExplanationSchemaType.Test, 209 | }, 210 | value: NaN, 211 | }, 212 | ]); 213 | expect(getExplanations(validator, { a: true })).toEqual([ 214 | { 215 | path: ["a"], 216 | innerExplanations: [], 217 | schema: { 218 | description: "/^a/", 219 | type: ExplanationSchemaType.Test, 220 | }, 221 | value: true, 222 | }, 223 | ]); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /src/__tests__/getAccessor.test.ts: -------------------------------------------------------------------------------- 1 | import { getAccessor } from "../utils"; 2 | import { describe, expect } from "vitest"; 3 | 4 | describe("getAccessor", (test) => { 5 | test("number", () => { 6 | expect(getAccessor(123)).toEqual("[123]"); 7 | }); 8 | test("not simple prop", () => { 9 | expect(getAccessor("a-b")).toEqual('["a-b"]'); 10 | }); 11 | test("simple prop", () => { 12 | expect(getAccessor("a")).toEqual(".a"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/__tests__/getAccessorWithAlloc.test.ts: -------------------------------------------------------------------------------- 1 | import { Z } from "../types"; 2 | import { getAccessorWithAlloc } from "../utils"; 3 | import { describe, expect } from "vitest"; 4 | 5 | const mockedAlloc = (a: string, b: Z, c: boolean = false) => 6 | JSON.stringify({ a, b, c }); 7 | 8 | describe("getAccessor", (test) => { 9 | test("number", () => { 10 | expect(getAccessorWithAlloc(123, mockedAlloc)).toEqual("[123]"); 11 | }); 12 | test("not simple number", () => { 13 | expect(getAccessorWithAlloc(123.531231, mockedAlloc)).toEqual( 14 | `[{"a":"c","b":123.531231,"c":false}]`, 15 | ); 16 | }); 17 | test("not simple prop", () => { 18 | expect(getAccessorWithAlloc("a-b", mockedAlloc)).toEqual(`["a-b"]`); 19 | }); 20 | test("simple prop", () => { 21 | expect(getAccessorWithAlloc("a", mockedAlloc)).toEqual(".a"); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/__tests__/getAlloc.test.ts: -------------------------------------------------------------------------------- 1 | import { getAlloc } from "../getAlloc"; 2 | import { Z } from "../types"; 3 | import { describe, expect } from "vitest"; 4 | 5 | describe("getAlloc", (test) => { 6 | test("single prefix", () => { 7 | const ctx: Record = {}; 8 | const alloc = getAlloc(ctx, "ctx"); 9 | 10 | expect(alloc("something", 42)).toEqual("ctx.something"); 11 | expect(ctx).toEqual({ something: 42 }); 12 | }); 13 | test("singleton alloc", () => { 14 | const ctx: Record = {}; 15 | const alloc = getAlloc(ctx, "ctx"); 16 | 17 | expect(alloc("something", 42, true)).toEqual("ctx.something"); 18 | expect(alloc("something", 42, true)).toEqual("ctx.something"); 19 | expect(ctx).toEqual({ something: 42 }); 20 | }); 21 | test("wrong singleton", () => { 22 | const ctx: Record = {}; 23 | const alloc = getAlloc(ctx, "ctx"); 24 | 25 | expect(alloc("something", 42, true)).toEqual("ctx.something"); 26 | expect(() => 27 | alloc("something", 43, true), 28 | ).toThrowErrorMatchingInlineSnapshot(`[Error: Wrong singleton usage]`); 29 | expect(ctx).toEqual({ something: 42 }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/__tests__/getExplanations.ts: -------------------------------------------------------------------------------- 1 | import { CompilationResult, Z } from "../types"; 2 | 3 | export function getExplanations( 4 | validator: CompilationResult, 5 | value: Z, 6 | ): E[] { 7 | if (validator(value)) { 8 | throw new Error("valid value" + JSON.stringify(value)); 9 | } 10 | return validator.explanations; 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/has.test.ts: -------------------------------------------------------------------------------- 1 | import { has } from "../utils"; 2 | import { describe, expect } from "vitest"; 3 | 4 | describe("has", (test) => { 5 | test('has(null, "toString")', () => { 6 | expect(has(null, "toString")).toBe(false); 7 | }); 8 | test('has(undefined, "toString")', () => { 9 | expect(has(undefined, "toString")).toBe(false); 10 | }); 11 | test('has(1, "toString"', () => { 12 | expect(has(1, "toString")).toBe(false); 13 | }); 14 | test('has({ a }, "a"', () => { 15 | expect(has({ a: undefined }, "a")).toBe(true); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/__tests__/ifInvalidReturnFalse.test.ts: -------------------------------------------------------------------------------- 1 | import { v } from "../v"; 2 | import { testValidator } from "./testValidator"; 3 | import { describe } from "vitest"; 4 | 5 | describe("coverage of ifInvalidReturnFalse", (test) => { 6 | test("schema: null", () => { 7 | const validator = v({ a: null }); 8 | testValidator( 9 | validator, 10 | [{ a: null }, { a: null, b: false }], 11 | [null, false, undefined, {}, { a: undefined }, { a: "null" }], 12 | ); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/__tests__/testValidator.ts: -------------------------------------------------------------------------------- 1 | import { CompilationResult, Z } from "../types"; 2 | import { expect } from "vitest"; 3 | 4 | export function testValidator( 5 | validator: CompilationResult, 6 | valids: Z[], 7 | invalids: Z[], 8 | ) { 9 | expect(typeof validator).toBe("function"); 10 | expect(Array.isArray(validator.explanations)).toBe(true); 11 | for (const valid of valids) { 12 | expect(validator(valid) === true ? valid : [valid]).toBe(valid); 13 | expect(validator.explanations).toEqual([]); 14 | } 15 | for (const invalid of invalids) { 16 | expect(validator(invalid) === false ? invalid : [invalid]).toBe(invalid); 17 | expect(validator.explanations).toEqual([]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/__tests__/testValidatorImpure.ts: -------------------------------------------------------------------------------- 1 | import { CompilationResult, Z } from "../types"; 2 | import { expect } from "vitest"; 3 | 4 | export function testValidatorImpure( 5 | validator: CompilationResult, 6 | valids: Z[], 7 | invalids: Z[], 8 | matchExplanationSnapshot: boolean = false, 9 | ) { 10 | expect(typeof validator).toBe("function"); 11 | expect(Array.isArray(validator.explanations)).toBe(true); 12 | for (const valid of valids) { 13 | expect(validator(valid) === true ? valid : [valid]).toBe(valid); 14 | expect(validator.explanations).toEqual([]); 15 | } 16 | for (const invalid of invalids) { 17 | expect(validator(invalid) === false ? invalid : [invalid]).toBe(invalid); 18 | expect(validator.explanations.length > 0 ? invalid : [invalid]).toBe( 19 | invalid, 20 | ); 21 | 22 | if (matchExplanationSnapshot) { 23 | expect(validator.explanations).toMatchSnapshot(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/__tests__/testValidatorWithExplanations.ts: -------------------------------------------------------------------------------- 1 | import { IExplanation } from "../explanations"; 2 | import { CompilationResult, Z } from "../types"; 3 | import { expect } from "vitest"; 4 | 5 | export function testValidatorWithExplanations( 6 | validator: CompilationResult, 7 | valids: Z[], 8 | invalids: Array<[Z, IExplanation[]]>, 9 | ) { 10 | expect(typeof validator).toBe("function"); 11 | expect(Array.isArray(validator.explanations)).toBe(true); 12 | for (const valid of valids) { 13 | expect(validator(valid) === true ? valid : [valid]).toBe(valid); 14 | } 15 | for (const [invalid, explanations] of invalids) { 16 | expect(validator(invalid) === false ? invalid : [invalid]).toBe(invalid); 17 | expect(validator.explanations).toEqual(explanations); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/__tests__/v.and.test.ts: -------------------------------------------------------------------------------- 1 | import { v } from ".."; 2 | import { testValidator } from "./testValidator"; 3 | import { describe, expect } from "vitest"; 4 | 5 | describe("v.and(...)", (test) => { 6 | test("v.and()", () => { 7 | testValidator( 8 | v(v.and()), 9 | [null, false, [], {}, 1, 0, NaN, undefined, true], 10 | [], 11 | ); 12 | }); 13 | test("v.and(v.safeInteger, v.min(1), v.max(5))", () => { 14 | const checkRating = v(v.and(v.safeInteger, v.min(1), v.max(5))); 15 | testValidator( 16 | checkRating, 17 | [1, 2, 3, 4, 5], 18 | [ 19 | "1", 20 | "2", 21 | "3", 22 | "4", 23 | "5", 24 | [1], 25 | ["1"], 26 | [2], 27 | ["2"], 28 | [5], 29 | ["5"], 30 | 10, 31 | 0, 32 | NaN, 33 | Infinity, 34 | -Infinity, 35 | 1.5, 36 | ], 37 | ); 38 | }); 39 | test("v.and fast path", () => { 40 | let flag = true; 41 | const validator = v( 42 | v.and( 43 | v.never, 44 | v.custom(() => { 45 | flag = false; 46 | return true; 47 | }), 48 | ), 49 | ); 50 | validator(1); 51 | expect(flag).toBe(true); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/__tests__/v.any.test.ts: -------------------------------------------------------------------------------- 1 | import { v } from "../v"; 2 | import { testValidator } from "./testValidator"; 3 | import { describe } from "vitest"; 4 | 5 | describe("v.any", (test) => { 6 | test("v.any", () => { 7 | testValidator( 8 | v(v.any), 9 | [null, false, [], {}, 1, 0, NaN, undefined, true], 10 | [], 11 | ); 12 | }); 13 | test("{ a: v.any }", () => { 14 | testValidator( 15 | v({ a: v.any }), 16 | [null, false, [], {}, 1, 0, NaN, undefined, true].map((a) => ({ a })), 17 | [null, undefined], 18 | ); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/__tests__/v.array.test.ts: -------------------------------------------------------------------------------- 1 | import { v } from "../v"; 2 | import { testValidator } from "./testValidator"; 3 | import { describe } from "vitest"; 4 | 5 | describe("v.array", (test) => { 6 | test("v.array", () => { 7 | testValidator( 8 | v(v.array), 9 | [[], [1, 2, "3"]], 10 | [{}, { length: 10 }, "Andrew"], 11 | ); 12 | }); 13 | test("{ a: v.array }", () => { 14 | testValidator( 15 | v({ a: v.array }), 16 | [[], [1, 2, "3"]].map((a) => ({ a })), 17 | [{}, { length: 10 }, "Andrew"].map((a) => ({ a })), 18 | ); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/__tests__/v.arrayOf.test.ts: -------------------------------------------------------------------------------- 1 | import { Z } from "../types"; 2 | import { v } from "../v"; 3 | import { testValidator } from "./testValidator"; 4 | import { describe } from "vitest"; 5 | 6 | describe("v.arrayOf", (test) => { 7 | test("v.arrayOf(v.number)", () => { 8 | testValidator( 9 | v(v.arrayOf(v.number)), 10 | [[], [1, NaN, 2]], 11 | [[1, 2, "3"], ["3"], [[1]], {}, { length: 10 }, "Andrew"], 12 | ); 13 | }); 14 | test("v.arrayOf(v.pair)", () => { 15 | const checkSquares = v( 16 | v.arrayOf( 17 | v.pair( 18 | v.custom(({ key, value }: { key: number; value: Z }) => { 19 | return value === key * key; 20 | }), 21 | ), 22 | ), 23 | ); 24 | testValidator( 25 | checkSquares, 26 | [[], [0, 1], [0, 1, 4]], 27 | [[1], [0, 2], [1, 2, "3"], ["3"], [[1]], {}, { length: 10 }, "Andrew"], 28 | ); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/__tests__/v.email.test.ts: -------------------------------------------------------------------------------- 1 | import { v } from "../v"; 2 | import { testValidator } from "./testValidator"; 3 | import { describe } from "vitest"; 4 | 5 | describe("v.email", (test) => { 6 | test("v.email", () => { 7 | testValidator( 8 | v(v.email), 9 | ["a@b.com", "vasil.sergiyovych+1@gmail.com"], 10 | [{}, { length: 10 }, "Andrew"], 11 | ); 12 | }); 13 | test("{ a: v.email }", () => { 14 | testValidator( 15 | v({ a: v.email }), 16 | ["a@b.com", "vasil.sergiyovych+1@gmail.com"].map((a) => ({ a })), 17 | [{}, { length: 10 }, "Andrew"].map((a) => ({ a })), 18 | ); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/__tests__/v.invalid.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect } from "vitest"; 2 | import { v } from "../v"; 3 | import { Z } from "../types"; 4 | 5 | describe("invalid", (it) => { 6 | it("fails when custom function passed", () => { 7 | expect(() => 8 | v({ 9 | a: (x) => x !== 42, 10 | } as Z), 11 | ).toThrowErrorMatchingInlineSnapshot( 12 | `[Error: Wrap your validation function with v.custom(...) instead of usage of the function directly]`, 13 | ); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/__tests__/v.minmax.test.ts: -------------------------------------------------------------------------------- 1 | import { v } from "../v"; 2 | import { testValidator } from "./testValidator"; 3 | import { describe } from "vitest"; 4 | 5 | describe("v.min, v.max, v.minLength, v.maxLength", (test) => { 6 | test("v.min", () => { 7 | const checkNonNegative = v(v.min(0)); 8 | testValidator( 9 | checkNonNegative, 10 | [0, 1, 2, 3.1415926, Infinity, [0], ["0"]], 11 | [-1, "-1", [-1]], 12 | ); 13 | }); 14 | test("{ a: v.min }", () => { 15 | const checkNonNegative = v({ a: v.min(0) }); 16 | testValidator( 17 | checkNonNegative, 18 | [0, 1, 2, 3.1415926, Infinity, [0], ["0"]].map((a) => ({ a })), 19 | [-1, "-1", [-1]].map((a) => ({ a })), 20 | ); 21 | }); 22 | test("v.min exclusive", () => { 23 | const checkPositive = v(v.min(0, true)); 24 | testValidator( 25 | checkPositive, 26 | [1, 2, 3.1415926, Infinity, [1], "1", ["1"]], 27 | [0, -1, "-1", [-1], [0], ["0"]], 28 | ); 29 | }); 30 | test("{ a: v.min } exclusive", () => { 31 | const checkPositive = v({ a: v.min(0, true) }); 32 | testValidator( 33 | checkPositive, 34 | [1, 2, 3.1415926, Infinity, [1], "1", ["1"]].map((a) => ({ a })), 35 | [0, -1, "-1", [-1], [0], ["0"]].map((a) => ({ a })), 36 | ); 37 | }); 38 | test("v.max", () => { 39 | const checkNonPositive = v(v.max(0)); 40 | testValidator( 41 | checkNonPositive, 42 | [0, -1, -2, -3.1415926, -Infinity, [0], ["0"]], 43 | [1, "1", [1]], 44 | ); 45 | }); 46 | test("{a : v.max }", () => { 47 | const checkNonPositive = v({ a: v.max(0) }); 48 | testValidator( 49 | checkNonPositive, 50 | [0, -1, -2, -3.1415926, -Infinity, [0], ["0"]].map((a) => ({ a })), 51 | [1, "1", [1]].map((a) => ({ a })), 52 | ); 53 | }); 54 | test("v.max exclusive", () => { 55 | const checkNegative = v(v.max(0, true)); 56 | testValidator( 57 | checkNegative, 58 | [-1, -2, -3.1415926, -Infinity, [-1], "-1", ["-1"]], 59 | [0, 1, "1", [0], ["0"]], 60 | ); 61 | }); 62 | test("{ a: v.max } exclusive", () => { 63 | const checkNegative = v({ a: v.max(0, true) }); 64 | testValidator( 65 | checkNegative, 66 | [-1, -2, -3.1415926, -Infinity, [-1], "-1", ["-1"]].map((a) => ({ a })), 67 | [0, 1, "1", [0], ["0"]].map((a) => ({ a })), 68 | ); 69 | }); 70 | test("v.minLength", () => { 71 | const validator = v(v.minLength(2)); 72 | testValidator( 73 | validator, 74 | ["ab", [1, 2], { length: 3 }], 75 | ["a", [1], {}, null, undefined, false, true], 76 | ); 77 | }); 78 | test("{ a: v.minLength }", () => { 79 | const validator = v({ a: v.minLength(2) }); 80 | testValidator( 81 | validator, 82 | ["ab", [1, 2], { length: 3 }].map((a) => ({ a })), 83 | [ 84 | "a", 85 | [1], 86 | {}, 87 | null, 88 | undefined, 89 | false, 90 | true, 91 | ...["a", [1], {}, null, undefined, false, true].map((a) => ({ a })), 92 | ], 93 | ); 94 | }); 95 | test("v.minLength exclusive", () => { 96 | const checkPositive = v(v.minLength(2, true)); 97 | testValidator( 98 | checkPositive, 99 | ["abc", [1, 2, 3], { length: 3 }], 100 | ["ab", [1, 2], "a", [1], {}, null, undefined, false, true], 101 | ); 102 | }); 103 | test("{ a: v.minLength } exclusive", () => { 104 | const checkPositive = v({ a: v.minLength(2, true) }); 105 | testValidator( 106 | checkPositive, 107 | ["abc", [1, 2, 3], { length: 3 }].map((a) => ({ a })), 108 | [ 109 | "ab", 110 | [1, 2], 111 | "a", 112 | [1], 113 | {}, 114 | null, 115 | undefined, 116 | false, 117 | true, 118 | ...["ab", [1, 2], "a", [1], {}, null, undefined, false, true].map( 119 | (a) => ({ a }), 120 | ), 121 | ], 122 | ); 123 | }); 124 | test("v.maxLength", () => { 125 | const checkNonPositive = v(v.maxLength(2)); 126 | testValidator( 127 | checkNonPositive, 128 | ["ab", [1, 2], "a", [1]], 129 | ["abc", [1, 2, 3], { length: 3 }, {}, null, undefined, false, true], 130 | ); 131 | }); 132 | test("{ a: v.maxLength }", () => { 133 | const checkNonPositive = v({ a: v.maxLength(2) }); 134 | testValidator( 135 | checkNonPositive, 136 | ["ab", [1, 2], "a", [1]].map((a) => ({ a })), 137 | [ 138 | null, 139 | undefined, 140 | ...[ 141 | "abc", 142 | [1, 2, 3], 143 | { length: 3 }, 144 | {}, 145 | null, 146 | undefined, 147 | false, 148 | true, 149 | ].map((a) => ({ 150 | a, 151 | })), 152 | ], 153 | ); 154 | }); 155 | test("v.maxLength exclusive", () => { 156 | const validator = v(v.maxLength(2, true)); 157 | testValidator( 158 | validator, 159 | ["a", [1]], 160 | ["ab", [1, 2], { length: 3 }, {}, null, undefined, false, true], 161 | ); 162 | }); 163 | test("{ a: v.maxLength } exclusive", () => { 164 | const validator = v({ a: v.maxLength(2, true) }); 165 | testValidator( 166 | validator, 167 | [{ a: "a" }, { a: [1] }], 168 | ["ab", [1, 2], { length: 3 }, {}, null, undefined, false, true].map( 169 | (a) => ({ a }), 170 | ), 171 | ); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /src/__tests__/v.never.test.ts: -------------------------------------------------------------------------------- 1 | import { v } from "../v"; 2 | import { testValidator } from "./testValidator"; 3 | import { describe } from "vitest"; 4 | 5 | describe("v.never", (test) => { 6 | test("v.never", () => { 7 | testValidator( 8 | v(v.never), 9 | [], 10 | [null, false, [], {}, 1, 0, NaN, undefined, true], 11 | ); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/__tests__/v.not.test.ts: -------------------------------------------------------------------------------- 1 | import { describe } from "vitest"; 2 | import { v } from "../v"; 3 | import { testValidator } from "./testValidator"; 4 | 5 | describe("v.not", (test) => { 6 | test("v.not", () => { 7 | testValidator( 8 | v(v.not(false)), 9 | [null, [], {}, 1, 0, NaN, undefined, true], 10 | [false], 11 | ); 12 | testValidator( 13 | v({ a: v.not(undefined) }), 14 | [null, [], {}, 1, 0, NaN, true].map((a) => ({ a })), 15 | [undefined].map((a) => ({ a })), 16 | ); 17 | testValidator( 18 | v({ a: v.not(null) }), 19 | [[], {}, 1, 0, NaN, undefined, true].map((a) => ({ a })), 20 | [null].map((a) => ({ a })), 21 | ); 22 | testValidator( 23 | v({ a: v.not(1) }), 24 | [null, [], {}, 0, NaN, true].map((a) => ({ a })), 25 | [1].map((a) => ({ a })), 26 | ); 27 | testValidator( 28 | v({ a: v.not(null) }), 29 | [[], {}, 1, 0, NaN, undefined, true].map((a) => ({ a })), 30 | [null].map((a) => ({ a })), 31 | ); 32 | testValidator( 33 | v({ a: v.not(v.number) }), 34 | [[], {}, undefined, true].map((a) => ({ a })), 35 | [1, 0, NaN].map((a) => ({ a })), 36 | ); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/__tests__/v.object.test.ts: -------------------------------------------------------------------------------- 1 | import { Z } from "../types"; 2 | import { v } from "../v"; 3 | import { testValidator } from "./testValidator"; 4 | import { describe, expect } from "vitest"; 5 | 6 | describe("v({ ... })", (test) => { 7 | test("v({})", () => { 8 | const notNull = v({}); 9 | testValidator(notNull, [{}, 1, 0, false, true, "", []], [null, undefined]); 10 | }); 11 | test("v({ a: number })", () => { 12 | const notNull = v({ a: v.number }); 13 | testValidator( 14 | notNull, 15 | [{ a: NaN }, { a: Infinity }, { a: 1, b: "123" }], 16 | [null, undefined, {}, 1, 0, false, true, "", []], 17 | ); 18 | }); 19 | test('v({ a: number, [restOmit]: ["valid"] })', () => { 20 | const notNull = v({ a: v.number, [v.restOmit]: ["valid"] }); 21 | testValidator( 22 | notNull, 23 | [{ a: NaN }, { a: Infinity }, { a: 1 }], 24 | [null, undefined, {}, 1, 0, false, true, "", []], 25 | ); 26 | }); 27 | test("v({ [restOmit]: something })", () => { 28 | const notNull = v({ 29 | [v.restOmit]: ["andrew"], 30 | }); 31 | testValidator(notNull, [{}, 1, 0, false, true, "", []], [null, undefined]); 32 | }); 33 | test("v({ [rest]: number })", () => { 34 | const notNull = v({ 35 | [v.rest]: v.number, 36 | }); 37 | testValidator( 38 | notNull, 39 | [{}, 1, 0, false, true, "", [], { a: 1 }], 40 | [{ b: "string" }, null, undefined], 41 | ); 42 | }); 43 | 44 | test('v({ [rest]: number, [restOmit]: ["valid"] })', () => { 45 | const notNull = v({ 46 | [v.rest]: v.number, 47 | [v.restOmit]: ["valid"], 48 | }); 49 | testValidator( 50 | notNull, 51 | [ 52 | {}, 53 | 1, 54 | 0, 55 | false, 56 | true, 57 | "", 58 | [], 59 | { a: 1 }, 60 | { a: 1, valid: null }, 61 | { valid: null }, 62 | ], 63 | [{ b: "string" }, null, undefined], 64 | ); 65 | }); 66 | test("v({ a: string, [rest]: number })", () => { 67 | const notNull = v({ 68 | a: v.string, 69 | [v.rest]: v.number, 70 | }); 71 | testValidator( 72 | notNull, 73 | [{ a: "" }, { a: "1" }, { a: "1", b: 1 }], 74 | [ 75 | { b: "string" }, 76 | null, 77 | undefined, 78 | { a: 1 }, 79 | {}, 80 | [], 81 | { a: null }, 82 | { a: "1", b: "1" }, 83 | ], 84 | ); 85 | }); 86 | test('v({ [rest]: number, a: string, [restOmit]: ["b"] })', () => { 87 | const notNull = v({ 88 | a: v.string, 89 | [v.rest]: v.number, 90 | [v.restOmit]: ["b"], 91 | }); 92 | testValidator( 93 | notNull, 94 | [ 95 | { a: "" }, 96 | { a: "1" }, 97 | { a: "1", b: 1 }, 98 | { a: "1", b: 1, c: 2 }, 99 | { a: "1", b: null }, 100 | { a: "1", b: 2 }, 101 | ], 102 | [{ b: "string" }, null, undefined, { a: 1 }, {}, [], { a: null }], 103 | ); 104 | }); 105 | test("{ v.pair }", () => { 106 | let pairFromValidator: Z | null = null; 107 | const checkDict = v({ 108 | [v.rest]: v.pair( 109 | v.custom((pair) => { 110 | pairFromValidator = pair; 111 | return true; 112 | }), 113 | ), 114 | }); 115 | expect( 116 | checkDict({ 117 | a: 123, 118 | }), 119 | ).toBe(true); 120 | expect(pairFromValidator).toEqual({ key: "a", value: 123 }); 121 | }); 122 | test("{ v.pair }", () => { 123 | let pairFromValidator: Z | null = null; 124 | const checkDict = v({ 125 | a: v.pair( 126 | v.custom((pair) => { 127 | pairFromValidator = pair; 128 | return true; 129 | }), 130 | ), 131 | }); 132 | expect( 133 | checkDict({ 134 | a: 123, 135 | }), 136 | ).toBe(true); 137 | expect(pairFromValidator).toEqual({ key: "a", value: 123 }); 138 | }); 139 | test("symbol prop", () => { 140 | const checkQuartet = v({ 141 | [Symbol.for("quartet")]: v.number, 142 | }); 143 | testValidator( 144 | checkQuartet, 145 | [ 146 | { [Symbol.for("quartet")]: 1 }, 147 | { [Symbol.for("quartet")]: "1" }, 148 | {}, 149 | 0, 150 | 1, 151 | false, 152 | true, 153 | ], 154 | [null, undefined], 155 | ); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /src/__tests__/v.pair.test.ts: -------------------------------------------------------------------------------- 1 | import { v } from "../v"; 2 | import { testValidator } from "./testValidator"; 3 | import { describe } from "vitest"; 4 | 5 | describe("v.pair", (test) => { 6 | test("v.pair", () => { 7 | testValidator( 8 | v(v.pair({ key: undefined, value: 10 })), 9 | [10], 10 | [null, false, undefined, "10", { valueOf: () => 10 }], 11 | ); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/__tests__/v.primitives.test.ts: -------------------------------------------------------------------------------- 1 | import { v } from "../v"; 2 | import { testValidator } from "./testValidator"; 3 | import { describe } from "vitest"; 4 | 5 | describe("v(primitive)", (test) => { 6 | test("v(null)", () => { 7 | const validator = v(null); 8 | testValidator( 9 | validator, 10 | [null], 11 | [undefined, true, false, 0, "0", Infinity, -Infinity, 0 / 0, {}, []], 12 | ); 13 | }); 14 | test("v(undefined)", () => { 15 | const validator = v(undefined); 16 | testValidator( 17 | validator, 18 | [undefined], 19 | [null, true, false, 0, "0", Infinity, -Infinity, 0 / 0, {}, []], 20 | ); 21 | }); 22 | test("v({ a: undefined })", () => { 23 | const validator = v({ a: undefined }); 24 | testValidator( 25 | validator, 26 | [{}, { a: undefined }], 27 | [null, undefined, { a: 1 }, { a: "null" }, { a: "undefined" }], 28 | ); 29 | }); 30 | test("v(true)", () => { 31 | const validator = v(true); 32 | testValidator( 33 | validator, 34 | [true], 35 | [null, undefined, false, 0, "0", Infinity, -Infinity, 0 / 0, {}, []], 36 | ); 37 | }); 38 | test("v(false)", () => { 39 | const validator = v(false); 40 | testValidator( 41 | validator, 42 | [false], 43 | [null, undefined, true, 0, "0", Infinity, -Infinity, 0 / 0, {}, []], 44 | ); 45 | }); 46 | test("v(NaN)", () => { 47 | const validator = v(NaN); 48 | testValidator( 49 | validator, 50 | [NaN], 51 | [null, undefined, true, false, 0, "0", Infinity, -Infinity, {}, []], 52 | ); 53 | }); 54 | test("v({ a: NaN })", () => { 55 | const validator = v({ a: NaN }); 56 | testValidator( 57 | validator, 58 | [{ a: NaN }, { a: NaN, b: 1 }], 59 | [ 60 | null, 61 | undefined, 62 | true, 63 | false, 64 | 0, 65 | "0", 66 | Infinity, 67 | -Infinity, 68 | {}, 69 | [], 70 | ...[ 71 | null, 72 | undefined, 73 | true, 74 | false, 75 | 0, 76 | "0", 77 | Infinity, 78 | -Infinity, 79 | {}, 80 | [], 81 | ].map((a) => ({ 82 | a, 83 | })), 84 | ], 85 | ); 86 | }); 87 | test("v(0)", () => { 88 | const validator = v(0); 89 | testValidator( 90 | validator, 91 | [0], 92 | [null, undefined, true, false, "0", Infinity, -Infinity, 0 / 0, {}, []], 93 | ); 94 | }); 95 | test("v(1)", () => { 96 | const validator = v(1); 97 | testValidator( 98 | validator, 99 | [1], 100 | [ 101 | null, 102 | undefined, 103 | true, 104 | false, 105 | 0, 106 | "0", 107 | Infinity, 108 | -Infinity, 109 | 0 / 0, 110 | {}, 111 | [], 112 | ], 113 | ); 114 | }); 115 | test('v("0")', () => { 116 | const validator = v("0"); 117 | testValidator( 118 | validator, 119 | ["0"], 120 | [null, undefined, true, false, 0, Infinity, -Infinity, 0 / 0, {}, []], 121 | ); 122 | }); 123 | test('v(Symbol.for("quartet"))', () => { 124 | const validator = v(Symbol.for("quartet")); 125 | testValidator( 126 | validator, 127 | [Symbol.for("quartet")], 128 | [ 129 | null, 130 | undefined, 131 | true, 132 | false, 133 | 0, 134 | "0", 135 | Infinity, 136 | -Infinity, 137 | 0 / 0, 138 | {}, 139 | [], 140 | ], 141 | ); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /src/__tests__/v.standard.test.ts: -------------------------------------------------------------------------------- 1 | import type { StandardSchemaV1 } from "@standard-schema/spec"; 2 | import { describe, expect } from "vitest"; 3 | import { v } from "../v"; 4 | 5 | const util = { 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | equal: (_value: a extends b ? (b extends a ? true : false) : false) => { 8 | // just for typechecking 9 | }, 10 | }; 11 | 12 | describe("standard schema compat", (test) => { 13 | test("assignability", () => { 14 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 15 | const _s1: StandardSchemaV1 = v(v.string); 16 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 17 | const _s4: StandardSchemaV1 = v(v.string); 18 | }); 19 | 20 | test("type inference", () => { 21 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 22 | const stringToNumber = v(v.safeInteger); 23 | type input = StandardSchemaV1.InferInput; 24 | util.equal(true); 25 | type output = StandardSchemaV1.InferOutput; 26 | util.equal(true); 27 | }); 28 | 29 | test("valid parse", () => { 30 | const schema = v(v.string); 31 | const result = schema["~standard"]["validate"]("hello"); 32 | if (result instanceof Promise) { 33 | throw new Error("Expected sync result"); 34 | } 35 | expect(result.issues).toEqual(undefined); 36 | if (result.issues) { 37 | throw new Error("Expected no issues"); 38 | } else { 39 | expect(result.value).toEqual("hello"); 40 | } 41 | }); 42 | 43 | test("invalid parse", () => { 44 | const schema = v(v.string); 45 | const result = schema["~standard"]["validate"](1234); 46 | if (result instanceof Promise) { 47 | throw new Error("Expected sync result"); 48 | } 49 | expect(result.issues).toBeDefined(); 50 | if (!result.issues) { 51 | throw new Error("Expected issues"); 52 | } 53 | expect(result.issues.length).toEqual(1); 54 | expect(result.issues[0].path).toEqual([]); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/__tests__/v.testMethod.test.ts: -------------------------------------------------------------------------------- 1 | import { v } from "../v"; 2 | import { testValidator } from "./testValidator"; 3 | import { describe } from "vitest"; 4 | 5 | describe("v.test", (test) => { 6 | test("v.test", () => { 7 | testValidator( 8 | v(v.test(/^a/)), 9 | ["a", "andrew"], 10 | ["A", null, false, [], {}, 1, 0, NaN, undefined, true], 11 | ); 12 | }); 13 | test("{ a: v.test }", () => { 14 | testValidator( 15 | v({ a: v.test(/^a/) }), 16 | ["a", "andrew"].map((a) => ({ a })), 17 | [ 18 | "A", 19 | null, 20 | false, 21 | [], 22 | {}, 23 | 1, 24 | 0, 25 | NaN, 26 | undefined, 27 | true, 28 | ...["A", null, false, [], {}, 1, 0, NaN, undefined, true].map((a) => ({ 29 | a, 30 | })), 31 | ], 32 | ); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/__tests__/v.types.test.ts: -------------------------------------------------------------------------------- 1 | import { describe } from "vitest"; 2 | import { v } from "../v"; 3 | import { testValidator } from "./testValidator"; 4 | 5 | describe("v.[type]", (test) => { 6 | test("v.boolean", () => { 7 | const checkBoolean = v(v.boolean); 8 | testValidator(checkBoolean, [true, false], ["true", "false", 1, 0]); 9 | }); 10 | test("{ a: v.boolean }", () => { 11 | const checkBoolean = v({ a: v.boolean }); 12 | testValidator( 13 | checkBoolean, 14 | [{ a: true }, { a: false }], 15 | ["true", "false", 1, 0].map((a) => ({ a })), 16 | ); 17 | }); 18 | test("v.number", () => { 19 | const checkNumber = v(v.number); 20 | testValidator( 21 | checkNumber, 22 | [1, 0, 1.5, Infinity, -Infinity, -1, NaN], 23 | [ 24 | "1", 25 | null, 26 | undefined, 27 | {}, 28 | [], 29 | { 30 | valueOf() { 31 | return 1; 32 | }, 33 | }, 34 | ], 35 | ); 36 | }); 37 | test("v.string", () => { 38 | const checkString = v(v.string); 39 | testValidator( 40 | checkString, 41 | ["1", ""], 42 | [Symbol.for("quartet"), String, null, 0, undefined, true, false], 43 | ); 44 | }); 45 | test("v.finite", () => { 46 | const checkFinite = v(v.finite); 47 | testValidator( 48 | checkFinite, 49 | [1, 0, 1.5, -1], 50 | [ 51 | "1", 52 | Infinity, 53 | -Infinity, 54 | NaN, 55 | null, 56 | undefined, 57 | {}, 58 | [], 59 | { 60 | valueOf() { 61 | return 1; 62 | }, 63 | }, 64 | ], 65 | ); 66 | }); 67 | test("{ a: v.finite }", () => { 68 | const checkFinite = v({ a: v.finite }); 69 | testValidator( 70 | checkFinite, 71 | [1, 0, 1.5, -1].map((a) => ({ a })), 72 | [ 73 | "1", 74 | Infinity, 75 | -Infinity, 76 | NaN, 77 | null, 78 | undefined, 79 | {}, 80 | [], 81 | { 82 | valueOf() { 83 | return 1; 84 | }, 85 | }, 86 | ...[ 87 | "1", 88 | Infinity, 89 | -Infinity, 90 | NaN, 91 | null, 92 | undefined, 93 | {}, 94 | [], 95 | { 96 | valueOf() { 97 | return 1; 98 | }, 99 | }, 100 | ].map((a) => ({ a })), 101 | ], 102 | ); 103 | }); 104 | test("v.safeInteger", () => { 105 | const checkSafeInteger = v(v.safeInteger); 106 | testValidator( 107 | checkSafeInteger, 108 | [1, 0, -1], 109 | [ 110 | "1", 111 | 1.5, 112 | Infinity, 113 | -Infinity, 114 | NaN, 115 | null, 116 | undefined, 117 | {}, 118 | [], 119 | { 120 | valueOf() { 121 | return 1; 122 | }, 123 | }, 124 | ], 125 | ); 126 | }); 127 | test("v.function", () => { 128 | const checkFunction = v(v.function); 129 | testValidator( 130 | checkFunction, 131 | [ 132 | () => true, 133 | function () { 134 | return true; 135 | }, 136 | new Function("return true"), 137 | ], 138 | [1, null, undefined], 139 | ); 140 | }); 141 | test("{ a: v.function}", () => { 142 | const checkFunction = v({ a: v.function }); 143 | testValidator( 144 | checkFunction, 145 | [ 146 | () => true, 147 | function () { 148 | return true; 149 | }, 150 | new Function("return true"), 151 | ].map((a) => ({ a })), 152 | [1, null, undefined, {}, [], { a: 1 }, { a: null }], 153 | ); 154 | }); 155 | test("v.symbol", () => { 156 | const checkSymbol = v(v.symbol); 157 | testValidator( 158 | checkSymbol, 159 | [Symbol.for("quartet"), Symbol.for("andrew"), Symbol("123")], 160 | ["symbol", null, undefined], 161 | ); 162 | }); 163 | test("{ a: v.symbol }", () => { 164 | const checkSymbol = v({ a: v.symbol }); 165 | testValidator( 166 | checkSymbol, 167 | [Symbol.for("quartet"), Symbol.for("andrew"), Symbol("123")].map((a) => ({ 168 | a, 169 | })), 170 | [ 171 | "symbol", 172 | null, 173 | undefined, 174 | ...["symbol", null, undefined].map((a) => ({ a })), 175 | ], 176 | ); 177 | }); 178 | test("v.positive", () => { 179 | const checkPositive = v(v.positive); 180 | testValidator( 181 | checkPositive, 182 | [ 183 | 1, 184 | 1.5, 185 | Infinity, 186 | [1], 187 | "1", 188 | { 189 | valueOf() { 190 | return 1; 191 | }, 192 | }, 193 | ], 194 | [0, null, -Infinity, -1, NaN, undefined, {}, []], 195 | ); 196 | }); 197 | test("{ a: v.positive }", () => { 198 | const checkPositive = v({ a: v.positive }); 199 | testValidator( 200 | checkPositive, 201 | [ 202 | 1, 203 | 1.5, 204 | Infinity, 205 | [1], 206 | "1", 207 | { 208 | valueOf() { 209 | return 1; 210 | }, 211 | }, 212 | ].map((a) => ({ a })), 213 | [ 214 | 0, 215 | null, 216 | -Infinity, 217 | -1, 218 | NaN, 219 | undefined, 220 | {}, 221 | [], 222 | ...[0, null, -Infinity, -1, NaN, undefined, {}, []].map((a) => ({ a })), 223 | ], 224 | ); 225 | }); 226 | test("v.negative", () => { 227 | const checkNegative = v(v.negative); 228 | testValidator( 229 | checkNegative, 230 | [ 231 | -1, 232 | -1.5, 233 | -Infinity, 234 | [-1], 235 | "-1", 236 | { 237 | valueOf() { 238 | return -1; 239 | }, 240 | }, 241 | ], 242 | [0, null, Infinity, 1, NaN, undefined, {}, []], 243 | ); 244 | }); 245 | test("{ a: v.negative }", () => { 246 | const checkNegative = v({ a: v.negative }); 247 | testValidator( 248 | checkNegative, 249 | [ 250 | -1, 251 | -1.5, 252 | -Infinity, 253 | [-1], 254 | "-1", 255 | { 256 | valueOf() { 257 | return -1; 258 | }, 259 | }, 260 | ].map((a) => ({ a })), 261 | [ 262 | 0, 263 | null, 264 | Infinity, 265 | 1, 266 | NaN, 267 | undefined, 268 | {}, 269 | [], 270 | ...[0, null, Infinity, 1, NaN, undefined, {}, []].map((a) => ({ a })), 271 | ], 272 | ); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /src/__tests__/v.variants.test.ts: -------------------------------------------------------------------------------- 1 | import { v } from "../v"; 2 | import { testValidator } from "./testValidator"; 3 | import { describe, expect } from "vitest"; 4 | 5 | describe("v([...])", (test) => { 6 | test("v([])", () => { 7 | testValidator(v([]), [], [null, false, [], {}, 1, 0, NaN, undefined, true]); 8 | }); 9 | test("v([1])", () => { 10 | testValidator(v([1]), [1], [null, false, [], {}, 0, NaN, undefined, true]); 11 | }); 12 | test("v([1, 2])", () => { 13 | testValidator( 14 | v([1, 2]), 15 | [1, 2], 16 | ["2", "1", null, false, [], {}, 0, NaN, undefined, true], 17 | ); 18 | }); 19 | test("v([1, '2'])", () => { 20 | testValidator( 21 | v([1, "2"]), 22 | [1, "2"], 23 | [2, "1", null, false, [], {}, 0, NaN, undefined, true], 24 | ); 25 | }); 26 | test("v([true, false])", () => { 27 | let flag = true; 28 | const validator = v([ 29 | v.any, 30 | v.custom(() => { 31 | flag = false; 32 | return false; 33 | }), 34 | ]); 35 | validator(1); 36 | expect(flag).toBeTruthy(); 37 | }); 38 | test("v({ a: [1, 2] })", () => { 39 | const validator = v({ a: [1, 2] }); 40 | testValidator( 41 | validator, 42 | [{ a: 1 }, { a: 2 }, { a: 1, b: 2 }, { a: 2, b: 3 }], 43 | [{}, null, undefined, { a: "1" }, { a: "2" }], 44 | ); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/compilers/beautifyStatements.ts: -------------------------------------------------------------------------------- 1 | function addTabs(line: string, tabsCount: number): string { 2 | let res = line; 3 | for (let i = 0; i < tabsCount; i++) { 4 | res = " " + res; 5 | } 6 | return res; 7 | } 8 | 9 | export function beautifyStatements( 10 | statements: string[], 11 | intialTabSize: number = 1, 12 | ): string[] { 13 | const res: string[] = []; 14 | let tabsCount = intialTabSize; 15 | for (let i = 0; i < statements.length; i++) { 16 | const lines = statements[i].split("\n"); 17 | for (let j = 0; j < lines.length; j++) { 18 | const line = lines[j].trim(); 19 | if (!line) { 20 | continue; 21 | } 22 | if (line === "}") { 23 | tabsCount--; 24 | } 25 | res.push(addTabs(line, tabsCount)); 26 | if (line[line.length - 1] === "{") { 27 | tabsCount++; 28 | } 29 | } 30 | } 31 | return res; 32 | } 33 | -------------------------------------------------------------------------------- /src/compilers/eCompiler/eCompileSchema.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:object-literal-sort-keys */ 2 | import { StandardSchemaV1 } from "@standard-schema/spec"; 3 | import { 4 | ExplanationSchemaType, 5 | IExplanation, 6 | TExplanationSchema, 7 | } from "../../explanations"; 8 | import { Z } from "../../types"; 9 | import { CompilationResult, TSchema, Validator } from "../../types"; 10 | import { implStandard } from "../implStandard"; 11 | import { getExplanator } from "./getExplanator"; 12 | 13 | function getExpectedTypeName(schema: TExplanationSchema): string { 14 | if (schema === undefined) return `undefined`; 15 | if (schema === null) return `null`; 16 | if ( 17 | typeof schema === "boolean" || 18 | typeof schema === "number" || 19 | typeof schema === "string" 20 | ) 21 | return `${JSON.stringify(schema)}`; 22 | if (typeof schema === "symbol") { 23 | return `${schema.toString()}`; 24 | } 25 | if (typeof schema === "bigint") { 26 | return `${schema}n`; 27 | } 28 | if (schema.type === ExplanationSchemaType.And) { 29 | return `and<${schema.schemas.map((t) => getExpectedTypeName(t)).join(",")}>`; 30 | } 31 | if (schema.type === ExplanationSchemaType.Any) { 32 | return `any`; 33 | } 34 | if (schema.type === ExplanationSchemaType.Array) { 35 | return `Array`; 36 | } 37 | if (schema.type === ExplanationSchemaType.ArrayOf) { 38 | return `Array<${getExpectedTypeName(schema.elementSchema)}>`; 39 | } 40 | if (schema.type === ExplanationSchemaType.Boolean) { 41 | return `boolean`; 42 | } 43 | if (schema.type === ExplanationSchemaType.Finite) { 44 | return `finite`; 45 | } 46 | if (schema.type === ExplanationSchemaType.Function) { 47 | return `function`; 48 | } 49 | if (schema.type === ExplanationSchemaType.Max) { 50 | if (schema.isExclusive) { 51 | return `lt<${schema.maxValue}>`; 52 | } else { 53 | return `le<${schema.maxValue}>`; 54 | } 55 | } 56 | if (schema.type === ExplanationSchemaType.MaxLength) { 57 | if (schema.isExclusive) { 58 | return `lengthLt<${schema.maxLength}>`; 59 | } 60 | return `lengthLe<${schema.maxLength}>`; 61 | } 62 | if (schema.type === ExplanationSchemaType.Min) { 63 | if (schema.isExclusive) { 64 | return `gt<${schema.minValue}>`; 65 | } else { 66 | return `ge<${schema.minValue}>`; 67 | } 68 | } 69 | if (schema.type === ExplanationSchemaType.MinLength) { 70 | if (schema.isExclusive) { 71 | return `lengthGt<${schema.minLength}>`; 72 | } 73 | return `lengthGe<${schema.minLength}>`; 74 | } 75 | if (schema.type === ExplanationSchemaType.Negative) { 76 | return `ge<0>`; 77 | } 78 | if (schema.type === ExplanationSchemaType.Never) { 79 | return `never`; 80 | } 81 | if (schema.type === ExplanationSchemaType.Not) { 82 | const inner = getExpectedTypeName(schema.schema); 83 | return `not<${inner}>`; 84 | } 85 | if (schema.type === ExplanationSchemaType.NotANumber) { 86 | return `NaN`; 87 | } 88 | if (schema.type === ExplanationSchemaType.Number) { 89 | return `number`; 90 | } 91 | if (schema.type === ExplanationSchemaType.Object) { 92 | return `{ ${Object.entries(schema.propsSchemas).map((x) => `${x[0]}: ${getExpectedTypeName(x[1])}`)} }`; 93 | } 94 | if (schema.type === ExplanationSchemaType.Pair) { 95 | return `pair<${getExpectedTypeName(schema.keyValueSchema)}>`; 96 | } 97 | if (schema.type === ExplanationSchemaType.Positive) { 98 | return `gt<0>`; 99 | } 100 | if (schema.type === ExplanationSchemaType.SafeInteger) { 101 | return `safeInteger`; 102 | } 103 | if (schema.type === ExplanationSchemaType.String) { 104 | return `string`; 105 | } 106 | if (schema.type === ExplanationSchemaType.Symbol) { 107 | return `symbol`; 108 | } 109 | if (schema.type === ExplanationSchemaType.Test) { 110 | return `test<${schema.description}>`; 111 | } 112 | if (schema.type === ExplanationSchemaType.Variant) { 113 | return `oneOf<${schema.variants.map((x) => getExpectedTypeName(x)).join(", ")}>`; 114 | } 115 | if (schema.type === ExplanationSchemaType.Custom) { 116 | return `custom<${schema.description}>`; 117 | } 118 | 119 | return JSON.stringify(schema); 120 | } 121 | 122 | function getMessage(explanation: IExplanation): string { 123 | const { schema } = explanation; 124 | 125 | return `expected type: ${getExpectedTypeName(schema)}`; 126 | } 127 | 128 | function getPath( 129 | explanation: IExplanation, 130 | ): ReadonlyArray | undefined { 131 | return [...explanation.path]; 132 | } 133 | 134 | export function eCompileSchema( 135 | schema: TSchema, 136 | ): CompilationResult { 137 | const explanator: (value: Z, path: KeyType[]) => null | IExplanation[] = 138 | getExplanator(schema); 139 | const explanations: IExplanation[] = []; 140 | function validator(value: Z) { 141 | const explanationsOrNull = explanator(value, []); 142 | if (explanationsOrNull) { 143 | ( 144 | validator as unknown as CompilationResult 145 | ).explanations = explanationsOrNull; 146 | return false; 147 | } else { 148 | ( 149 | validator as unknown as CompilationResult 150 | ).explanations = []; 151 | return true; 152 | } 153 | } 154 | 155 | const res = Object.assign(validator as Validator, { 156 | explanations, 157 | schema, 158 | cast() { 159 | return this as Z; 160 | }, 161 | }) as Z; 162 | res["~standard"] = implStandard( 163 | res as CompilationResult, 164 | (explanations) => { 165 | return explanations.map((explanation) => { 166 | const message = getMessage(explanation); 167 | const path = getPath(explanation); 168 | return { 169 | /** The error message of the issue. */ 170 | message, 171 | /** The path of the issue, if any. */ 172 | path, 173 | }; 174 | }); 175 | }, 176 | ); 177 | return res; 178 | } 179 | -------------------------------------------------------------------------------- /src/compilers/eCompiler/eCompiler.ts: -------------------------------------------------------------------------------- 1 | import { RawSchema } from "../../IRawSchema"; 2 | import { rawSchemaToSchema } from "../../rawSchemaToSchema"; 3 | import { CompilationResult, Z } from "../../types"; 4 | import { eCompileSchema } from "./eCompileSchema"; 5 | 6 | export function eCompiler( 7 | rawSchema: RawSchema, 8 | ): CompilationResult { 9 | return eCompileSchema(rawSchemaToSchema(rawSchema)); 10 | } 11 | -------------------------------------------------------------------------------- /src/compilers/eCompiler/explanation.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:object-literal-sort-keys */ 2 | import { IExplanation, schemaToExplanationSchema } from "../../explanations"; 3 | import { KeyType, TSchema, Z } from "../../types"; 4 | const EMPTY_PATH: KeyType[] = []; 5 | 6 | export function explanation( 7 | value: Z, 8 | path: KeyType[], 9 | schema: TSchema, 10 | innerExplanations: IExplanation[] = [], 11 | ): IExplanation { 12 | return { 13 | value, 14 | schema: schemaToExplanationSchema(schema), 15 | path: EMPTY_PATH.concat(path), 16 | innerExplanations, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/compilers/eCompiler/getExplanator.ts: -------------------------------------------------------------------------------- 1 | import { IExplanation } from "../../explanations"; 2 | import { getAlloc } from "../../getAlloc"; 3 | import { SchemaType } from "../../schemas/SchemaType"; 4 | import { TSchema, Z } from "../../types"; 5 | import { beautifyStatements } from "../beautifyStatements"; 6 | import { explanation } from "./explanation"; 7 | import { returnExplanations } from "./returnExplanations"; 8 | 9 | export function getExplanator( 10 | schema: TSchema, 11 | ): (value: Z, path: KeyType[]) => null | IExplanation[] { 12 | if (typeof schema !== "object" || schema === null) { 13 | return (value, path) => 14 | value === schema ? null : [explanation(value, path, schema)]; 15 | } 16 | switch (schema.type) { 17 | case SchemaType.And: 18 | case SchemaType.ArrayOf: 19 | case SchemaType.Object: { 20 | const context: Record = {}; 21 | const contextParamName = "ctx"; 22 | const pathParamName = "path"; 23 | const alloc = getAlloc(context, contextParamName); 24 | const statements = returnExplanations( 25 | schema, 26 | alloc, 27 | "value", 28 | pathParamName, 29 | [], 30 | ); 31 | const funcBody = `${beautifyStatements(statements).join( 32 | "\n", 33 | )}\n return null`; 34 | 35 | const explanator = new Function( 36 | "value", 37 | contextParamName, 38 | pathParamName, 39 | funcBody, 40 | ) as (value: Z, context: Z, path: KeyType[]) => null | IExplanation[]; 41 | return (value: Z, path: KeyType[]) => explanator(value, context, path); 42 | } 43 | case SchemaType.Any: 44 | return () => null; 45 | case SchemaType.Array: 46 | return (value, path) => 47 | Array.isArray(value) ? null : [explanation(value, path, schema)]; 48 | case SchemaType.Boolean: 49 | return (value, path) => 50 | typeof value === "boolean" ? null : [explanation(value, path, schema)]; 51 | case SchemaType.Finite: 52 | return (value, path) => 53 | Number.isFinite(value) ? null : [explanation(value, path, schema)]; 54 | case SchemaType.Function: 55 | return (value, path) => 56 | typeof value === "function" ? null : [explanation(value, path, schema)]; 57 | case SchemaType.Max: 58 | return schema.isExclusive 59 | ? (value, path) => 60 | value < schema.maxValue ? null : [explanation(value, path, schema)] 61 | : (value, path) => 62 | value <= schema.maxValue 63 | ? null 64 | : [explanation(value, path, schema)]; 65 | case SchemaType.MaxLength: 66 | return schema.isExclusive 67 | ? (value, path) => 68 | value != null && value.length < schema.maxLength 69 | ? null 70 | : [explanation(value, path, schema)] 71 | : (value, path) => 72 | value != null && value.length <= schema.maxLength 73 | ? null 74 | : [explanation(value, path, schema)]; 75 | case SchemaType.Min: 76 | return schema.isExclusive 77 | ? (value, path) => 78 | value > schema.minValue ? null : [explanation(value, path, schema)] 79 | : (value, path) => 80 | value >= schema.minValue 81 | ? null 82 | : [explanation(value, path, schema)]; 83 | case SchemaType.MinLength: 84 | return schema.isExclusive 85 | ? (value, path) => 86 | value != null && value.length > schema.minLength 87 | ? null 88 | : [explanation(value, path, schema)] 89 | : (value, path) => 90 | value != null && value.length >= schema.minLength 91 | ? null 92 | : [explanation(value, path, schema)]; 93 | case SchemaType.Negative: 94 | return (value, path) => 95 | value < 0 ? null : [explanation(value, path, schema)]; 96 | 97 | case SchemaType.Never: 98 | return (value, path) => [explanation(value, path, schema)]; 99 | case SchemaType.Not: { 100 | const oppositeExplanator = getExplanator(schema.schema); 101 | return (value, path) => 102 | oppositeExplanator(value, path) 103 | ? null 104 | : [explanation(value, path, schema)]; 105 | } 106 | case SchemaType.NotANumber: 107 | return (value, path) => 108 | Number.isNaN(value) ? null : [explanation(value, path, schema)]; 109 | case SchemaType.Number: 110 | return (value, path) => 111 | typeof value === "number" ? null : [explanation(value, path, schema)]; 112 | case SchemaType.Pair: { 113 | const pairValidationExplanator = getExplanator(schema.keyValueSchema); 114 | return (value, path) => { 115 | const pair = { value, key: path[path.length - 1] }; 116 | return pairValidationExplanator(pair, path); 117 | }; 118 | } 119 | case SchemaType.Positive: 120 | return (value, path) => 121 | value > 0 ? null : [explanation(value, path, schema)]; 122 | case SchemaType.SafeInteger: 123 | return (value, path) => 124 | Number.isSafeInteger(value) ? null : [explanation(value, path, schema)]; 125 | case SchemaType.String: 126 | return (value, path) => 127 | typeof value === "string" ? null : [explanation(value, path, schema)]; 128 | case SchemaType.Symbol: 129 | return (value, path) => 130 | typeof value === "symbol" ? null : [explanation(value, path, schema)]; 131 | case SchemaType.Test: 132 | return (value, path) => 133 | schema.tester.test(value) ? null : [explanation(value, path, schema)]; 134 | case SchemaType.Custom: 135 | return (value, path) => { 136 | const { customValidator } = schema; 137 | if (customValidator(value)) { 138 | return null; 139 | } 140 | return [explanation(value, path, schema)]; 141 | }; 142 | case SchemaType.Variant: { 143 | const explanators = schema.variants.map((variantSchema) => 144 | getExplanator(variantSchema), 145 | ); 146 | return (value, path) => { 147 | const innerExplanations: IExplanation[] = []; 148 | for (let i = 0; i < explanators.length; i++) { 149 | const explanator = explanators[i]; 150 | const innerExps = explanator(value, path); 151 | if (!innerExps) { 152 | return null; 153 | } 154 | for (let j = 0; j < innerExps.length; j++) { 155 | innerExplanations.push(innerExps[j]); 156 | } 157 | } 158 | return [explanation(value, path, schema, innerExplanations)]; 159 | }; 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/compilers/eCompiler/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./eCompiler"; 2 | -------------------------------------------------------------------------------- /src/compilers/eCompiler/returnExplanations.test.ts: -------------------------------------------------------------------------------- 1 | import { returnExplanations } from "./returnExplanations"; 2 | import { describe, expect } from "vitest"; 3 | 4 | describe("return explanations", (test) => { 5 | test("primitive", () => { 6 | expect(returnExplanations(123, (x) => x, "value", "path", ["// todo"])) 7 | .toMatchInlineSnapshot(` 8 | [ 9 | "if (value !== c) {", 10 | "es = [e(value, path)]", 11 | "// todo", 12 | "return es", 13 | "}", 14 | ] 15 | `); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/compilers/eCompiler/returnExplanations.ts: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../schemas/SchemaType"; 2 | import { TSchema, Z } from "../../types"; 3 | import { getAccessorWithAlloc, has } from "../../utils"; 4 | import { explanation } from "./explanation"; 5 | import { getExplanator } from "./getExplanator"; 6 | 7 | function renderExplanation( 8 | valueVar: string, 9 | pathVar: string, 10 | schema: TSchema, 11 | alloc: (varName: string, initialValue: Z, singleton?: boolean) => string, 12 | innerExplanationsVar?: string, 13 | ): string { 14 | const getExpVar = alloc( 15 | "e", 16 | (value: Z, path: KeyType[], innerExplanations: Z[] = []) => 17 | explanation(value, path, schema, innerExplanations), 18 | ); 19 | 20 | return innerExplanationsVar 21 | ? `${getExpVar}(${valueVar}, ${pathVar}, ${innerExplanationsVar})` 22 | : `${getExpVar}(${valueVar}, ${pathVar})`; 23 | } 24 | 25 | export function returnExplanations( 26 | schema: TSchema, 27 | alloc: (varName: string, initialValue: Z, singleton?: boolean) => string, 28 | valueVar: string, 29 | pathVar: string, 30 | statementsBeforeInvalidReturn: string[], 31 | ): string[] { 32 | const expsVar = alloc("es", []); 33 | if (typeof schema !== "object" || schema === null) { 34 | const constantVar = alloc("c", schema); 35 | const statements: string[] = []; 36 | statements.push( 37 | `if (${valueVar} !== ${constantVar}) {`, 38 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 39 | ); 40 | for (let i = 0; i < statementsBeforeInvalidReturn.length; i++) { 41 | statements.push(statementsBeforeInvalidReturn[i]); 42 | } 43 | statements.push(`return ${expsVar}`, "}"); 44 | return statements; 45 | } 46 | switch (schema.type) { 47 | case SchemaType.Not: 48 | case SchemaType.Variant: 49 | case SchemaType.Pair: { 50 | const explanator = getExplanator(schema); 51 | const explanatorVar = alloc("variantExp", explanator); 52 | const funcStatements: string[] = []; 53 | funcStatements.push( 54 | `${expsVar} = ${explanatorVar}(${valueVar}, ${pathVar})`, 55 | `if (${expsVar}) {`, 56 | ); 57 | for (let i = 0; i < statementsBeforeInvalidReturn.length; i++) { 58 | funcStatements.push(statementsBeforeInvalidReturn[i]); 59 | } 60 | funcStatements.push(`return ${expsVar}`, `}`); 61 | return funcStatements; 62 | } 63 | case SchemaType.And: { 64 | const andStatements: string[] = []; 65 | for (let i = 0; i < schema.schemas.length; i++) { 66 | const innerSchema = schema.schemas[i]; 67 | const innerStatements = returnExplanations( 68 | innerSchema, 69 | alloc, 70 | valueVar, 71 | pathVar, 72 | statementsBeforeInvalidReturn, 73 | ); 74 | for (let j = 0; j < innerStatements.length; j++) { 75 | andStatements.push(innerStatements[j]); 76 | } 77 | } 78 | return andStatements; 79 | } 80 | case SchemaType.Any: 81 | return []; 82 | case SchemaType.Array: { 83 | const arrayStatements = [ 84 | `if (!Array.isArray(${valueVar})) {`, 85 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 86 | ]; 87 | for (let i = 0; i < statementsBeforeInvalidReturn.length; i++) { 88 | arrayStatements.push(statementsBeforeInvalidReturn[i]); 89 | } 90 | arrayStatements.push(`return ${expsVar}`, `}`); 91 | return arrayStatements; 92 | } 93 | case SchemaType.ArrayOf: { 94 | const arrayOfStatements: string[] = [ 95 | `if (!Array.isArray(${valueVar})) {`, 96 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 97 | ]; 98 | for (let i = 0; i < statementsBeforeInvalidReturn.length; i++) { 99 | arrayOfStatements.push(statementsBeforeInvalidReturn[i]); 100 | } 101 | arrayOfStatements.push(`return ${expsVar}`, `}`); 102 | const indexVar = alloc("i", 0); 103 | const elemVar = alloc("e", undefined); 104 | arrayOfStatements.push( 105 | `for (${indexVar} = 0; ${indexVar} < ${valueVar}.length; ${indexVar}++) {`, 106 | `${elemVar} = ${valueVar}[${indexVar}]`, 107 | `${pathVar}.push(${indexVar})`, 108 | ); 109 | const handleElementStatements = returnExplanations( 110 | schema.elementSchema, 111 | alloc, 112 | elemVar, 113 | pathVar, 114 | statementsBeforeInvalidReturn.concat([`${pathVar}.pop()`]), 115 | ); 116 | 117 | for (let i = 0; i < handleElementStatements.length; i++) { 118 | arrayOfStatements.push(handleElementStatements[i]); 119 | } 120 | 121 | arrayOfStatements.push(`${pathVar}.pop()`, `}`); 122 | 123 | return arrayOfStatements; 124 | } 125 | case SchemaType.Boolean: 126 | return [ 127 | `if (typeof ${valueVar} !== 'boolean') {`, 128 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 129 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 130 | 131 | case SchemaType.Finite: 132 | return [ 133 | `if (!Number.isFinite(${valueVar})) {`, 134 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 135 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 136 | case SchemaType.Function: 137 | return [ 138 | `if (typeof ${valueVar} !== 'function') {`, 139 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 140 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 141 | case SchemaType.Max: { 142 | const maxValueVar = alloc("mv", schema.maxValue); 143 | const cmpMax = schema.isExclusive ? "<" : "<="; 144 | return [ 145 | `if (!(${valueVar} ${cmpMax} ${maxValueVar})) {`, 146 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 147 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 148 | } 149 | case SchemaType.MaxLength: { 150 | const maxLengthVar = alloc("ml", schema.maxLength); 151 | const cmpMaxLen = schema.isExclusive ? "<" : "<="; 152 | return [ 153 | `if (${valueVar} == null || !(${valueVar}.length ${cmpMaxLen} ${maxLengthVar})) {`, 154 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 155 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 156 | } 157 | case SchemaType.Min: { 158 | const minValueVar = alloc("mv", schema.minValue); 159 | const cmpMin = schema.isExclusive ? ">" : ">="; 160 | return [ 161 | `if (!(${valueVar} ${cmpMin} ${minValueVar})) {`, 162 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 163 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 164 | } 165 | 166 | case SchemaType.MinLength: { 167 | const minLengthVar = alloc("ml", schema.minLength); 168 | const cmpMinLen = schema.isExclusive ? ">" : ">="; 169 | return [ 170 | `if (${valueVar} == null || !(${valueVar}.length ${cmpMinLen} ${minLengthVar})) {`, 171 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 172 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 173 | } 174 | case SchemaType.Negative: 175 | return [ 176 | `if (!(${valueVar} < 0)) {`, 177 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 178 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 179 | case SchemaType.Never: 180 | return [ 181 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 182 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`]); 183 | case SchemaType.NotANumber: 184 | return [ 185 | `if (!Number.isNaN(${valueVar})) {`, 186 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 187 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 188 | case SchemaType.Number: 189 | return [ 190 | `if (typeof ${valueVar} !== 'number') {`, 191 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 192 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 193 | case SchemaType.Object: { 194 | const statements: string[] = []; 195 | statements.push( 196 | `if (${valueVar} == null) {`, 197 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 198 | ); 199 | for (let i = 0; i < statementsBeforeInvalidReturn.length; i++) { 200 | statements.push(statementsBeforeInvalidReturn[i]); 201 | } 202 | statements.push(`return ${expsVar}`, `}`); 203 | const { props, propsSchemas } = schema; 204 | for (let i = 0; i < props.length; i++) { 205 | const prop = props[i]; 206 | const propSchema = propsSchemas[prop]; 207 | 208 | statements.push(`${pathVar}.push(${JSON.stringify(prop)})`); 209 | const handlePropStatements = returnExplanations( 210 | propSchema, 211 | alloc, 212 | `${valueVar}${getAccessorWithAlloc(prop, alloc)}`, 213 | pathVar, 214 | statementsBeforeInvalidReturn.concat([`${pathVar}.pop()`]), 215 | ); 216 | for (let j = 0; j < handlePropStatements.length; j++) { 217 | statements.push(handlePropStatements[j]); 218 | } 219 | statements.push(`${pathVar}.pop()`); 220 | } 221 | 222 | if (schema.hasRestValidator) { 223 | const restPropsVar = alloc("rps", []); 224 | const indexVar = alloc("i", 0); 225 | const restPropVar = alloc("rp", ""); 226 | const restPropValueVar = alloc("rpv", undefined); 227 | const hasVar = alloc("has", has, true); 228 | const propsSchemasVar = alloc("ps", schema.propsSchemas); 229 | const restOmitDictVar = alloc("rod", schema.restOmitDict); 230 | 231 | statements.push( 232 | `${restPropsVar} = Object.keys(${valueVar})`, 233 | `for (${indexVar} = 0; ${indexVar} < ${restPropsVar}.length; ${indexVar}++) {`, 234 | `${restPropVar} = ${restPropsVar}[${indexVar}]`, 235 | `if (${hasVar}(${propsSchemasVar}, ${restPropVar}) || ${restOmitDictVar}[${restPropVar}] === true) continue;`, 236 | `${restPropValueVar} = ${valueVar}[${restPropVar}]`, 237 | `${pathVar}.push(${restPropVar})`, 238 | ); 239 | const handleRestPropStatements = returnExplanations( 240 | schema.rest, 241 | alloc, 242 | restPropValueVar, 243 | pathVar, 244 | [`${pathVar}.pop()`], 245 | ); 246 | for (let j = 0; j < handleRestPropStatements.length; j++) { 247 | statements.push(handleRestPropStatements[j]); 248 | } 249 | statements.push(`${pathVar}.pop()`, `}`); 250 | } 251 | 252 | return statements; 253 | } 254 | 255 | case SchemaType.Positive: 256 | return [ 257 | `if (!(${valueVar} > 0)) {`, 258 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 259 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 260 | case SchemaType.SafeInteger: 261 | return [ 262 | `if (!Number.isSafeInteger(${valueVar})) {`, 263 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 264 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 265 | case SchemaType.String: 266 | return [ 267 | `if (typeof ${valueVar} !== 'string') {`, 268 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 269 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 270 | case SchemaType.Symbol: 271 | return [ 272 | `if (typeof ${valueVar} !== 'symbol') {`, 273 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 274 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 275 | 276 | case SchemaType.Test: { 277 | const testerVar = alloc("tester", schema.tester); 278 | return [ 279 | `if (!${testerVar}.test(${valueVar})) {`, 280 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 281 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 282 | } 283 | 284 | case SchemaType.Custom: { 285 | const customValidatorVar = alloc("tester", schema.customValidator); 286 | return [ 287 | `if (!${customValidatorVar}(${valueVar})) {`, 288 | `${expsVar} = [${renderExplanation(valueVar, pathVar, schema, alloc)}]`, 289 | ].concat(statementsBeforeInvalidReturn, [`return ${expsVar}`, `}`]); 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/compilers/implStandard.ts: -------------------------------------------------------------------------------- 1 | import { StandardSchemaV1 } from "@standard-schema/spec"; 2 | import { CompilationResult } from "../types"; 3 | 4 | export function implStandard( 5 | v: CompilationResult, 6 | explanationsToIssues: (explanation: readonly E[]) => StandardSchemaV1.Issue[], 7 | ): StandardSchemaV1["~standard"] { 8 | return { 9 | validate: (value) => 10 | v(value) 11 | ? { 12 | value: value as T, 13 | issues: undefined, 14 | } 15 | : { 16 | issues: explanationsToIssues(v.explanations), 17 | }, 18 | vendor: "quartet", 19 | version: 1, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/compilers/vCompiler/getValidatorFromSchema.ts: -------------------------------------------------------------------------------- 1 | import { getAlloc } from "../../getAlloc"; 2 | import { SchemaType } from "../../schemas/SchemaType"; 3 | import { TSchema, Z } from "../../types"; 4 | import { beautifyStatements } from "../beautifyStatements"; 5 | import { ifInvalidReturnFalse } from "./ifInvalidReturnFalse"; 6 | 7 | export function getValidatorFromSchema( 8 | schema: TSchema, 9 | key: string | number | undefined, 10 | ): (value: Z) => boolean { 11 | if (typeof schema !== "object" || schema === null) { 12 | return (value: Z) => value === schema; 13 | } 14 | switch (schema.type) { 15 | case SchemaType.And: 16 | case SchemaType.Object: 17 | case SchemaType.ArrayOf: { 18 | const context: Record = {}; 19 | const contextParamName = "ctx"; 20 | const alloc = getAlloc(context, contextParamName); 21 | const funcBody = `${beautifyStatements([ 22 | ifInvalidReturnFalse(schema, alloc, "value", key), 23 | ]).join("\n")}\n return true`; 24 | const isValid = new Function("value", contextParamName, funcBody) as ( 25 | value: Z, 26 | context: Z, 27 | ) => boolean; 28 | return (value) => isValid(value, context); 29 | } 30 | case SchemaType.Any: 31 | return () => true; 32 | case SchemaType.Array: 33 | return (value) => Array.isArray(value); 34 | case SchemaType.Boolean: 35 | return (value) => typeof value === "boolean"; 36 | case SchemaType.Finite: 37 | return (value) => Number.isFinite(value); 38 | case SchemaType.Function: 39 | return (value) => typeof value === "function"; 40 | case SchemaType.Max: 41 | return schema.isExclusive 42 | ? (value) => value < schema.maxValue 43 | : (value) => value <= schema.maxValue; 44 | case SchemaType.MaxLength: 45 | return schema.isExclusive 46 | ? (value) => value != null && value.length < schema.maxLength 47 | : (value) => value != null && value.length <= schema.maxLength; 48 | case SchemaType.Min: 49 | return schema.isExclusive 50 | ? (value) => value > schema.minValue 51 | : (value) => value >= schema.minValue; 52 | case SchemaType.MinLength: 53 | return schema.isExclusive 54 | ? (value) => value != null && value.length > schema.minLength 55 | : (value) => value != null && value.length >= schema.minLength; 56 | case SchemaType.Negative: 57 | return (value) => value < 0; 58 | case SchemaType.Never: 59 | return () => false; 60 | case SchemaType.Not: { 61 | const isNotValid = getValidatorFromSchema(schema.schema, key); 62 | return (value) => !isNotValid(value); 63 | } 64 | case SchemaType.NotANumber: 65 | return (value) => Number.isNaN(value); 66 | case SchemaType.Number: 67 | return (value) => typeof value === "number"; 68 | case SchemaType.Pair: { 69 | const isValidPair = getValidatorFromSchema(schema.keyValueSchema, key); 70 | const pair = { 71 | value: undefined, 72 | key, 73 | }; 74 | return (value) => { 75 | pair.value = value; 76 | return isValidPair(pair); 77 | }; 78 | } 79 | case SchemaType.Positive: 80 | return (value) => value > 0; 81 | case SchemaType.SafeInteger: 82 | return (value) => Number.isSafeInteger(value); 83 | case SchemaType.String: 84 | return (value) => typeof value === "string"; 85 | case SchemaType.Symbol: 86 | return (value) => typeof value === "symbol"; 87 | case SchemaType.Test: 88 | return (value) => schema.tester.test(value); 89 | case SchemaType.Custom: 90 | return schema.customValidator; 91 | case SchemaType.Variant: { 92 | const { variants } = schema; 93 | const compiledVariants = variants.map((innerSchema) => 94 | getValidatorFromSchema(innerSchema, key), 95 | ); 96 | return (value) => { 97 | for (let i = 0; i < compiledVariants.length; i++) { 98 | const compiledVariant = compiledVariants[i]; 99 | if (compiledVariant(value)) { 100 | return true; 101 | } 102 | } 103 | return false; 104 | }; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/compilers/vCompiler/ifInvalidReturnFalse.ts: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../schemas"; 2 | import { TSchema, Z } from "../../types"; 3 | import { getAccessorWithAlloc, has } from "../../utils"; 4 | import { getValidatorFromSchema } from "./getValidatorFromSchema"; 5 | 6 | function fromNegation(negationConditionCode: string) { 7 | return `if (${negationConditionCode}) return false`; 8 | } 9 | 10 | export function ifInvalidReturnFalse( 11 | schema: TSchema, 12 | alloc: (varName: string, initialValue: Z, singleton?: boolean) => string, 13 | valueAddress: string, 14 | keyAddress: string | number | undefined, 15 | ): string { 16 | if (schema === null) { 17 | return fromNegation(`${valueAddress} !== null`); 18 | } 19 | if (schema === undefined) { 20 | return fromNegation(`${valueAddress} !== undefined`); 21 | } 22 | if (typeof schema !== "object") { 23 | const primitiveValueAddress = alloc("primitive", schema); 24 | return fromNegation(`${valueAddress} !== ${primitiveValueAddress}`); 25 | } 26 | switch (schema.type) { 27 | case SchemaType.And: { 28 | const ifInvalidReturnFalseStatements = schema.schemas.map((innerSchema) => 29 | ifInvalidReturnFalse(innerSchema, alloc, valueAddress, keyAddress), 30 | ); 31 | return ifInvalidReturnFalseStatements.join("\n"); 32 | } 33 | case SchemaType.Any: 34 | return ""; 35 | case SchemaType.Array: 36 | return fromNegation(`!Array.isArray(${valueAddress})`); 37 | case SchemaType.ArrayOf: { 38 | const { elementSchema } = schema; 39 | const incrementVar = alloc("i", 0); 40 | const elementVar = alloc("e", undefined); 41 | const checkIsArray = `if (!Array.isArray(${valueAddress})) return false;`; 42 | const checkElements = ` 43 | for (${incrementVar} = 0; ${incrementVar} < ${valueAddress}.length; ${incrementVar}++) { 44 | ${elementVar} = ${valueAddress}[${incrementVar}]; 45 | ${ifInvalidReturnFalse( 46 | elementSchema, 47 | alloc, 48 | elementVar, 49 | incrementVar, 50 | )} 51 | } 52 | `; 53 | return [checkIsArray, checkElements].join("\n"); 54 | } 55 | case SchemaType.Boolean: 56 | return fromNegation(`typeof ${valueAddress} !== 'boolean'`); 57 | case SchemaType.Finite: 58 | return fromNegation(`!Number.isFinite(${valueAddress})`); 59 | case SchemaType.Function: 60 | return fromNegation(`typeof ${valueAddress} !== 'function'`); 61 | case SchemaType.Max: { 62 | const maxValueVar = alloc("maxValue", schema.maxValue); 63 | const cmpMax = schema.isExclusive ? "<" : "<="; 64 | return fromNegation(`!(${valueAddress} ${cmpMax} ${maxValueVar})`); 65 | } 66 | case SchemaType.MaxLength: { 67 | const maxLengthVar = alloc("maxLength", schema.maxLength); 68 | const cmpMaxLength = schema.isExclusive ? "<" : "<="; 69 | return fromNegation( 70 | `${valueAddress} == null || !(${valueAddress}.length ${cmpMaxLength} ${maxLengthVar})`, 71 | ); 72 | } 73 | case SchemaType.Min: { 74 | const minValueVar = alloc("minValue", schema.minValue); 75 | const cmpMin = schema.isExclusive ? ">" : ">="; 76 | return fromNegation(`!(${valueAddress} ${cmpMin} ${minValueVar})`); 77 | } 78 | case SchemaType.MinLength: { 79 | const minLengthVar = alloc("minLength", schema.minLength); 80 | const cmp = schema.isExclusive ? ">" : ">="; 81 | return fromNegation( 82 | `${valueAddress} == null || !(${valueAddress}.length ${cmp} ${minLengthVar})`, 83 | ); 84 | } 85 | case SchemaType.Negative: 86 | return fromNegation(`!(${valueAddress} < 0)`); 87 | case SchemaType.Never: 88 | return "return false"; 89 | case SchemaType.Not: { 90 | if (typeof schema.schema !== "object" || schema.schema === null) { 91 | if (schema.schema === null) { 92 | return fromNegation(`${valueAddress} === null`); 93 | } 94 | if (schema.schema === undefined) { 95 | return fromNegation(`${valueAddress} === undefined`); 96 | } 97 | const primitiveValueAddress = alloc("primitive", schema.schema); 98 | return fromNegation(`${valueAddress} === ${primitiveValueAddress}`); 99 | } 100 | const isValid = getValidatorFromSchema(schema.schema, keyAddress); 101 | const isValidVar = alloc("isValid", isValid); 102 | return `if (${isValidVar}(${valueAddress})) return false`; 103 | } 104 | case SchemaType.NotANumber: 105 | return fromNegation(`!Number.isNaN(${valueAddress})`); 106 | case SchemaType.Number: 107 | return fromNegation(`typeof ${valueAddress} !== 'number'`); 108 | case SchemaType.Object: { 109 | const statements: string[] = [ 110 | `if (${valueAddress} == null) return false`, 111 | ]; 112 | for (let i = 0; i < schema.props.length; i++) { 113 | const prop = schema.props[i]; 114 | const propSchema = schema.propsSchemas[prop]; 115 | const propAccessor = getAccessorWithAlloc(prop, alloc); 116 | const checkPropStatement = ifInvalidReturnFalse( 117 | propSchema, 118 | alloc, 119 | `${valueAddress}${propAccessor}`, 120 | JSON.stringify(prop), 121 | ); 122 | statements.push(checkPropStatement); 123 | } 124 | if (schema.hasRestValidator) { 125 | const restOmitDictVar = alloc("ro", schema.restOmitDict); 126 | const restPropsVar = alloc("r", []); 127 | const incVar = alloc("i", 0); 128 | statements.push(`${restPropsVar} = Object.keys(${valueAddress})`); 129 | statements.push( 130 | `for (${incVar} = 0; ${incVar} < ${restPropsVar}.length; ${incVar}++) {`, 131 | ); 132 | const restPropVar = alloc("rp", undefined); 133 | statements.push(`${restPropVar} = ${restPropsVar}[${incVar}];`); 134 | const propsSchemasVar = alloc("ps", schema.propsSchemas); 135 | const hasVar = alloc("has", has, true); 136 | statements.push( 137 | `if (${hasVar}(${propsSchemasVar}, ${restPropVar}) || ${restOmitDictVar}[${restPropVar}] === true) continue;`, 138 | ); 139 | const restValueVar = alloc("rv", undefined); 140 | statements.push(`${restValueVar} = ${valueAddress}[${restPropVar}]`); 141 | statements.push( 142 | ifInvalidReturnFalse(schema.rest, alloc, restValueVar, restPropVar), 143 | ); 144 | statements.push("}"); 145 | } 146 | return statements.join("\n"); 147 | } 148 | case SchemaType.Pair: { 149 | const pairVar = alloc("pair", { value: undefined, key: undefined }); 150 | return [ 151 | `${pairVar}.value = ${valueAddress}`, 152 | `${pairVar}.key = ${keyAddress}`, 153 | `${ifInvalidReturnFalse( 154 | schema.keyValueSchema, 155 | alloc, 156 | pairVar, 157 | keyAddress, 158 | )}`, 159 | ].join("\n"); 160 | } 161 | case SchemaType.Positive: 162 | return fromNegation(`!(${valueAddress} > 0)`); 163 | case SchemaType.SafeInteger: 164 | return fromNegation(`!Number.isSafeInteger(${valueAddress})`); 165 | case SchemaType.String: 166 | return fromNegation(`typeof ${valueAddress} !== 'string'`); 167 | case SchemaType.Symbol: 168 | return fromNegation(`typeof ${valueAddress} !== 'symbol'`); 169 | case SchemaType.Test: { 170 | const testerVar = alloc("t", schema.tester); 171 | return fromNegation(`!${testerVar}.test(${valueAddress})`); 172 | } 173 | case SchemaType.Variant: { 174 | const isOneOf = getValidatorFromSchema(schema, keyAddress); 175 | const isOneOfVar = alloc("cv", isOneOf); 176 | return fromNegation(`!${isOneOfVar}(${valueAddress})`); 177 | } 178 | case SchemaType.Custom: { 179 | const customVar = alloc("custom", schema.customValidator); 180 | return fromNegation(`!${customVar}(${valueAddress})`); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/compilers/vCompiler/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./vCompiler"; 2 | -------------------------------------------------------------------------------- /src/compilers/vCompiler/vCompileSchema.ts: -------------------------------------------------------------------------------- 1 | import { Z } from "../../types"; 2 | import { CompilationResult, TSchema, Validator } from "../../types"; 3 | import { getValidatorFromSchema } from "./getValidatorFromSchema"; 4 | import { implStandard } from "../implStandard"; 5 | 6 | export function vCompileSchema( 7 | schema: TSchema, 8 | ): CompilationResult { 9 | const explanations: Z[] = []; 10 | const validator = getValidatorFromSchema(schema, undefined) as Validator; 11 | const res = Object.assign(validator, { 12 | explanations, 13 | schema, 14 | cast() { 15 | return this as Z; 16 | }, 17 | }) as Z; 18 | res["~standard"] = implStandard(res, () => [ 19 | { 20 | message: "invalid value", 21 | path: [], 22 | }, 23 | ]); 24 | return res; 25 | } 26 | -------------------------------------------------------------------------------- /src/compilers/vCompiler/vCompiler.ts: -------------------------------------------------------------------------------- 1 | import { RawSchema } from "../../IRawSchema"; 2 | import { rawSchemaToSchema } from "../../rawSchemaToSchema"; 3 | import { CompilationResult, Z } from "../../types"; 4 | import { vCompileSchema } from "./vCompileSchema"; 5 | 6 | export function vCompiler( 7 | rawSchema: RawSchema, 8 | ): CompilationResult { 9 | return vCompileSchema(rawSchemaToSchema(rawSchema)); 10 | } 11 | -------------------------------------------------------------------------------- /src/e.ts: -------------------------------------------------------------------------------- 1 | import { eCompiler } from "./compilers/eCompiler"; 2 | import { IExplanation } from "./explanations/types"; 3 | import { IQuartetInstance } from "./IQuartetInstance"; 4 | import { methods } from "./methods"; 5 | 6 | export const e: IQuartetInstance = Object.assign( 7 | eCompiler, 8 | methods, 9 | ); 10 | -------------------------------------------------------------------------------- /src/empty.ts: -------------------------------------------------------------------------------- 1 | export const EMPTY_OBJ = Object.create(null); 2 | -------------------------------------------------------------------------------- /src/explanations/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./schemaToExplanationSchema"; 3 | -------------------------------------------------------------------------------- /src/explanations/schemaToExplanationSchema.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | and, 3 | anySchema, 4 | array, 5 | arrayOf, 6 | boolean, 7 | custom, 8 | finite, 9 | functionSchema, 10 | max, 11 | maxLength, 12 | min, 13 | minLength, 14 | negative, 15 | neverSchema, 16 | not, 17 | notANumber, 18 | number, 19 | objectSchemaWithoutRest, 20 | objectSchemaWithRest, 21 | pair, 22 | positive, 23 | safeInteger, 24 | string, 25 | symbol, 26 | testSchema, 27 | variant, 28 | } from "../schemas"; 29 | import { v } from "../v"; 30 | import { schemaToExplanationSchema } from "./schemaToExplanationSchema"; 31 | import { describe, expect } from "vitest"; 32 | 33 | describe("schemaToExplanationSchema", (test) => { 34 | test("primitives", () => { 35 | expect(schemaToExplanationSchema(null)).toMatchInlineSnapshot(`null`); 36 | expect(schemaToExplanationSchema(undefined)).toMatchInlineSnapshot( 37 | `undefined`, 38 | ); 39 | expect(schemaToExplanationSchema(NaN)).toMatchInlineSnapshot(`NaN`); 40 | expect(schemaToExplanationSchema(false)).toMatchInlineSnapshot(`false`); 41 | expect(schemaToExplanationSchema(true)).toMatchInlineSnapshot(`true`); 42 | expect(schemaToExplanationSchema(0)).toMatchInlineSnapshot(`0`); 43 | expect(schemaToExplanationSchema(1)).toMatchInlineSnapshot(`1`); 44 | expect(schemaToExplanationSchema(Infinity)).toMatchInlineSnapshot( 45 | `Infinity`, 46 | ); 47 | expect(schemaToExplanationSchema(-Infinity)).toMatchInlineSnapshot( 48 | `-Infinity`, 49 | ); 50 | expect( 51 | schemaToExplanationSchema(Symbol.for("quartet")), 52 | ).toMatchInlineSnapshot(`Symbol(quartet)`); 53 | }); 54 | test("and", () => { 55 | const schema = and([safeInteger(), min(1, false), max(5, false)]); 56 | const explanationSchema = schemaToExplanationSchema(schema); 57 | expect(explanationSchema).toMatchInlineSnapshot(` 58 | { 59 | "schemas": [ 60 | { 61 | "type": "SafeInteger", 62 | }, 63 | { 64 | "isExclusive": false, 65 | "minValue": 1, 66 | "type": "Min", 67 | }, 68 | { 69 | "isExclusive": false, 70 | "maxValue": 5, 71 | "type": "Max", 72 | }, 73 | ], 74 | "type": "And", 75 | } 76 | `); 77 | }); 78 | test("anySchema", () => { 79 | const schema = anySchema(); 80 | const explanationSchema = schemaToExplanationSchema(schema); 81 | expect(explanationSchema).toMatchInlineSnapshot(` 82 | { 83 | "type": "Any", 84 | } 85 | `); 86 | }); 87 | test("array", () => { 88 | const schema = array(); 89 | const explanationSchema = schemaToExplanationSchema(schema); 90 | expect(explanationSchema).toMatchInlineSnapshot(` 91 | { 92 | "type": "Array", 93 | } 94 | `); 95 | }); 96 | test("arrayOf", () => { 97 | const schema = arrayOf(number()); 98 | const explanationSchema = schemaToExplanationSchema(schema); 99 | expect(explanationSchema).toMatchInlineSnapshot(` 100 | { 101 | "elementSchema": { 102 | "type": "Number", 103 | }, 104 | "type": "ArrayOf", 105 | } 106 | `); 107 | }); 108 | test("boolean", () => { 109 | const schema = boolean(); 110 | const explanationSchema = schemaToExplanationSchema(schema); 111 | expect(explanationSchema).toMatchInlineSnapshot(` 112 | { 113 | "type": "Boolean", 114 | } 115 | `); 116 | }); 117 | test("custom", () => { 118 | const schema = custom((value) => typeof value === "number"); 119 | const explanationSchema = schemaToExplanationSchema(schema); 120 | expect(explanationSchema).toMatchInlineSnapshot(` 121 | { 122 | "description": "custom", 123 | "innerExplanations": [], 124 | "type": "Custom", 125 | } 126 | `); 127 | }); 128 | test("finite", () => { 129 | const schema = finite(); 130 | const explanationSchema = schemaToExplanationSchema(schema); 131 | expect(explanationSchema).toMatchInlineSnapshot(` 132 | { 133 | "type": "Finite", 134 | } 135 | `); 136 | }); 137 | test("functionSchema", () => { 138 | const schema = functionSchema(); 139 | const explanationSchema = schemaToExplanationSchema(schema); 140 | expect(explanationSchema).toMatchInlineSnapshot(` 141 | { 142 | "type": "Function", 143 | } 144 | `); 145 | }); 146 | test("max", () => { 147 | expect(schemaToExplanationSchema(max(5, true))).toMatchInlineSnapshot(` 148 | { 149 | "isExclusive": true, 150 | "maxValue": 5, 151 | "type": "Max", 152 | } 153 | `); 154 | expect(schemaToExplanationSchema(max(5, false))).toMatchInlineSnapshot(` 155 | { 156 | "isExclusive": false, 157 | "maxValue": 5, 158 | "type": "Max", 159 | } 160 | `); 161 | }); 162 | test("maxLength", () => { 163 | expect(schemaToExplanationSchema(maxLength(5, true))) 164 | .toMatchInlineSnapshot(` 165 | { 166 | "isExclusive": true, 167 | "maxLength": 5, 168 | "type": "MaxLength", 169 | } 170 | `); 171 | expect(schemaToExplanationSchema(maxLength(5, false))) 172 | .toMatchInlineSnapshot(` 173 | { 174 | "isExclusive": false, 175 | "maxLength": 5, 176 | "type": "MaxLength", 177 | } 178 | `); 179 | }); 180 | test("min", () => { 181 | expect(schemaToExplanationSchema(min(5, true))).toMatchInlineSnapshot(` 182 | { 183 | "isExclusive": true, 184 | "minValue": 5, 185 | "type": "Min", 186 | } 187 | `); 188 | expect(schemaToExplanationSchema(min(5, false))).toMatchInlineSnapshot(` 189 | { 190 | "isExclusive": false, 191 | "minValue": 5, 192 | "type": "Min", 193 | } 194 | `); 195 | }); 196 | test("minLength", () => { 197 | expect(schemaToExplanationSchema(minLength(5, true))) 198 | .toMatchInlineSnapshot(` 199 | { 200 | "isExclusive": true, 201 | "minLength": 5, 202 | "type": "MinLength", 203 | } 204 | `); 205 | expect(schemaToExplanationSchema(minLength(5, false))) 206 | .toMatchInlineSnapshot(` 207 | { 208 | "isExclusive": false, 209 | "minLength": 5, 210 | "type": "MinLength", 211 | } 212 | `); 213 | }); 214 | test("negative", () => { 215 | const schema = negative(); 216 | const explanationSchema = schemaToExplanationSchema(schema); 217 | expect(explanationSchema).toMatchInlineSnapshot(` 218 | { 219 | "type": "Negative", 220 | } 221 | `); 222 | }); 223 | test("neverSchema", () => { 224 | const schema = neverSchema(); 225 | const explanationSchema = schemaToExplanationSchema(schema); 226 | expect(explanationSchema).toMatchInlineSnapshot(` 227 | { 228 | "type": "Never", 229 | } 230 | `); 231 | }); 232 | test("not", () => { 233 | const schema = not(number()); 234 | const explanationSchema = schemaToExplanationSchema(schema); 235 | expect(explanationSchema).toMatchInlineSnapshot(` 236 | { 237 | "schema": { 238 | "type": "Number", 239 | }, 240 | "type": "Not", 241 | } 242 | `); 243 | }); 244 | test("notANumber", () => { 245 | const schema = notANumber(); 246 | const explanationSchema = schemaToExplanationSchema(schema); 247 | expect(explanationSchema).toMatchInlineSnapshot(` 248 | { 249 | "type": "NotANumber", 250 | } 251 | `); 252 | }); 253 | test("number", () => { 254 | const schema = number(); 255 | const explanationSchema = schemaToExplanationSchema(schema); 256 | expect(explanationSchema).toMatchInlineSnapshot(` 257 | { 258 | "type": "Number", 259 | } 260 | `); 261 | }); 262 | test("object schema", () => { 263 | expect(schemaToExplanationSchema(objectSchemaWithoutRest({ a: number() }))) 264 | .toMatchInlineSnapshot(` 265 | { 266 | "propsSchemas": { 267 | "a": { 268 | "type": "Number", 269 | }, 270 | }, 271 | "type": "Object", 272 | } 273 | `); 274 | expect( 275 | schemaToExplanationSchema( 276 | objectSchemaWithRest({ a: number() }, v.string, {}), 277 | ), 278 | ).toMatchInlineSnapshot(` 279 | { 280 | "[v.restOmit]": [], 281 | "[v.rest]": { 282 | "type": "String", 283 | }, 284 | "propsSchemas": { 285 | "a": { 286 | "type": "Number", 287 | }, 288 | }, 289 | "type": "Object", 290 | } 291 | `); 292 | expect( 293 | schemaToExplanationSchema( 294 | objectSchemaWithRest({ a: number() }, v.string, { valid: true }), 295 | ), 296 | ).toMatchInlineSnapshot(` 297 | { 298 | "[v.restOmit]": [ 299 | "valid", 300 | ], 301 | "[v.rest]": { 302 | "type": "String", 303 | }, 304 | "propsSchemas": { 305 | "a": { 306 | "type": "Number", 307 | }, 308 | }, 309 | "type": "Object", 310 | } 311 | `); 312 | }); 313 | test("pair", () => { 314 | const schema = pair( 315 | objectSchemaWithoutRest({ key: testSchema(/^valid/), value: number() }), 316 | ); 317 | const explanationSchema = schemaToExplanationSchema(schema); 318 | expect(explanationSchema).toMatchInlineSnapshot(` 319 | { 320 | "keyValueSchema": { 321 | "propsSchemas": { 322 | "key": { 323 | "description": "/^valid/", 324 | "type": "Test", 325 | }, 326 | "value": { 327 | "type": "Number", 328 | }, 329 | }, 330 | "type": "Object", 331 | }, 332 | "type": "Pair", 333 | } 334 | `); 335 | }); 336 | test("positive", () => { 337 | const schema = positive(); 338 | const explanationSchema = schemaToExplanationSchema(schema); 339 | expect(explanationSchema).toMatchInlineSnapshot(` 340 | { 341 | "type": "Positive", 342 | } 343 | `); 344 | }); 345 | test("safeInteger", () => { 346 | const schema = safeInteger(); 347 | const explanationSchema = schemaToExplanationSchema(schema); 348 | expect(explanationSchema).toMatchInlineSnapshot(` 349 | { 350 | "type": "SafeInteger", 351 | } 352 | `); 353 | }); 354 | test("string", () => { 355 | const schema = string(); 356 | const explanationSchema = schemaToExplanationSchema(schema); 357 | expect(explanationSchema).toMatchInlineSnapshot(` 358 | { 359 | "type": "String", 360 | } 361 | `); 362 | }); 363 | test("symbol", () => { 364 | const schema = symbol(); 365 | const explanationSchema = schemaToExplanationSchema(schema); 366 | expect(explanationSchema).toMatchInlineSnapshot(` 367 | { 368 | "type": "Symbol", 369 | } 370 | `); 371 | }); 372 | test("testSchema", () => { 373 | const schema = testSchema(/^valid/); 374 | const explanationSchema = schemaToExplanationSchema(schema); 375 | expect(explanationSchema).toMatchInlineSnapshot(` 376 | { 377 | "description": "/^valid/", 378 | "type": "Test", 379 | } 380 | `); 381 | }); 382 | test("variant", () => { 383 | const schema = variant([1, 2, 3, 4, 5]); 384 | const explanationSchema = schemaToExplanationSchema(schema); 385 | expect(explanationSchema).toMatchInlineSnapshot(` 386 | { 387 | "type": "Variant", 388 | "variants": [ 389 | 1, 390 | 2, 391 | 3, 392 | 4, 393 | 5, 394 | ], 395 | } 396 | `); 397 | }); 398 | }); 399 | -------------------------------------------------------------------------------- /src/explanations/schemaToExplanationSchema.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:object-literal-sort-keys */ 2 | import { SchemaType } from "../schemas"; 3 | import { KeyType, TSchema, Z } from "../types"; 4 | import { ExplanationSchemaType, TExplanationSchema } from "./types"; 5 | 6 | function getCustomValidatorDescription(customVailidator: Z): string { 7 | if (typeof customVailidator.description === "string") { 8 | return customVailidator.description; 9 | } 10 | return customVailidator.name || "custom"; 11 | } 12 | 13 | function getTesterDescription(tester: Z): string { 14 | if (typeof tester.description === "string") { 15 | return tester.description; 16 | } 17 | if (typeof tester.name === "string") { 18 | return tester.name; 19 | } 20 | if (typeof tester.toString === "function") { 21 | return tester.toString(); 22 | } 23 | return "tester"; 24 | } 25 | 26 | export function schemaToExplanationSchema(schema: TSchema): TExplanationSchema { 27 | if (typeof schema !== "object" || schema === null) { 28 | return schema; 29 | } 30 | switch (schema.type) { 31 | case SchemaType.And: 32 | return { 33 | type: ExplanationSchemaType.And, 34 | schemas: schema.schemas.map(schemaToExplanationSchema), 35 | }; 36 | case SchemaType.Any: 37 | return { 38 | type: ExplanationSchemaType.Any, 39 | }; 40 | case SchemaType.Array: 41 | return { 42 | type: ExplanationSchemaType.Array, 43 | }; 44 | case SchemaType.ArrayOf: 45 | return { 46 | type: ExplanationSchemaType.ArrayOf, 47 | elementSchema: schemaToExplanationSchema(schema.elementSchema), 48 | }; 49 | case SchemaType.Boolean: 50 | return { 51 | type: ExplanationSchemaType.Boolean, 52 | }; 53 | case SchemaType.Finite: 54 | return { 55 | type: ExplanationSchemaType.Finite, 56 | }; 57 | case SchemaType.Function: 58 | return { 59 | type: ExplanationSchemaType.Function, 60 | }; 61 | case SchemaType.Max: 62 | return { 63 | type: ExplanationSchemaType.Max, 64 | maxValue: schema.maxValue, 65 | isExclusive: schema.isExclusive, 66 | }; 67 | case SchemaType.MaxLength: 68 | return { 69 | type: ExplanationSchemaType.MaxLength, 70 | maxLength: schema.maxLength, 71 | isExclusive: schema.isExclusive, 72 | }; 73 | case SchemaType.Min: 74 | return { 75 | type: ExplanationSchemaType.Min, 76 | minValue: schema.minValue, 77 | isExclusive: schema.isExclusive, 78 | }; 79 | case SchemaType.MinLength: 80 | return { 81 | type: ExplanationSchemaType.MinLength, 82 | minLength: schema.minLength, 83 | isExclusive: schema.isExclusive, 84 | }; 85 | case SchemaType.Negative: 86 | return { 87 | type: ExplanationSchemaType.Negative, 88 | }; 89 | case SchemaType.Never: 90 | return { 91 | type: ExplanationSchemaType.Never, 92 | }; 93 | case SchemaType.Not: 94 | return { 95 | type: ExplanationSchemaType.Not, 96 | schema: schemaToExplanationSchema(schema.schema), 97 | }; 98 | case SchemaType.NotANumber: 99 | return { 100 | type: ExplanationSchemaType.NotANumber, 101 | }; 102 | case SchemaType.Number: 103 | return { 104 | type: ExplanationSchemaType.Number, 105 | }; 106 | case SchemaType.Object: { 107 | const propsSchemas: Record = 108 | Object.create(null); 109 | const { props } = schema; 110 | for (let i = 0; i < props.length; i++) { 111 | const prop = props[i]; 112 | propsSchemas[prop] = schemaToExplanationSchema( 113 | schema.propsSchemas[prop as string], 114 | ); 115 | } 116 | if (!schema.hasRestValidator) { 117 | return { 118 | type: ExplanationSchemaType.Object, 119 | propsSchemas, 120 | }; 121 | } 122 | return { 123 | type: ExplanationSchemaType.Object, 124 | propsSchemas, 125 | "[v.rest]": schemaToExplanationSchema(schema.rest), 126 | "[v.restOmit]": Object.keys(schema.restOmitDict), 127 | }; 128 | } 129 | case SchemaType.Pair: 130 | return { 131 | type: ExplanationSchemaType.Pair, 132 | keyValueSchema: schemaToExplanationSchema(schema.keyValueSchema), 133 | }; 134 | case SchemaType.Positive: 135 | return { 136 | type: ExplanationSchemaType.Positive, 137 | }; 138 | case SchemaType.SafeInteger: 139 | return { 140 | type: ExplanationSchemaType.SafeInteger, 141 | }; 142 | case SchemaType.String: 143 | return { 144 | type: ExplanationSchemaType.String, 145 | }; 146 | case SchemaType.Symbol: 147 | return { 148 | type: ExplanationSchemaType.Symbol, 149 | }; 150 | case SchemaType.Test: 151 | return { 152 | type: ExplanationSchemaType.Test, 153 | description: getTesterDescription(schema.tester), 154 | }; 155 | case SchemaType.Variant: 156 | return { 157 | type: ExplanationSchemaType.Variant, 158 | variants: schema.variants.map(schemaToExplanationSchema), 159 | }; 160 | case SchemaType.Custom: { 161 | const { customValidator, description } = schema; 162 | let innerExplanations: Z[] = []; 163 | if ("explanations" in customValidator) { 164 | const { explanations } = customValidator; 165 | if (Array.isArray(explanations)) { 166 | innerExplanations = explanations; 167 | } 168 | } 169 | return { 170 | type: ExplanationSchemaType.Custom, 171 | description: 172 | description == null 173 | ? getCustomValidatorDescription(customValidator) 174 | : description, 175 | innerExplanations, 176 | }; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/explanations/types.ts: -------------------------------------------------------------------------------- 1 | import { KeyType, TPrimitiveSchema, Z } from "../types"; 2 | 3 | export enum ExplanationSchemaType { 4 | And = "And", 5 | Any = "Any", 6 | Array = "Array", 7 | ArrayOf = "ArrayOf", 8 | Boolean = "Boolean", 9 | Finite = "Finite", 10 | Function = "Function", 11 | Max = "Max", 12 | MaxLength = "MaxLength", 13 | Min = "Min", 14 | MinLength = "MinLength", 15 | Negative = "Negative", 16 | Never = "Never", 17 | Not = "Not", 18 | NotANumber = "NotANumber", 19 | Number = "Number", 20 | Object = "Object", 21 | Pair = "Pair", 22 | Positive = "Positive", 23 | SafeInteger = "SafeInteger", 24 | String = "String", 25 | Symbol = "Symbol", 26 | Test = "Test", 27 | Variant = "Variant", 28 | Custom = "Custom", 29 | } 30 | 31 | export interface IAndExplanationSchema { 32 | type: ExplanationSchemaType.And; 33 | schemas: TExplanationSchema[]; 34 | } 35 | export interface IAnyExplanationSchema { 36 | type: ExplanationSchemaType.Any; 37 | } 38 | 39 | export interface IArrayExplanationSchema { 40 | type: ExplanationSchemaType.Array; 41 | } 42 | 43 | export interface IArrayOfExplanationSchema { 44 | type: ExplanationSchemaType.ArrayOf; 45 | elementSchema: TExplanationSchema; 46 | } 47 | 48 | export interface IBooleanExplanationSchema { 49 | type: ExplanationSchemaType.Boolean; 50 | } 51 | 52 | export interface IFiniteExplanationSchema { 53 | type: ExplanationSchemaType.Finite; 54 | } 55 | 56 | export interface IFunctionExplanationSchema { 57 | type: ExplanationSchemaType.Function; 58 | } 59 | export interface IMaxExplanationSchema { 60 | type: ExplanationSchemaType.Max; 61 | maxValue: number; 62 | isExclusive: boolean; 63 | } 64 | 65 | export interface IMaxLengthExplanationSchema { 66 | type: ExplanationSchemaType.MaxLength; 67 | maxLength: number; 68 | isExclusive: boolean; 69 | } 70 | export interface IMinExplanationSchema { 71 | type: ExplanationSchemaType.Min; 72 | minValue: number; 73 | isExclusive: boolean; 74 | } 75 | 76 | export interface IMinLengthExplanationSchema { 77 | type: ExplanationSchemaType.MinLength; 78 | minLength: number; 79 | isExclusive: boolean; 80 | } 81 | 82 | export interface INegativeExplanationSchema { 83 | type: ExplanationSchemaType.Negative; 84 | } 85 | export interface INeverExplanationSchema { 86 | type: ExplanationSchemaType.Never; 87 | } 88 | export interface INotExplanationSchema { 89 | type: ExplanationSchemaType.Not; 90 | schema: TExplanationSchema; 91 | } 92 | 93 | export interface INumberExplanationSchema { 94 | type: ExplanationSchemaType.Number; 95 | } 96 | 97 | export interface INotANumberExplanationSchema { 98 | type: ExplanationSchemaType.NotANumber; 99 | } 100 | 101 | export interface IObjectExplanationSchemaWithoutRest { 102 | type: ExplanationSchemaType.Object; 103 | propsSchemas: { 104 | [key: string]: TExplanationSchema; 105 | }; 106 | } 107 | 108 | export interface IObjectExplanationSchemaWithRest { 109 | type: ExplanationSchemaType.Object; 110 | propsSchemas: { 111 | [key: string]: TExplanationSchema; 112 | }; 113 | "[v.rest]": TExplanationSchema; 114 | "[v.restOmit]"?: KeyType[]; 115 | } 116 | 117 | export type TObjectExplanationSchema = 118 | | IObjectExplanationSchemaWithoutRest 119 | | IObjectExplanationSchemaWithRest; 120 | 121 | export interface IPairExplanationSchema { 122 | type: ExplanationSchemaType.Pair; 123 | keyValueSchema: TExplanationSchema; 124 | } 125 | 126 | export interface IPositiveExplanationSchema { 127 | type: ExplanationSchemaType.Positive; 128 | } 129 | 130 | export interface ISafeIntegerExplanationSchema { 131 | type: ExplanationSchemaType.SafeInteger; 132 | } 133 | 134 | export interface IStringExplanationSchema { 135 | type: ExplanationSchemaType.String; 136 | } 137 | 138 | export interface ISymbolExplanationSchema { 139 | type: ExplanationSchemaType.Symbol; 140 | } 141 | 142 | export interface ITestExplanationSchema { 143 | type: ExplanationSchemaType.Test; 144 | description: string; 145 | } 146 | 147 | export interface IVariantExplanationSchema { 148 | type: ExplanationSchemaType.Variant; 149 | variants: TExplanationSchema[]; 150 | } 151 | 152 | export interface ICustomExplanationSchema { 153 | type: ExplanationSchemaType.Custom; 154 | description: string; 155 | innerExplanations: Z[]; 156 | } 157 | 158 | export type TExplanationSchema = 159 | | TPrimitiveSchema 160 | | IAndExplanationSchema 161 | | IAnyExplanationSchema 162 | | IArrayExplanationSchema 163 | | IArrayOfExplanationSchema 164 | | IBooleanExplanationSchema 165 | | IFiniteExplanationSchema 166 | | IFunctionExplanationSchema 167 | | IMaxExplanationSchema 168 | | IMaxLengthExplanationSchema 169 | | IMinExplanationSchema 170 | | IMinLengthExplanationSchema 171 | | INegativeExplanationSchema 172 | | INeverExplanationSchema 173 | | INotExplanationSchema 174 | | INumberExplanationSchema 175 | | INotANumberExplanationSchema 176 | | TObjectExplanationSchema 177 | | IPairExplanationSchema 178 | | IPositiveExplanationSchema 179 | | ISafeIntegerExplanationSchema 180 | | IStringExplanationSchema 181 | | ISymbolExplanationSchema 182 | | ITestExplanationSchema 183 | | IVariantExplanationSchema 184 | | ICustomExplanationSchema; 185 | 186 | export interface IExplanation { 187 | value: Z; 188 | path: KeyType[]; 189 | schema: TExplanationSchema; 190 | innerExplanations: IExplanation[]; 191 | } 192 | -------------------------------------------------------------------------------- /src/getAlloc.ts: -------------------------------------------------------------------------------- 1 | import { Z } from "./types"; 2 | import { getAccessor } from "./utils"; 3 | 4 | export function getAlloc(context: Record, contextVar: string) { 5 | const prefixesCounters: Record = {}; 6 | 7 | return (prefix: string, initialValue: Z, singleton?: boolean) => { 8 | if (!prefixesCounters[prefix]) { 9 | context[prefix] = initialValue; 10 | prefixesCounters[prefix] = 1; 11 | return `${contextVar}${getAccessor(prefix)}`; 12 | } 13 | if (singleton) { 14 | if (context[prefix] !== initialValue) { 15 | throw new Error("Wrong singleton usage"); 16 | } 17 | return `${contextVar}${getAccessor(prefix)}`; 18 | } 19 | const newCounter = prefixesCounters[prefix] + 1; 20 | const newAddress = `${prefix}_${newCounter}`; 21 | prefixesCounters[prefix]++; 22 | context[newAddress] = initialValue; 23 | return `${contextVar}${getAccessor(newAddress)}`; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./e"; 2 | export * from "./explanations/types"; 3 | export * from "./types"; 4 | export * from "./v"; 5 | export * from "./IQuartetInstance"; 6 | export * from "./IRawSchema"; 7 | export { ValidatedBy, ValidateBySchema } from "./infer"; 8 | -------------------------------------------------------------------------------- /src/infer.ts: -------------------------------------------------------------------------------- 1 | import { IfAny } from "./IfAny"; 2 | import { SchemaType, SpecialProp } from "./schemas"; 3 | import { TNumberString } from "./strnum"; 4 | import { CompilationResult, TPrimitiveSchema, Z } from "./types"; 5 | import { KeyType } from "./types"; 6 | 7 | type Values = T[keyof T]; 8 | 9 | export interface IFromRawSchema { 10 | __infer_from_raw_schema: R; 11 | } 12 | 13 | export type GetFromRawSchema = T extends { __infer_from_raw_schema: infer R } 14 | ? R 15 | : unknown; 16 | 17 | export type InferOr = Values<{ 18 | [K in Extract]: ValidateBySchema; 19 | }>; 20 | 21 | export type UnionToIntersection = ( 22 | T extends Z ? (param: T) => Z : never 23 | ) extends (param: infer TI) => Z 24 | ? TI 25 | : never; 26 | 27 | type AnyFunction = (...args: Z[]) => Z; 28 | 29 | export type Intersect = IfAny>; 30 | 31 | type InferAnd = T extends readonly [] 32 | ? never 33 | : T extends readonly [infer U] 34 | ? Intersect> | Intersect, Acc> 35 | : T extends readonly [infer U, ...infer R] 36 | ? InferAnd< 37 | R, 38 | | Intersect> 39 | | Intersect, Acc> 40 | > 41 | : Z; 42 | 43 | type OfT = T extends T ? { readonly type: T } : never; 44 | 45 | type TMinSchemaValue = number | TNumberString | bigint | null; 46 | 47 | /// It is not strictly right, but it is almost impossible for a person to use regexps not for strings 48 | type InferTester = T extends RegExp ? string : Z; 49 | 50 | type InferCustom = T extends (value: Z) => value is infer X ? X : Z; 51 | 52 | type TToT = 53 | R extends OfT 54 | ? ValidateBySchema>[] 55 | : R extends OfT 56 | ? Z[] 57 | : R extends OfT 58 | ? InferAnd, Z> 59 | : R extends OfT 60 | ? Z 61 | : R extends OfT 62 | ? boolean 63 | : R extends OfT 64 | ? AnyFunction 65 | : R extends OfT< 66 | | SchemaType.Max 67 | | SchemaType.Min 68 | | SchemaType.Negative 69 | | SchemaType.Positive 70 | > 71 | ? TMinSchemaValue 72 | : R extends OfT 73 | ? { length: TMinSchemaValue } 74 | : R extends OfT< 75 | | SchemaType.Number 76 | | SchemaType.NotANumber 77 | | SchemaType.Finite 78 | | SchemaType.SafeInteger 79 | > 80 | ? number 81 | : R extends OfT 82 | ? string 83 | : R extends OfT 84 | ? symbol 85 | : R extends OfT 86 | ? never 87 | : R extends OfT 88 | ? Z 89 | : R extends OfT 90 | ? InferTester> 91 | : R extends OfT 92 | ? InferCustom> 93 | : Z; 94 | // TODO: IPairSchema; 95 | 96 | type InferRestRestOmit = InferRestless & 97 | Record>, ValidateBySchema>; 98 | 99 | type InferRest = InferRestless & 100 | Record>; 101 | 102 | type RestWithOmit = typeof SpecialProp.Rest | typeof SpecialProp.RestOmit; 103 | 104 | type InferRestless = { -readonly [K in keyof R]: ValidateBySchema }; 105 | type InferObj = R extends { 106 | [SpecialProp.Rest]: infer Rest; 107 | [SpecialProp.RestOmit]: infer RestOmit; 108 | } 109 | ? InferRestRestOmit, Rest, RestOmit> 110 | : R extends { 111 | [SpecialProp.RestOmit]: Z; 112 | } 113 | ? InferRestless> 114 | : R extends { 115 | [SpecialProp.Rest]: infer Rest; 116 | } 117 | ? InferRest, Rest> 118 | : InferRestless; 119 | 120 | export type ValidateBySchema = R extends TPrimitiveSchema 121 | ? R 122 | : R extends readonly [...infer T] 123 | ? InferOr 124 | : R extends OfT 125 | ? TToT 126 | : R extends CompilationResult 127 | ? X 128 | : InferObj; 129 | 130 | export type ValidatedBy = F extends CompilationResult ? X : Z; 131 | -------------------------------------------------------------------------------- /src/methods.ts: -------------------------------------------------------------------------------- 1 | import { IFromRawSchema } from "./infer"; 2 | import { RawSchema } from "./IRawSchema"; 3 | import { rawSchemaToSchema } from "./rawSchemaToSchema"; 4 | import { 5 | and, 6 | anySchema, 7 | array, 8 | arrayOf, 9 | boolean, 10 | custom, 11 | finite, 12 | functionSchema, 13 | max, 14 | maxLength, 15 | min, 16 | minLength, 17 | negative, 18 | neverSchema, 19 | not, 20 | number, 21 | pair, 22 | positive, 23 | safeInteger, 24 | SpecialProp, 25 | string, 26 | symbol, 27 | testSchema, 28 | } from "./schemas"; 29 | import { 30 | IAndSchema, 31 | IArrayOfSchema, 32 | ICustomSchema, 33 | IStringSchema, 34 | ITester, 35 | ITestSchema, 36 | TCustomValidator, 37 | } from "./types"; 38 | 39 | const EMAIL_REGEX = 40 | /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9-]*\.)+[A-Z]{2,}$/i; 41 | 42 | export const methods = { 43 | and( 44 | ...rawSchemas: RR 45 | ): IAndSchema & IFromRawSchema { 46 | return and(rawSchemas.map(rawSchemaToSchema)) as IAndSchema & 47 | IFromRawSchema; 48 | }, 49 | any: anySchema(), 50 | array: array(), 51 | arrayOf( 52 | rawElementSchema: E, 53 | ): IArrayOfSchema & IFromRawSchema { 54 | const elementSchema = rawSchemaToSchema(rawElementSchema); 55 | return arrayOf(elementSchema); 56 | }, 57 | boolean: boolean(), 58 | finite: finite(), 59 | function: functionSchema(), 60 | max(maxValue: number, isExclusive: boolean = false) { 61 | return max(maxValue, isExclusive); 62 | }, 63 | maxLength(maxLengthValue: number, isExclusive: boolean = false) { 64 | return maxLength(maxLengthValue, isExclusive); 65 | }, 66 | min(minValue: number, isExclusive: boolean = false) { 67 | return min(minValue, isExclusive); 68 | }, 69 | minLength(minLengthValue: number, isExclusive: boolean = false) { 70 | return minLength(minLengthValue, isExclusive); 71 | }, 72 | negative: negative(), 73 | never: neverSchema(), 74 | not(rawSchema: RawSchema) { 75 | const schema = rawSchemaToSchema(rawSchema); 76 | return not(schema); 77 | }, 78 | number: number(), 79 | pair(rawKeyValueSchema: RawSchema) { 80 | const keyValueSchema = rawSchemaToSchema(rawKeyValueSchema); 81 | return pair(keyValueSchema); 82 | }, 83 | positive: positive(), 84 | rest: SpecialProp.Rest as SpecialProp.Rest, 85 | restOmit: SpecialProp.RestOmit as SpecialProp.RestOmit, 86 | safeInteger: safeInteger(), 87 | string: string(), 88 | symbol: symbol(), 89 | email: and([string(), testSchema(EMAIL_REGEX)]) as IAndSchema & 90 | IFromRawSchema< 91 | readonly [IStringSchema, ITestSchema & IFromRawSchema] 92 | >, 93 | test(tester: T): ITestSchema & IFromRawSchema { 94 | return testSchema(tester); 95 | }, 96 | custom( 97 | customValidator: T, 98 | description?: string, 99 | ): ICustomSchema & IFromRawSchema { 100 | return custom(customValidator, description) as ICustomSchema & 101 | IFromRawSchema; 102 | }, 103 | }; 104 | 105 | export type IMethods = typeof methods; 106 | -------------------------------------------------------------------------------- /src/rawSchemaToSchema.ts: -------------------------------------------------------------------------------- 1 | import { EMPTY_OBJ } from "./empty"; 2 | import { IRawSchemaDict, RawSchema } from "./IRawSchema"; 3 | import { 4 | neverSchema, 5 | notANumber, 6 | objectSchemaWithoutRest, 7 | objectSchemaWithRest, 8 | SchemaType, 9 | variant as variantSchema, 10 | } from "./schemas"; 11 | import { SpecialProp } from "./schemas/SpecialProp"; 12 | import { KeyType, TSchema, Z } from "./types"; 13 | import { arrToDict, has } from "./utils"; 14 | 15 | const schemaTypeDict = arrToDict(Object.values(SchemaType)); 16 | 17 | function rawPropsSchemasToPropsSchemas( 18 | rawPropsSchemas: IRawSchemaDict, 19 | ): Record { 20 | const propsSchemas = Object.create(null); 21 | const keys = Object.keys(rawPropsSchemas); 22 | for (let i = 0; i < keys.length; i++) { 23 | const key = keys[i]; 24 | const rawSchema = rawPropsSchemas[key]; 25 | const schema = rawSchemaToSchema(rawSchema); 26 | propsSchemas[key] = schema; 27 | } 28 | return propsSchemas; 29 | } 30 | 31 | export function rawSchemaToSchema(rawSchema: RawSchema): TSchema { 32 | if (rawSchema == null) { 33 | return rawSchema as null | undefined; 34 | } 35 | if (typeof rawSchema === "function") { 36 | if (!("schema" in rawSchema)) { 37 | throw new Error( 38 | `Wrap your validation function with v.custom(...) instead of usage of the function directly`, 39 | ); 40 | } 41 | return rawSchema.schema; 42 | } 43 | if (typeof rawSchema !== "object") { 44 | if (typeof rawSchema === "number" && Number.isNaN(rawSchema)) { 45 | return notANumber(); 46 | } 47 | return rawSchema; 48 | } 49 | if ( 50 | (Array.isArray as (value: RawSchema) => value is readonly RawSchema[])( 51 | rawSchema, 52 | ) 53 | ) { 54 | if (rawSchema.length === 0) { 55 | return neverSchema(); 56 | } 57 | if (rawSchema.length === 1) { 58 | return rawSchemaToSchema(rawSchema[0]); 59 | } 60 | const variants: TSchema[] = []; 61 | for (let i = 0; i < rawSchema.length; i++) { 62 | const rawVariant = rawSchema[i]; 63 | const variant = rawSchemaToSchema(rawVariant); 64 | variants.push(variant); 65 | } 66 | return variantSchema(variants); 67 | } 68 | if ( 69 | has(rawSchema, "type") && 70 | schemaTypeDict[rawSchema.type as SchemaType] === true 71 | ) { 72 | return rawSchema as TSchema; 73 | } 74 | if (has(rawSchema, SpecialProp.Rest)) { 75 | if (has(rawSchema, SpecialProp.RestOmit)) { 76 | const { 77 | [SpecialProp.Rest]: rawRest, 78 | [SpecialProp.RestOmit]: restOmit, 79 | ...rawPropsSchemas 80 | } = rawSchema as Z; 81 | const restSchema = rawSchemaToSchema(rawRest); 82 | const propsSchemas = rawPropsSchemasToPropsSchemas( 83 | rawPropsSchemas as Record, 84 | ); 85 | return objectSchemaWithRest( 86 | propsSchemas, 87 | restSchema, 88 | arrToDict(restOmit), 89 | ); 90 | } 91 | const { [SpecialProp.Rest]: rawRest, ...rawPropsSchemas } = rawSchema as Z; 92 | const restSchema = rawSchemaToSchema(rawRest); 93 | const propsSchemas = rawPropsSchemasToPropsSchemas( 94 | rawPropsSchemas as Record, 95 | ); 96 | return objectSchemaWithRest(propsSchemas, restSchema, EMPTY_OBJ); 97 | } 98 | if (has(rawSchema, SpecialProp.RestOmit)) { 99 | const rawPropsSchemas = { ...rawSchema } as Z; 100 | delete rawPropsSchemas[SpecialProp.RestOmit]; 101 | const propsSchemas = rawPropsSchemasToPropsSchemas( 102 | rawPropsSchemas as Record, 103 | ); 104 | return objectSchemaWithoutRest(propsSchemas); 105 | } 106 | const propsSchemas = rawPropsSchemasToPropsSchemas( 107 | rawSchema as Record, 108 | ); 109 | return objectSchemaWithoutRest(propsSchemas); 110 | } 111 | -------------------------------------------------------------------------------- /src/schemas/SchemaType.ts: -------------------------------------------------------------------------------- 1 | export enum SchemaType { 2 | And = "__!quartet/And!__", 3 | Any = "__!quartet/Any!__", 4 | Array = "__!quartet/Array!__", 5 | ArrayOf = "__!quartet/ArrayOf!__", 6 | Boolean = "__!quartet/Boolean!__", 7 | Finite = "__!quartet/Finite!__", 8 | Function = "__!quartet/Function!__", 9 | Max = "__!quartet/Max!__", 10 | MaxLength = "__!quartet/MaxLength!__", 11 | Min = "__!quartet/Min!__", 12 | MinLength = "__!quartet/MinLength!__", 13 | Negative = "__!quartet/Negative!__", 14 | Never = "__!quartet/Never!__", 15 | Not = "__!quartet/Not!__", 16 | NotANumber = "__!quartet/NotANumber!__", 17 | Number = "__!quartet/Number!__", 18 | Object = "__!quartet/Object!__", 19 | Pair = "__!quartet/Pair!__", 20 | Positive = "__!quartet/Positive!__", 21 | SafeInteger = "__!quartet/SafeInteger!__", 22 | String = "__!quartet/String!__", 23 | Symbol = "__!quartet/Symbol!__", 24 | Test = "__!quartet/Test!__", 25 | Variant = "__!quartet/Variant!__", 26 | Custom = "__!quartet/Custom!__", 27 | } 28 | -------------------------------------------------------------------------------- /src/schemas/SpecialProp.ts: -------------------------------------------------------------------------------- 1 | export enum SpecialProp { 2 | Rest = "__!quartet/Rest!__", 3 | RestOmit = "__!quartet/RestOmit!__", 4 | } 5 | -------------------------------------------------------------------------------- /src/schemas/and.ts: -------------------------------------------------------------------------------- 1 | import { IAndSchema, TSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | export function and(schemas: TSchema[]): IAndSchema { 5 | return { 6 | type: SchemaType.And, 7 | schemas, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/schemas/anySchema.ts: -------------------------------------------------------------------------------- 1 | import { IAnySchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | const ANY_SCHEMA: IAnySchema = { 5 | type: SchemaType.Any, 6 | }; 7 | 8 | export function anySchema(): IAnySchema { 9 | return ANY_SCHEMA; 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/array.ts: -------------------------------------------------------------------------------- 1 | import { IArraySchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | const ARRAY_SCHEMA: IArraySchema = { 5 | type: SchemaType.Array, 6 | }; 7 | 8 | export function array(): IArraySchema { 9 | return ARRAY_SCHEMA; 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/arrayOf.ts: -------------------------------------------------------------------------------- 1 | import { IFromRawSchema } from "../infer"; 2 | import { IArrayOfSchema, TSchema } from "../types"; 3 | import { SchemaType } from "./SchemaType"; 4 | 5 | export function arrayOf( 6 | elementSchema: TSchema, 7 | ): IArrayOfSchema & IFromRawSchema { 8 | return { 9 | type: SchemaType.ArrayOf, 10 | elementSchema, 11 | } as IArrayOfSchema & IFromRawSchema; 12 | } 13 | -------------------------------------------------------------------------------- /src/schemas/boolean.ts: -------------------------------------------------------------------------------- 1 | import { IBooleanSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | const BOOLEAN_SCHEMA: IBooleanSchema = { 5 | type: SchemaType.Boolean, 6 | }; 7 | 8 | export function boolean(): IBooleanSchema { 9 | return BOOLEAN_SCHEMA; 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/custom.ts: -------------------------------------------------------------------------------- 1 | import { ICustomSchema, TCustomValidator } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | export function custom( 5 | customValidator: TCustomValidator, 6 | description?: string, 7 | ): ICustomSchema { 8 | return { 9 | customValidator, 10 | description, 11 | type: SchemaType.Custom, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/schemas/finite.ts: -------------------------------------------------------------------------------- 1 | import { IFiniteSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | const FINITE_SCHEMA: IFiniteSchema = { 5 | type: SchemaType.Finite, 6 | }; 7 | 8 | export function finite(): IFiniteSchema { 9 | return FINITE_SCHEMA; 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/functionSchema.ts: -------------------------------------------------------------------------------- 1 | import { IFunctionSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | const FUNCTION_SCHEMA: IFunctionSchema = { 5 | type: SchemaType.Function, 6 | }; 7 | 8 | export function functionSchema(): IFunctionSchema { 9 | return FUNCTION_SCHEMA; 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./and"; 2 | export * from "./anySchema"; 3 | export * from "./array"; 4 | export * from "./arrayOf"; 5 | export * from "./boolean"; 6 | export * from "./custom"; 7 | export * from "./finite"; 8 | export * from "./functionSchema"; 9 | export * from "./max"; 10 | export * from "./maxLength"; 11 | export * from "./min"; 12 | export * from "./minLength"; 13 | export * from "./negative"; 14 | export * from "./neverSchema"; 15 | export * from "./not"; 16 | export * from "./notANumber"; 17 | export * from "./number"; 18 | export * from "./objectSchema"; 19 | export * from "./pair"; 20 | export * from "./positive"; 21 | export * from "./safeInteger"; 22 | export * from "./SchemaType"; 23 | export * from "./SpecialProp"; 24 | export * from "./string"; 25 | export * from "./symbol"; 26 | export * from "./testSchema"; 27 | export * from "./variant"; 28 | -------------------------------------------------------------------------------- /src/schemas/max.ts: -------------------------------------------------------------------------------- 1 | import { IMaxSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | export function max(maxValue: number, isExclusive: boolean): IMaxSchema { 5 | return { 6 | isExclusive, 7 | maxValue, 8 | type: SchemaType.Max, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/maxLength.ts: -------------------------------------------------------------------------------- 1 | import { IMaxLengthSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | export function maxLength( 5 | maxLength: number, 6 | isExclusive: boolean, 7 | ): IMaxLengthSchema { 8 | return { 9 | isExclusive, 10 | maxLength, 11 | type: SchemaType.MaxLength, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/schemas/min.ts: -------------------------------------------------------------------------------- 1 | import { IMinSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | export function min(minValue: number, isExclusive: boolean): IMinSchema { 5 | return { 6 | isExclusive, 7 | minValue, 8 | type: SchemaType.Min, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/minLength.ts: -------------------------------------------------------------------------------- 1 | import { IMinLengthSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | export function minLength( 5 | minLength: number, 6 | isExclusive: boolean, 7 | ): IMinLengthSchema { 8 | return { 9 | isExclusive, 10 | minLength, 11 | type: SchemaType.MinLength, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/schemas/negative.ts: -------------------------------------------------------------------------------- 1 | import { INegativeSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | const NEGATIVE_SCHEMA: INegativeSchema = { 5 | type: SchemaType.Negative, 6 | }; 7 | 8 | export function negative(): INegativeSchema { 9 | return NEGATIVE_SCHEMA; 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/neverSchema.ts: -------------------------------------------------------------------------------- 1 | import { INeverSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | const NEVER_SCHEMA: INeverSchema = { 5 | type: SchemaType.Never, 6 | }; 7 | 8 | export function neverSchema(): INeverSchema { 9 | return NEVER_SCHEMA; 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/not.ts: -------------------------------------------------------------------------------- 1 | import { INotSchema, TSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | export function not(schema: TSchema): INotSchema { 5 | return { 6 | schema, 7 | type: SchemaType.Not, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/schemas/notANumber.ts: -------------------------------------------------------------------------------- 1 | import { INotANumberSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | const NOT_A_NUMBER_SCHEMA: INotANumberSchema = { 5 | type: SchemaType.NotANumber, 6 | }; 7 | export function notANumber(): INotANumberSchema { 8 | return NOT_A_NUMBER_SCHEMA; 9 | } 10 | -------------------------------------------------------------------------------- /src/schemas/number.ts: -------------------------------------------------------------------------------- 1 | import { INumberSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | const NUMBER_SCHEMA: INumberSchema = { 5 | type: SchemaType.Number, 6 | }; 7 | 8 | export function number(): INumberSchema { 9 | return NUMBER_SCHEMA; 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/objectSchema.ts: -------------------------------------------------------------------------------- 1 | import { EMPTY_OBJ } from "../empty"; 2 | import { IObjectSchema, KeyType, TSchema } from "../types"; 3 | import { SchemaType } from "./SchemaType"; 4 | 5 | export function objectSchemaWithRest( 6 | propsSchemas: Record, 7 | rest: TSchema, 8 | restOmitDict: Record, 9 | ): IObjectSchema { 10 | return { 11 | hasRestValidator: true, 12 | props: Object.keys(propsSchemas), 13 | propsSchemas, 14 | rest, 15 | restOmitDict, 16 | type: SchemaType.Object, 17 | }; 18 | } 19 | export function objectSchemaWithoutRest( 20 | propsSchemas: Record, 21 | ): IObjectSchema { 22 | return { 23 | hasRestValidator: false, 24 | props: Object.keys(propsSchemas), 25 | propsSchemas, 26 | rest: null, 27 | restOmitDict: EMPTY_OBJ, 28 | type: SchemaType.Object, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/schemas/pair.ts: -------------------------------------------------------------------------------- 1 | import { IPairSchema, TSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | export function pair(keyValueSchema: TSchema): IPairSchema { 5 | return { 6 | keyValueSchema, 7 | type: SchemaType.Pair, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/schemas/positive.ts: -------------------------------------------------------------------------------- 1 | import { IPositiveSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | const POSITIVE_SCHEMA: IPositiveSchema = { 5 | type: SchemaType.Positive, 6 | }; 7 | 8 | export function positive(): IPositiveSchema { 9 | return POSITIVE_SCHEMA; 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/safeInteger.ts: -------------------------------------------------------------------------------- 1 | import { ISafeIntegerSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | const SAFE_INTEGER_SCHEMA: ISafeIntegerSchema = { 5 | type: SchemaType.SafeInteger, 6 | }; 7 | 8 | export function safeInteger(): ISafeIntegerSchema { 9 | return SAFE_INTEGER_SCHEMA; 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/string.ts: -------------------------------------------------------------------------------- 1 | import { IStringSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | const STRING_SCHEMA: IStringSchema = { 5 | type: SchemaType.String, 6 | }; 7 | 8 | export function string(): IStringSchema { 9 | return STRING_SCHEMA; 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/symbol.ts: -------------------------------------------------------------------------------- 1 | import { ISymbolSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | const SYMBOL_SCHEMA: ISymbolSchema = { 5 | type: SchemaType.Symbol, 6 | }; 7 | 8 | export function symbol(): ISymbolSchema { 9 | return SYMBOL_SCHEMA; 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/testSchema.ts: -------------------------------------------------------------------------------- 1 | import { IFromRawSchema } from "../infer"; 2 | import { ITester, ITestSchema, Z } from "../types"; 3 | import { SchemaType } from "./SchemaType"; 4 | 5 | export function testSchema( 6 | tester: T, 7 | ): ITestSchema & IFromRawSchema { 8 | return { 9 | tester, 10 | type: SchemaType.Test, 11 | } as Z as ITestSchema & IFromRawSchema; 12 | } 13 | -------------------------------------------------------------------------------- /src/schemas/variant.ts: -------------------------------------------------------------------------------- 1 | import { IVariantSchema, TSchema } from "../types"; 2 | import { SchemaType } from "./SchemaType"; 3 | 4 | export function variant(variants: TSchema[]): IVariantSchema { 5 | return { 6 | type: SchemaType.Variant, 7 | variants, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/strnum.ts: -------------------------------------------------------------------------------- 1 | export type TNumberString = `${number}`; 2 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { StandardSchemaV1 } from "@standard-schema/spec"; 2 | import { IfAny } from "./IfAny"; 3 | import { SchemaType } from "./schemas/SchemaType"; 4 | 5 | export interface IAndSchema { 6 | readonly type: SchemaType.And; 7 | readonly schemas: readonly TSchema[]; 8 | } 9 | export interface IAnySchema { 10 | readonly type: SchemaType.Any; 11 | } 12 | 13 | export interface IArraySchema { 14 | readonly type: SchemaType.Array; 15 | } 16 | 17 | export interface IArrayOfSchema { 18 | readonly type: SchemaType.ArrayOf; 19 | readonly elementSchema: TSchema; 20 | } 21 | 22 | export interface IBooleanSchema { 23 | readonly type: SchemaType.Boolean; 24 | } 25 | 26 | export interface IFiniteSchema { 27 | readonly type: SchemaType.Finite; 28 | } 29 | 30 | export interface IFunctionSchema { 31 | readonly type: SchemaType.Function; 32 | } 33 | export interface IMaxSchema { 34 | readonly type: SchemaType.Max; 35 | readonly maxValue: number; 36 | readonly isExclusive: boolean; 37 | } 38 | 39 | export interface IMaxLengthSchema { 40 | readonly type: SchemaType.MaxLength; 41 | readonly maxLength: number; 42 | readonly isExclusive: boolean; 43 | } 44 | export interface IMinSchema { 45 | readonly type: SchemaType.Min; 46 | readonly minValue: number; 47 | readonly isExclusive: boolean; 48 | } 49 | 50 | export interface IMinLengthSchema { 51 | readonly type: SchemaType.MinLength; 52 | readonly minLength: number; 53 | readonly isExclusive: boolean; 54 | } 55 | 56 | export interface INegativeSchema { 57 | readonly type: SchemaType.Negative; 58 | } 59 | export interface INeverSchema { 60 | readonly type: SchemaType.Never; 61 | } 62 | export interface INotSchema { 63 | readonly type: SchemaType.Not; 64 | readonly schema: TSchema; 65 | } 66 | 67 | export interface INumberSchema { 68 | readonly type: SchemaType.Number; 69 | } 70 | 71 | export interface INotANumberSchema { 72 | readonly type: SchemaType.NotANumber; 73 | } 74 | 75 | export type KeyType = string | number; 76 | 77 | export interface IObjectSchema { 78 | readonly type: SchemaType.Object; 79 | readonly propsSchemas: Readonly>; 80 | readonly props: readonly KeyType[]; 81 | readonly hasRestValidator: boolean; 82 | readonly rest: TSchema; 83 | readonly restOmitDict: Readonly>; 84 | } 85 | 86 | export interface IPairSchema { 87 | readonly type: SchemaType.Pair; 88 | readonly keyValueSchema: TSchema; 89 | } 90 | 91 | export interface IPositiveSchema { 92 | readonly type: SchemaType.Positive; 93 | } 94 | 95 | export interface ISafeIntegerSchema { 96 | readonly type: SchemaType.SafeInteger; 97 | } 98 | 99 | export interface IStringSchema { 100 | readonly type: SchemaType.String; 101 | } 102 | 103 | export interface ISymbolSchema { 104 | readonly type: SchemaType.Symbol; 105 | } 106 | 107 | export interface ITester { 108 | readonly test: (value: Z) => boolean; 109 | } 110 | 111 | export interface ITestSchema { 112 | readonly type: SchemaType.Test; 113 | readonly tester: ITester; 114 | } 115 | 116 | export interface IVariantSchema { 117 | readonly type: SchemaType.Variant; 118 | readonly variants: readonly TSchema[]; 119 | } 120 | 121 | export type TCustomValidator = (value: Z) => boolean & { explanations?: Z[] }; 122 | 123 | export interface ICustomSchema { 124 | readonly type: SchemaType.Custom; 125 | readonly description: string | undefined; 126 | readonly customValidator: TCustomValidator; 127 | } 128 | 129 | export type TNonPrimitiveSchema = 130 | | IAndSchema 131 | | IAnySchema 132 | | IArraySchema 133 | | IArrayOfSchema 134 | | IBooleanSchema 135 | | IFiniteSchema 136 | | IFunctionSchema 137 | | IMaxSchema 138 | | IMaxLengthSchema 139 | | IMinSchema 140 | | IMinLengthSchema 141 | | INegativeSchema 142 | | INeverSchema 143 | | INotSchema 144 | | INotANumberSchema 145 | | INumberSchema 146 | | IObjectSchema 147 | | IPairSchema 148 | | IPositiveSchema 149 | | ISafeIntegerSchema 150 | | IStringSchema 151 | | ISymbolSchema 152 | | ITestSchema 153 | | IVariantSchema 154 | | ICustomSchema; 155 | 156 | export type TPrimitiveSchema = 157 | | null 158 | | undefined 159 | | boolean 160 | | symbol 161 | | string 162 | | number; 163 | 164 | export type TSchema = TNonPrimitiveSchema | TPrimitiveSchema; 165 | 166 | export interface ICompilationResultProps { 167 | explanations: Explanation[]; 168 | [key: string]: Z; 169 | } 170 | 171 | export type Validator = IfAny< 172 | T, 173 | (value: Z) => boolean, 174 | (value: Z) => value is T 175 | >; 176 | 177 | export type CompilationResult = Validator & 178 | ICompilationResultProps & { 179 | readonly schema: TSchema; 180 | cast(): CompilationResult; 181 | } & StandardSchemaV1; 182 | 183 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 184 | export type Z = any; 185 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Z } from "./types"; 2 | 3 | const EMPTY: Z = {}; 4 | 5 | export function has(obj: Z, key: string | number): boolean { 6 | if (obj == null) { 7 | return false; 8 | } 9 | if (EMPTY[key] !== undefined) { 10 | return Object.prototype.hasOwnProperty.call(obj, key); 11 | } 12 | return obj[key] !== undefined || key in obj; 13 | } 14 | 15 | export function arrToDict( 16 | values: T[], 17 | ): { [key in T]: boolean } { 18 | const res: { [key in T]: boolean } = Object.create(null); 19 | 20 | for (let i = 0; i < values.length; i++) { 21 | const value = values[i]; 22 | res[value] = true; 23 | } 24 | 25 | return res; 26 | } 27 | 28 | const isSimplePropRegex = /^[a-zA-Z_][_a-zA-Z0-9]*$/; 29 | export function getAccessor(prop: string | number) { 30 | if (typeof prop === "number") { 31 | return `[${prop}]`; 32 | } 33 | if (!isSimplePropRegex.test(prop)) { 34 | return `[${JSON.stringify(prop)}]`; 35 | } 36 | return `.${prop}`; 37 | } 38 | export function getAccessorWithAlloc( 39 | prop: string | number, 40 | alloc: (varName: string, initialValue: Z, singleton?: boolean) => string, 41 | ) { 42 | if (typeof prop === "number") { 43 | if (Number.isSafeInteger(prop)) { 44 | return `[${prop}]`; 45 | } 46 | const propVar = alloc("c", prop); 47 | return `[${propVar}]`; 48 | } 49 | if (!isSimplePropRegex.test(prop)) { 50 | return `[${JSON.stringify(prop)}]`; 51 | } 52 | return `.${prop}`; 53 | } 54 | -------------------------------------------------------------------------------- /src/v.ts: -------------------------------------------------------------------------------- 1 | import { vCompiler } from "./compilers/vCompiler"; 2 | import { IQuartetInstance } from "./IQuartetInstance"; 3 | import { methods } from "./methods"; 4 | import { Z } from "./types"; 5 | 6 | export const v: IQuartetInstance = Object.assign(vCompiler, methods); 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./lib", 7 | "strict": true, 8 | "lib": ["es2017", "dom"], 9 | "downlevelIteration": true 10 | }, 11 | "include": ["src", "src/index.d.ts"], 12 | "exclude": ["node_modules", "**/__tests__/*", "**/*.test*", "examples"] 13 | } 14 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "only-arrow-functions": false, 5 | "prefer-for-of": false, 6 | "no-shadowed-variable": false, 7 | "object-literal-sort-keys": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | // ... 6 | coverage: { 7 | reporter: [["lcov", { projectRoot: "." }]], 8 | exclude: ["lib/**", "examples/**"], 9 | }, 10 | }, 11 | }); 12 | --------------------------------------------------------------------------------