├── .cliff-jumperrc.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── CODEOWNERS ├── hooks │ ├── commit-msg │ └── pre-commit ├── renovate.json └── workflows │ ├── auto-deprecate.yml │ ├── codeql-analysis.yml │ ├── continuous-delivery.yml │ ├── continuous-integration.yml │ ├── deprecate-on-merge.yml │ ├── documentation.yml │ ├── labelsync.yml │ └── publish.yml ├── .gitignore ├── .npm-deprecaterc.yml ├── .prettierignore ├── .prettierrc.mjs ├── .typedoc-json-parserrc.yml ├── .vscode ├── extensions.json └── settings.json ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-git-hooks.cjs └── releases │ └── yarn-4.10.3.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── UPGRADING-v3-v4.md ├── cliff.toml ├── package.json ├── scripts └── rename-cjs-index.mjs ├── sonar-project.properties ├── src ├── constraints │ ├── ArrayConstraints.ts │ ├── BigIntConstraints.ts │ ├── BooleanConstraints.ts │ ├── DateConstraints.ts │ ├── NumberConstraints.ts │ ├── ObjectConstrains.ts │ ├── StringConstraints.ts │ ├── TypedArrayLengthConstraints.ts │ ├── base │ │ └── IConstraint.ts │ ├── type-exports.ts │ └── util │ │ ├── common │ │ ├── combinedResultFn.ts │ │ └── vowels.ts │ │ ├── emailValidator.ts │ │ ├── isUnique.ts │ │ ├── net.ts │ │ ├── operators.ts │ │ ├── phoneValidator.ts │ │ ├── typedArray.ts │ │ └── urlValidators.ts ├── index.ts ├── lib │ ├── Result.ts │ ├── Shapes.ts │ ├── configs.ts │ ├── errors │ │ ├── BaseConstraintError.ts │ │ ├── BaseError.ts │ │ ├── CombinedError.ts │ │ ├── CombinedPropertyError.ts │ │ ├── ExpectedConstraintError.ts │ │ ├── ExpectedValidationError.ts │ │ ├── MissingPropertyError.ts │ │ ├── MultiplePossibilitiesConstraintError.ts │ │ ├── UnknownEnumValueError.ts │ │ ├── UnknownPropertyError.ts │ │ ├── ValidationError.ts │ │ └── error-types.ts │ └── util-types.ts ├── tsconfig.json ├── type-exports.ts └── validators │ ├── ArrayValidator.ts │ ├── BaseValidator.ts │ ├── BigIntValidator.ts │ ├── BooleanValidator.ts │ ├── DateValidator.ts │ ├── DefaultValidator.ts │ ├── InstanceValidator.ts │ ├── LazyValidator.ts │ ├── LiteralValidator.ts │ ├── MapValidator.ts │ ├── NativeEnumValidator.ts │ ├── NeverValidator.ts │ ├── NullishValidator.ts │ ├── NumberValidator.ts │ ├── ObjectValidator.ts │ ├── PassthroughValidator.ts │ ├── RecordValidator.ts │ ├── SetValidator.ts │ ├── StringValidator.ts │ ├── TupleValidator.ts │ ├── TypedArrayValidator.ts │ ├── UnionValidator.ts │ ├── imports.ts │ └── util │ └── getValue.ts ├── tests ├── tsconfig.json └── unit │ ├── common │ └── macros │ │ └── comparators.ts │ ├── lib │ ├── configs.test.ts │ └── errors │ │ ├── BaseConstraintError.test.ts │ │ ├── BaseError.test.ts │ │ ├── CombinedError.test.ts │ │ ├── CombinedPropertyError.test.ts │ │ ├── ExpectedConstraintError.test.ts │ │ ├── ExpectedValidationError.test.ts │ │ ├── MissingPropertyError.test.ts │ │ ├── MultiplePossibilitiesConstraintError.test.ts │ │ ├── UnknownEnumValueError.test.ts │ │ ├── UnknownPropertyError.test.ts │ │ └── ValidationError.test.ts │ ├── util │ └── common │ │ └── combinedResultFn.test.ts │ └── validators │ ├── array.test.ts │ ├── base.test.ts │ ├── bigint.test.ts │ ├── boolean.test.ts │ ├── date.test.ts │ ├── enum.test.ts │ ├── instance.test.ts │ ├── lazy.test.ts │ ├── literal.test.ts │ ├── map.test.ts │ ├── nativeEnum.test.ts │ ├── never.test.ts │ ├── null.test.ts │ ├── nullish.test.ts │ ├── number.test.ts │ ├── object.test.ts │ ├── passthrough.test.ts │ ├── record.test.ts │ ├── set.test.ts │ ├── string.test.ts │ ├── tuple.test.ts │ ├── typedArray.test.ts │ ├── undefined.test.ts │ └── union.test.ts ├── tsconfig.base.json ├── tsconfig.eslint.json ├── tsconfig.typecheck.json ├── tsup.config.ts ├── typedoc.json ├── vitest.config.ts └── yarn.lock /.cliff-jumperrc.yml: -------------------------------------------------------------------------------- 1 | name: shapeshift 2 | packagePath: . 3 | org: sapphire 4 | monoRepo: false 5 | commitMessageTemplate: 'chore(release): release {{new-version}}' 6 | tagTemplate: v{{new-version}} 7 | identifierBase: false 8 | pushTag: true 9 | githubRelease: true 10 | githubReleaseLatest: true 11 | gitRepo: sapphiredev/shapeshift 12 | gitHostVariant: github 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.{js,ts}] 10 | indent_size = 4 11 | indent_style = tab 12 | block_comment_start = /* 13 | block_comment = * 14 | block_comment_end = */ 15 | 16 | [*.{yml,yaml}] 17 | indent_size = 2 18 | indent_style = space 19 | 20 | [*.{md,rmd,mkd,mkdn,mdwn,mdown,markdown,litcoffee}] 21 | tab_width = 4 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sapphire", 3 | "overrides": [ 4 | { 5 | "files": ["tests/**/*.ts"], 6 | "rules": { 7 | "@typescript-eslint/dot-notation": "off" 8 | } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /src/ @kyranet @favna @vladfrangu 2 | /tests/ @kyranet @favna @vladfrangu 3 | LICENSE.md @kyranet @favna @vladfrangu 4 | 5 | /scripts/ @favna 6 | /.github/ @favna 7 | /.vscode/ @favna 8 | .npm-deprecaterc.yml @favna 9 | vitest.config.ts @favna 10 | package.json @favna 11 | README.md @favna 12 | -------------------------------------------------------------------------------- /.github/hooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | yarn commitlint --edit $1 -------------------------------------------------------------------------------- /.github/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | yarn lint-staged 4 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>sapphiredev/.github:sapphire-renovate"], 4 | "npm": { 5 | "packageRules": [ 6 | { 7 | "matchPackagePatterns": ["@types/node"], 8 | "enabled": false 9 | }, 10 | { 11 | "matchPackagePatterns": ["@vitest/browser", "@vitest/coverage-istanbul", "vitest"], 12 | "description": "These packages should be locked until https://github.com/vitest-dev/vitest/issues/5477 is resolved", 13 | "enabled": false 14 | } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/auto-deprecate.yml: -------------------------------------------------------------------------------- 1 | name: NPM Auto Deprecate 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | auto-deprecate: 9 | name: NPM Auto Deprecate 10 | uses: sapphiredev/.github/.github/workflows/reusable-yarn-job.yml@main 11 | with: 12 | script-name: npm-deprecate 13 | secrets: 14 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Code scanning 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: '30 1 * * 0' 12 | 13 | jobs: 14 | codeql: 15 | name: Analysis 16 | uses: sapphiredev/.github/.github/workflows/reusable-codeql.yml@main 17 | -------------------------------------------------------------------------------- /.github/workflows/continuous-delivery.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Delivery 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | prNumber: 7 | description: The number of the PR that is being deployed 8 | required: false 9 | type: string 10 | ref: 11 | description: The branch that is being deployed. Should be a branch on the given repository 12 | required: false 13 | default: main 14 | type: string 15 | repository: 16 | description: The {owner}/{repository} that is being deployed. 17 | required: false 18 | default: sapphiredev/shapeshift 19 | type: string 20 | push: 21 | branches: 22 | - main 23 | 24 | jobs: 25 | Publish: 26 | name: Publish Next to npm 27 | uses: sapphiredev/.github/.github/workflows/reusable-continuous-delivery.yml@main 28 | with: 29 | pr-number: ${{ github.event.inputs.prNumber }} 30 | ref: ${{ github.event.inputs.ref }} 31 | repository: ${{ github.event.inputs.repository }} 32 | secrets: 33 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | linting: 11 | name: Linting 12 | uses: sapphiredev/.github/.github/workflows/reusable-lint.yml@main 13 | 14 | docs: 15 | name: Docgen 16 | if: github.event_name == 'pull_request' 17 | uses: sapphiredev/.github/.github/workflows/reusable-yarn-job.yml@main 18 | with: 19 | script-name: docs 20 | 21 | build: 22 | name: Building 23 | uses: sapphiredev/.github/.github/workflows/reusable-build.yml@main 24 | 25 | test: 26 | name: Tests 27 | uses: sapphiredev/.github/.github/workflows/reusable-tests.yml@main 28 | with: 29 | enable-sonar: true 30 | secrets: 31 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 32 | 33 | typecheck: 34 | name: Typecheck 35 | uses: sapphiredev/.github/.github/workflows/reusable-typecheck.yml@main 36 | -------------------------------------------------------------------------------- /.github/workflows/deprecate-on-merge.yml: -------------------------------------------------------------------------------- 1 | name: NPM Deprecate PR versions On Merge 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | deprecate-on-merge: 10 | name: NPM Deprecate PR versions On Merge 11 | uses: sapphiredev/.github/.github/workflows/reusable-yarn-job.yml@main 12 | with: 13 | script-name: npm-deprecate --name "*pr-${{ github.event.number }}*" -d -v 14 | secrets: 15 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' 9 | 10 | jobs: 11 | docgen: 12 | uses: sapphiredev/.github/.github/workflows/reusable-documentation.yml@main 13 | with: 14 | project-name: shapeshift 15 | secrets: 16 | SKYRA_TOKEN: ${{ secrets.SKYRA_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/labelsync.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Label Sync 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | label_sync: 10 | uses: sapphiredev/.github/.github/workflows/reusable-labelsync.yml@main 11 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | PublishPackage: 8 | name: Publish @sapphire/framework 9 | uses: sapphiredev/.github/.github/workflows/reusable-publish.yml@main 10 | with: 11 | project-name: '@sapphire/framework' 12 | secrets: 13 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 14 | SKYRA_TOKEN: ${{ secrets.SKYRA_TOKEN }} 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore a blackhole and the folder for development 2 | node_modules/ 3 | .vs/ 4 | .idea/ 5 | *.iml 6 | coverage/ 7 | docs/ 8 | 9 | # Yarn files 10 | .yarn/install-state.gz 11 | .yarn/build-state.yml 12 | 13 | # Ignore dist folder 14 | dist/ 15 | *.tsbuildinfo 16 | 17 | # Ignore heapsnapshot and log files 18 | *.heapsnapshot 19 | *.log 20 | 21 | # Ignore package locks 22 | package-lock.json 23 | -------------------------------------------------------------------------------- /.npm-deprecaterc.yml: -------------------------------------------------------------------------------- 1 | name: '*next*' 2 | package: 3 | - '@sapphire/shapeshift' 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn/ 3 | coverage/ 4 | dist/ 5 | CHANGELOG.md 6 | -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | import sapphirePrettierConfig from '@sapphire/prettier-config'; 2 | 3 | export default { 4 | ...sapphirePrettierConfig, 5 | overrides: [ 6 | ...sapphirePrettierConfig.overrides, 7 | { 8 | files: ['README.md', 'UPGRADING-v3-v4.md'], 9 | options: { 10 | tabWidth: 2, 11 | useTabs: false, 12 | printWidth: 120, 13 | proseWrap: 'always' 14 | } 15 | } 16 | ] 17 | }; 18 | -------------------------------------------------------------------------------- /.typedoc-json-parserrc.yml: -------------------------------------------------------------------------------- 1 | json: 'docs/api.json' 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["bierner.github-markdown-preview", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n", 3 | "search.exclude": { 4 | "**/node_modules": true, 5 | "**/bower_components": true, 6 | "**/*.code-search": true, 7 | "**/.yarn": true, 8 | "**/dist/": true, 9 | "**/.git/": true 10 | }, 11 | "cSpell.words": ["bortzmeyer", "fhqwhgads", "Jsonified", "manisharaan", "midichlorians", "Pallas", "Plagueis"], 12 | "sonarlint.connectedMode.project": { 13 | "connectionId": "sapphiredev", 14 | "projectKey": "sapphiredev_shapeshift" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-git-hooks.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-git-hooks", 5 | factory: function (require) { 6 | var plugin=(()=>{var p=Object.create;var i=Object.defineProperty;var u=Object.getOwnPropertyDescriptor;var l=Object.getOwnPropertyNames;var P=Object.getPrototypeOf,m=Object.prototype.hasOwnProperty;var _=(n=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(n,{get:(e,E)=>(typeof require<"u"?require:e)[E]}):n)(function(n){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+n+'" is not supported')});var c=(n,e)=>()=>(e||n((e={exports:{}}).exports,e),e.exports),A=(n,e)=>{for(var E in e)i(n,E,{get:e[E],enumerable:!0})},C=(n,e,E,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let I of l(e))!m.call(n,I)&&I!==E&&i(n,I,{get:()=>e[I],enumerable:!(s=u(e,I))||s.enumerable});return n};var U=(n,e,E)=>(E=n!=null?p(P(n)):{},C(e||!n||!n.__esModule?i(E,"default",{value:n,enumerable:!0}):E,n)),v=n=>C(i({},"__esModule",{value:!0}),n);var L=c((M,B)=>{B.exports=[{name:"Appcircle",constant:"APPCIRCLE",env:"AC_APPCIRCLE"},{name:"AppVeyor",constant:"APPVEYOR",env:"APPVEYOR",pr:"APPVEYOR_PULL_REQUEST_NUMBER"},{name:"AWS CodeBuild",constant:"CODEBUILD",env:"CODEBUILD_BUILD_ARN"},{name:"Azure Pipelines",constant:"AZURE_PIPELINES",env:"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI",pr:"SYSTEM_PULLREQUEST_PULLREQUESTID"},{name:"Bamboo",constant:"BAMBOO",env:"bamboo_planKey"},{name:"Bitbucket Pipelines",constant:"BITBUCKET",env:"BITBUCKET_COMMIT",pr:"BITBUCKET_PR_ID"},{name:"Bitrise",constant:"BITRISE",env:"BITRISE_IO",pr:"BITRISE_PULL_REQUEST"},{name:"Buddy",constant:"BUDDY",env:"BUDDY_WORKSPACE_ID",pr:"BUDDY_EXECUTION_PULL_REQUEST_ID"},{name:"Buildkite",constant:"BUILDKITE",env:"BUILDKITE",pr:{env:"BUILDKITE_PULL_REQUEST",ne:"false"}},{name:"CircleCI",constant:"CIRCLE",env:"CIRCLECI",pr:"CIRCLE_PULL_REQUEST"},{name:"Cirrus CI",constant:"CIRRUS",env:"CIRRUS_CI",pr:"CIRRUS_PR"},{name:"Codefresh",constant:"CODEFRESH",env:"CF_BUILD_ID",pr:{any:["CF_PULL_REQUEST_NUMBER","CF_PULL_REQUEST_ID"]}},{name:"Codemagic",constant:"CODEMAGIC",env:"CM_BUILD_ID",pr:"CM_PULL_REQUEST"},{name:"Codeship",constant:"CODESHIP",env:{CI_NAME:"codeship"}},{name:"Drone",constant:"DRONE",env:"DRONE",pr:{DRONE_BUILD_EVENT:"pull_request"}},{name:"dsari",constant:"DSARI",env:"DSARI"},{name:"Expo Application Services",constant:"EAS",env:"EAS_BUILD"},{name:"Gerrit",constant:"GERRIT",env:"GERRIT_PROJECT"},{name:"GitHub Actions",constant:"GITHUB_ACTIONS",env:"GITHUB_ACTIONS",pr:{GITHUB_EVENT_NAME:"pull_request"}},{name:"GitLab CI",constant:"GITLAB",env:"GITLAB_CI",pr:"CI_MERGE_REQUEST_ID"},{name:"GoCD",constant:"GOCD",env:"GO_PIPELINE_LABEL"},{name:"Google Cloud Build",constant:"GOOGLE_CLOUD_BUILD",env:"BUILDER_OUTPUT"},{name:"Harness CI",constant:"HARNESS",env:"HARNESS_BUILD_ID"},{name:"Heroku",constant:"HEROKU",env:{env:"NODE",includes:"/app/.heroku/node/bin/node"}},{name:"Hudson",constant:"HUDSON",env:"HUDSON_URL"},{name:"Jenkins",constant:"JENKINS",env:["JENKINS_URL","BUILD_ID"],pr:{any:["ghprbPullId","CHANGE_ID"]}},{name:"LayerCI",constant:"LAYERCI",env:"LAYERCI",pr:"LAYERCI_PULL_REQUEST"},{name:"Magnum CI",constant:"MAGNUM",env:"MAGNUM"},{name:"Netlify CI",constant:"NETLIFY",env:"NETLIFY",pr:{env:"PULL_REQUEST",ne:"false"}},{name:"Nevercode",constant:"NEVERCODE",env:"NEVERCODE",pr:{env:"NEVERCODE_PULL_REQUEST",ne:"false"}},{name:"ReleaseHub",constant:"RELEASEHUB",env:"RELEASE_BUILD_ID"},{name:"Render",constant:"RENDER",env:"RENDER",pr:{IS_PULL_REQUEST:"true"}},{name:"Sail CI",constant:"SAIL",env:"SAILCI",pr:"SAIL_PULL_REQUEST_NUMBER"},{name:"Screwdriver",constant:"SCREWDRIVER",env:"SCREWDRIVER",pr:{env:"SD_PULL_REQUEST",ne:"false"}},{name:"Semaphore",constant:"SEMAPHORE",env:"SEMAPHORE",pr:"PULL_REQUEST_NUMBER"},{name:"Shippable",constant:"SHIPPABLE",env:"SHIPPABLE",pr:{IS_PULL_REQUEST:"true"}},{name:"Solano CI",constant:"SOLANO",env:"TDDIUM",pr:"TDDIUM_PR_ID"},{name:"Sourcehut",constant:"SOURCEHUT",env:{CI_NAME:"sourcehut"}},{name:"Strider CD",constant:"STRIDER",env:"STRIDER"},{name:"TaskCluster",constant:"TASKCLUSTER",env:["TASK_ID","RUN_ID"]},{name:"TeamCity",constant:"TEAMCITY",env:"TEAMCITY_VERSION"},{name:"Travis CI",constant:"TRAVIS",env:"TRAVIS",pr:{env:"TRAVIS_PULL_REQUEST",ne:"false"}},{name:"Vercel",constant:"VERCEL",env:{any:["NOW_BUILDER","VERCEL"]}},{name:"Visual Studio App Center",constant:"APPCENTER",env:"APPCENTER_BUILD_ID"},{name:"Woodpecker",constant:"WOODPECKER",env:{CI:"woodpecker"},pr:{CI_BUILD_EVENT:"pull_request"}},{name:"Xcode Cloud",constant:"XCODE_CLOUD",env:"CI_XCODE_PROJECT",pr:"CI_PULL_REQUEST_NUMBER"},{name:"Xcode Server",constant:"XCODE_SERVER",env:"XCS"}]});var T=c(a=>{"use strict";var D=L(),t=process.env;Object.defineProperty(a,"_vendors",{value:D.map(function(n){return n.constant})});a.name=null;a.isPR=null;D.forEach(function(n){let E=(Array.isArray(n.env)?n.env:[n.env]).every(function(s){return S(s)});if(a[n.constant]=E,!!E)switch(a.name=n.name,typeof n.pr){case"string":a.isPR=!!t[n.pr];break;case"object":"env"in n.pr?a.isPR=n.pr.env in t&&t[n.pr.env]!==n.pr.ne:"any"in n.pr?a.isPR=n.pr.any.some(function(s){return!!t[s]}):a.isPR=S(n.pr);break;default:a.isPR=null}});a.isCI=!!(t.CI!=="false"&&(t.BUILD_ID||t.BUILD_NUMBER||t.CI||t.CI_APP_ID||t.CI_BUILD_ID||t.CI_BUILD_NUMBER||t.CI_NAME||t.CONTINUOUS_INTEGRATION||t.RUN_ID||a.name||!1));function S(n){return typeof n=="string"?!!t[n]:"env"in n?t[n.env]&&t[n.env].includes(n.includes):"any"in n?n.any.some(function(e){return!!t[e]}):Object.keys(n).every(function(e){return t[e]===n[e]})}});var d={};A(d,{default:()=>O});var o=U(_("process")),r=_("@yarnpkg/core"),R=U(T()),N={configuration:{gitHooksPath:{description:"Path to git hooks directory (recommended: .github/hooks)",type:r.SettingsType.STRING,default:null},disableGitHooks:{description:"Disable automatic git hooks installation",type:r.SettingsType.BOOLEAN,default:R.default.isCI}},hooks:{afterAllInstalled:async n=>{let e=n.configuration.get("gitHooksPath"),E=n.configuration.get("disableGitHooks"),s=Boolean(n.cwd?.endsWith(`dlx-${o.default.pid}`));if(e&&!R.default.isCI&&!s&&!E)return r.execUtils.pipevp("git",["config","core.hooksPath",e],{cwd:n.cwd,strict:!0,stdin:o.default.stdin,stdout:o.default.stdout,stderr:o.default.stderr})}}},O=N;return v(d);})(); 7 | return plugin; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: true 4 | 5 | gitHooksPath: .github/hooks 6 | 7 | nodeLinker: node-modules 8 | 9 | plugins: 10 | - path: .yarn/plugins/@yarnpkg/plugin-git-hooks.cjs 11 | spec: 'https://raw.githubusercontent.com/trufflehq/yarn-plugin-git-hooks/main/bundles/%40yarnpkg/plugin-git-hooks.js' 12 | 13 | yarnPath: .yarn/releases/yarn-4.10.3.cjs 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright © `2021` `The Sapphire Community and its contributors` 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the “Software”), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | [changelog] 2 | header = """ 3 | # Changelog 4 | 5 | All notable changes to this project will be documented in this file.\n 6 | """ 7 | body = """ 8 | {%- macro remote_url() -%} 9 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} 10 | {%- endmacro -%} 11 | {% if version %}\ 12 | # [{{ version | trim_start_matches(pat="v") }}]\ 13 | {% if previous %}\ 14 | {% if previous.version %}\ 15 | ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\ 16 | {% else %}\ 17 | ({{ self::remote_url() }}/tree/{{ version }})\ 18 | {% endif %}\ 19 | {% endif %} \ 20 | - ({{ timestamp | date(format="%Y-%m-%d") }}) 21 | {% else %}\ 22 | # [unreleased] 23 | {% endif %}\ 24 | {% for group, commits in commits | group_by(attribute="group") %} 25 | ## {{ group | upper_first }} 26 | {% for commit in commits %} 27 | - {% if commit.scope %}\ 28 | **{{commit.scope}}:** \ 29 | {% endif %}\ 30 | {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ 31 | {% if commit.github.pr_number %} (\ 32 | [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) by @{{ commit.github.username }}) \ 33 | {%- endif %}\ 34 | {% if commit.breaking %}\ 35 | {% for breakingChange in commit.footers %}\ 36 | \n{% raw %} {% endraw %}- 💥 **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ 37 | {% endfor %}\ 38 | {% endif %}\ 39 | {% endfor %} 40 | {% endfor %}\n 41 | """ 42 | trim = true 43 | footer = "" 44 | 45 | [git] 46 | conventional_commits = true 47 | filter_unconventional = true 48 | commit_parsers = [ 49 | { message = "^feat", group = "🚀 Features" }, 50 | { message = "^fix", group = "🐛 Bug Fixes" }, 51 | { message = "^docs", group = "📝 Documentation" }, 52 | { message = "^perf", group = "🏃 Performance" }, 53 | { message = "^refactor", group = "🏠 Refactor" }, 54 | { message = "^typings", group = "⌨️ Typings" }, 55 | { message = "^types", group = "⌨️ Typings" }, 56 | { message = ".*deprecated", body = ".*deprecated", group = "🚨 Deprecation" }, 57 | { message = "^revert", skip = true }, 58 | { message = "^style", group = "🪞 Styling" }, 59 | { message = "^test", group = "🧪 Testing" }, 60 | { message = "^chore", skip = true }, 61 | { message = "^ci", skip = true }, 62 | { message = "^build", skip = true }, 63 | { body = ".*security", group = "🛡️ Security" }, 64 | ] 65 | commit_preprocessors = [ 66 | # remove issue numbers from commits 67 | { pattern = '\s\((\w+\s)?#([0-9]+)\)', replace = "" }, 68 | ] 69 | filter_commits = true 70 | tag_pattern = "v[0-9]*" 71 | ignore_tags = "" 72 | topo_order = false 73 | sort_commits = "newest" 74 | 75 | [remote.github] 76 | owner = "sapphiredev" 77 | repo = "shapeshift" 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sapphire/shapeshift", 3 | "version": "4.0.0", 4 | "description": "Blazing fast input validation and transformation ⚡", 5 | "author": "@sapphire", 6 | "license": "MIT", 7 | "main": "dist/cjs/index.cjs", 8 | "module": "dist/esm/index.mjs", 9 | "browser": "dist/iife/index.global.js", 10 | "unpkg": "dist/iife/index.global.js", 11 | "types": "dist/cjs/index.d.cts", 12 | "exports": { 13 | "import": { 14 | "types": "./dist/esm/index.d.mts", 15 | "default": "./dist/esm/index.mjs" 16 | }, 17 | "require": { 18 | "types": "./dist/cjs/index.d.cts", 19 | "default": "./dist/cjs/index.cjs" 20 | }, 21 | "browser": "./dist/iife/index.global.js" 22 | }, 23 | "sideEffects": false, 24 | "homepage": "https://www.sapphirejs.dev", 25 | "scripts": { 26 | "lint": "eslint src tests --ext ts --fix", 27 | "format": "prettier --write \"{src,tests}/**/*.ts\"", 28 | "docs": "typedoc-json-parser", 29 | "test": "vitest run", 30 | "build": "tsup && yarn build:rename-cjs-index", 31 | "build:rename-cjs-index": "node scripts/rename-cjs-index.mjs", 32 | "typecheck": "tsc -p tsconfig.typecheck.json", 33 | "bump": "cliff-jumper", 34 | "check-update": "cliff-jumper --dry-run", 35 | "prepack": "yarn build" 36 | }, 37 | "packageManager": "yarn@4.10.3", 38 | "resolutions": { 39 | "@types/node": "20.19.17", 40 | "ansi-regex": "^5.0.1", 41 | "minimist": "^1.2.8" 42 | }, 43 | "dependencies": { 44 | "fast-deep-equal": "^3.1.3", 45 | "lodash": "^4.17.21" 46 | }, 47 | "devDependencies": { 48 | "@commitlint/cli": "^20.1.0", 49 | "@commitlint/config-conventional": "^20.0.0", 50 | "@favware/cliff-jumper": "^6.0.0", 51 | "@favware/npm-deprecate": "^2.0.0", 52 | "@sapphire/eslint-config": "^5.0.6", 53 | "@sapphire/prettier-config": "^2.0.0", 54 | "@sapphire/ts-config": "^5.0.1", 55 | "@testing-library/jest-dom": "^6.9.1", 56 | "@testing-library/vue": "^8.1.0", 57 | "@types/lodash": "^4.17.20", 58 | "@types/node": "20.19.17", 59 | "@typescript-eslint/eslint-plugin": "^7.18.0", 60 | "@typescript-eslint/parser": "^7.18.0", 61 | "@vitest/coverage-v8": "3.1.3", 62 | "cz-conventional-changelog": "^3.3.0", 63 | "esbuild-plugins-node-modules-polyfill": "^1.7.1", 64 | "eslint": "^8.57.1", 65 | "eslint-config-prettier": "^10.1.8", 66 | "eslint-plugin-prettier": "^5.5.4", 67 | "lint-staged": "^16.2.6", 68 | "prettier": "^3.6.2", 69 | "tsup": "^8.5.0", 70 | "typedoc": "^0.25.13", 71 | "typedoc-json-parser": "^10.2.0", 72 | "typescript": "~5.4.5", 73 | "vite-plugin-node-polyfills": "^0.24.0", 74 | "vitest": "3.1.3" 75 | }, 76 | "repository": { 77 | "type": "git", 78 | "url": "git+https://github.com/sapphiredev/shapeshift.git" 79 | }, 80 | "files": [ 81 | "dist/", 82 | "UPGRADING-v3-v4.md" 83 | ], 84 | "engines": { 85 | "node": ">=20.x" 86 | }, 87 | "keywords": [ 88 | "@sapphire/shapeshift", 89 | "shapeshift", 90 | "bot", 91 | "typescript", 92 | "ts", 93 | "yarn", 94 | "sapphire", 95 | "schema", 96 | "validation", 97 | "type-checking", 98 | "checking", 99 | "input-validation", 100 | "runtime-validation", 101 | "ow", 102 | "type-validation", 103 | "zod" 104 | ], 105 | "bugs": { 106 | "url": "https://github.com/sapphiredev/shapeshift/issues" 107 | }, 108 | "commitlint": { 109 | "extends": [ 110 | "@commitlint/config-conventional" 111 | ] 112 | }, 113 | "lint-staged": { 114 | "*": "prettier --ignore-unknown --write", 115 | "*.{mjs,js,ts}": "eslint --fix --ext mjs,js,ts" 116 | }, 117 | "config": { 118 | "commitizen": { 119 | "path": "./node_modules/cz-conventional-changelog" 120 | } 121 | }, 122 | "publishConfig": { 123 | "access": "public" 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /scripts/rename-cjs-index.mjs: -------------------------------------------------------------------------------- 1 | import { green } from 'colorette'; 2 | import { rename } from 'node:fs/promises'; 3 | import { join } from 'node:path'; 4 | 5 | const inputPath = 'dist/cjs/index.d.ts'; 6 | const outputPath = 'dist/cjs/index.d.cts'; 7 | 8 | const fullInputPathUrl = join(process.cwd(), inputPath); 9 | const fullOutputPathUrl = join(process.cwd(), outputPath); 10 | 11 | await rename(fullInputPathUrl, fullOutputPathUrl); 12 | 13 | console.log(green(`✅ Renamed index.d.ts to index.d.cts`)); 14 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=sapphiredev_shapeshift 2 | sonar.organization=sapphiredev 3 | sonar.javascript.lcov.reportPaths=./coverage/lcov.info 4 | sonar.pullrequest.github.summary_comment=false 5 | -------------------------------------------------------------------------------- /src/constraints/ArrayConstraints.ts: -------------------------------------------------------------------------------- 1 | import { ExpectedConstraintError } from '../lib/errors/ExpectedConstraintError'; 2 | import { Result } from '../lib/Result'; 3 | import type { ValidatorOptions } from '../lib/util-types'; 4 | import type { IConstraint } from './base/IConstraint'; 5 | import { isUnique } from './util/isUnique'; 6 | import { equal, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, notEqual, type Comparator } from './util/operators'; 7 | 8 | export type ArrayConstraintName = `s.array(T).${ 9 | | 'unique' 10 | | `length${ 11 | | 'LessThan' 12 | | 'LessThanOrEqual' 13 | | 'GreaterThan' 14 | | 'GreaterThanOrEqual' 15 | | 'Equal' 16 | | 'NotEqual' 17 | | 'Range' 18 | | 'RangeInclusive' 19 | | 'RangeExclusive'}`}()`; 20 | 21 | function arrayLengthComparator( 22 | comparator: Comparator, 23 | name: ArrayConstraintName, 24 | expected: string, 25 | length: number, 26 | options?: ValidatorOptions 27 | ): IConstraint { 28 | return { 29 | run(input: T[]) { 30 | return comparator(input.length, length) // 31 | ? Result.ok(input) 32 | : Result.err(new ExpectedConstraintError(name, options?.message ?? 'Invalid Array length', input, expected)); 33 | } 34 | }; 35 | } 36 | 37 | export function arrayLengthLessThan(value: number, options?: ValidatorOptions): IConstraint { 38 | const expected = `expected.length < ${value}`; 39 | return arrayLengthComparator(lessThan, 's.array(T).lengthLessThan()', expected, value, options); 40 | } 41 | 42 | export function arrayLengthLessThanOrEqual(value: number, options?: ValidatorOptions): IConstraint { 43 | const expected = `expected.length <= ${value}`; 44 | return arrayLengthComparator(lessThanOrEqual, 's.array(T).lengthLessThanOrEqual()', expected, value, options); 45 | } 46 | 47 | export function arrayLengthGreaterThan(value: number, options?: ValidatorOptions): IConstraint { 48 | const expected = `expected.length > ${value}`; 49 | return arrayLengthComparator(greaterThan, 's.array(T).lengthGreaterThan()', expected, value, options); 50 | } 51 | 52 | export function arrayLengthGreaterThanOrEqual(value: number, options?: ValidatorOptions): IConstraint { 53 | const expected = `expected.length >= ${value}`; 54 | return arrayLengthComparator(greaterThanOrEqual, 's.array(T).lengthGreaterThanOrEqual()', expected, value, options); 55 | } 56 | 57 | export function arrayLengthEqual(value: number, options?: ValidatorOptions): IConstraint { 58 | const expected = `expected.length === ${value}`; 59 | return arrayLengthComparator(equal, 's.array(T).lengthEqual()', expected, value, options); 60 | } 61 | 62 | export function arrayLengthNotEqual(value: number, options?: ValidatorOptions): IConstraint { 63 | const expected = `expected.length !== ${value}`; 64 | return arrayLengthComparator(notEqual, 's.array(T).lengthNotEqual()', expected, value, options); 65 | } 66 | 67 | export function arrayLengthRange(start: number, endBefore: number, options?: ValidatorOptions): IConstraint { 68 | const expected = `expected.length >= ${start} && expected.length < ${endBefore}`; 69 | return { 70 | run(input: T[]) { 71 | return input.length >= start && input.length < endBefore // 72 | ? Result.ok(input) 73 | : Result.err(new ExpectedConstraintError('s.array(T).lengthRange()', options?.message ?? 'Invalid Array length', input, expected)); 74 | } 75 | }; 76 | } 77 | 78 | export function arrayLengthRangeInclusive(start: number, end: number, options?: ValidatorOptions): IConstraint { 79 | const expected = `expected.length >= ${start} && expected.length <= ${end}`; 80 | return { 81 | run(input: T[]) { 82 | return input.length >= start && input.length <= end // 83 | ? Result.ok(input) 84 | : Result.err( 85 | new ExpectedConstraintError('s.array(T).lengthRangeInclusive()', options?.message ?? 'Invalid Array length', input, expected) 86 | ); 87 | } 88 | }; 89 | } 90 | 91 | export function arrayLengthRangeExclusive(startAfter: number, endBefore: number, options?: ValidatorOptions): IConstraint { 92 | const expected = `expected.length > ${startAfter} && expected.length < ${endBefore}`; 93 | return { 94 | run(input: T[]) { 95 | return input.length > startAfter && input.length < endBefore // 96 | ? Result.ok(input) 97 | : Result.err( 98 | new ExpectedConstraintError('s.array(T).lengthRangeExclusive()', options?.message ?? 'Invalid Array length', input, expected) 99 | ); 100 | } 101 | }; 102 | } 103 | 104 | export function uniqueArray(options?: ValidatorOptions): IConstraint { 105 | return { 106 | run(input: unknown[]) { 107 | return isUnique(input) // 108 | ? Result.ok(input) 109 | : Result.err( 110 | new ExpectedConstraintError( 111 | 's.array(T).unique()', 112 | options?.message ?? 'Array values are not unique', 113 | input, 114 | 'Expected all values to be unique' 115 | ) 116 | ); 117 | } 118 | }; 119 | } 120 | -------------------------------------------------------------------------------- /src/constraints/BigIntConstraints.ts: -------------------------------------------------------------------------------- 1 | import { ExpectedConstraintError } from '../lib/errors/ExpectedConstraintError'; 2 | import { Result } from '../lib/Result'; 3 | import type { ValidatorOptions } from '../lib/util-types'; 4 | import type { IConstraint } from './base/IConstraint'; 5 | import { equal, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, notEqual, type Comparator } from './util/operators'; 6 | 7 | export type BigIntConstraintName = `s.bigint().${ 8 | | 'lessThan' 9 | | 'lessThanOrEqual' 10 | | 'greaterThan' 11 | | 'greaterThanOrEqual' 12 | | 'equal' 13 | | 'notEqual' 14 | | 'divisibleBy'}()`; 15 | 16 | function bigintComparator( 17 | comparator: Comparator, 18 | name: BigIntConstraintName, 19 | expected: string, 20 | number: bigint, 21 | options?: ValidatorOptions 22 | ): IConstraint { 23 | return { 24 | run(input: bigint) { 25 | return comparator(input, number) // 26 | ? Result.ok(input) 27 | : Result.err(new ExpectedConstraintError(name, options?.message ?? 'Invalid bigint value', input, expected)); 28 | } 29 | }; 30 | } 31 | 32 | export function bigintLessThan(value: bigint, options?: ValidatorOptions): IConstraint { 33 | const expected = `expected < ${value}n`; 34 | return bigintComparator(lessThan, 's.bigint().lessThan()', expected, value, options); 35 | } 36 | 37 | export function bigintLessThanOrEqual(value: bigint, options?: ValidatorOptions): IConstraint { 38 | const expected = `expected <= ${value}n`; 39 | return bigintComparator(lessThanOrEqual, 's.bigint().lessThanOrEqual()', expected, value, options); 40 | } 41 | 42 | export function bigintGreaterThan(value: bigint, options?: ValidatorOptions): IConstraint { 43 | const expected = `expected > ${value}n`; 44 | return bigintComparator(greaterThan, 's.bigint().greaterThan()', expected, value, options); 45 | } 46 | 47 | export function bigintGreaterThanOrEqual(value: bigint, options?: ValidatorOptions): IConstraint { 48 | const expected = `expected >= ${value}n`; 49 | return bigintComparator(greaterThanOrEqual, 's.bigint().greaterThanOrEqual()', expected, value, options); 50 | } 51 | 52 | export function bigintEqual(value: bigint, options?: ValidatorOptions): IConstraint { 53 | const expected = `expected === ${value}n`; 54 | return bigintComparator(equal, 's.bigint().equal()', expected, value, options); 55 | } 56 | 57 | export function bigintNotEqual(value: bigint, options?: ValidatorOptions): IConstraint { 58 | const expected = `expected !== ${value}n`; 59 | return bigintComparator(notEqual, 's.bigint().notEqual()', expected, value, options); 60 | } 61 | 62 | export function bigintDivisibleBy(divider: bigint, options?: ValidatorOptions): IConstraint { 63 | const expected = `expected % ${divider}n === 0n`; 64 | return { 65 | run(input: bigint) { 66 | return input % divider === 0n // 67 | ? Result.ok(input) 68 | : Result.err(new ExpectedConstraintError('s.bigint().divisibleBy()', options?.message ?? 'BigInt is not divisible', input, expected)); 69 | } 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /src/constraints/BooleanConstraints.ts: -------------------------------------------------------------------------------- 1 | import { ExpectedConstraintError } from '../lib/errors/ExpectedConstraintError'; 2 | import { Result } from '../lib/Result'; 3 | import type { ValidatorOptions } from '../lib/util-types'; 4 | import type { IConstraint } from './base/IConstraint'; 5 | 6 | export type BooleanConstraintName = `s.boolean().${boolean}()`; 7 | 8 | export function booleanTrue(options?: ValidatorOptions): IConstraint { 9 | return { 10 | run(input: boolean) { 11 | return input // 12 | ? Result.ok(input) 13 | : Result.err(new ExpectedConstraintError('s.boolean().true()', options?.message ?? 'Invalid boolean value', input, 'true')); 14 | } 15 | }; 16 | } 17 | 18 | export function booleanFalse(options?: ValidatorOptions): IConstraint { 19 | return { 20 | run(input: boolean) { 21 | return input // 22 | ? Result.err(new ExpectedConstraintError('s.boolean().false()', options?.message ?? 'Invalid boolean value', input, 'false')) 23 | : Result.ok(input); 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/constraints/DateConstraints.ts: -------------------------------------------------------------------------------- 1 | import { ExpectedConstraintError } from '../lib/errors/ExpectedConstraintError'; 2 | import { Result } from '../lib/Result'; 3 | import type { ValidatorOptions } from '../lib/util-types'; 4 | import type { IConstraint } from './base/IConstraint'; 5 | import { equal, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, notEqual, type Comparator } from './util/operators'; 6 | 7 | export type DateConstraintName = `s.date().${ 8 | | 'lessThan' 9 | | 'lessThanOrEqual' 10 | | 'greaterThan' 11 | | 'greaterThanOrEqual' 12 | | 'equal' 13 | | 'notEqual' 14 | | 'valid' 15 | | 'invalid'}()`; 16 | 17 | function dateComparator( 18 | comparator: Comparator, 19 | name: DateConstraintName, 20 | expected: string, 21 | number: number, 22 | options?: ValidatorOptions 23 | ): IConstraint { 24 | return { 25 | run(input: Date) { 26 | return comparator(input.getTime(), number) // 27 | ? Result.ok(input) 28 | : Result.err(new ExpectedConstraintError(name, options?.message ?? 'Invalid Date value', input, expected)); 29 | } 30 | }; 31 | } 32 | 33 | export function dateLessThan(value: Date, options?: ValidatorOptions): IConstraint { 34 | const expected = `expected < ${value.toISOString()}`; 35 | return dateComparator(lessThan, 's.date().lessThan()', expected, value.getTime(), options); 36 | } 37 | 38 | export function dateLessThanOrEqual(value: Date, options?: ValidatorOptions): IConstraint { 39 | const expected = `expected <= ${value.toISOString()}`; 40 | return dateComparator(lessThanOrEqual, 's.date().lessThanOrEqual()', expected, value.getTime(), options); 41 | } 42 | 43 | export function dateGreaterThan(value: Date, options?: ValidatorOptions): IConstraint { 44 | const expected = `expected > ${value.toISOString()}`; 45 | return dateComparator(greaterThan, 's.date().greaterThan()', expected, value.getTime(), options); 46 | } 47 | 48 | export function dateGreaterThanOrEqual(value: Date, options?: ValidatorOptions): IConstraint { 49 | const expected = `expected >= ${value.toISOString()}`; 50 | return dateComparator(greaterThanOrEqual, 's.date().greaterThanOrEqual()', expected, value.getTime(), options); 51 | } 52 | 53 | export function dateEqual(value: Date, options?: ValidatorOptions): IConstraint { 54 | const expected = `expected === ${value.toISOString()}`; 55 | return dateComparator(equal, 's.date().equal()', expected, value.getTime(), options); 56 | } 57 | 58 | export function dateNotEqual(value: Date, options?: ValidatorOptions): IConstraint { 59 | const expected = `expected !== ${value.toISOString()}`; 60 | return dateComparator(notEqual, 's.date().notEqual()', expected, value.getTime(), options); 61 | } 62 | 63 | export function dateInvalid(options?: ValidatorOptions): IConstraint { 64 | return { 65 | run(input: Date) { 66 | return Number.isNaN(input.getTime()) // 67 | ? Result.ok(input) 68 | : Result.err(new ExpectedConstraintError('s.date().invalid()', options?.message ?? 'Invalid Date value', input, 'expected === NaN')); 69 | } 70 | }; 71 | } 72 | 73 | export function dateValid(options?: ValidatorOptions): IConstraint { 74 | return { 75 | run(input: Date) { 76 | return Number.isNaN(input.getTime()) // 77 | ? Result.err(new ExpectedConstraintError('s.date().valid()', options?.message ?? 'Invalid Date value', input, 'expected !== NaN')) 78 | : Result.ok(input); 79 | } 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /src/constraints/NumberConstraints.ts: -------------------------------------------------------------------------------- 1 | import { ExpectedConstraintError } from '../lib/errors/ExpectedConstraintError'; 2 | import { Result } from '../lib/Result'; 3 | import type { ValidatorOptions } from '../lib/util-types'; 4 | 5 | import type { IConstraint } from './base/IConstraint'; 6 | import { equal, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, notEqual, type Comparator } from './util/operators'; 7 | 8 | export type NumberConstraintName = `s.number().${ 9 | | 'lessThan' 10 | | 'lessThanOrEqual' 11 | | 'greaterThan' 12 | | 'greaterThanOrEqual' 13 | | 'equal' 14 | | 'equal' 15 | | 'notEqual' 16 | | 'notEqual' 17 | | 'int' 18 | | 'safeInt' 19 | | 'finite' 20 | | 'divisibleBy'}(${string})`; 21 | 22 | function numberComparator( 23 | comparator: Comparator, 24 | name: NumberConstraintName, 25 | expected: string, 26 | number: number, 27 | options?: ValidatorOptions 28 | ): IConstraint { 29 | return { 30 | run(input: number) { 31 | return comparator(input, number) // 32 | ? Result.ok(input) 33 | : Result.err(new ExpectedConstraintError(name, options?.message ?? 'Invalid number value', input, expected)); 34 | } 35 | }; 36 | } 37 | 38 | export function numberLessThan(value: number, options?: ValidatorOptions): IConstraint { 39 | const expected = `expected < ${value}`; 40 | return numberComparator(lessThan, 's.number().lessThan()', expected, value, options); 41 | } 42 | 43 | export function numberLessThanOrEqual(value: number, options?: ValidatorOptions): IConstraint { 44 | const expected = `expected <= ${value}`; 45 | return numberComparator(lessThanOrEqual, 's.number().lessThanOrEqual()', expected, value, options); 46 | } 47 | 48 | export function numberGreaterThan(value: number, options?: ValidatorOptions): IConstraint { 49 | const expected = `expected > ${value}`; 50 | return numberComparator(greaterThan, 's.number().greaterThan()', expected, value, options); 51 | } 52 | 53 | export function numberGreaterThanOrEqual(value: number, options?: ValidatorOptions): IConstraint { 54 | const expected = `expected >= ${value}`; 55 | return numberComparator(greaterThanOrEqual, 's.number().greaterThanOrEqual()', expected, value, options); 56 | } 57 | 58 | export function numberEqual(value: number, options?: ValidatorOptions): IConstraint { 59 | const expected = `expected === ${value}`; 60 | return numberComparator(equal, 's.number().equal()', expected, value, options); 61 | } 62 | 63 | export function numberNotEqual(value: number, options?: ValidatorOptions): IConstraint { 64 | const expected = `expected !== ${value}`; 65 | return numberComparator(notEqual, 's.number().notEqual()', expected, value, options); 66 | } 67 | 68 | export function numberInt(options?: ValidatorOptions): IConstraint { 69 | return { 70 | run(input: number) { 71 | return Number.isInteger(input) // 72 | ? Result.ok(input) 73 | : Result.err( 74 | new ExpectedConstraintError( 75 | 's.number().int()', 76 | options?.message ?? 'Given value is not an integer', 77 | input, 78 | 'Number.isInteger(expected) to be true' 79 | ) 80 | ); 81 | } 82 | }; 83 | } 84 | 85 | export function numberSafeInt(options?: ValidatorOptions): IConstraint { 86 | return { 87 | run(input: number) { 88 | return Number.isSafeInteger(input) // 89 | ? Result.ok(input) 90 | : Result.err( 91 | new ExpectedConstraintError( 92 | 's.number().safeInt()', 93 | options?.message ?? 'Given value is not a safe integer', 94 | input, 95 | 'Number.isSafeInteger(expected) to be true' 96 | ) 97 | ); 98 | } 99 | }; 100 | } 101 | 102 | export function numberFinite(options?: ValidatorOptions): IConstraint { 103 | return { 104 | run(input: number) { 105 | return Number.isFinite(input) // 106 | ? Result.ok(input) 107 | : Result.err( 108 | new ExpectedConstraintError( 109 | 's.number().finite()', 110 | options?.message ?? 'Given value is not finite', 111 | input, 112 | 'Number.isFinite(expected) to be true' 113 | ) 114 | ); 115 | } 116 | }; 117 | } 118 | 119 | export function numberNaN(options?: ValidatorOptions): IConstraint { 120 | return { 121 | run(input: number) { 122 | return Number.isNaN(input) // 123 | ? Result.ok(input) 124 | : Result.err( 125 | new ExpectedConstraintError('s.number().equal(NaN)', options?.message ?? 'Invalid number value', input, 'expected === NaN') 126 | ); 127 | } 128 | }; 129 | } 130 | 131 | export function numberNotNaN(options?: ValidatorOptions): IConstraint { 132 | return { 133 | run(input: number) { 134 | return Number.isNaN(input) // 135 | ? Result.err( 136 | new ExpectedConstraintError('s.number().notEqual(NaN)', options?.message ?? 'Invalid number value', input, 'expected !== NaN') 137 | ) 138 | : Result.ok(input); 139 | } 140 | }; 141 | } 142 | 143 | export function numberDivisibleBy(divider: number, options?: ValidatorOptions): IConstraint { 144 | const expected = `expected % ${divider} === 0`; 145 | return { 146 | run(input: number) { 147 | return input % divider === 0 // 148 | ? Result.ok(input) 149 | : Result.err(new ExpectedConstraintError('s.number().divisibleBy()', options?.message ?? 'Number is not divisible', input, expected)); 150 | } 151 | }; 152 | } 153 | -------------------------------------------------------------------------------- /src/constraints/ObjectConstrains.ts: -------------------------------------------------------------------------------- 1 | import get from 'lodash/get.js'; 2 | import { ExpectedConstraintError } from '../lib/errors/ExpectedConstraintError'; 3 | import { Result } from '../lib/Result'; 4 | import type { ValidatorOptions } from '../lib/util-types'; 5 | import type { BaseValidator } from '../validators/BaseValidator'; 6 | import type { IConstraint } from './base/IConstraint'; 7 | 8 | export type ObjectConstraintName = `s.object(T.when)`; 9 | 10 | export type WhenKey = PropertyKey | PropertyKey[]; 11 | 12 | export interface WhenOptions, Key extends WhenKey> { 13 | is?: boolean | ((value: Key extends Array ? any[] : any) => boolean); 14 | then: (predicate: T) => T; 15 | otherwise?: (predicate: T) => T; 16 | } 17 | 18 | export function whenConstraint, I, Key extends WhenKey>( 19 | key: Key, 20 | options: WhenOptions, 21 | validator: T, 22 | validatorOptions?: ValidatorOptions 23 | ): IConstraint { 24 | return { 25 | run(input: I, parent?: any) { 26 | if (!parent) { 27 | return Result.err( 28 | new ExpectedConstraintError( 29 | 's.object(T.when)', 30 | validatorOptions?.message ?? 'Validator has no parent', 31 | parent, 32 | 'Validator to have a parent' 33 | ) 34 | ); 35 | } 36 | 37 | const isKeyArray = Array.isArray(key); 38 | 39 | const value = isKeyArray ? key.map((k) => get(parent, k)) : get(parent, key); 40 | 41 | const predicate = resolveBooleanIs(options, value, isKeyArray) ? options.then : options.otherwise; 42 | 43 | if (predicate) { 44 | return predicate(validator).run(input) as Result>; 45 | } 46 | 47 | return Result.ok(input); 48 | } 49 | }; 50 | } 51 | 52 | function resolveBooleanIs, Key extends WhenKey>(options: WhenOptions, value: any, isKeyArray: boolean) { 53 | if (options.is === undefined) { 54 | return isKeyArray ? !value.some((val: any) => !val) : Boolean(value); 55 | } 56 | 57 | if (typeof options.is === 'function') { 58 | return options.is(value); 59 | } 60 | 61 | return value === options.is; 62 | } 63 | -------------------------------------------------------------------------------- /src/constraints/base/IConstraint.ts: -------------------------------------------------------------------------------- 1 | import type { BaseConstraintError } from '../../lib/errors/BaseConstraintError'; 2 | import type { Result } from '../../lib/Result'; 3 | 4 | export interface IConstraint { 5 | run(input: Input, parent?: any): Result>; 6 | } 7 | -------------------------------------------------------------------------------- /src/constraints/type-exports.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | ArrayConstraintName, 3 | arrayLengthEqual, 4 | arrayLengthGreaterThan, 5 | arrayLengthGreaterThanOrEqual, 6 | arrayLengthLessThan, 7 | arrayLengthLessThanOrEqual, 8 | arrayLengthNotEqual, 9 | arrayLengthRange, 10 | arrayLengthRangeExclusive, 11 | arrayLengthRangeInclusive 12 | } from './ArrayConstraints'; 13 | export type { IConstraint } from './base/IConstraint'; 14 | export type { 15 | BigIntConstraintName, 16 | bigintDivisibleBy, 17 | bigintEqual, 18 | bigintGreaterThan, 19 | bigintGreaterThanOrEqual, 20 | bigintLessThan, 21 | bigintLessThanOrEqual, 22 | bigintNotEqual 23 | } from './BigIntConstraints'; 24 | export type { BooleanConstraintName, booleanFalse, booleanTrue } from './BooleanConstraints'; 25 | export type { 26 | DateConstraintName, 27 | dateEqual, 28 | dateGreaterThan, 29 | dateGreaterThanOrEqual, 30 | dateInvalid, 31 | dateLessThan, 32 | dateLessThanOrEqual, 33 | dateNotEqual, 34 | dateValid 35 | } from './DateConstraints'; 36 | export type { 37 | NumberConstraintName, 38 | numberDivisibleBy, 39 | numberEqual, 40 | numberFinite, 41 | numberGreaterThan, 42 | numberGreaterThanOrEqual, 43 | numberInt, 44 | numberLessThan, 45 | numberLessThanOrEqual, 46 | numberNaN, 47 | numberNotEqual, 48 | numberNotNaN, 49 | numberSafeInt 50 | } from './NumberConstraints'; 51 | export type { ObjectConstraintName, WhenOptions } from './ObjectConstrains'; 52 | export type { 53 | StringConstraintName, 54 | StringDomain, 55 | stringEmail, 56 | stringIp, 57 | stringLengthEqual, 58 | stringLengthGreaterThan, 59 | stringLengthGreaterThanOrEqual, 60 | stringLengthLessThan, 61 | stringLengthLessThanOrEqual, 62 | stringLengthNotEqual, 63 | StringProtocol, 64 | stringRegex, 65 | stringUrl, 66 | stringUuid, 67 | StringUuidOptions, 68 | UrlOptions, 69 | UUIDVersion 70 | } from './StringConstraints'; 71 | export type { 72 | typedArrayByteLengthEqual, 73 | typedArrayByteLengthGreaterThan, 74 | typedArrayByteLengthGreaterThanOrEqual, 75 | typedArrayByteLengthLessThan, 76 | typedArrayByteLengthLessThanOrEqual, 77 | typedArrayByteLengthNotEqual, 78 | typedArrayByteLengthRange, 79 | typedArrayByteLengthRangeExclusive, 80 | typedArrayByteLengthRangeInclusive, 81 | TypedArrayConstraintName, 82 | typedArrayLengthEqual, 83 | typedArrayLengthGreaterThan, 84 | typedArrayLengthGreaterThanOrEqual, 85 | typedArrayLengthLessThan, 86 | typedArrayLengthLessThanOrEqual, 87 | typedArrayLengthNotEqual, 88 | typedArrayLengthRange, 89 | typedArrayLengthRangeExclusive, 90 | typedArrayLengthRangeInclusive 91 | } from './TypedArrayLengthConstraints'; 92 | -------------------------------------------------------------------------------- /src/constraints/util/common/combinedResultFn.ts: -------------------------------------------------------------------------------- 1 | export function combinedErrorFn

(...fns: ErrorFn[]): ErrorFn { 2 | switch (fns.length) { 3 | case 0: 4 | return () => null; 5 | case 1: 6 | return fns[0]; 7 | case 2: { 8 | const [fn0, fn1] = fns; 9 | return (...params) => fn0(...params) || fn1(...params); 10 | } 11 | default: { 12 | return (...params) => { 13 | for (const fn of fns) { 14 | const result = fn(...params); 15 | if (result) return result; 16 | } 17 | 18 | return null; 19 | }; 20 | } 21 | } 22 | } 23 | 24 | export type ErrorFn

= (...params: P) => E | null; 25 | -------------------------------------------------------------------------------- /src/constraints/util/common/vowels.ts: -------------------------------------------------------------------------------- 1 | const vowels = ['a', 'e', 'i', 'o', 'u']; 2 | 3 | export const aOrAn = (word: string) => { 4 | return `${vowels.includes(word[0].toLowerCase()) ? 'an' : 'a'} ${word}`; 5 | }; 6 | -------------------------------------------------------------------------------- /src/constraints/util/emailValidator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * @copyright 2020 Colin McDonnell 4 | * @see https://github.com/colinhacks/zod/blob/master/LICENSE 5 | */ 6 | const accountRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_+-\.]*)[A-Z0-9_+-]$/i; 7 | 8 | /** 9 | * Validates an email address string based on various checks: 10 | * - It must be a non nullish and non empty string 11 | * - It must include at least an `@` symbol` 12 | * - The account name may not exceed 64 characters 13 | * - The domain name may not exceed 255 characters 14 | * - The domain must include at least one `.` symbol 15 | * - Each part of the domain, split by `.` must not exceed 63 characters 16 | * - The email address must be [RFC-5322](https://datatracker.ietf.org/doc/html/rfc5322) compliant 17 | * @param email The email to validate 18 | * @returns `true` if the email is valid, `false` otherwise 19 | * 20 | * @remark Based on the following sources: 21 | * - `email-validator` by [manisharaan](https://github.com/manishsaraan) ([code](https://github.com/manishsaraan/email-validator/blob/master/index.js)) 22 | * - [Comparing E-mail Address Validating Regular Expressions](http://fightingforalostcause.net/misc/2006/compare-email-regex.php) 23 | * - [Validating Email Addresses by Derrick Pallas](http://thedailywtf.com/Articles/Validating_Email_Addresses.aspx) 24 | * - [StackOverflow answer by bortzmeyer](http://stackoverflow.com/questions/201323/what-is-the-best-regular-expression-for-validating-email-addresses/201378#201378) 25 | * - [The wikipedia page on Email addresses](https://en.wikipedia.org/wiki/Email_address) 26 | */ 27 | export function validateEmail(email: string): boolean { 28 | // 1. Non-nullish and non-empty string check. 29 | // 30 | // If a nullish or empty email was provided then do an early exit 31 | if (!email) return false; 32 | 33 | // Find the location of the @ symbol: 34 | const atIndex = email.indexOf('@'); 35 | 36 | // 2. @ presence check. 37 | // 38 | // If the email does not have the @ symbol, it's automatically invalid: 39 | if (atIndex === -1) return false; 40 | 41 | // 3. maximum length check. 42 | // 43 | // From @, if exceeds 64 characters, then the 44 | // position of the @ symbol is 64 or greater. In this case, the email is 45 | // invalid: 46 | if (atIndex > 64) return false; 47 | 48 | const domainIndex = atIndex + 1; 49 | 50 | // 7.1. Duplicated @ symbol check. 51 | // 52 | // If there's a second @ symbol, the email is automatically invalid: 53 | if (email.includes('@', domainIndex)) return false; 54 | 55 | // 4. maximum length check. 56 | // 57 | // From @, if exceeds 255 characters, then it 58 | // means that the amount of characters between the start of and the 59 | // end of the string is separated by 255 or more characters. 60 | if (email.length - domainIndex > 255) return false; 61 | 62 | // Find the location of the . symbol in : 63 | let dotIndex = email.indexOf('.', domainIndex); 64 | 65 | // 5. dot (.) symbol check. 66 | // 67 | // From @, if does not contain a dot (.) symbol, 68 | // then it means the domain is invalid. 69 | if (dotIndex === -1) return false; 70 | 71 | // 6. parts length. 72 | // 73 | // Assign a temporary variable to store the start of the last read domain 74 | // part, this would be at the start of . 75 | // 76 | // For a part to be correct, it must have at most, 63 characters. 77 | // We repeat this step for every sub-section of contained within 78 | // dot (.) symbols. 79 | // 80 | // The following step is a more optimized version of the following code: 81 | // 82 | // ```javascript 83 | // domain.split('.').some((part) => part.length > 63); 84 | // ``` 85 | let lastDotIndex = domainIndex; 86 | do { 87 | if (dotIndex - lastDotIndex > 63) return false; 88 | 89 | lastDotIndex = dotIndex + 1; 90 | } while ((dotIndex = email.indexOf('.', lastDotIndex)) !== -1); 91 | 92 | // The loop iterates from the first to the n - 1 part, this line checks for 93 | // the last (n) part: 94 | if (email.length - lastDotIndex > 63) return false; 95 | 96 | // 7.2. Character checks. 97 | // 98 | // From @: 99 | // - Extract the part by slicing the input from start to the @ 100 | // character. Validate afterwards. 101 | // - Extract the part by slicing the input from the start of 102 | // . Validate afterwards. 103 | // 104 | // Note: we inline the variables so isn't created unless the 105 | // check passes. 106 | return accountRegex.test(email.slice(0, atIndex)) && validateEmailDomain(email.slice(domainIndex)); 107 | } 108 | 109 | function validateEmailDomain(domain: string): boolean { 110 | try { 111 | return new URL(`http://${domain}`).hostname === domain; 112 | } catch { 113 | return false; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/constraints/util/isUnique.ts: -------------------------------------------------------------------------------- 1 | import fastDeepEqual from 'fast-deep-equal/es6/index.js'; 2 | import uniqWith from 'lodash/uniqWith.js'; 3 | 4 | export function isUnique(input: unknown[]) { 5 | if (input.length < 2) return true; 6 | const uniqueArray = uniqWith(input, fastDeepEqual); 7 | return uniqueArray.length === input.length; 8 | } 9 | -------------------------------------------------------------------------------- /src/constraints/util/net.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Code ported from https://github.com/nodejs/node/blob/5fad0b93667ffc6e4def52996b9529ac99b26319/lib/internal/net.js 3 | */ 4 | 5 | // IPv4 Segment 6 | const v4Seg = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])'; 7 | const v4Str = `(${v4Seg}[.]){3}${v4Seg}`; 8 | const IPv4Reg = new RegExp(`^${v4Str}$`); 9 | 10 | // IPv6 Segment 11 | const v6Seg = '(?:[0-9a-fA-F]{1,4})'; 12 | const IPv6Reg = new RegExp( 13 | '^(' + 14 | `(?:${v6Seg}:){7}(?:${v6Seg}|:)|` + 15 | `(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` + 16 | `(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|` + 17 | `(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|` + 18 | `(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|` + 19 | `(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|` + 20 | `(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|` + 21 | `(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` + 22 | ')(%[0-9a-zA-Z-.:]{1,})?$' 23 | ); 24 | 25 | export function isIPv4(s: string): boolean { 26 | return IPv4Reg.test(s); 27 | } 28 | 29 | export function isIPv6(s: string): boolean { 30 | return IPv6Reg.test(s); 31 | } 32 | 33 | export function isIP(s: string): number { 34 | if (isIPv4(s)) return 4; 35 | if (isIPv6(s)) return 6; 36 | return 0; 37 | } 38 | -------------------------------------------------------------------------------- /src/constraints/util/operators.ts: -------------------------------------------------------------------------------- 1 | export function lessThan(a: number, b: number): boolean; 2 | export function lessThan(a: bigint, b: bigint): boolean; 3 | export function lessThan(a: number | bigint, b: number | bigint): boolean { 4 | return a < b; 5 | } 6 | 7 | export function lessThanOrEqual(a: number, b: number): boolean; 8 | export function lessThanOrEqual(a: bigint, b: bigint): boolean; 9 | export function lessThanOrEqual(a: number | bigint, b: number | bigint): boolean { 10 | return a <= b; 11 | } 12 | 13 | export function greaterThan(a: number, b: number): boolean; 14 | export function greaterThan(a: bigint, b: bigint): boolean; 15 | export function greaterThan(a: number | bigint, b: number | bigint): boolean { 16 | return a > b; 17 | } 18 | 19 | export function greaterThanOrEqual(a: number, b: number): boolean; 20 | export function greaterThanOrEqual(a: bigint, b: bigint): boolean; 21 | export function greaterThanOrEqual(a: number | bigint, b: number | bigint): boolean { 22 | return a >= b; 23 | } 24 | 25 | export function equal(a: number, b: number): boolean; 26 | export function equal(a: bigint, b: bigint): boolean; 27 | export function equal(a: number | bigint, b: number | bigint): boolean { 28 | return a === b; 29 | } 30 | 31 | export function notEqual(a: number, b: number): boolean; 32 | export function notEqual(a: bigint, b: bigint): boolean; 33 | export function notEqual(a: number | bigint, b: number | bigint): boolean { 34 | return a !== b; 35 | } 36 | 37 | export interface Comparator { 38 | (a: number, b: number): boolean; 39 | (a: bigint, b: bigint): boolean; 40 | } 41 | -------------------------------------------------------------------------------- /src/constraints/util/phoneValidator.ts: -------------------------------------------------------------------------------- 1 | export const phoneNumberRegex = /^((?:\+|0{0,2})\d{1,2}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/; 2 | 3 | export function validatePhoneNumber(input: string) { 4 | return phoneNumberRegex.test(input); 5 | } 6 | -------------------------------------------------------------------------------- /src/constraints/util/typedArray.ts: -------------------------------------------------------------------------------- 1 | export type TypedArray = 2 | | Int8Array 3 | | Uint8Array 4 | | Uint8ClampedArray 5 | | Int16Array 6 | | Uint16Array 7 | | Int32Array 8 | | Uint32Array 9 | | Float32Array 10 | | Float64Array 11 | | BigInt64Array 12 | | BigUint64Array; 13 | 14 | export const TypedArrays = { 15 | Int8Array: (x: unknown): x is Int8Array => x instanceof Int8Array, 16 | Uint8Array: (x: unknown): x is Uint8Array => x instanceof Uint8Array, 17 | Uint8ClampedArray: (x: unknown): x is Uint8ClampedArray => x instanceof Uint8ClampedArray, 18 | Int16Array: (x: unknown): x is Int16Array => x instanceof Int16Array, 19 | Uint16Array: (x: unknown): x is Uint16Array => x instanceof Uint16Array, 20 | Int32Array: (x: unknown): x is Int32Array => x instanceof Int32Array, 21 | Uint32Array: (x: unknown): x is Uint32Array => x instanceof Uint32Array, 22 | Float32Array: (x: unknown): x is Float32Array => x instanceof Float32Array, 23 | Float64Array: (x: unknown): x is Float64Array => x instanceof Float64Array, 24 | BigInt64Array: (x: unknown): x is BigInt64Array => x instanceof BigInt64Array, 25 | BigUint64Array: (x: unknown): x is BigUint64Array => x instanceof BigUint64Array, 26 | TypedArray: (x: unknown): x is TypedArray => ArrayBuffer.isView(x) && !(x instanceof DataView) 27 | } as const; 28 | 29 | export type TypedArrayName = keyof typeof TypedArrays; 30 | -------------------------------------------------------------------------------- /src/constraints/util/urlValidators.ts: -------------------------------------------------------------------------------- 1 | import { MultiplePossibilitiesConstraintError } from '../../lib/errors/MultiplePossibilitiesConstraintError'; 2 | import type { ValidatorOptions } from '../../lib/util-types'; 3 | import { combinedErrorFn, type ErrorFn } from './common/combinedResultFn'; 4 | 5 | export type StringProtocol = `${string}:`; 6 | 7 | export type StringDomain = `${string}.${string}`; 8 | 9 | export interface UrlOptions { 10 | allowedProtocols?: StringProtocol[]; 11 | allowedDomains?: StringDomain[]; 12 | } 13 | 14 | export function createUrlValidators(options?: UrlOptions, validatorOptions?: ValidatorOptions) { 15 | const fns: ErrorFn<[input: string, url: URL], MultiplePossibilitiesConstraintError>[] = []; 16 | 17 | if (options?.allowedProtocols?.length) fns.push(allowedProtocolsFn(options.allowedProtocols, validatorOptions)); 18 | if (options?.allowedDomains?.length) fns.push(allowedDomainsFn(options.allowedDomains, validatorOptions)); 19 | 20 | return combinedErrorFn(...fns); 21 | } 22 | 23 | function allowedProtocolsFn(allowedProtocols: StringProtocol[], options?: ValidatorOptions) { 24 | return (input: string, url: URL) => 25 | allowedProtocols.includes(url.protocol as StringProtocol) 26 | ? null 27 | : new MultiplePossibilitiesConstraintError('s.string().url()', options?.message ?? 'Invalid URL protocol', input, allowedProtocols); 28 | } 29 | 30 | function allowedDomainsFn(allowedDomains: StringDomain[], options?: ValidatorOptions) { 31 | return (input: string, url: URL) => 32 | allowedDomains.includes(url.hostname as StringDomain) 33 | ? null 34 | : new MultiplePossibilitiesConstraintError('s.string().url()', options?.message ?? 'Invalid URL domain', input, allowedDomains); 35 | } 36 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Shapes } from './lib/Shapes'; 2 | 3 | export const s = new Shapes(); 4 | 5 | export * from './lib/Result'; 6 | export * from './lib/configs'; 7 | export * from './lib/errors/BaseError'; 8 | export * from './lib/errors/CombinedError'; 9 | export * from './lib/errors/CombinedPropertyError'; 10 | export * from './lib/errors/ExpectedConstraintError'; 11 | export * from './lib/errors/ExpectedValidationError'; 12 | export * from './lib/errors/MissingPropertyError'; 13 | export * from './lib/errors/MultiplePossibilitiesConstraintError'; 14 | export * from './lib/errors/UnknownEnumValueError'; 15 | export * from './lib/errors/UnknownPropertyError'; 16 | export * from './lib/errors/ValidationError'; 17 | export * from './type-exports'; 18 | -------------------------------------------------------------------------------- /src/lib/Result.ts: -------------------------------------------------------------------------------- 1 | export class Result { 2 | public readonly success: boolean; 3 | public readonly value?: T; 4 | public readonly error?: E; 5 | 6 | private constructor(success: boolean, value?: T, error?: E) { 7 | this.success = success; 8 | if (success) { 9 | this.value = value; 10 | } else { 11 | this.error = error; 12 | } 13 | } 14 | 15 | public isOk(): this is { success: true; value: T } { 16 | return this.success; 17 | } 18 | 19 | public isErr(): this is { success: false; error: E } { 20 | return !this.success; 21 | } 22 | 23 | public unwrap(): T { 24 | if (this.isOk()) return this.value; 25 | throw this.error as Error; 26 | } 27 | 28 | public static ok(value: T): Result { 29 | return new Result(true, value); 30 | } 31 | 32 | public static err(error: E): Result { 33 | return new Result(false, undefined, error); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/Shapes.ts: -------------------------------------------------------------------------------- 1 | import type { TypedArray, TypedArrayName } from '../constraints/util/typedArray'; 2 | import type { Unwrap, UnwrapTuple, ValidatorOptions } from '../lib/util-types'; 3 | import { 4 | ArrayValidator, 5 | BaseValidator, 6 | BigIntValidator, 7 | BooleanValidator, 8 | DateValidator, 9 | InstanceValidator, 10 | LiteralValidator, 11 | MapValidator, 12 | NeverValidator, 13 | NullishValidator, 14 | NumberValidator, 15 | ObjectValidator, 16 | ObjectValidatorStrategy, 17 | PassthroughValidator, 18 | RecordValidator, 19 | SetValidator, 20 | StringValidator, 21 | TupleValidator, 22 | UnionValidator 23 | } from '../validators/imports'; 24 | import { LazyValidator } from '../validators/LazyValidator'; 25 | import { NativeEnumValidator, type NativeEnumLike } from '../validators/NativeEnumValidator'; 26 | import { TypedArrayValidator } from '../validators/TypedArrayValidator'; 27 | import type { Constructor, MappedObjectValidator } from './util-types'; 28 | 29 | export class Shapes { 30 | public string(options?: ValidatorOptions) { 31 | return new StringValidator(options); 32 | } 33 | 34 | public number(options?: ValidatorOptions) { 35 | return new NumberValidator(options); 36 | } 37 | 38 | public bigint(options?: ValidatorOptions) { 39 | return new BigIntValidator(options); 40 | } 41 | 42 | public boolean(options?: ValidatorOptions) { 43 | return new BooleanValidator(options); 44 | } 45 | 46 | public date(options?: ValidatorOptions) { 47 | return new DateValidator(options); 48 | } 49 | 50 | public object(shape: MappedObjectValidator, options?: ValidatorOptions) { 51 | return new ObjectValidator(shape, ObjectValidatorStrategy.Ignore, options); 52 | } 53 | 54 | public undefined(options?: ValidatorOptions) { 55 | return this.literal(undefined, { equalsOptions: options }); 56 | } 57 | 58 | public null(options?: ValidatorOptions) { 59 | return this.literal(null, { equalsOptions: options }); 60 | } 61 | 62 | public nullish(options?: ValidatorOptions) { 63 | return new NullishValidator(options); 64 | } 65 | 66 | public any(options?: ValidatorOptions) { 67 | return new PassthroughValidator(options); 68 | } 69 | 70 | public unknown(options?: ValidatorOptions) { 71 | return new PassthroughValidator(options); 72 | } 73 | 74 | public never(options?: ValidatorOptions) { 75 | return new NeverValidator(options); 76 | } 77 | 78 | public enum(values: readonly T[], options?: ValidatorOptions) { 79 | return this.union( 80 | values.map((value) => this.literal(value, { equalsOptions: options })), 81 | options 82 | ); 83 | } 84 | 85 | public nativeEnum(enumShape: T, options?: ValidatorOptions): NativeEnumValidator { 86 | return new NativeEnumValidator(enumShape, options); 87 | } 88 | 89 | public literal(value: T, options?: { dateOptions?: ValidatorOptions; equalsOptions?: ValidatorOptions }): BaseValidator { 90 | if (value instanceof Date) { 91 | return this.date(options?.dateOptions).equal(value, options?.equalsOptions) as unknown as BaseValidator; 92 | } 93 | 94 | return new LiteralValidator(value, options?.equalsOptions); 95 | } 96 | 97 | public instance(expected: Constructor, options?: ValidatorOptions): InstanceValidator { 98 | return new InstanceValidator(expected, options); 99 | } 100 | 101 | public union[]>(validators: T, options?: ValidatorOptions): UnionValidator> { 102 | return new UnionValidator(validators, options); 103 | } 104 | 105 | public array(validator: BaseValidator, options?: ValidatorOptions): ArrayValidator; 106 | public array(validator: BaseValidator, options?: ValidatorOptions): ArrayValidator; 107 | public array(validator: BaseValidator, options?: ValidatorOptions) { 108 | return new ArrayValidator(validator, options); 109 | } 110 | 111 | public typedArray(type: TypedArrayName = 'TypedArray', options?: ValidatorOptions) { 112 | return new TypedArrayValidator(type, options); 113 | } 114 | 115 | public int8Array(options?: ValidatorOptions) { 116 | return this.typedArray('Int8Array', options); 117 | } 118 | 119 | public uint8Array(options?: ValidatorOptions) { 120 | return this.typedArray('Uint8Array', options); 121 | } 122 | 123 | public uint8ClampedArray(options?: ValidatorOptions) { 124 | return this.typedArray('Uint8ClampedArray', options); 125 | } 126 | 127 | public int16Array(options?: ValidatorOptions) { 128 | return this.typedArray('Int16Array', options); 129 | } 130 | 131 | public uint16Array(options?: ValidatorOptions) { 132 | return this.typedArray('Uint16Array', options); 133 | } 134 | 135 | public int32Array(options?: ValidatorOptions) { 136 | return this.typedArray('Int32Array', options); 137 | } 138 | 139 | public uint32Array(options?: ValidatorOptions) { 140 | return this.typedArray('Uint32Array', options); 141 | } 142 | 143 | public float32Array(options?: ValidatorOptions) { 144 | return this.typedArray('Float32Array', options); 145 | } 146 | 147 | public float64Array(options?: ValidatorOptions) { 148 | return this.typedArray('Float64Array', options); 149 | } 150 | 151 | public bigInt64Array(options?: ValidatorOptions) { 152 | return this.typedArray('BigInt64Array', options); 153 | } 154 | 155 | public bigUint64Array(options?: ValidatorOptions) { 156 | return this.typedArray('BigUint64Array', options); 157 | } 158 | 159 | public tuple[]]>(validators: [...T], options?: ValidatorOptions): TupleValidator> { 160 | return new TupleValidator(validators, options); 161 | } 162 | 163 | public set(validator: BaseValidator, options?: ValidatorOptions) { 164 | return new SetValidator(validator, options); 165 | } 166 | 167 | public record(validator: BaseValidator, options?: ValidatorOptions) { 168 | return new RecordValidator(validator, options); 169 | } 170 | 171 | public map(keyValidator: BaseValidator, valueValidator: BaseValidator, options?: ValidatorOptions) { 172 | return new MapValidator(keyValidator, valueValidator, options); 173 | } 174 | 175 | public lazy>(validator: (value: unknown) => T, options?: ValidatorOptions) { 176 | return new LazyValidator(validator, options); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/lib/configs.ts: -------------------------------------------------------------------------------- 1 | let validationEnabled = true; 2 | 3 | /** 4 | * Sets whether validators should run on the input, or if the input should be passed through. 5 | * @param enabled Whether validation should be done on inputs 6 | */ 7 | export function setGlobalValidationEnabled(enabled: boolean) { 8 | validationEnabled = enabled; 9 | } 10 | 11 | /** 12 | * @returns Whether validation is enabled 13 | */ 14 | export function getGlobalValidationEnabled() { 15 | return validationEnabled; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/errors/BaseConstraintError.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ArrayConstraintName, 3 | BigIntConstraintName, 4 | BooleanConstraintName, 5 | DateConstraintName, 6 | NumberConstraintName, 7 | ObjectConstraintName, 8 | StringConstraintName, 9 | TypedArrayConstraintName 10 | } from '../../constraints/type-exports'; 11 | import { BaseError } from './BaseError'; 12 | import type { BaseConstraintErrorJsonified } from './error-types'; 13 | 14 | export type ConstraintErrorNames = 15 | | TypedArrayConstraintName 16 | | ArrayConstraintName 17 | | BigIntConstraintName 18 | | BooleanConstraintName 19 | | DateConstraintName 20 | | NumberConstraintName 21 | | ObjectConstraintName 22 | | StringConstraintName; 23 | 24 | export abstract class BaseConstraintError extends BaseError { 25 | public readonly constraint: ConstraintErrorNames; 26 | public readonly given: T; 27 | 28 | public constructor(constraint: ConstraintErrorNames, message: string, given: T) { 29 | super(message); 30 | this.constraint = constraint; 31 | this.given = given; 32 | } 33 | 34 | public override toJSON(): BaseConstraintErrorJsonified { 35 | return { 36 | name: this.name, 37 | constraint: this.constraint, 38 | given: this.given, 39 | message: this.message 40 | }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/errors/BaseError.ts: -------------------------------------------------------------------------------- 1 | import type { InspectOptionsStylized } from 'util'; 2 | import type { BaseErrorJsonified } from './error-types'; 3 | 4 | export const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom'); 5 | export const customInspectSymbolStackLess = Symbol.for('nodejs.util.inspect.custom.stack-less'); 6 | 7 | export abstract class BaseError extends Error { 8 | public toJSON(): BaseErrorJsonified { 9 | return { 10 | name: this.name, 11 | message: this.message 12 | }; 13 | } 14 | 15 | protected [customInspectSymbol](depth: number, options: InspectOptionsStylized) { 16 | return `${this[customInspectSymbolStackLess](depth, options)}\n${this.stack!.slice(this.stack!.indexOf('\n'))}`; 17 | } 18 | 19 | protected abstract [customInspectSymbolStackLess](depth: number, options: InspectOptionsStylized): string; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/errors/CombinedError.ts: -------------------------------------------------------------------------------- 1 | import type { InspectOptionsStylized } from 'util'; 2 | import type { ValidatorOptions } from '../util-types'; 3 | import { BaseError, customInspectSymbolStackLess } from './BaseError'; 4 | 5 | export class CombinedError extends BaseError { 6 | public readonly errors: readonly BaseError[]; 7 | 8 | public constructor(errors: readonly BaseError[], validatorOptions?: ValidatorOptions) { 9 | super(validatorOptions?.message ?? 'Received one or more errors'); 10 | 11 | this.errors = errors; 12 | } 13 | 14 | protected [customInspectSymbolStackLess](depth: number, options: InspectOptionsStylized): string { 15 | if (depth < 0) { 16 | return options.stylize('[CombinedError]', 'special'); 17 | } 18 | 19 | const newOptions = { ...options, depth: options.depth === null ? null : options.depth! - 1, compact: true }; 20 | 21 | const padding = `\n ${options.stylize('|', 'undefined')} `; 22 | 23 | const header = `${options.stylize('CombinedError', 'special')} (${options.stylize(this.errors.length.toString(), 'number')})`; 24 | const message = options.stylize(this.message, 'regexp'); 25 | const errors = this.errors 26 | .map((error, i) => { 27 | const index = options.stylize((i + 1).toString(), 'number'); 28 | const body = error[customInspectSymbolStackLess](depth - 1, newOptions).replace(/\n/g, padding); 29 | 30 | return ` ${index} ${body}`; 31 | }) 32 | .join('\n\n'); 33 | return `${header}\n ${message}\n\n${errors}`; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/errors/CombinedPropertyError.ts: -------------------------------------------------------------------------------- 1 | import type { InspectOptionsStylized } from 'util'; 2 | import type { ValidatorOptions } from '../util-types'; 3 | import { BaseError, customInspectSymbolStackLess } from './BaseError'; 4 | 5 | export class CombinedPropertyError extends BaseError { 6 | public readonly errors: [PropertyKey, BaseError][]; 7 | 8 | public constructor(errors: [PropertyKey, BaseError][], validatorOptions?: ValidatorOptions) { 9 | super(validatorOptions?.message ?? 'Received one or more errors'); 10 | 11 | this.errors = errors; 12 | } 13 | 14 | protected [customInspectSymbolStackLess](depth: number, options: InspectOptionsStylized): string { 15 | if (depth < 0) { 16 | return options.stylize('[CombinedPropertyError]', 'special'); 17 | } 18 | 19 | const newOptions = { ...options, depth: options.depth === null ? null : options.depth! - 1, compact: true }; 20 | 21 | const padding = `\n ${options.stylize('|', 'undefined')} `; 22 | 23 | const header = `${options.stylize('CombinedPropertyError', 'special')} (${options.stylize(this.errors.length.toString(), 'number')})`; 24 | const message = options.stylize(this.message, 'regexp'); 25 | const errors = this.errors 26 | .map(([key, error]) => { 27 | const property = CombinedPropertyError.formatProperty(key, options); 28 | const body = error[customInspectSymbolStackLess](depth - 1, newOptions).replace(/\n/g, padding); 29 | 30 | return ` input${property}${padding}${body}`; 31 | }) 32 | .join('\n\n'); 33 | return `${header}\n ${message}\n\n${errors}`; 34 | } 35 | 36 | private static formatProperty(key: PropertyKey, options: InspectOptionsStylized): string { 37 | if (typeof key === 'string') return options.stylize(`.${key}`, 'symbol'); 38 | if (typeof key === 'number') return `[${options.stylize(key.toString(), 'number')}]`; 39 | return `[${options.stylize('Symbol', 'symbol')}(${key.description})]`; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/errors/ExpectedConstraintError.ts: -------------------------------------------------------------------------------- 1 | import { inspect, type InspectOptionsStylized } from 'util'; 2 | import { BaseConstraintError, type ConstraintErrorNames } from './BaseConstraintError'; 3 | import { customInspectSymbolStackLess } from './BaseError'; 4 | import type { ExpectedConstraintErrorJsonified } from './error-types'; 5 | 6 | export class ExpectedConstraintError extends BaseConstraintError { 7 | public readonly expected: string; 8 | 9 | public constructor(constraint: ConstraintErrorNames, message: string, given: T, expected: string) { 10 | super(constraint, message, given); 11 | this.expected = expected; 12 | } 13 | 14 | public override toJSON(): ExpectedConstraintErrorJsonified { 15 | return { 16 | name: this.name, 17 | constraint: this.constraint, 18 | given: this.given, 19 | expected: this.expected, 20 | message: this.message 21 | }; 22 | } 23 | 24 | protected [customInspectSymbolStackLess](depth: number, options: InspectOptionsStylized): string { 25 | const constraint = options.stylize(this.constraint, 'string'); 26 | if (depth < 0) { 27 | return options.stylize(`[ExpectedConstraintError: ${constraint}]`, 'special'); 28 | } 29 | 30 | const newOptions = { ...options, depth: options.depth === null ? null : options.depth! - 1 }; 31 | 32 | const padding = `\n ${options.stylize('|', 'undefined')} `; 33 | const given = inspect(this.given, newOptions).replace(/\n/g, padding); 34 | 35 | const header = `${options.stylize('ExpectedConstraintError', 'special')} > ${constraint}`; 36 | const message = options.stylize(this.message, 'regexp'); 37 | const expectedBlock = `\n ${options.stylize('Expected: ', 'string')}${options.stylize(this.expected, 'boolean')}`; 38 | const givenBlock = `\n ${options.stylize('Received:', 'regexp')}${padding}${given}`; 39 | return `${header}\n ${message}\n${expectedBlock}\n${givenBlock}`; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/errors/ExpectedValidationError.ts: -------------------------------------------------------------------------------- 1 | import { inspect, type InspectOptionsStylized } from 'util'; 2 | import { customInspectSymbolStackLess } from './BaseError'; 3 | import type { ExpectedValidationErrorJsonified } from './error-types'; 4 | import { ValidationError } from './ValidationError'; 5 | 6 | export class ExpectedValidationError extends ValidationError { 7 | public readonly expected: T; 8 | 9 | public constructor(validator: string, message: string, given: unknown, expected: T) { 10 | super(validator, message, given); 11 | this.expected = expected; 12 | } 13 | 14 | public override toJSON(): ExpectedValidationErrorJsonified { 15 | return { 16 | name: this.name, 17 | validator: this.validator, 18 | given: this.given, 19 | expected: this.expected, 20 | message: this.message 21 | }; 22 | } 23 | 24 | protected [customInspectSymbolStackLess](depth: number, options: InspectOptionsStylized): string { 25 | const validator = options.stylize(this.validator, 'string'); 26 | if (depth < 0) { 27 | return options.stylize(`[ExpectedValidationError: ${validator}]`, 'special'); 28 | } 29 | 30 | const newOptions = { ...options, depth: options.depth === null ? null : options.depth! - 1 }; 31 | 32 | const padding = `\n ${options.stylize('|', 'undefined')} `; 33 | const expected = inspect(this.expected, newOptions).replace(/\n/g, padding); 34 | const given = inspect(this.given, newOptions).replace(/\n/g, padding); 35 | 36 | const header = `${options.stylize('ExpectedValidationError', 'special')} > ${validator}`; 37 | const message = options.stylize(this.message, 'regexp'); 38 | const expectedBlock = `\n ${options.stylize('Expected:', 'string')}${padding}${expected}`; 39 | const givenBlock = `\n ${options.stylize('Received:', 'regexp')}${padding}${given}`; 40 | return `${header}\n ${message}\n${expectedBlock}\n${givenBlock}`; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/errors/MissingPropertyError.ts: -------------------------------------------------------------------------------- 1 | import type { InspectOptionsStylized } from 'util'; 2 | import type { ValidatorOptions } from '../util-types'; 3 | import { BaseError, customInspectSymbolStackLess } from './BaseError'; 4 | import type { MissingPropertyErrorJsonified } from './error-types'; 5 | 6 | export class MissingPropertyError extends BaseError { 7 | public readonly property: PropertyKey; 8 | 9 | public constructor(property: PropertyKey, validatorOptions?: ValidatorOptions) { 10 | super(validatorOptions?.message ?? 'A required property is missing'); 11 | this.property = property; 12 | } 13 | 14 | public override toJSON(): MissingPropertyErrorJsonified { 15 | return { 16 | name: this.name, 17 | message: this.message, 18 | property: this.property 19 | }; 20 | } 21 | 22 | protected [customInspectSymbolStackLess](depth: number, options: InspectOptionsStylized): string { 23 | const property = options.stylize(this.property.toString(), 'string'); 24 | if (depth < 0) { 25 | return options.stylize(`[MissingPropertyError: ${property}]`, 'special'); 26 | } 27 | 28 | const header = `${options.stylize('MissingPropertyError', 'special')} > ${property}`; 29 | const message = options.stylize(this.message, 'regexp'); 30 | return `${header}\n ${message}`; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/errors/MultiplePossibilitiesConstraintError.ts: -------------------------------------------------------------------------------- 1 | import { inspect, type InspectOptionsStylized } from 'util'; 2 | import { BaseConstraintError, type ConstraintErrorNames } from './BaseConstraintError'; 3 | import { customInspectSymbolStackLess } from './BaseError'; 4 | import type { MultiplePossibilitiesConstraintErrorJsonified } from './error-types'; 5 | 6 | export class MultiplePossibilitiesConstraintError extends BaseConstraintError { 7 | public readonly expected: readonly string[]; 8 | 9 | public constructor(constraint: ConstraintErrorNames, message: string, given: T, expected: readonly string[]) { 10 | super(constraint, message, given); 11 | this.expected = expected; 12 | } 13 | 14 | public override toJSON(): MultiplePossibilitiesConstraintErrorJsonified { 15 | return { 16 | name: this.name, 17 | message: this.message, 18 | constraint: this.constraint, 19 | given: this.given, 20 | expected: this.expected 21 | }; 22 | } 23 | 24 | protected [customInspectSymbolStackLess](depth: number, options: InspectOptionsStylized): string { 25 | const constraint = options.stylize(this.constraint, 'string'); 26 | if (depth < 0) { 27 | return options.stylize(`[MultiplePossibilitiesConstraintError: ${constraint}]`, 'special'); 28 | } 29 | 30 | const newOptions = { ...options, depth: options.depth === null ? null : options.depth! - 1 }; 31 | 32 | const verticalLine = options.stylize('|', 'undefined'); 33 | const padding = `\n ${verticalLine} `; 34 | const given = inspect(this.given, newOptions).replace(/\n/g, padding); 35 | 36 | const header = `${options.stylize('MultiplePossibilitiesConstraintError', 'special')} > ${constraint}`; 37 | const message = options.stylize(this.message, 'regexp'); 38 | 39 | const expectedPadding = `\n ${verticalLine} - `; 40 | const expectedBlock = `\n ${options.stylize('Expected any of the following:', 'string')}${expectedPadding}${this.expected 41 | .map((possible) => options.stylize(possible, 'boolean')) 42 | .join(expectedPadding)}`; 43 | const givenBlock = `\n ${options.stylize('Received:', 'regexp')}${padding}${given}`; 44 | return `${header}\n ${message}\n${expectedBlock}\n${givenBlock}`; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/errors/UnknownEnumValueError.ts: -------------------------------------------------------------------------------- 1 | import type { InspectOptionsStylized } from 'util'; 2 | import type { ValidatorOptions } from '../util-types'; 3 | import { BaseError, customInspectSymbolStackLess } from './BaseError'; 4 | import type { UnknownEnumValueErrorJsonified } from './error-types'; 5 | 6 | export class UnknownEnumValueError extends BaseError { 7 | public readonly value: string | number; 8 | public readonly enumKeys: string[]; 9 | public readonly enumMappings: Map; 10 | 11 | public constructor( 12 | value: string | number, 13 | keys: string[], 14 | enumMappings: Map, 15 | validatorOptions?: ValidatorOptions 16 | ) { 17 | super(validatorOptions?.message ?? 'Expected the value to be one of the following enum values:'); 18 | 19 | this.value = value; 20 | this.enumKeys = keys; 21 | this.enumMappings = enumMappings; 22 | } 23 | 24 | public override toJSON(): UnknownEnumValueErrorJsonified { 25 | return { 26 | name: this.name, 27 | message: this.message, 28 | value: this.value, 29 | enumKeys: this.enumKeys, 30 | enumMappings: [...this.enumMappings.entries()] 31 | }; 32 | } 33 | 34 | protected [customInspectSymbolStackLess](depth: number, options: InspectOptionsStylized): string { 35 | const value = options.stylize(this.value.toString(), 'string'); 36 | if (depth < 0) { 37 | return options.stylize(`[UnknownEnumValueError: ${value}]`, 'special'); 38 | } 39 | 40 | const padding = `\n ${options.stylize('|', 'undefined')} `; 41 | const pairs = this.enumKeys 42 | .map((key) => { 43 | const enumValue = this.enumMappings.get(key)!; 44 | return `${options.stylize(key, 'string')} or ${options.stylize( 45 | enumValue.toString(), 46 | typeof enumValue === 'number' ? 'number' : 'string' 47 | )}`; 48 | }) 49 | .join(padding); 50 | 51 | const header = `${options.stylize('UnknownEnumValueError', 'special')} > ${value}`; 52 | const message = options.stylize(this.message, 'regexp'); 53 | const pairsBlock = `${padding}${pairs}`; 54 | return `${header}\n ${message}\n${pairsBlock}`; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/lib/errors/UnknownPropertyError.ts: -------------------------------------------------------------------------------- 1 | import { inspect, type InspectOptionsStylized } from 'util'; 2 | import type { ValidatorOptions } from '../util-types'; 3 | import { BaseError, customInspectSymbolStackLess } from './BaseError'; 4 | import type { UnknownEnumKeyErrorJsonified } from './error-types'; 5 | 6 | export class UnknownPropertyError extends BaseError { 7 | public readonly property: PropertyKey; 8 | public readonly value: unknown; 9 | 10 | public constructor(property: PropertyKey, value: unknown, options?: ValidatorOptions) { 11 | super(options?.message ?? 'Received unexpected property'); 12 | 13 | this.property = property; 14 | this.value = value; 15 | } 16 | 17 | public override toJSON(): UnknownEnumKeyErrorJsonified { 18 | return { 19 | name: this.name, 20 | message: this.message, 21 | property: this.property, 22 | value: this.value 23 | }; 24 | } 25 | 26 | protected [customInspectSymbolStackLess](depth: number, options: InspectOptionsStylized): string { 27 | const property = options.stylize(this.property.toString(), 'string'); 28 | if (depth < 0) { 29 | return options.stylize(`[UnknownPropertyError: ${property}]`, 'special'); 30 | } 31 | 32 | const newOptions = { ...options, depth: options.depth === null ? null : options.depth! - 1, compact: true }; 33 | 34 | const padding = `\n ${options.stylize('|', 'undefined')} `; 35 | const given = inspect(this.value, newOptions).replace(/\n/g, padding); 36 | 37 | const header = `${options.stylize('UnknownPropertyError', 'special')} > ${property}`; 38 | const message = options.stylize(this.message, 'regexp'); 39 | const givenBlock = `\n ${options.stylize('Received:', 'regexp')}${padding}${given}`; 40 | return `${header}\n ${message}\n${givenBlock}`; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/errors/ValidationError.ts: -------------------------------------------------------------------------------- 1 | import { inspect, type InspectOptionsStylized } from 'util'; 2 | import { BaseError, customInspectSymbolStackLess } from './BaseError'; 3 | import type { ValidationErrorJsonified } from './error-types'; 4 | 5 | export class ValidationError extends BaseError { 6 | public readonly validator: string; 7 | public readonly given: unknown; 8 | 9 | public constructor(validator: string, message: string, given: unknown) { 10 | super(message); 11 | 12 | this.validator = validator; 13 | this.given = given; 14 | } 15 | 16 | public override toJSON(): ValidationErrorJsonified { 17 | return { 18 | name: this.name, 19 | message: 'Unknown validation error occurred.', 20 | validator: this.validator, 21 | given: this.given 22 | }; 23 | } 24 | 25 | protected [customInspectSymbolStackLess](depth: number, options: InspectOptionsStylized): string { 26 | const validator = options.stylize(this.validator, 'string'); 27 | if (depth < 0) { 28 | return options.stylize(`[ValidationError: ${validator}]`, 'special'); 29 | } 30 | 31 | const newOptions = { ...options, depth: options.depth === null ? null : options.depth! - 1, compact: true }; 32 | 33 | const padding = `\n ${options.stylize('|', 'undefined')} `; 34 | const given = inspect(this.given, newOptions).replace(/\n/g, padding); 35 | 36 | const header = `${options.stylize('ValidationError', 'special')} > ${validator}`; 37 | const message = options.stylize(this.message, 'regexp'); 38 | const givenBlock = `\n ${options.stylize('Received:', 'regexp')}${padding}${given}`; 39 | return `${header}\n ${message}\n${givenBlock}`; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/errors/error-types.ts: -------------------------------------------------------------------------------- 1 | import type { ConstraintErrorNames } from './BaseConstraintError'; 2 | 3 | export interface BaseErrorJsonified { 4 | name: string; 5 | message: string; 6 | } 7 | 8 | export interface BaseConstraintErrorJsonified extends BaseErrorJsonified { 9 | constraint: ConstraintErrorNames; 10 | given: T; 11 | } 12 | 13 | export interface ExpectedConstraintErrorJsonified extends BaseConstraintErrorJsonified { 14 | expected: string; 15 | } 16 | 17 | export interface ValidationErrorJsonified extends BaseErrorJsonified { 18 | validator: string; 19 | given: unknown; 20 | } 21 | 22 | export interface ExpectedValidationErrorJsonified extends ValidationErrorJsonified { 23 | expected: T; 24 | } 25 | 26 | export interface MissingPropertyErrorJsonified extends BaseErrorJsonified { 27 | property: PropertyKey; 28 | } 29 | 30 | export interface MultiplePossibilitiesConstraintErrorJsonified extends BaseConstraintErrorJsonified { 31 | expected: readonly string[]; 32 | } 33 | 34 | export interface UnknownEnumValueErrorJsonified extends BaseErrorJsonified { 35 | value: string | number; 36 | enumKeys: string[]; 37 | enumMappings: readonly (readonly [string | number, string | number])[]; 38 | } 39 | 40 | export interface UnknownEnumKeyErrorJsonified extends BaseErrorJsonified { 41 | property: PropertyKey; 42 | value: unknown; 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/util-types.ts: -------------------------------------------------------------------------------- 1 | import type { BaseValidator } from '../validators/BaseValidator'; 2 | import type { ObjectValidator } from '../validators/ObjectValidator'; 3 | import type { Result } from './Result'; 4 | 5 | export type Constructor = (new (...args: readonly any[]) => T) | (abstract new (...args: readonly any[]) => T); 6 | 7 | export type Type = V extends BaseValidator ? T : never; 8 | 9 | /** 10 | * Additional options to pass to the validator. 11 | * Right now this only supports a custom error message, but we provide an option for future expansion. 12 | */ 13 | export interface ValidatorOptions { 14 | /** 15 | * The custom message to throw when this validation fails. 16 | */ 17 | message?: string; 18 | } 19 | 20 | type FilterDefinedKeys = Exclude< 21 | { 22 | [TKey in keyof TObj]: undefined extends TObj[TKey] ? never : TKey; 23 | }[keyof TObj], 24 | undefined 25 | >; 26 | 27 | export type UndefinedToOptional = Pick> & { 28 | [k in keyof Omit>]?: Exclude; 29 | }; 30 | 31 | export type MappedObjectValidator = { [key in keyof T]: BaseValidator }; 32 | 33 | /** 34 | * An alias of {@link ObjectValidator} with a name more common among object validation libraries. 35 | * This is the type of a schema after using `s.object({ ... })` 36 | * @example 37 | * ```typescript 38 | * import { s, SchemaOf } from '@sapphire/shapeshift'; 39 | * 40 | * interface IIngredient { 41 | * ingredientId: string | undefined; 42 | * name: string | undefined; 43 | * } 44 | * 45 | * interface IInstruction { 46 | * instructionId: string | undefined; 47 | * message: string | undefined; 48 | * } 49 | * 50 | * interface IRecipe { 51 | * recipeId: string | undefined; 52 | * title: string; 53 | * description: string; 54 | * instructions: IInstruction[]; 55 | * ingredients: IIngredient[]; 56 | * } 57 | * 58 | * type InstructionSchemaType = SchemaOf; 59 | * // Expected Type: ObjectValidator 60 | * 61 | * type IngredientSchemaType = SchemaOf; 62 | * // Expected Type: ObjectValidator 63 | * 64 | * type RecipeSchemaType = SchemaOf; 65 | * // Expected Type: ObjectValidator 66 | * 67 | * const instructionSchema: InstructionSchemaType = s.object({ 68 | * instructionId: s.string.optional, 69 | * message: s.string 70 | * }); 71 | * 72 | * const ingredientSchema: IngredientSchemaType = s.object({ 73 | * ingredientId: s.string.optional, 74 | * name: s.string 75 | * }); 76 | * 77 | * const recipeSchema: RecipeSchemaType = s.object({ 78 | * recipeId: s.string.optional, 79 | * title: s.string, 80 | * description: s.string, 81 | * instructions: s.array(instructionSchema), 82 | * ingredients: s.array(ingredientSchema) 83 | * }); 84 | * ``` 85 | */ 86 | export type SchemaOf = ObjectValidator; 87 | 88 | /** 89 | * Infers the type of a schema object given `typeof schema`. 90 | * The schema has to extend {@link ObjectValidator}. 91 | * @example 92 | * ```typescript 93 | * import { InferType, s } from '@sapphire/shapeshift'; 94 | * 95 | * const schema = s.object({ 96 | * foo: s.string, 97 | * bar: s.number, 98 | * baz: s.boolean, 99 | * qux: s.bigint, 100 | * quux: s.date 101 | * }); 102 | * 103 | * type Inferredtype = InferType; 104 | * // Expected type: 105 | * // type Inferredtype = { 106 | * // foo: string; 107 | * // bar: number; 108 | * // baz: boolean; 109 | * // qux: bigint; 110 | * // quux: Date; 111 | * // }; 112 | * ``` 113 | */ 114 | export type InferType> = T extends ObjectValidator ? U : never; 115 | 116 | export type InferResultType> = T extends Result ? U : never; 117 | 118 | export type UnwrapTuple = T extends [infer Head, ...infer Tail] ? [Unwrap, ...UnwrapTuple] : []; 119 | export type Unwrap = T extends BaseValidator ? V : never; 120 | 121 | export type UnshiftTuple = T extends [T[0], ...infer Tail] ? Tail : never; 122 | export type ExpandSmallerTuples = T extends [T[0], ...infer Tail] ? T | ExpandSmallerTuples : []; 123 | 124 | // https://github.com/microsoft/TypeScript/issues/26223#issuecomment-755067958 125 | export type Shift> = ((...args: A) => void) extends (...args: [A[0], ...infer R]) => void ? R : never; 126 | 127 | export type GrowExpRev, N extends number, P extends Array>> = A['length'] extends N 128 | ? A 129 | : GrowExpRev<[...A, ...P[0]][N] extends undefined ? [...A, ...P[0]] : A, N, Shift

>; 130 | 131 | export type GrowExp, N extends number, P extends Array>> = [...A, ...A][N] extends undefined 132 | ? GrowExp<[...A, ...A], N, [A, ...P]> 133 | : GrowExpRev; 134 | 135 | export type Tuple = N extends number 136 | ? number extends N 137 | ? Array 138 | : N extends 0 139 | ? [] 140 | : N extends 1 141 | ? [T] 142 | : GrowExp<[T], N, [[]]> 143 | : never; 144 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./", 5 | "outDir": "../dist", 6 | "incremental": false 7 | }, 8 | "include": ["."] 9 | } 10 | -------------------------------------------------------------------------------- /src/type-exports.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | ArrayConstraintName, 3 | arrayLengthEqual, 4 | arrayLengthGreaterThan, 5 | arrayLengthGreaterThanOrEqual, 6 | arrayLengthLessThan, 7 | arrayLengthLessThanOrEqual, 8 | arrayLengthNotEqual, 9 | arrayLengthRange, 10 | arrayLengthRangeExclusive, 11 | arrayLengthRangeInclusive, 12 | BigIntConstraintName, 13 | bigintDivisibleBy, 14 | bigintEqual, 15 | bigintGreaterThan, 16 | bigintGreaterThanOrEqual, 17 | bigintLessThan, 18 | bigintLessThanOrEqual, 19 | bigintNotEqual, 20 | BooleanConstraintName, 21 | booleanFalse, 22 | booleanTrue, 23 | DateConstraintName, 24 | dateEqual, 25 | dateGreaterThan, 26 | dateGreaterThanOrEqual, 27 | dateInvalid, 28 | dateLessThan, 29 | dateLessThanOrEqual, 30 | dateNotEqual, 31 | dateValid, 32 | IConstraint, 33 | NumberConstraintName, 34 | numberDivisibleBy, 35 | numberEqual, 36 | numberFinite, 37 | numberGreaterThan, 38 | numberGreaterThanOrEqual, 39 | numberInt, 40 | numberLessThan, 41 | numberLessThanOrEqual, 42 | numberNaN, 43 | numberNotEqual, 44 | numberNotNaN, 45 | numberSafeInt, 46 | StringConstraintName, 47 | StringDomain, 48 | stringEmail, 49 | stringIp, 50 | stringLengthEqual, 51 | stringLengthGreaterThan, 52 | stringLengthGreaterThanOrEqual, 53 | stringLengthLessThan, 54 | stringLengthLessThanOrEqual, 55 | stringLengthNotEqual, 56 | StringProtocol, 57 | stringRegex, 58 | stringUrl, 59 | stringUuid, 60 | StringUuidOptions, 61 | typedArrayByteLengthEqual, 62 | typedArrayByteLengthGreaterThan, 63 | typedArrayByteLengthGreaterThanOrEqual, 64 | typedArrayByteLengthLessThan, 65 | typedArrayByteLengthLessThanOrEqual, 66 | typedArrayByteLengthNotEqual, 67 | typedArrayByteLengthRange, 68 | typedArrayByteLengthRangeExclusive, 69 | typedArrayByteLengthRangeInclusive, 70 | TypedArrayConstraintName, 71 | typedArrayLengthEqual, 72 | typedArrayLengthGreaterThan, 73 | typedArrayLengthGreaterThanOrEqual, 74 | typedArrayLengthLessThan, 75 | typedArrayLengthLessThanOrEqual, 76 | typedArrayLengthNotEqual, 77 | typedArrayLengthRange, 78 | typedArrayLengthRangeExclusive, 79 | typedArrayLengthRangeInclusive, 80 | UrlOptions, 81 | UUIDVersion 82 | } from './constraints/type-exports'; 83 | export type { BaseConstraintError, ConstraintErrorNames } from './lib/errors/BaseConstraintError'; 84 | // 85 | export type { CombinedError } from './lib/errors/CombinedError'; 86 | export type { ExpectedValidationError } from './lib/errors/ExpectedValidationError'; 87 | export type { MissingPropertyError } from './lib/errors/MissingPropertyError'; 88 | export type { MultiplePossibilitiesConstraintError } from './lib/errors/MultiplePossibilitiesConstraintError'; 89 | export type { UnknownEnumValueError } from './lib/errors/UnknownEnumValueError'; 90 | export type { UnknownPropertyError } from './lib/errors/UnknownPropertyError'; 91 | export type { ValidationError } from './lib/errors/ValidationError'; 92 | // 93 | export type { Shapes } from './lib/Shapes'; 94 | // 95 | export * from './lib/util-types'; 96 | export * from './lib/errors/error-types'; 97 | // 98 | export type { ArrayValidator } from './validators/ArrayValidator'; 99 | export type { BaseValidator, ValidatorError } from './validators/BaseValidator'; 100 | export type { BigIntValidator } from './validators/BigIntValidator'; 101 | export type { BooleanValidator } from './validators/BooleanValidator'; 102 | export type { DateValidator } from './validators/DateValidator'; 103 | export type { DefaultValidator } from './validators/DefaultValidator'; 104 | export type { InstanceValidator } from './validators/InstanceValidator'; 105 | export type { LiteralValidator } from './validators/LiteralValidator'; 106 | export type { MapValidator } from './validators/MapValidator'; 107 | export type { NativeEnumLike, NativeEnumValidator } from './validators/NativeEnumValidator'; 108 | export type { NeverValidator } from './validators/NeverValidator'; 109 | export type { NullishValidator } from './validators/NullishValidator'; 110 | export type { NumberValidator } from './validators/NumberValidator'; 111 | export type { ObjectValidator, ObjectValidatorStrategy } from './validators/ObjectValidator'; 112 | export type { PassthroughValidator } from './validators/PassthroughValidator'; 113 | export type { RecordValidator } from './validators/RecordValidator'; 114 | export type { SetValidator } from './validators/SetValidator'; 115 | export type { StringValidator } from './validators/StringValidator'; 116 | export type { TupleValidator } from './validators/TupleValidator'; 117 | export type { TypedArrayValidator } from './validators/TypedArrayValidator'; 118 | export type { UnionValidator } from './validators/UnionValidator'; 119 | -------------------------------------------------------------------------------- /src/validators/ArrayValidator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | arrayLengthEqual, 3 | arrayLengthGreaterThan, 4 | arrayLengthGreaterThanOrEqual, 5 | arrayLengthLessThan, 6 | arrayLengthLessThanOrEqual, 7 | arrayLengthNotEqual, 8 | arrayLengthRange, 9 | arrayLengthRangeExclusive, 10 | arrayLengthRangeInclusive, 11 | uniqueArray 12 | } from '../constraints/ArrayConstraints'; 13 | import type { IConstraint } from '../constraints/base/IConstraint'; 14 | import type { BaseError } from '../lib/errors/BaseError'; 15 | import { CombinedPropertyError } from '../lib/errors/CombinedPropertyError'; 16 | import { ValidationError } from '../lib/errors/ValidationError'; 17 | import { Result } from '../lib/Result'; 18 | import type { ExpandSmallerTuples, Tuple, UnshiftTuple, ValidatorOptions } from '../lib/util-types'; 19 | import { BaseValidator } from './imports'; 20 | 21 | export class ArrayValidator extends BaseValidator { 22 | private readonly validator: BaseValidator; 23 | 24 | public constructor(validator: BaseValidator, validatorOptions: ValidatorOptions = {}, constraints: readonly IConstraint[] = []) { 25 | super(validatorOptions, constraints); 26 | this.validator = validator; 27 | } 28 | 29 | public lengthLessThan( 30 | length: N, 31 | options: ValidatorOptions = this.validatorOptions 32 | ): ArrayValidator]>>> { 33 | return this.addConstraint(arrayLengthLessThan(length, options) as IConstraint) as any; 34 | } 35 | 36 | public lengthLessThanOrEqual( 37 | length: N, 38 | options: ValidatorOptions = this.validatorOptions 39 | ): ArrayValidator]>> { 40 | return this.addConstraint(arrayLengthLessThanOrEqual(length, options) as IConstraint) as any; 41 | } 42 | 43 | public lengthGreaterThan( 44 | length: N, 45 | options: ValidatorOptions = this.validatorOptions 46 | ): ArrayValidator<[...Tuple, I, ...T]> { 47 | return this.addConstraint(arrayLengthGreaterThan(length, options) as IConstraint) as any; 48 | } 49 | 50 | public lengthGreaterThanOrEqual( 51 | length: N, 52 | options: ValidatorOptions = this.validatorOptions 53 | ): ArrayValidator<[...Tuple, ...T]> { 54 | return this.addConstraint(arrayLengthGreaterThanOrEqual(length, options) as IConstraint) as any; 55 | } 56 | 57 | public lengthEqual(length: N, options: ValidatorOptions = this.validatorOptions): ArrayValidator<[...Tuple]> { 58 | return this.addConstraint(arrayLengthEqual(length, options) as IConstraint) as any; 59 | } 60 | 61 | public lengthNotEqual(length: N, options: ValidatorOptions = this.validatorOptions): ArrayValidator<[...Tuple]> { 62 | return this.addConstraint(arrayLengthNotEqual(length, options) as IConstraint) as any; 63 | } 64 | 65 | public lengthRange( 66 | start: S, 67 | endBefore: E, 68 | options: ValidatorOptions = this.validatorOptions 69 | ): ArrayValidator]>>, ExpandSmallerTuples]>>>> { 70 | return this.addConstraint(arrayLengthRange(start, endBefore, options) as IConstraint) as any; 71 | } 72 | 73 | public lengthRangeInclusive( 74 | startAt: S, 75 | endAt: E, 76 | options: ValidatorOptions = this.validatorOptions 77 | ): ArrayValidator]>, ExpandSmallerTuples]>>>> { 78 | return this.addConstraint(arrayLengthRangeInclusive(startAt, endAt, options) as IConstraint) as any; 79 | } 80 | 81 | public lengthRangeExclusive( 82 | startAfter: S, 83 | endBefore: E, 84 | options: ValidatorOptions = this.validatorOptions 85 | ): ArrayValidator]>>, ExpandSmallerTuples<[...Tuple]>>> { 86 | return this.addConstraint(arrayLengthRangeExclusive(startAfter, endBefore, options) as IConstraint) as any; 87 | } 88 | 89 | public unique(options: ValidatorOptions = this.validatorOptions): this { 90 | return this.addConstraint(uniqueArray(options) as IConstraint); 91 | } 92 | 93 | protected override clone(): this { 94 | return Reflect.construct(this.constructor, [this.validator, this.validatorOptions, this.constraints]); 95 | } 96 | 97 | protected handle(values: unknown): Result { 98 | if (!Array.isArray(values)) { 99 | return Result.err(new ValidationError('s.array(T)', this.validatorOptions.message ?? 'Expected an array', values)); 100 | } 101 | 102 | if (!this.shouldRunConstraints) { 103 | return Result.ok(values as T); 104 | } 105 | 106 | const errors: [number, BaseError][] = []; 107 | const transformed: T = [] as unknown as T; 108 | 109 | for (let i = 0; i < values.length; i++) { 110 | const result = this.validator.run(values[i]); 111 | if (result.isOk()) transformed.push(result.value); 112 | else errors.push([i, result.error!]); 113 | } 114 | 115 | return errors.length === 0 // 116 | ? Result.ok(transformed) 117 | : Result.err(new CombinedPropertyError(errors, this.validatorOptions)); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/validators/BigIntValidator.ts: -------------------------------------------------------------------------------- 1 | import type { IConstraint } from '../constraints/base/IConstraint'; 2 | import { 3 | bigintDivisibleBy, 4 | bigintEqual, 5 | bigintGreaterThan, 6 | bigintGreaterThanOrEqual, 7 | bigintLessThan, 8 | bigintLessThanOrEqual, 9 | bigintNotEqual 10 | } from '../constraints/BigIntConstraints'; 11 | import { ValidationError } from '../lib/errors/ValidationError'; 12 | import { Result } from '../lib/Result'; 13 | import type { ValidatorOptions } from '../lib/util-types'; 14 | import { BaseValidator } from './imports'; 15 | 16 | export class BigIntValidator extends BaseValidator { 17 | public lessThan(number: bigint, options: ValidatorOptions = this.validatorOptions): this { 18 | return this.addConstraint(bigintLessThan(number, options) as IConstraint); 19 | } 20 | 21 | public lessThanOrEqual(number: bigint, options: ValidatorOptions = this.validatorOptions): this { 22 | return this.addConstraint(bigintLessThanOrEqual(number, options) as IConstraint); 23 | } 24 | 25 | public greaterThan(number: bigint, options: ValidatorOptions = this.validatorOptions): this { 26 | return this.addConstraint(bigintGreaterThan(number, options) as IConstraint); 27 | } 28 | 29 | public greaterThanOrEqual(number: bigint, options: ValidatorOptions = this.validatorOptions): this { 30 | return this.addConstraint(bigintGreaterThanOrEqual(number, options) as IConstraint); 31 | } 32 | 33 | public equal(number: N, options: ValidatorOptions = this.validatorOptions): BigIntValidator { 34 | return this.addConstraint(bigintEqual(number, options) as IConstraint) as unknown as BigIntValidator; 35 | } 36 | 37 | public notEqual(number: bigint, options: ValidatorOptions = this.validatorOptions): this { 38 | return this.addConstraint(bigintNotEqual(number, options) as IConstraint); 39 | } 40 | 41 | public positive(options: ValidatorOptions = this.validatorOptions): this { 42 | return this.greaterThanOrEqual(0n, options); 43 | } 44 | 45 | public negative(options: ValidatorOptions = this.validatorOptions): this { 46 | return this.lessThan(0n, options); 47 | } 48 | 49 | public divisibleBy(number: bigint, options: ValidatorOptions = this.validatorOptions): this { 50 | return this.addConstraint(bigintDivisibleBy(number, options) as IConstraint); 51 | } 52 | 53 | public abs(options: ValidatorOptions = this.validatorOptions): this { 54 | return this.transform((value) => (value < 0 ? -value : value) as T, options); 55 | } 56 | 57 | public intN(bits: number, options: ValidatorOptions = this.validatorOptions): this { 58 | return this.transform((value) => BigInt.asIntN(bits, value) as T, options); 59 | } 60 | 61 | public uintN(bits: number, options: ValidatorOptions = this.validatorOptions): this { 62 | return this.transform((value) => BigInt.asUintN(bits, value) as T, options); 63 | } 64 | 65 | protected handle(value: unknown): Result { 66 | return typeof value === 'bigint' // 67 | ? Result.ok(value as T) 68 | : Result.err(new ValidationError('s.bigint()', this.validatorOptions.message ?? 'Expected a bigint primitive', value)); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/validators/BooleanValidator.ts: -------------------------------------------------------------------------------- 1 | import type { IConstraint } from '../constraints/base/IConstraint'; 2 | import { booleanFalse, booleanTrue } from '../constraints/BooleanConstraints'; 3 | import { ValidationError } from '../lib/errors/ValidationError'; 4 | import { Result } from '../lib/Result'; 5 | import type { ValidatorOptions } from '../lib/util-types'; 6 | import { BaseValidator } from './imports'; 7 | 8 | export class BooleanValidator extends BaseValidator { 9 | public true(options: ValidatorOptions = this.validatorOptions): BooleanValidator { 10 | return this.addConstraint(booleanTrue(options) as IConstraint) as BooleanValidator; 11 | } 12 | 13 | public false(options: ValidatorOptions = this.validatorOptions): BooleanValidator { 14 | return this.addConstraint(booleanFalse(options) as IConstraint) as BooleanValidator; 15 | } 16 | 17 | public equal(value: R, options: ValidatorOptions = this.validatorOptions): BooleanValidator { 18 | return (value ? this.true(options) : this.false(options)) as BooleanValidator; 19 | } 20 | 21 | public notEqual(value: R, options: ValidatorOptions = this.validatorOptions): BooleanValidator { 22 | return (value ? this.false(options) : this.true(options)) as BooleanValidator; 23 | } 24 | 25 | protected handle(value: unknown): Result { 26 | return typeof value === 'boolean' // 27 | ? Result.ok(value as T) 28 | : Result.err(new ValidationError('s.boolean()', this.validatorOptions.message ?? 'Expected a boolean primitive', value)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/validators/DateValidator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | dateEqual, 3 | dateGreaterThan, 4 | dateGreaterThanOrEqual, 5 | dateInvalid, 6 | dateLessThan, 7 | dateLessThanOrEqual, 8 | dateNotEqual, 9 | dateValid 10 | } from '../constraints/DateConstraints'; 11 | import { ValidationError } from '../lib/errors/ValidationError'; 12 | import { Result } from '../lib/Result'; 13 | import type { ValidatorOptions } from '../lib/util-types'; 14 | import { BaseValidator } from './imports'; 15 | 16 | export class DateValidator extends BaseValidator { 17 | public lessThan(date: Date | number | string, options: ValidatorOptions = this.validatorOptions): this { 18 | return this.addConstraint(dateLessThan(new Date(date), options)); 19 | } 20 | 21 | public lessThanOrEqual(date: Date | number | string, options: ValidatorOptions = this.validatorOptions): this { 22 | return this.addConstraint(dateLessThanOrEqual(new Date(date), options)); 23 | } 24 | 25 | public greaterThan(date: Date | number | string, options: ValidatorOptions = this.validatorOptions): this { 26 | return this.addConstraint(dateGreaterThan(new Date(date), options)); 27 | } 28 | 29 | public greaterThanOrEqual(date: Date | number | string, options: ValidatorOptions = this.validatorOptions): this { 30 | return this.addConstraint(dateGreaterThanOrEqual(new Date(date), options)); 31 | } 32 | 33 | public equal(date: Date | number | string, options: ValidatorOptions = this.validatorOptions): this { 34 | const resolved = new Date(date); 35 | return Number.isNaN(resolved.getTime()) // 36 | ? this.invalid(options) 37 | : this.addConstraint(dateEqual(resolved, options)); 38 | } 39 | 40 | public notEqual(date: Date | number | string, options: ValidatorOptions = this.validatorOptions): this { 41 | const resolved = new Date(date); 42 | return Number.isNaN(resolved.getTime()) // 43 | ? this.valid(options) 44 | : this.addConstraint(dateNotEqual(resolved, options)); 45 | } 46 | 47 | public valid(options: ValidatorOptions = this.validatorOptions): this { 48 | return this.addConstraint(dateValid(options)); 49 | } 50 | 51 | public invalid(options: ValidatorOptions = this.validatorOptions): this { 52 | return this.addConstraint(dateInvalid(options)); 53 | } 54 | 55 | protected handle(value: unknown): Result { 56 | return value instanceof Date // 57 | ? Result.ok(value) 58 | : Result.err(new ValidationError('s.date()', this.validatorOptions.message ?? 'Expected a Date', value)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/validators/DefaultValidator.ts: -------------------------------------------------------------------------------- 1 | import type { IConstraint } from '../constraints/base/IConstraint'; 2 | import { Result } from '../lib/Result'; 3 | import type { ValidatorOptions } from '../lib/util-types'; 4 | import type { ValidatorError } from './BaseValidator'; 5 | import { BaseValidator } from './imports'; 6 | import { getValue } from './util/getValue'; 7 | 8 | export class DefaultValidator extends BaseValidator { 9 | private readonly validator: BaseValidator; 10 | private defaultValue: T | (() => T); 11 | 12 | public constructor( 13 | validator: BaseValidator, 14 | value: T | (() => T), 15 | validatorOptions: ValidatorOptions = {}, 16 | constraints: readonly IConstraint[] = [] 17 | ) { 18 | super(validatorOptions, constraints); 19 | this.validator = validator; 20 | this.defaultValue = value; 21 | } 22 | 23 | public override default( 24 | value: Exclude | (() => Exclude), 25 | options = this.validatorOptions 26 | ): DefaultValidator> { 27 | const clone = this.clone() as unknown as DefaultValidator>; 28 | clone.validatorOptions = options; 29 | clone.defaultValue = value; 30 | return clone; 31 | } 32 | 33 | protected handle(value: unknown): Result { 34 | return typeof value === 'undefined' // 35 | ? Result.ok(getValue(this.defaultValue)) 36 | : this.validator['handle'](value); // eslint-disable-line @typescript-eslint/dot-notation 37 | } 38 | 39 | protected override clone(): this { 40 | return Reflect.construct(this.constructor, [this.validator, this.defaultValue, this.validatorOptions, this.constraints]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/validators/InstanceValidator.ts: -------------------------------------------------------------------------------- 1 | import type { IConstraint } from '../constraints/base/IConstraint'; 2 | import { ExpectedValidationError } from '../lib/errors/ExpectedValidationError'; 3 | import { Result } from '../lib/Result'; 4 | import type { Constructor, ValidatorOptions } from '../lib/util-types'; 5 | import { BaseValidator } from './imports'; 6 | 7 | export class InstanceValidator extends BaseValidator { 8 | public readonly expected: Constructor; 9 | 10 | public constructor(expected: Constructor, validatorOptions: ValidatorOptions = {}, constraints: readonly IConstraint[] = []) { 11 | super(validatorOptions, constraints); 12 | this.expected = expected; 13 | } 14 | 15 | protected handle(value: unknown): Result>> { 16 | return value instanceof this.expected // 17 | ? Result.ok(value) 18 | : Result.err(new ExpectedValidationError('s.instance(V)', this.validatorOptions.message ?? 'Expected', value, this.expected)); 19 | } 20 | 21 | protected override clone(): this { 22 | return Reflect.construct(this.constructor, [this.expected, this.validatorOptions, this.constraints]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/validators/LazyValidator.ts: -------------------------------------------------------------------------------- 1 | import type { IConstraint } from '../constraints/base/IConstraint'; 2 | import type { Result } from '../lib/Result'; 3 | import type { Unwrap, ValidatorOptions } from '../lib/util-types'; 4 | import { BaseValidator, type ValidatorError } from './imports'; 5 | 6 | export class LazyValidator, R = Unwrap> extends BaseValidator { 7 | private readonly validator: (value: unknown) => T; 8 | 9 | public constructor(validator: (value: unknown) => T, validatorOptions: ValidatorOptions = {}, constraints: readonly IConstraint[] = []) { 10 | super(validatorOptions, constraints); 11 | this.validator = validator; 12 | } 13 | 14 | protected override clone(): this { 15 | return Reflect.construct(this.constructor, [this.validator, this.validatorOptions, this.constraints]); 16 | } 17 | 18 | protected handle(values: unknown): Result { 19 | return this.validator(values).run(values) as Result; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/validators/LiteralValidator.ts: -------------------------------------------------------------------------------- 1 | import type { IConstraint } from '../constraints/base/IConstraint'; 2 | import { ExpectedValidationError } from '../lib/errors/ExpectedValidationError'; 3 | import { Result } from '../lib/Result'; 4 | import type { ValidatorOptions } from '../lib/util-types'; 5 | import { BaseValidator } from './imports'; 6 | 7 | export class LiteralValidator extends BaseValidator { 8 | public readonly expected: T; 9 | 10 | public constructor(literal: T, validatorOptions: ValidatorOptions = {}, constraints: readonly IConstraint[] = []) { 11 | super(validatorOptions, constraints); 12 | this.expected = literal; 13 | } 14 | 15 | protected handle(value: unknown): Result> { 16 | return Object.is(value, this.expected) // 17 | ? Result.ok(value as T) 18 | : Result.err( 19 | new ExpectedValidationError('s.literal(V)', this.validatorOptions.message ?? 'Expected values to be equals', value, this.expected) 20 | ); 21 | } 22 | 23 | protected override clone(): this { 24 | return Reflect.construct(this.constructor, [this.expected, this.validatorOptions, this.constraints]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/validators/MapValidator.ts: -------------------------------------------------------------------------------- 1 | import type { IConstraint } from '../constraints/base/IConstraint'; 2 | import type { BaseError } from '../lib/errors/BaseError'; 3 | import { CombinedPropertyError } from '../lib/errors/CombinedPropertyError'; 4 | import { ValidationError } from '../lib/errors/ValidationError'; 5 | import { Result } from '../lib/Result'; 6 | import type { ValidatorOptions } from '../lib/util-types'; 7 | import { BaseValidator } from './imports'; 8 | 9 | export class MapValidator extends BaseValidator> { 10 | private readonly keyValidator: BaseValidator; 11 | private readonly valueValidator: BaseValidator; 12 | 13 | public constructor( 14 | keyValidator: BaseValidator, 15 | valueValidator: BaseValidator, 16 | validatorOptions: ValidatorOptions = {}, 17 | constraints: readonly IConstraint>[] = [] 18 | ) { 19 | super(validatorOptions, constraints); 20 | this.keyValidator = keyValidator; 21 | this.valueValidator = valueValidator; 22 | } 23 | 24 | protected override clone(): this { 25 | return Reflect.construct(this.constructor, [this.keyValidator, this.valueValidator, this.validatorOptions, this.constraints]); 26 | } 27 | 28 | protected handle(value: unknown): Result, ValidationError | CombinedPropertyError> { 29 | if (!(value instanceof Map)) { 30 | return Result.err(new ValidationError('s.map(K, V)', this.validatorOptions.message ?? 'Expected a map', value)); 31 | } 32 | 33 | if (!this.shouldRunConstraints) { 34 | return Result.ok(value); 35 | } 36 | 37 | const errors: [string, BaseError][] = []; 38 | const transformed = new Map(); 39 | 40 | for (const [key, val] of value.entries()) { 41 | const keyResult = this.keyValidator.run(key); 42 | const valueResult = this.valueValidator.run(val); 43 | const { length } = errors; 44 | if (keyResult.isErr()) errors.push([key, keyResult.error]); 45 | if (valueResult.isErr()) errors.push([key, valueResult.error]); 46 | if (errors.length === length) transformed.set(keyResult.value!, valueResult.value!); 47 | } 48 | 49 | return errors.length === 0 // 50 | ? Result.ok(transformed) 51 | : Result.err(new CombinedPropertyError(errors, this.validatorOptions)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/validators/NativeEnumValidator.ts: -------------------------------------------------------------------------------- 1 | import { UnknownEnumValueError } from '../lib/errors/UnknownEnumValueError'; 2 | import { ValidationError } from '../lib/errors/ValidationError'; 3 | import { Result } from '../lib/Result'; 4 | import type { ValidatorOptions } from '../lib/util-types'; 5 | import { BaseValidator } from './imports'; 6 | 7 | export class NativeEnumValidator extends BaseValidator { 8 | public readonly enumShape: T; 9 | public readonly hasNumericElements: boolean = false; 10 | private readonly enumKeys: string[]; 11 | private readonly enumMapping = new Map(); 12 | 13 | public constructor(enumShape: T, validatorOptions: ValidatorOptions = {}) { 14 | super(validatorOptions); 15 | this.enumShape = enumShape; 16 | 17 | this.enumKeys = Object.keys(enumShape).filter((key) => { 18 | return typeof enumShape[enumShape[key]] !== 'number'; 19 | }); 20 | 21 | for (const key of this.enumKeys) { 22 | const enumValue = enumShape[key] as T[keyof T]; 23 | 24 | this.enumMapping.set(key, enumValue); 25 | this.enumMapping.set(enumValue, enumValue); 26 | 27 | if (typeof enumValue === 'number') { 28 | this.hasNumericElements = true; 29 | this.enumMapping.set(`${enumValue}`, enumValue); 30 | } 31 | } 32 | } 33 | 34 | protected override handle(value: unknown): Result { 35 | const typeOfValue = typeof value; 36 | 37 | if (typeOfValue === 'number') { 38 | if (!this.hasNumericElements) { 39 | return Result.err( 40 | new ValidationError('s.nativeEnum(T)', this.validatorOptions.message ?? 'Expected the value to be a string', value) 41 | ); 42 | } 43 | } else if (typeOfValue !== 'string') { 44 | // typeOfValue !== 'number' is implied here 45 | return Result.err( 46 | new ValidationError('s.nativeEnum(T)', this.validatorOptions.message ?? 'Expected the value to be a string or number', value) 47 | ); 48 | } 49 | 50 | const casted = value as string | number; 51 | 52 | const possibleEnumValue = this.enumMapping.get(casted); 53 | 54 | return typeof possibleEnumValue === 'undefined' 55 | ? Result.err(new UnknownEnumValueError(casted, this.enumKeys, this.enumMapping, this.validatorOptions)) 56 | : Result.ok(possibleEnumValue); 57 | } 58 | 59 | protected override clone(): this { 60 | return Reflect.construct(this.constructor, [this.enumShape, this.validatorOptions]); 61 | } 62 | } 63 | 64 | export interface NativeEnumLike { 65 | [key: string]: string | number; 66 | [key: number]: string; 67 | } 68 | -------------------------------------------------------------------------------- /src/validators/NeverValidator.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from '../lib/errors/ValidationError'; 2 | import { Result } from '../lib/Result'; 3 | import { BaseValidator } from './imports'; 4 | 5 | export class NeverValidator extends BaseValidator { 6 | protected handle(value: unknown): Result { 7 | return Result.err(new ValidationError('s.never()', this.validatorOptions.message ?? 'Expected a value to not be passed', value)); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/validators/NullishValidator.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from '../lib/errors/ValidationError'; 2 | import { Result } from '../lib/Result'; 3 | import { BaseValidator } from './imports'; 4 | 5 | export class NullishValidator extends BaseValidator { 6 | protected handle(value: unknown): Result { 7 | return value === undefined || value === null // 8 | ? Result.ok(value) 9 | : Result.err(new ValidationError('s.nullish()', this.validatorOptions.message ?? 'Expected undefined or null', value)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/validators/NumberValidator.ts: -------------------------------------------------------------------------------- 1 | import type { IConstraint } from '../constraints/base/IConstraint'; 2 | import { 3 | numberDivisibleBy, 4 | numberEqual, 5 | numberFinite, 6 | numberGreaterThan, 7 | numberGreaterThanOrEqual, 8 | numberInt, 9 | numberLessThan, 10 | numberLessThanOrEqual, 11 | numberNaN, 12 | numberNotEqual, 13 | numberNotNaN, 14 | numberSafeInt 15 | } from '../constraints/NumberConstraints'; 16 | import { ValidationError } from '../lib/errors/ValidationError'; 17 | import { Result } from '../lib/Result'; 18 | import type { ValidatorOptions } from '../lib/util-types'; 19 | import { BaseValidator } from './imports'; 20 | 21 | export class NumberValidator extends BaseValidator { 22 | public lessThan(number: number, options: ValidatorOptions = this.validatorOptions): this { 23 | return this.addConstraint(numberLessThan(number, options) as IConstraint); 24 | } 25 | 26 | public lessThanOrEqual(number: number, options: ValidatorOptions = this.validatorOptions): this { 27 | return this.addConstraint(numberLessThanOrEqual(number, options) as IConstraint); 28 | } 29 | 30 | public greaterThan(number: number, options: ValidatorOptions = this.validatorOptions): this { 31 | return this.addConstraint(numberGreaterThan(number, options) as IConstraint); 32 | } 33 | 34 | public greaterThanOrEqual(number: number, options: ValidatorOptions = this.validatorOptions): this { 35 | return this.addConstraint(numberGreaterThanOrEqual(number, options) as IConstraint); 36 | } 37 | 38 | public equal(number: N, options: ValidatorOptions = this.validatorOptions): NumberValidator { 39 | return Number.isNaN(number) // 40 | ? (this.addConstraint(numberNaN(options) as IConstraint) as unknown as NumberValidator) 41 | : (this.addConstraint(numberEqual(number, options) as IConstraint) as unknown as NumberValidator); 42 | } 43 | 44 | public notEqual(number: number, options: ValidatorOptions = this.validatorOptions): this { 45 | return Number.isNaN(number) // 46 | ? this.addConstraint(numberNotNaN(options) as IConstraint) 47 | : this.addConstraint(numberNotEqual(number, options) as IConstraint); 48 | } 49 | 50 | public int(options: ValidatorOptions = this.validatorOptions): this { 51 | return this.addConstraint(numberInt(options) as IConstraint); 52 | } 53 | 54 | public safeInt(options: ValidatorOptions = this.validatorOptions): this { 55 | return this.addConstraint(numberSafeInt(options) as IConstraint); 56 | } 57 | 58 | public finite(options: ValidatorOptions = this.validatorOptions): this { 59 | return this.addConstraint(numberFinite(options) as IConstraint); 60 | } 61 | 62 | public positive(options: ValidatorOptions = this.validatorOptions): this { 63 | return this.greaterThanOrEqual(0, options); 64 | } 65 | 66 | public negative(options: ValidatorOptions = this.validatorOptions): this { 67 | return this.lessThan(0, options); 68 | } 69 | 70 | public divisibleBy(divider: number, options: ValidatorOptions = this.validatorOptions): this { 71 | return this.addConstraint(numberDivisibleBy(divider, options) as IConstraint); 72 | } 73 | 74 | public abs(options: ValidatorOptions = this.validatorOptions): this { 75 | return this.transform(Math.abs as (value: number) => T, options); 76 | } 77 | 78 | public sign(options: ValidatorOptions = this.validatorOptions): this { 79 | return this.transform(Math.sign as (value: number) => T, options); 80 | } 81 | 82 | public trunc(options: ValidatorOptions = this.validatorOptions): this { 83 | return this.transform(Math.trunc as (value: number) => T, options); 84 | } 85 | 86 | public floor(options: ValidatorOptions = this.validatorOptions): this { 87 | return this.transform(Math.floor as (value: number) => T, options); 88 | } 89 | 90 | public fround(options: ValidatorOptions = this.validatorOptions): this { 91 | return this.transform(Math.fround as (value: number) => T, options); 92 | } 93 | 94 | public round(options: ValidatorOptions = this.validatorOptions): this { 95 | return this.transform(Math.round as (value: number) => T, options); 96 | } 97 | 98 | public ceil(options: ValidatorOptions = this.validatorOptions): this { 99 | return this.transform(Math.ceil as (value: number) => T, options); 100 | } 101 | 102 | protected handle(value: unknown): Result { 103 | return typeof value === 'number' // 104 | ? Result.ok(value as T) 105 | : Result.err(new ValidationError('s.number()', this.validatorOptions.message ?? 'Expected a number primitive', value)); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/validators/PassthroughValidator.ts: -------------------------------------------------------------------------------- 1 | import type { ValidationError } from '../lib/errors/ValidationError'; 2 | import { Result } from '../lib/Result'; 3 | import { BaseValidator } from './imports'; 4 | 5 | export class PassthroughValidator extends BaseValidator { 6 | protected handle(value: unknown): Result { 7 | return Result.ok(value as T); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/validators/RecordValidator.ts: -------------------------------------------------------------------------------- 1 | import type { IConstraint } from '../constraints/base/IConstraint'; 2 | import type { BaseError } from '../lib/errors/BaseError'; 3 | import { CombinedPropertyError } from '../lib/errors/CombinedPropertyError'; 4 | import { ValidationError } from '../lib/errors/ValidationError'; 5 | import { Result } from '../lib/Result'; 6 | import type { ValidatorOptions } from '../lib/util-types'; 7 | import { BaseValidator } from './imports'; 8 | 9 | export class RecordValidator extends BaseValidator> { 10 | private readonly validator: BaseValidator; 11 | 12 | public constructor( 13 | validator: BaseValidator, 14 | validatorOptions: ValidatorOptions = {}, 15 | constraints: readonly IConstraint>[] = [] 16 | ) { 17 | super(validatorOptions, constraints); 18 | this.validator = validator; 19 | } 20 | 21 | protected override clone(): this { 22 | return Reflect.construct(this.constructor, [this.validator, this.validatorOptions, this.constraints]); 23 | } 24 | 25 | protected handle(value: unknown): Result, ValidationError | CombinedPropertyError> { 26 | if (typeof value !== 'object') { 27 | return Result.err(new ValidationError('s.record(T)', this.validatorOptions.message ?? 'Expected an object', value)); 28 | } 29 | 30 | if (value === null) { 31 | return Result.err(new ValidationError('s.record(T)', this.validatorOptions.message ?? 'Expected the value to not be null', value)); 32 | } 33 | 34 | if (Array.isArray(value)) { 35 | return Result.err(new ValidationError('s.record(T)', this.validatorOptions.message ?? 'Expected the value to not be an array', value)); 36 | } 37 | 38 | if (!this.shouldRunConstraints) { 39 | return Result.ok(value as Record); 40 | } 41 | 42 | const errors: [string, BaseError][] = []; 43 | const transformed: Record = {}; 44 | 45 | for (const [key, val] of Object.entries(value!)) { 46 | const result = this.validator.run(val); 47 | if (result.isOk()) transformed[key] = result.value; 48 | else errors.push([key, result.error!]); 49 | } 50 | 51 | return errors.length === 0 // 52 | ? Result.ok(transformed) 53 | : Result.err(new CombinedPropertyError(errors, this.validatorOptions)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/validators/SetValidator.ts: -------------------------------------------------------------------------------- 1 | import type { IConstraint } from '../constraints/base/IConstraint'; 2 | import type { BaseError } from '../lib/errors/BaseError'; 3 | import { CombinedError } from '../lib/errors/CombinedError'; 4 | import { ValidationError } from '../lib/errors/ValidationError'; 5 | import { Result } from '../lib/Result'; 6 | import type { ValidatorOptions } from '../lib/util-types'; 7 | import { BaseValidator } from './imports'; 8 | 9 | export class SetValidator extends BaseValidator> { 10 | private readonly validator: BaseValidator; 11 | 12 | public constructor(validator: BaseValidator, validatorOptions?: ValidatorOptions, constraints: readonly IConstraint>[] = []) { 13 | super(validatorOptions, constraints); 14 | this.validator = validator; 15 | } 16 | 17 | protected override clone(): this { 18 | return Reflect.construct(this.constructor, [this.validator, this.validatorOptions, this.constraints]); 19 | } 20 | 21 | protected handle(values: unknown): Result, ValidationError | CombinedError> { 22 | if (!(values instanceof Set)) { 23 | return Result.err(new ValidationError('s.set(T)', this.validatorOptions.message ?? 'Expected a set', values)); 24 | } 25 | 26 | if (!this.shouldRunConstraints) { 27 | return Result.ok(values); 28 | } 29 | 30 | const errors: BaseError[] = []; 31 | const transformed = new Set(); 32 | 33 | for (const value of values) { 34 | const result = this.validator.run(value); 35 | if (result.isOk()) transformed.add(result.value); 36 | else errors.push(result.error!); 37 | } 38 | 39 | return errors.length === 0 // 40 | ? Result.ok(transformed) 41 | : Result.err(new CombinedError(errors, this.validatorOptions)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/validators/StringValidator.ts: -------------------------------------------------------------------------------- 1 | import type { IConstraint } from '../constraints/base/IConstraint'; 2 | import { 3 | stringDate, 4 | stringEmail, 5 | stringIp, 6 | stringLengthEqual, 7 | stringLengthGreaterThan, 8 | stringLengthGreaterThanOrEqual, 9 | stringLengthLessThan, 10 | stringLengthLessThanOrEqual, 11 | stringLengthNotEqual, 12 | stringPhone, 13 | stringRegex, 14 | stringUrl, 15 | stringUuid, 16 | type StringUuidOptions, 17 | type UrlOptions 18 | } from '../constraints/StringConstraints'; 19 | import { ValidationError } from '../lib/errors/ValidationError'; 20 | import { Result } from '../lib/Result'; 21 | import type { ValidatorOptions } from '../lib/util-types'; 22 | import { BaseValidator } from './imports'; 23 | 24 | export class StringValidator extends BaseValidator { 25 | public lengthLessThan(length: number, options: ValidatorOptions = this.validatorOptions): this { 26 | return this.addConstraint(stringLengthLessThan(length, options) as IConstraint); 27 | } 28 | 29 | public lengthLessThanOrEqual(length: number, options: ValidatorOptions = this.validatorOptions): this { 30 | return this.addConstraint(stringLengthLessThanOrEqual(length, options) as IConstraint); 31 | } 32 | 33 | public lengthGreaterThan(length: number, options: ValidatorOptions = this.validatorOptions): this { 34 | return this.addConstraint(stringLengthGreaterThan(length, options) as IConstraint); 35 | } 36 | 37 | public lengthGreaterThanOrEqual(length: number, options: ValidatorOptions = this.validatorOptions): this { 38 | return this.addConstraint(stringLengthGreaterThanOrEqual(length, options) as IConstraint); 39 | } 40 | 41 | public lengthEqual(length: number, options: ValidatorOptions = this.validatorOptions): this { 42 | return this.addConstraint(stringLengthEqual(length, options) as IConstraint); 43 | } 44 | 45 | public lengthNotEqual(length: number, options: ValidatorOptions = this.validatorOptions): this { 46 | return this.addConstraint(stringLengthNotEqual(length, options) as IConstraint); 47 | } 48 | 49 | public email(options: ValidatorOptions = this.validatorOptions): this { 50 | return this.addConstraint(stringEmail(options) as IConstraint); 51 | } 52 | 53 | public url(validatorOptions?: ValidatorOptions): this; 54 | public url(options?: UrlOptions, validatorOptions?: ValidatorOptions): this; 55 | public url(options?: UrlOptions | ValidatorOptions, validatorOptions: ValidatorOptions = this.validatorOptions): this { 56 | const urlOptions = this.isUrlOptions(options); 57 | 58 | if (urlOptions) { 59 | return this.addConstraint(stringUrl(options, validatorOptions) as IConstraint); 60 | } 61 | 62 | return this.addConstraint(stringUrl(undefined, validatorOptions) as IConstraint); 63 | } 64 | 65 | public uuid(validatorOptions?: ValidatorOptions): this; 66 | public uuid(options?: StringUuidOptions, validatorOptions?: ValidatorOptions): this; 67 | public uuid(options?: StringUuidOptions | ValidatorOptions, validatorOptions: ValidatorOptions = this.validatorOptions): this { 68 | const stringUuidOptions = this.isStringUuidOptions(options); 69 | 70 | if (stringUuidOptions) { 71 | return this.addConstraint(stringUuid(options, validatorOptions) as IConstraint); 72 | } 73 | 74 | return this.addConstraint(stringUuid(undefined, validatorOptions) as IConstraint); 75 | } 76 | 77 | public regex(regex: RegExp, options: ValidatorOptions = this.validatorOptions): this { 78 | return this.addConstraint(stringRegex(regex, options) as IConstraint); 79 | } 80 | 81 | public date(options: ValidatorOptions = this.validatorOptions) { 82 | return this.addConstraint(stringDate(options) as IConstraint); 83 | } 84 | 85 | public ipv4(options: ValidatorOptions = this.validatorOptions): this { 86 | return this.ip(4, options); 87 | } 88 | 89 | public ipv6(options: ValidatorOptions = this.validatorOptions): this { 90 | return this.ip(6, options); 91 | } 92 | 93 | public ip(version?: 4 | 6, options: ValidatorOptions = this.validatorOptions): this { 94 | return this.addConstraint(stringIp(version, options) as IConstraint); 95 | } 96 | 97 | public phone(options: ValidatorOptions = this.validatorOptions): this { 98 | return this.addConstraint(stringPhone(options) as IConstraint); 99 | } 100 | 101 | protected handle(value: unknown): Result { 102 | return typeof value === 'string' // 103 | ? Result.ok(value as T) 104 | : Result.err(new ValidationError('s.string()', this.validatorOptions.message ?? 'Expected a string primitive', value)); 105 | } 106 | 107 | private isUrlOptions(options?: UrlOptions | ValidatorOptions): options is UrlOptions { 108 | return (options as ValidatorOptions)?.message === undefined; 109 | } 110 | 111 | private isStringUuidOptions(options?: StringUuidOptions | ValidatorOptions): options is StringUuidOptions { 112 | return (options as ValidatorOptions)?.message === undefined; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/validators/TupleValidator.ts: -------------------------------------------------------------------------------- 1 | import type { IConstraint } from '../constraints/base/IConstraint'; 2 | import type { BaseError } from '../lib/errors/BaseError'; 3 | import { CombinedPropertyError } from '../lib/errors/CombinedPropertyError'; 4 | import { ValidationError } from '../lib/errors/ValidationError'; 5 | import { Result } from '../lib/Result'; 6 | import type { ValidatorOptions } from '../lib/util-types'; 7 | import { BaseValidator } from './imports'; 8 | 9 | export class TupleValidator extends BaseValidator<[...T]> { 10 | private readonly validators: BaseValidator<[...T]>[] = []; 11 | 12 | public constructor( 13 | validators: BaseValidator<[...T]>[], 14 | validatorOptions: ValidatorOptions = {}, 15 | constraints: readonly IConstraint<[...T]>[] = [] 16 | ) { 17 | super(validatorOptions, constraints); 18 | this.validators = validators; 19 | } 20 | 21 | protected override clone(): this { 22 | return Reflect.construct(this.constructor, [this.validators, this.validatorOptions, this.constraints]); 23 | } 24 | 25 | protected handle(values: unknown): Result<[...T], ValidationError | CombinedPropertyError> { 26 | if (!Array.isArray(values)) { 27 | return Result.err(new ValidationError('s.tuple(T)', this.validatorOptions.message ?? 'Expected an array', values)); 28 | } 29 | 30 | if (values.length !== this.validators.length) { 31 | return Result.err( 32 | new ValidationError('s.tuple(T)', this.validatorOptions.message ?? `Expected an array of length ${this.validators.length}`, values) 33 | ); 34 | } 35 | 36 | if (!this.shouldRunConstraints) { 37 | return Result.ok(values as [...T]); 38 | } 39 | 40 | const errors: [number, BaseError][] = []; 41 | const transformed: T = [] as unknown as T; 42 | 43 | for (let i = 0; i < values.length; i++) { 44 | const result = this.validators[i].run(values[i]); 45 | if (result.isOk()) transformed.push(result.value); 46 | else errors.push([i, result.error!]); 47 | } 48 | 49 | return errors.length === 0 // 50 | ? Result.ok(transformed) 51 | : Result.err(new CombinedPropertyError(errors, this.validatorOptions)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/validators/TypedArrayValidator.ts: -------------------------------------------------------------------------------- 1 | import type { IConstraint } from '../constraints/base/IConstraint'; 2 | import { 3 | typedArrayByteLengthEqual, 4 | typedArrayByteLengthGreaterThan, 5 | typedArrayByteLengthGreaterThanOrEqual, 6 | typedArrayByteLengthLessThan, 7 | typedArrayByteLengthLessThanOrEqual, 8 | typedArrayByteLengthNotEqual, 9 | typedArrayByteLengthRange, 10 | typedArrayByteLengthRangeExclusive, 11 | typedArrayByteLengthRangeInclusive, 12 | typedArrayLengthEqual, 13 | typedArrayLengthGreaterThan, 14 | typedArrayLengthGreaterThanOrEqual, 15 | typedArrayLengthLessThan, 16 | typedArrayLengthLessThanOrEqual, 17 | typedArrayLengthNotEqual, 18 | typedArrayLengthRange, 19 | typedArrayLengthRangeExclusive, 20 | typedArrayLengthRangeInclusive 21 | } from '../constraints/TypedArrayLengthConstraints'; 22 | import { aOrAn } from '../constraints/util/common/vowels'; 23 | import { TypedArrays, type TypedArray, type TypedArrayName } from '../constraints/util/typedArray'; 24 | import { ValidationError } from '../lib/errors/ValidationError'; 25 | import { Result } from '../lib/Result'; 26 | import type { ValidatorOptions } from '../lib/util-types'; 27 | import { BaseValidator } from './imports'; 28 | 29 | export class TypedArrayValidator extends BaseValidator { 30 | private readonly type: TypedArrayName; 31 | 32 | public constructor(type: TypedArrayName, validatorOptions: ValidatorOptions = {}, constraints: readonly IConstraint[] = []) { 33 | super(validatorOptions, constraints); 34 | this.type = type; 35 | } 36 | 37 | public byteLengthLessThan(length: number, options: ValidatorOptions = this.validatorOptions) { 38 | return this.addConstraint(typedArrayByteLengthLessThan(length, options)); 39 | } 40 | 41 | public byteLengthLessThanOrEqual(length: number, options: ValidatorOptions = this.validatorOptions) { 42 | return this.addConstraint(typedArrayByteLengthLessThanOrEqual(length, options)); 43 | } 44 | 45 | public byteLengthGreaterThan(length: number, options: ValidatorOptions = this.validatorOptions) { 46 | return this.addConstraint(typedArrayByteLengthGreaterThan(length, options)); 47 | } 48 | 49 | public byteLengthGreaterThanOrEqual(length: number, options: ValidatorOptions = this.validatorOptions) { 50 | return this.addConstraint(typedArrayByteLengthGreaterThanOrEqual(length, options)); 51 | } 52 | 53 | public byteLengthEqual(length: number, options: ValidatorOptions = this.validatorOptions) { 54 | return this.addConstraint(typedArrayByteLengthEqual(length, options)); 55 | } 56 | 57 | public byteLengthNotEqual(length: number, options: ValidatorOptions = this.validatorOptions) { 58 | return this.addConstraint(typedArrayByteLengthNotEqual(length, options)); 59 | } 60 | 61 | public byteLengthRange(start: number, endBefore: number, options: ValidatorOptions = this.validatorOptions) { 62 | return this.addConstraint(typedArrayByteLengthRange(start, endBefore, options)); 63 | } 64 | 65 | public byteLengthRangeInclusive(startAt: number, endAt: number, options: ValidatorOptions = this.validatorOptions) { 66 | return this.addConstraint(typedArrayByteLengthRangeInclusive(startAt, endAt, options) as IConstraint); 67 | } 68 | 69 | public byteLengthRangeExclusive(startAfter: number, endBefore: number, options: ValidatorOptions = this.validatorOptions) { 70 | return this.addConstraint(typedArrayByteLengthRangeExclusive(startAfter, endBefore, options)); 71 | } 72 | 73 | public lengthLessThan(length: number, options: ValidatorOptions = this.validatorOptions) { 74 | return this.addConstraint(typedArrayLengthLessThan(length, options)); 75 | } 76 | 77 | public lengthLessThanOrEqual(length: number, options: ValidatorOptions = this.validatorOptions) { 78 | return this.addConstraint(typedArrayLengthLessThanOrEqual(length, options)); 79 | } 80 | 81 | public lengthGreaterThan(length: number, options: ValidatorOptions = this.validatorOptions) { 82 | return this.addConstraint(typedArrayLengthGreaterThan(length, options)); 83 | } 84 | 85 | public lengthGreaterThanOrEqual(length: number, options: ValidatorOptions = this.validatorOptions) { 86 | return this.addConstraint(typedArrayLengthGreaterThanOrEqual(length, options)); 87 | } 88 | 89 | public lengthEqual(length: number, options: ValidatorOptions = this.validatorOptions) { 90 | return this.addConstraint(typedArrayLengthEqual(length, options)); 91 | } 92 | 93 | public lengthNotEqual(length: number, options: ValidatorOptions = this.validatorOptions) { 94 | return this.addConstraint(typedArrayLengthNotEqual(length, options)); 95 | } 96 | 97 | public lengthRange(start: number, endBefore: number, options: ValidatorOptions = this.validatorOptions) { 98 | return this.addConstraint(typedArrayLengthRange(start, endBefore, options)); 99 | } 100 | 101 | public lengthRangeInclusive(startAt: number, endAt: number, options: ValidatorOptions = this.validatorOptions) { 102 | return this.addConstraint(typedArrayLengthRangeInclusive(startAt, endAt, options)); 103 | } 104 | 105 | public lengthRangeExclusive(startAfter: number, endBefore: number, options: ValidatorOptions = this.validatorOptions) { 106 | return this.addConstraint(typedArrayLengthRangeExclusive(startAfter, endBefore, options)); 107 | } 108 | 109 | protected override clone(): this { 110 | return Reflect.construct(this.constructor, [this.type, this.validatorOptions, this.constraints]); 111 | } 112 | 113 | protected handle(value: unknown): Result { 114 | return TypedArrays[this.type](value) 115 | ? Result.ok(value as T) 116 | : Result.err(new ValidationError('s.typedArray()', this.validatorOptions.message ?? `Expected ${aOrAn(this.type)}`, value)); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/validators/UnionValidator.ts: -------------------------------------------------------------------------------- 1 | import type { IConstraint } from '../constraints/base/IConstraint'; 2 | import type { BaseError } from '../lib/errors/BaseError'; 3 | import { CombinedError } from '../lib/errors/CombinedError'; 4 | import type { ValidationError } from '../lib/errors/ValidationError'; 5 | import { Result } from '../lib/Result'; 6 | import type { ValidatorOptions } from '../lib/util-types'; 7 | import { BaseValidator, LiteralValidator, NullishValidator } from './imports'; 8 | 9 | export class UnionValidator extends BaseValidator { 10 | private validators: readonly BaseValidator[]; 11 | 12 | public constructor(validators: readonly BaseValidator[], validatorOptions?: ValidatorOptions, constraints: readonly IConstraint[] = []) { 13 | super(validatorOptions, constraints); 14 | this.validators = validators; 15 | } 16 | 17 | public override optional(options: ValidatorOptions = this.validatorOptions): UnionValidator { 18 | if (this.validators.length === 0) 19 | return new UnionValidator([new LiteralValidator(undefined, options)], this.validatorOptions, this.constraints); 20 | 21 | const [validator] = this.validators; 22 | if (validator instanceof LiteralValidator) { 23 | // If already optional, return a clone: 24 | if (validator.expected === undefined) return this.clone(); 25 | 26 | // If it's nullable, convert the nullable validator into a nullish validator to optimize `null | undefined`: 27 | if (validator.expected === null) { 28 | return new UnionValidator( 29 | [new NullishValidator(options), ...this.validators.slice(1)], 30 | this.validatorOptions, 31 | this.constraints 32 | ) as UnionValidator; 33 | } 34 | } else if (validator instanceof NullishValidator) { 35 | // If it's already nullish (which validates optional), return a clone: 36 | return this.clone(); 37 | } 38 | 39 | return new UnionValidator([new LiteralValidator(undefined, options), ...this.validators], this.validatorOptions); 40 | } 41 | 42 | public required(options: ValidatorOptions = this.validatorOptions): UnionValidator> { 43 | type RequiredValidator = UnionValidator>; 44 | 45 | if (this.validators.length === 0) return this.clone() as unknown as RequiredValidator; 46 | 47 | const [validator] = this.validators; 48 | if (validator instanceof LiteralValidator) { 49 | if (validator.expected === undefined) { 50 | return new UnionValidator(this.validators.slice(1), this.validatorOptions, this.constraints) as RequiredValidator; 51 | } 52 | } else if (validator instanceof NullishValidator) { 53 | return new UnionValidator( 54 | [new LiteralValidator(null, options), ...this.validators.slice(1)], 55 | this.validatorOptions, 56 | this.constraints 57 | ) as RequiredValidator; 58 | } 59 | 60 | return this.clone() as unknown as RequiredValidator; 61 | } 62 | 63 | public override nullable(options: ValidatorOptions = this.validatorOptions): UnionValidator { 64 | if (this.validators.length === 0) { 65 | return new UnionValidator([new LiteralValidator(null, options)], this.validatorOptions, this.constraints); 66 | } 67 | 68 | const [validator] = this.validators; 69 | if (validator instanceof LiteralValidator) { 70 | // If already nullable, return a clone: 71 | if (validator.expected === null) return this.clone(); 72 | 73 | // If it's optional, convert the optional validator into a nullish validator to optimize `null | undefined`: 74 | if (validator.expected === undefined) { 75 | return new UnionValidator( 76 | [new NullishValidator(options), ...this.validators.slice(1)], 77 | this.validatorOptions, 78 | this.constraints 79 | ) as UnionValidator; 80 | } 81 | } else if (validator instanceof NullishValidator) { 82 | // If it's already nullish (which validates nullable), return a clone: 83 | return this.clone(); 84 | } 85 | 86 | return new UnionValidator([new LiteralValidator(null, options), ...this.validators], this.validatorOptions); 87 | } 88 | 89 | public override nullish(options: ValidatorOptions = this.validatorOptions): UnionValidator { 90 | if (this.validators.length === 0) { 91 | return new UnionValidator([new NullishValidator(options)], options, this.constraints); 92 | } 93 | 94 | const [validator] = this.validators; 95 | if (validator instanceof LiteralValidator) { 96 | // If already nullable or optional, promote the union to nullish: 97 | if (validator.expected === null || validator.expected === undefined) { 98 | return new UnionValidator( 99 | [new NullishValidator(options), ...this.validators.slice(1)], 100 | options, 101 | this.constraints 102 | ); 103 | } 104 | } else if (validator instanceof NullishValidator) { 105 | // If it's already nullish, return a clone: 106 | return this.clone(); 107 | } 108 | 109 | return new UnionValidator([new NullishValidator(options), ...this.validators], options); 110 | } 111 | 112 | public override or(...predicates: readonly BaseValidator[]): UnionValidator { 113 | return new UnionValidator([...this.validators, ...predicates], this.validatorOptions); 114 | } 115 | 116 | protected override clone(): this { 117 | return Reflect.construct(this.constructor, [this.validators, this.validatorOptions, this.constraints]); 118 | } 119 | 120 | protected handle(value: unknown): Result { 121 | const errors: BaseError[] = []; 122 | 123 | for (const validator of this.validators) { 124 | const result = validator.run(value); 125 | if (result.isOk()) return result as Result; 126 | errors.push(result.error!); 127 | } 128 | 129 | return Result.err(new CombinedError(errors, this.validatorOptions)); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/validators/imports.ts: -------------------------------------------------------------------------------- 1 | export * from './BaseValidator'; 2 | 3 | export * from './ArrayValidator'; 4 | export * from './BigIntValidator'; 5 | export * from './BooleanValidator'; 6 | export * from './DateValidator'; 7 | export * from './InstanceValidator'; 8 | export * from './LiteralValidator'; 9 | export * from './NeverValidator'; 10 | export * from './NullishValidator'; 11 | export * from './NumberValidator'; 12 | export * from './ObjectValidator'; 13 | export * from './PassthroughValidator'; 14 | export * from './RecordValidator'; 15 | export * from './SetValidator'; 16 | export * from './StringValidator'; 17 | export * from './TupleValidator'; 18 | export * from './UnionValidator'; 19 | export * from './MapValidator'; 20 | export * from './DefaultValidator'; 21 | -------------------------------------------------------------------------------- /src/validators/util/getValue.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/microsoft/TypeScript/issues/37663 2 | type Fn = (...args: unknown[]) => unknown; 3 | 4 | export function getValue : T>(valueOrFn: T): U { 5 | return typeof valueOrFn === 'function' ? valueOrFn() : valueOrFn; 6 | } 7 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "include": ["."], 4 | "compilerOptions": { 5 | "types": ["vitest/globals"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/unit/common/macros/comparators.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CombinedError, 3 | CombinedPropertyError, 4 | ExpectedConstraintError, 5 | ExpectedValidationError, 6 | MissingPropertyError, 7 | MultiplePossibilitiesConstraintError, 8 | UnknownPropertyError, 9 | ValidationError, 10 | type BaseError, 11 | type BaseValidator 12 | } from '../../../../src'; 13 | 14 | export function expectClonedValidator(expected: BaseValidator, received: BaseValidator) { 15 | expect(received).not.toBe(expected); 16 | expect(received).toBeInstanceOf(expected.constructor); 17 | } 18 | 19 | export function expectIdenticalValidator(expected: BaseValidator, actual: BaseValidator) { 20 | expectClonedValidator(expected, actual); 21 | expect(expected).toStrictEqual(actual); 22 | } 23 | 24 | export function expectModifiedClonedValidator(expected: BaseValidator, actual: BaseValidator) { 25 | expectClonedValidator(expected, actual); 26 | expect(expected).not.toStrictEqual(actual); 27 | } 28 | 29 | export function expectError(cb: () => T, expected: Error) { 30 | try { 31 | cb(); 32 | } catch (error) { 33 | expect(error).toBeDefined(); 34 | expectIdenticalError(error as BaseError, expected); 35 | return; 36 | } 37 | 38 | throw new Error('Expected to throw, but failed to do so'); 39 | } 40 | 41 | function expectIdenticalError(actual: Error, expected: Error) { 42 | expect(actual.constructor).toBe(expected.constructor); 43 | expect(actual.name).toBe(expected.name); 44 | expect(actual.message).toBe(expected.message); 45 | 46 | if (actual instanceof CombinedError) expectIdenticalCombinedError(actual, expected as CombinedError); 47 | if (actual instanceof CombinedPropertyError) expectIdenticalCombinedPropertyError(actual, expected as CombinedPropertyError); 48 | if (actual instanceof ExpectedValidationError) expectIdenticalExpectedValidationError(actual, expected as ExpectedValidationError); 49 | if (actual instanceof MissingPropertyError) expectIdenticalMissingPropertyError(actual, expected as MissingPropertyError); 50 | if (actual instanceof UnknownPropertyError) expectIdenticalUnknownPropertyError(actual, expected as UnknownPropertyError); 51 | if (actual instanceof ValidationError) expectIdenticalValidationError(actual, expected as ValidationError); 52 | if (actual instanceof ExpectedConstraintError) expectIdenticalExpectedConstraintError(actual, expected as ExpectedConstraintError); 53 | if (actual instanceof MultiplePossibilitiesConstraintError) { 54 | expectIdenticalMultiplePossibilitiesConstraintError(actual, expected as MultiplePossibilitiesConstraintError); 55 | } 56 | } 57 | 58 | function expectIdenticalCombinedError(actual: CombinedError, expected: CombinedError) { 59 | expect(actual.errors).toHaveLength(expected.errors.length); 60 | for (let i = 0; i < actual.errors.length; ++i) { 61 | expectIdenticalError(actual.errors[i], expected.errors[i]); 62 | } 63 | } 64 | 65 | function expectIdenticalCombinedPropertyError(actual: CombinedPropertyError, expected: CombinedPropertyError) { 66 | expect(actual.errors).toHaveLength(expected.errors.length); 67 | for (let i = 0; i < actual.errors.length; ++i) { 68 | expect(actual.errors[i][0]).toBe(expected.errors[i][0]); 69 | expectIdenticalError(actual.errors[i][1], expected.errors[i][1]); 70 | } 71 | } 72 | 73 | function expectIdenticalExpectedConstraintError(actual: ExpectedConstraintError, expected: ExpectedConstraintError) { 74 | expect(actual.constraint).toBe(expected.constraint); 75 | expect(actual.given).toStrictEqual(expected.given); 76 | expect(actual.expected).toBe(expected.expected); 77 | } 78 | 79 | function expectIdenticalMultiplePossibilitiesConstraintError( 80 | actual: MultiplePossibilitiesConstraintError, 81 | expected: MultiplePossibilitiesConstraintError 82 | ) { 83 | expect(actual.constraint).toBe(expected.constraint); 84 | expect(actual.given).toStrictEqual(expected.given); 85 | expect(actual.expected).toStrictEqual(expected.expected); 86 | } 87 | 88 | function expectIdenticalExpectedValidationError(actual: ExpectedValidationError, expected: ExpectedValidationError) { 89 | expect(actual.expected).toStrictEqual(expected.expected); 90 | } 91 | 92 | function expectIdenticalMissingPropertyError(actual: MissingPropertyError, expected: MissingPropertyError) { 93 | expect(actual.property).toBe(expected.property); 94 | } 95 | 96 | function expectIdenticalUnknownPropertyError(actual: UnknownPropertyError, expected: UnknownPropertyError) { 97 | expect(actual.property).toBe(expected.property); 98 | expect(actual.value).toStrictEqual(expected.value); 99 | } 100 | 101 | function expectIdenticalValidationError(actual: ValidationError, expected: ValidationError) { 102 | expect(actual.validator).toBe(expected.validator); 103 | expect(actual.given).toStrictEqual(expected.given); 104 | } 105 | -------------------------------------------------------------------------------- /tests/unit/lib/configs.test.ts: -------------------------------------------------------------------------------- 1 | import { s, setGlobalValidationEnabled, type BaseValidator } from '../../../src'; 2 | 3 | describe('Validation enabled and disabled configurations', () => { 4 | const stringPredicate = s.string().lengthGreaterThan(5); 5 | const arrayPredicate = s.array(s.string()).lengthGreaterThan(2); 6 | const mapPredicate = s.map(s.string(), s.number()); 7 | const objectPredicate = s.object({ 8 | owo: s.boolean() 9 | }); 10 | const recordPredicate = s.record(s.number()); 11 | const setPredicate = s.set(s.number()); 12 | const tuplePredicate = s.tuple([s.string(), s.number()]); 13 | 14 | const predicateAndValues: [string, BaseValidator, unknown][] = [ 15 | // 16 | ['string', stringPredicate, ''], 17 | ['array', arrayPredicate, []], 18 | ['map', mapPredicate, new Map([[0, '']])], 19 | ['object', objectPredicate, { owo: 'string' }], 20 | ['record', recordPredicate, { one: 'one' }], 21 | ['set', setPredicate, new Set(['1'])], 22 | ['tuple', tuplePredicate, [0, 'zero']] 23 | ]; 24 | 25 | describe('Global configurations', () => { 26 | beforeAll(() => { 27 | setGlobalValidationEnabled(false); 28 | }); 29 | 30 | afterAll(() => { 31 | setGlobalValidationEnabled(true); 32 | }); 33 | 34 | test.each(predicateAndValues)('GIVEN globally disabled %j predicate THEN returns the input', (_, inputPredicate, input) => { 35 | expect(inputPredicate.parse(input)).toStrictEqual(input); 36 | }); 37 | }); 38 | 39 | describe('Validator level configurations', () => { 40 | test.each(predicateAndValues)('GIVEN disabled %j predicate THEN returns the input', (_, inputPredicate, input) => { 41 | const predicate = inputPredicate.setValidationEnabled(false); 42 | 43 | expect(predicate.parse(input)).toStrictEqual(input); 44 | }); 45 | 46 | test.each(predicateAndValues)('GIVEN function to disable %j predicate THEN returns the input', (_, inputPredicate, input) => { 47 | const predicate = inputPredicate.setValidationEnabled(() => false); 48 | 49 | expect(predicate.parse(input)).toStrictEqual(input); 50 | }); 51 | 52 | test("GIVEN disabled predicate THEN checking if it's disabled should return true", () => { 53 | const predicate = s.string().setValidationEnabled(false); 54 | 55 | expect(predicate.getValidationEnabled()).toBe(false); 56 | }); 57 | }); 58 | 59 | describe('Globally disabled but locally enabled', () => { 60 | beforeAll(() => { 61 | setGlobalValidationEnabled(false); 62 | }); 63 | 64 | afterAll(() => { 65 | setGlobalValidationEnabled(true); 66 | }); 67 | 68 | test.each(predicateAndValues)( 69 | 'GIVEN enabled %j predicate while the global option is set to false THEN it should throw validation errors', 70 | (_, inputPredicate, input) => { 71 | const predicate = inputPredicate.setValidationEnabled(true); 72 | 73 | expect(() => predicate.parse(input)).toThrowError(); 74 | } 75 | ); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /tests/unit/lib/errors/BaseConstraintError.test.ts: -------------------------------------------------------------------------------- 1 | import { BaseConstraintError, type ConstraintErrorNames } from '../../../../src/lib/errors/BaseConstraintError'; 2 | 3 | describe('BaseConstraintError', () => { 4 | test('GIVEN constraint, message and value THEN converts to JSON', () => { 5 | const constraint: ConstraintErrorNames = 's.string().url()'; 6 | const message = 'Test message'; 7 | const given = ['test']; 8 | 9 | // @ts-expect-error abstract class 10 | const error = new BaseConstraintError(constraint, message, given); 11 | const json = error.toJSON(); 12 | 13 | expect(json).toEqual({ 14 | name: error.name, 15 | constraint: error.constraint, 16 | given: error.given, 17 | message: error.message 18 | }); 19 | }); 20 | 21 | test('GIVEN object formatted constraint, message, and value THEN converts to JSON', () => { 22 | const constraint: ConstraintErrorNames = 's.array(T).lengthEqual()'; 23 | const message = 'Different test message'; 24 | const given = { test: 'value' }; 25 | 26 | // @ts-expect-error abstract class 27 | const error = new BaseConstraintError(constraint, message, given); 28 | const json = error.toJSON(); 29 | 30 | expect(json).toEqual({ 31 | name: error.name, 32 | constraint: error.constraint, 33 | given: error.given, 34 | message: error.message 35 | }); 36 | }); 37 | 38 | test('GIVEN empty message and value THEN converts to JSON', () => { 39 | const constraint: ConstraintErrorNames = 's.boolean().false()'; 40 | const message = ''; 41 | const given = null; 42 | 43 | // @ts-expect-error abstract class 44 | const error = new BaseConstraintError(constraint, message, given); 45 | const json = error.toJSON(); 46 | 47 | expect(json).toEqual({ 48 | name: error.name, 49 | constraint: error.constraint, 50 | given: error.given, 51 | message: error.message 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/unit/lib/errors/BaseError.test.ts: -------------------------------------------------------------------------------- 1 | import { BaseError, customInspectSymbolStackLess } from '../../../../src/lib/errors/BaseError'; 2 | import type { InspectOptionsStylized } from 'node:util'; 3 | 4 | describe('BaseError', () => { 5 | test('GIVEN method call of toJson THEN converts to JSON correctly', () => { 6 | // @ts-expect-error abstract class 7 | const error = new BaseError(); 8 | const json = error.toJSON(); 9 | 10 | expect(json).toEqual({ 11 | name: error.name, 12 | message: error.message 13 | }); 14 | }); 15 | 16 | test('GIVEN thrown error when customInspectSymbolStackLess is called THEN rethrows error', () => { 17 | // @ts-expect-error abstract class 18 | const error = new BaseError(); 19 | const depth = 0; 20 | // @ts-expect-error dummy object 21 | const options: InspectOptionsStylized = {}; 22 | 23 | expect(() => error[customInspectSymbolStackLess](depth, options)).toThrow(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/unit/lib/errors/CombinedError.test.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'node:util'; 2 | import { CombinedError, ValidationError } from '../../../../src'; 3 | 4 | describe('CombinedError', () => { 5 | const error = new CombinedError([ 6 | new ValidationError('StringValidator', 'Expected a string primitive', 42), 7 | new ValidationError('StringValidator', 'Expected a string primitive', true) 8 | ]); 9 | 10 | test('GIVEN an instance THEN assigns fields correctly', () => { 11 | expect(error.message).toBe('Received one or more errors'); 12 | expect(error.errors).toHaveLength(2); 13 | expect(error.errors[0]).toBeInstanceOf(ValidationError); 14 | expect(error.errors[1]).toBeInstanceOf(ValidationError); 15 | }); 16 | 17 | describe('inspect', () => { 18 | test('GIVEN an inspected instance THEN formats data correctly', () => { 19 | const content = inspect(error, { colors: false }); 20 | const expected = [ 21 | 'CombinedError (2)', 22 | ' Received one or more errors', 23 | '', 24 | ' 1 ValidationError > StringValidator', 25 | ' | Expected a string primitive', 26 | ' | ', 27 | ' | Received:', 28 | ' | | 42', 29 | '', 30 | ' 2 ValidationError > StringValidator', 31 | ' | Expected a string primitive', 32 | ' | ', 33 | ' | Received:', 34 | ' | | true', 35 | '' 36 | ]; 37 | 38 | expect(content.startsWith(expected.join('\n'))).toBe(true); 39 | }); 40 | 41 | test('GIVEN an inspected instance with negative depth THEN formats name only', () => { 42 | const content = inspect(error, { colors: false, depth: -1 }); 43 | const expected = [ 44 | '[CombinedError]' // 45 | ]; 46 | 47 | expect(content.startsWith(expected.join('\n'))).toBe(true); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/unit/lib/errors/CombinedPropertyError.test.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'node:util'; 2 | import { CombinedPropertyError, ValidationError } from '../../../../src'; 3 | 4 | describe('CombinedError', () => { 5 | const error = new CombinedPropertyError([ 6 | ['foo', new ValidationError('StringValidator', 'Expected a string primitive', 42)], 7 | [2, new ValidationError('StringValidator', 'Expected a string primitive', true)], 8 | [Symbol('hello.there'), new ValidationError('StringValidator', 'Expected a string primitive', 75n)] 9 | ]); 10 | 11 | test('GIVEN an instance THEN assigns fields correctly', () => { 12 | expect(error.message).toBe('Received one or more errors'); 13 | expect(error.errors).toHaveLength(3); 14 | expect(error.errors[0][1]).toBeInstanceOf(ValidationError); 15 | expect(error.errors[1][1]).toBeInstanceOf(ValidationError); 16 | expect(error.errors[2][1]).toBeInstanceOf(ValidationError); 17 | }); 18 | 19 | describe('inspect', () => { 20 | test('GIVEN an inspected instance THEN formats data correctly', () => { 21 | const content = inspect(error, { colors: false }); 22 | const expected = [ 23 | 'CombinedPropertyError (3)', 24 | ' Received one or more errors', 25 | '', 26 | ' input.foo', 27 | ' | ValidationError > StringValidator', 28 | ' | Expected a string primitive', 29 | ' | ', 30 | ' | Received:', 31 | ' | | 42', 32 | '', 33 | ' input[2]', 34 | ' | ValidationError > StringValidator', 35 | ' | Expected a string primitive', 36 | ' | ', 37 | ' | Received:', 38 | ' | | true', 39 | '', 40 | ' input[Symbol(hello.there)]', 41 | ' | ValidationError > StringValidator', 42 | ' | Expected a string primitive', 43 | ' | ', 44 | ' | Received:', 45 | ' | | 75n', 46 | '' 47 | ]; 48 | 49 | expect(content.startsWith(expected.join('\n'))).toBe(true); 50 | }); 51 | 52 | test('GIVEN an inspected instance with negative depth THEN formats name only', () => { 53 | const content = inspect(error, { colors: false, depth: -1 }); 54 | const expected = [ 55 | '[CombinedPropertyError]' // 56 | ]; 57 | 58 | expect(content.startsWith(expected.join('\n'))).toBe(true); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/unit/lib/errors/ExpectedConstraintError.test.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'node:util'; 2 | import { ExpectedConstraintError } from '../../../../src/lib/errors/ExpectedConstraintError'; 3 | 4 | describe('ExpectedConstraintError', () => { 5 | const error = new ExpectedConstraintError('s.number().int()', 'Given value is not an integer', 42.1, 'Number.isInteger(expected) to be true'); 6 | 7 | test('GIVEN an instance THEN assigns fields correctly', () => { 8 | expect(error.message).toBe('Given value is not an integer'); 9 | expect(error.constraint).toBe('s.number().int()'); 10 | expect(error.given).toBe(42.1); 11 | expect(error.expected).toBe('Number.isInteger(expected) to be true'); 12 | }); 13 | 14 | describe('inspect', () => { 15 | test('GIVEN an inspected instance THEN formats data correctly', () => { 16 | const content = inspect(error, { colors: false }); 17 | const expected = [ 18 | 'ExpectedConstraintError > s.number().int()', // 19 | ' Given value is not an integer', 20 | '', 21 | ' Expected: Number.isInteger(expected) to be true', 22 | '', 23 | ' Received:', 24 | ' | 42.1', 25 | '' 26 | ]; 27 | 28 | expect(content).toEqual(expect.stringContaining(expected.join('\n'))); 29 | }); 30 | 31 | test('GIVEN an inspected instance with negative depth THEN formats name only', () => { 32 | const content = inspect(error, { colors: false, depth: -1 }); 33 | const expected = [ 34 | '[ExpectedConstraintError: s.number().int()]' // 35 | ]; 36 | 37 | expect(content.startsWith(expected.join('\n'))).toBe(true); 38 | }); 39 | }); 40 | 41 | describe('toJSON', () => { 42 | test('toJSON should return an object with name, message, constraint, given and expected', () => { 43 | expect(error.toJSON()).toStrictEqual({ 44 | name: 'Error', 45 | message: 'Given value is not an integer', 46 | constraint: 's.number().int()', 47 | given: 42.1, 48 | expected: 'Number.isInteger(expected) to be true' 49 | }); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/unit/lib/errors/ExpectedValidationError.test.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'node:util'; 2 | import { ExpectedValidationError } from '../../../../src'; 3 | 4 | describe('ExpectedValidationError', () => { 5 | const error = new ExpectedValidationError('LiteralValidator', 'Expected values to be equals', 'world', 'hello'); 6 | 7 | test('GIVEN an instance THEN assigns fields correctly', () => { 8 | expect(error.message).toBe('Expected values to be equals'); 9 | expect(error.validator).toBe('LiteralValidator'); 10 | expect(error.given).toBe('world'); 11 | expect(error.expected).toBe('hello'); 12 | }); 13 | 14 | describe('inspect', () => { 15 | test('GIVEN an inspected instance THEN formats data correctly', () => { 16 | const content = inspect(error, { colors: false }); 17 | const expected = [ 18 | 'ExpectedValidationError > LiteralValidator', // 19 | ' Expected values to be equals', 20 | '', 21 | ' Expected:', 22 | " | 'hello'", 23 | '', 24 | ' Received:', 25 | " | 'world'", 26 | '' 27 | ]; 28 | 29 | expect(content.startsWith(expected.join('\n'))).toBe(true); 30 | }); 31 | 32 | test('GIVEN an inspected instance with negative depth THEN formats name only', () => { 33 | const content = inspect(error, { colors: false, depth: -1 }); 34 | const expected = [ 35 | '[ExpectedValidationError: LiteralValidator]' // 36 | ]; 37 | 38 | expect(content.startsWith(expected.join('\n'))).toBe(true); 39 | }); 40 | }); 41 | 42 | describe('toJSON', () => { 43 | test('toJSON should return an object with name, message, validator, given and expected', () => { 44 | expect(error.toJSON()).toStrictEqual({ 45 | name: 'Error', 46 | message: 'Expected values to be equals', 47 | validator: 'LiteralValidator', 48 | given: 'world', 49 | expected: 'hello' 50 | }); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/unit/lib/errors/MissingPropertyError.test.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'node:util'; 2 | import { MissingPropertyError } from '../../../../src'; 3 | 4 | describe('MissingPropertyError', () => { 5 | const error = new MissingPropertyError('foo'); 6 | 7 | test('GIVEN an instance THEN assigns fields correctly', () => { 8 | expect(error.message).toBe('A required property is missing'); 9 | expect(error.property).toBe('foo'); 10 | }); 11 | 12 | describe('inspect', () => { 13 | test('GIVEN an inspected instance THEN formats data correctly', () => { 14 | const content = inspect(error, { colors: false }); 15 | const expected = [ 16 | 'MissingPropertyError > foo', // 17 | ' A required property is missing', 18 | '' 19 | ]; 20 | 21 | expect(content.startsWith(expected.join('\n'))).toBe(true); 22 | }); 23 | 24 | test('GIVEN an inspected instance with negative depth THEN formats name only', () => { 25 | const content = inspect(error, { colors: false, depth: -1 }); 26 | const expected = [ 27 | '[MissingPropertyError: foo]' // 28 | ]; 29 | 30 | expect(content.startsWith(expected.join('\n'))).toBe(true); 31 | }); 32 | }); 33 | 34 | describe('toJSON', () => { 35 | test('toJSON should return an object with name, message, and property', () => { 36 | expect(error.toJSON()).toEqual({ 37 | name: 'Error', 38 | message: 'A required property is missing', 39 | property: 'foo' 40 | }); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/unit/lib/errors/MultiplePossibilitiesConstraintError.test.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'node:util'; 2 | import { MultiplePossibilitiesConstraintError } from '../../../../src'; 3 | 4 | describe('MultiplePossibilitiesConstraintError', () => { 5 | const error = new MultiplePossibilitiesConstraintError('s.string().url()', 'Invalid URL domain', 'https://example.org', [ 6 | 'discord.js.org', 7 | 'sapphirejs.dev' 8 | ]); 9 | 10 | test('GIVEN an instance THEN assigns fields correctly', () => { 11 | expect(error.message).toBe('Invalid URL domain'); 12 | expect(error.constraint).toBe('s.string().url()'); 13 | expect(error.given).toBe('https://example.org'); 14 | expect(error.expected).toStrictEqual(['discord.js.org', 'sapphirejs.dev']); 15 | }); 16 | 17 | describe('inspect', () => { 18 | test('GIVEN an inspected instance THEN formats data correctly', () => { 19 | const content = inspect(error, { colors: false }); 20 | const expected = [ 21 | 'MultiplePossibilitiesConstraintError > s.string().url()', 22 | ' Invalid URL domain', 23 | '', 24 | ' Expected any of the following:', 25 | ' | - discord.js.org', 26 | ' | - sapphirejs.dev', 27 | '', 28 | ' Received:', 29 | " | 'https://example.org'", 30 | '' 31 | ]; 32 | 33 | expect(content).toEqual(expect.stringContaining(expected.join('\n'))); 34 | }); 35 | 36 | test('GIVEN an inspected instance with negative depth THEN formats name only', () => { 37 | const content = inspect(error, { colors: false, depth: -1 }); 38 | const expected = [ 39 | '[MultiplePossibilitiesConstraintError: s.string().url()]' // 40 | ]; 41 | 42 | expect(content.startsWith(expected.join('\n'))).toBe(true); 43 | }); 44 | }); 45 | 46 | describe('toJSON', () => { 47 | test('toJSON should return an object with name, message, constraint, given and expected', () => { 48 | expect(error.toJSON()).toStrictEqual({ 49 | name: 'Error', 50 | message: 'Invalid URL domain', 51 | constraint: 's.string().url()', 52 | given: 'https://example.org', 53 | expected: ['discord.js.org', 'sapphirejs.dev'] 54 | }); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/unit/lib/errors/UnknownEnumValueError.test.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'node:util'; 2 | import { UnknownEnumValueError } from '../../../../src'; 3 | 4 | describe('UnknownEnumValueError', () => { 5 | const error = new UnknownEnumValueError( 6 | 'foo', 7 | ['bar', 'baz'], 8 | new Map([ 9 | ['bar', 1], 10 | ['baz', 'boo'] 11 | ]) 12 | ); 13 | 14 | test('GIVEN an instance THEN assigns fields correctly', () => { 15 | expect(error.message).toBe('Expected the value to be one of the following enum values:'); 16 | expect(error.value).toBe('foo'); 17 | expect(error.enumKeys).toEqual(['bar', 'baz']); 18 | expect(error.enumMappings).toStrictEqual( 19 | new Map([ 20 | ['bar', 1], 21 | ['baz', 'boo'] 22 | ]) 23 | ); 24 | }); 25 | 26 | describe('inspect', () => { 27 | test('GIVEN an inspected instance THEN formats data correctly', () => { 28 | const content = inspect(error, { colors: false }); 29 | const expected = [ 30 | 'UnknownEnumValueError > foo', // 31 | ' Expected the value to be one of the following enum values:', 32 | '', 33 | ' | bar or 1', 34 | ' | baz or boo' 35 | ]; 36 | 37 | expect(content.startsWith(expected.join('\n'))).toBe(true); 38 | }); 39 | 40 | test('GIVEN an inspected instance with negative depth THEN formats name only', () => { 41 | const content = inspect(error, { colors: false, depth: -1 }); 42 | const expected = [ 43 | '[UnknownEnumValueError: foo]' // 44 | ]; 45 | 46 | expect(content.startsWith(expected.join('\n'))).toBe(true); 47 | }); 48 | }); 49 | 50 | describe('toJSON', () => { 51 | test('toJSON should return an object with name, message, value, enumKeys, and enumMappings', () => { 52 | expect(error.toJSON()).toEqual({ 53 | name: 'Error', 54 | message: 'Expected the value to be one of the following enum values:', 55 | value: 'foo', 56 | enumKeys: ['bar', 'baz'], 57 | enumMappings: [ 58 | ['bar', 1], 59 | ['baz', 'boo'] 60 | ] 61 | }); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/unit/lib/errors/UnknownPropertyError.test.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'node:util'; 2 | import { UnknownPropertyError } from '../../../../src'; 3 | 4 | describe('UnknownPropertyError', () => { 5 | const error = new UnknownPropertyError('foo', 42); 6 | 7 | test('GIVEN an instance THEN assigns fields correctly', () => { 8 | expect(error.message).toBe('Received unexpected property'); 9 | expect(error.property).toBe('foo'); 10 | expect(error.value).toBe(42); 11 | }); 12 | 13 | describe('inspect', () => { 14 | test('GIVEN an inspected instance THEN formats data correctly', () => { 15 | const content = inspect(error, { colors: false }); 16 | const expected = [ 17 | 'UnknownPropertyError > foo', // 18 | ' Received unexpected property', 19 | '', 20 | ' Received:', 21 | ' | 42', 22 | '' 23 | ]; 24 | 25 | expect(content.startsWith(expected.join('\n'))).toBe(true); 26 | }); 27 | 28 | test('GIVEN an inspected instance with negative depth THEN formats name only', () => { 29 | const content = inspect(error, { colors: false, depth: -1 }); 30 | const expected = [ 31 | '[UnknownPropertyError: foo]' // 32 | ]; 33 | 34 | expect(content.startsWith(expected.join('\n'))).toBe(true); 35 | }); 36 | }); 37 | 38 | describe('toJSON', () => { 39 | test('toJSON should return an object with name, message, property and value', () => { 40 | expect(error.toJSON()).toStrictEqual({ 41 | name: 'Error', 42 | message: 'Received unexpected property', 43 | property: 'foo', 44 | value: 42 45 | }); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/unit/lib/errors/ValidationError.test.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'node:util'; 2 | import { ValidationError } from '../../../../src'; 3 | 4 | describe('ValidationError', () => { 5 | const error = new ValidationError('StringValidator', 'Expected a string primitive', 42); 6 | 7 | test('GIVEN an instance THEN assigns fields correctly', () => { 8 | expect(error.message).toBe('Expected a string primitive'); 9 | expect(error.given).toBe(42); 10 | expect(error.validator).toBe('StringValidator'); 11 | }); 12 | 13 | describe('inspect', () => { 14 | test('GIVEN an inspected instance THEN formats data correctly', () => { 15 | const content = inspect(error, { colors: false }); 16 | const expected = [ 17 | 'ValidationError > StringValidator', // 18 | ' Expected a string primitive', 19 | '', 20 | ' Received:', 21 | ' | 42', 22 | '' 23 | ]; 24 | 25 | expect(content.startsWith(expected.join('\n'))).toBe(true); 26 | }); 27 | 28 | test('GIVEN an inspected instance with negative depth THEN formats name only', () => { 29 | const content = inspect(error, { colors: false, depth: -1 }); 30 | const expected = [ 31 | '[ValidationError: StringValidator]' // 32 | ]; 33 | 34 | expect(content.startsWith(expected.join('\n'))).toBe(true); 35 | }); 36 | }); 37 | 38 | describe('toJSON', () => { 39 | test('toJSON should return an object with name, message, validator and given', () => { 40 | expect(error.toJSON()).toStrictEqual({ 41 | name: 'Error', 42 | message: 'Unknown validation error occurred.', 43 | validator: 'StringValidator', 44 | given: 42 45 | }); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/unit/util/common/combinedResultFn.test.ts: -------------------------------------------------------------------------------- 1 | import { combinedErrorFn } from '../../../../src/constraints/util/common/combinedResultFn'; 2 | 3 | describe('combinedErrorFn', () => { 4 | describe('0 functions', () => { 5 | test('GIVEN no functions THEN always returns null', () => { 6 | const fn = combinedErrorFn(); 7 | expect(fn()).toBe(null); 8 | }); 9 | }); 10 | 11 | describe('1 function', () => { 12 | test('GIVEN one function returning null THEN returns null', () => { 13 | const cb = vi.fn().mockReturnValue(null); 14 | const fn = combinedErrorFn(cb); 15 | 16 | expect(fn('foo', 'bar')).toBe(null); 17 | 18 | expect(cb).toBeCalledTimes(1); 19 | expect(cb).toHaveBeenCalledWith('foo', 'bar'); 20 | }); 21 | 22 | test('GIVEN one function returning error THEN returns the same error', () => { 23 | const error = new Error('my precious'); 24 | const cb = vi.fn().mockReturnValue(error); 25 | const fn = combinedErrorFn(cb); 26 | 27 | expect(fn('foo', 'bar')).toBe(error); 28 | 29 | expect(cb).toBeCalledTimes(1); 30 | expect(cb).toHaveBeenCalledWith('foo', 'bar'); 31 | }); 32 | }); 33 | 34 | describe('2 functions', () => { 35 | test('GIVEN (null, null) THEN returns null', () => { 36 | const cb0 = vi.fn().mockReturnValue(null); 37 | const cb1 = vi.fn().mockReturnValue(null); 38 | const fn = combinedErrorFn(cb0, cb1); 39 | 40 | expect(fn('foo', 'bar')).toBe(null); 41 | 42 | expect(cb0).toBeCalledTimes(1); 43 | expect(cb0).toHaveBeenCalledWith('foo', 'bar'); 44 | 45 | expect(cb1).toBeCalledTimes(1); 46 | expect(cb1).toHaveBeenCalledWith('foo', 'bar'); 47 | }); 48 | 49 | test('GIVEN (null, error) THEN returns error', () => { 50 | const error = new Error('not all those who wander are lost'); 51 | const cb0 = vi.fn().mockReturnValue(null); 52 | const cb1 = vi.fn().mockReturnValue(error); 53 | const fn = combinedErrorFn(cb0, cb1); 54 | 55 | expect(fn('foo', 'bar')).toBe(error); 56 | 57 | expect(cb0).toBeCalledTimes(1); 58 | expect(cb0).toHaveBeenCalledWith('foo', 'bar'); 59 | 60 | expect(cb1).toBeCalledTimes(1); 61 | expect(cb1).toHaveBeenCalledWith('foo', 'bar'); 62 | }); 63 | 64 | test('GIVEN (error, null) THEN returns error', () => { 65 | const error = new Error('it is a dangerous business'); 66 | const cb0 = vi.fn().mockReturnValue(error); 67 | const cb1 = vi.fn().mockReturnValue(null); 68 | const fn = combinedErrorFn(cb0, cb1); 69 | 70 | expect(fn('foo', 'bar')).toBe(error); 71 | 72 | expect(cb0).toBeCalledTimes(1); 73 | expect(cb0).toHaveBeenCalledWith('foo', 'bar'); 74 | 75 | expect(cb1).not.toHaveBeenCalled(); 76 | }); 77 | }); 78 | 79 | describe('3 functions', () => { 80 | test('GIVEN (null, null, null) THEN returns null', () => { 81 | const cb0 = vi.fn().mockReturnValue(null); 82 | const cb1 = vi.fn().mockReturnValue(null); 83 | const cb2 = vi.fn().mockReturnValue(null); 84 | const fn = combinedErrorFn(cb0, cb1, cb2); 85 | 86 | expect(fn('foo', 'bar')).toBe(null); 87 | 88 | expect(cb0).toBeCalledTimes(1); 89 | expect(cb0).toHaveBeenCalledWith('foo', 'bar'); 90 | 91 | expect(cb1).toBeCalledTimes(1); 92 | expect(cb1).toHaveBeenCalledWith('foo', 'bar'); 93 | 94 | expect(cb2).toBeCalledTimes(1); 95 | expect(cb2).toHaveBeenCalledWith('foo', 'bar'); 96 | }); 97 | 98 | test('GIVEN (null, error, null) THEN returns error', () => { 99 | const error = new Error('go where you must go, and hope!'); 100 | const cb0 = vi.fn().mockReturnValue(null); 101 | const cb1 = vi.fn().mockReturnValue(error); 102 | const cb2 = vi.fn().mockReturnValue(null); 103 | const fn = combinedErrorFn(cb0, cb1, cb2); 104 | 105 | expect(fn('foo', 'bar')).toBe(error); 106 | 107 | expect(cb0).toBeCalledTimes(1); 108 | expect(cb0).toHaveBeenCalledWith('foo', 'bar'); 109 | 110 | expect(cb1).toBeCalledTimes(1); 111 | expect(cb1).toHaveBeenCalledWith('foo', 'bar'); 112 | 113 | expect(cb2).not.toHaveBeenCalled(); 114 | }); 115 | 116 | test('GIVEN (error, null, null) THEN returns error', () => { 117 | const error = new Error('all is well that ends better'); 118 | const cb0 = vi.fn().mockReturnValue(error); 119 | const cb1 = vi.fn().mockReturnValue(null); 120 | const cb2 = vi.fn().mockReturnValue(null); 121 | const fn = combinedErrorFn(cb0, cb1, cb2); 122 | 123 | expect(fn('foo', 'bar')).toBe(error); 124 | 125 | expect(cb0).toBeCalledTimes(1); 126 | expect(cb0).toHaveBeenCalledWith('foo', 'bar'); 127 | 128 | expect(cb1).not.toHaveBeenCalled(); 129 | expect(cb2).not.toHaveBeenCalled(); 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /tests/unit/validators/boolean.test.ts: -------------------------------------------------------------------------------- 1 | import { ExpectedConstraintError, s, ValidationError } from '../../../src'; 2 | import { expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('BooleanValidator (%s)', (message) => { 5 | const predicate = s.boolean({ message }); 6 | 7 | const invalidBooleanErrorMessage = message ?? 'Invalid boolean value'; 8 | 9 | test('GIVEN a boolean THEN returns the given value', () => { 10 | expect(predicate.parse(true)).toBe(true); 11 | }); 12 | 13 | test('GIVEN a non-boolean THEN throws ValidationError', () => { 14 | const errorMessage = message ?? 'Expected a boolean primitive'; 15 | expectError(() => predicate.parse('Hello there'), new ValidationError('s.boolean()', errorMessage, 'Hello there')); 16 | }); 17 | 18 | describe('Comparators', () => { 19 | // equal, notEqual 20 | describe('equal', () => { 21 | const eqPredicate = s.boolean().equal(true, { message }); 22 | 23 | test('GIVEN true THEN returns given value', () => { 24 | expect(eqPredicate.parse(true)).toBe(true); 25 | }); 26 | 27 | test('GIVEN false THEN throws ConstraintError', () => { 28 | expectError( 29 | () => eqPredicate.parse(false), 30 | new ExpectedConstraintError('s.boolean().true()', invalidBooleanErrorMessage, false, 'true') 31 | ); 32 | }); 33 | }); 34 | 35 | describe('notEqual', () => { 36 | const nePredicate = s.boolean().notEqual(true, { message }); 37 | 38 | test('GIVEN false THEN returns given value', () => { 39 | expect(nePredicate.parse(false)).toBe(false); 40 | }); 41 | 42 | test('GIVEN true THEN throws ConstraintError', () => { 43 | expectError( 44 | () => nePredicate.parse(true), 45 | new ExpectedConstraintError('s.boolean().false()', invalidBooleanErrorMessage, true, 'false') 46 | ); 47 | }); 48 | }); 49 | }); 50 | 51 | describe('Constraints', () => { 52 | describe('true', () => { 53 | const truePredicate = s.boolean().true({ message }); 54 | 55 | test('GIVEN true THEN returns given value', () => { 56 | expect(truePredicate.parse(true)).toBe(true); 57 | }); 58 | 59 | test('GIVEN false THEN throws ConstraintError', () => { 60 | expectError( 61 | () => truePredicate.parse(false), 62 | new ExpectedConstraintError('s.boolean().true()', invalidBooleanErrorMessage, false, 'true') 63 | ); 64 | }); 65 | }); 66 | 67 | describe('false', () => { 68 | const falsePredicate = s.boolean().false({ message }); 69 | 70 | test('GIVEN false THEN returns given value', () => { 71 | expect(falsePredicate.parse(false)).toBe(false); 72 | }); 73 | 74 | test('GIVEN true THEN throws ConstraintError', () => { 75 | expectError( 76 | () => falsePredicate.parse(true), 77 | new ExpectedConstraintError('s.boolean().false()', invalidBooleanErrorMessage, true, 'false') 78 | ); 79 | }); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /tests/unit/validators/date.test.ts: -------------------------------------------------------------------------------- 1 | import { ExpectedConstraintError, s, ValidationError } from '../../../src'; 2 | import { expectClonedValidator, expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('DateValidator (%s)', (message) => { 5 | const predicate = s.date({ message }); 6 | 7 | const invalidDateErrorMessage = message ?? 'Invalid Date value'; 8 | 9 | test('GIVEN a date THEN returns the given value', () => { 10 | const date = new Date(); 11 | expect(predicate.parse(date)).toBe(date); 12 | }); 13 | 14 | test.each(['abc', '', null, undefined])('GIVEN a non-date (%j) THEN throws ValidationError', (input) => { 15 | const errorMessage = message ?? 'Expected a Date'; 16 | expectError(() => predicate.parse(input), new ValidationError('s.date()', errorMessage, input)); 17 | }); 18 | 19 | describe('Comparator', () => { 20 | const date = new Date('2022-02-01'); 21 | const datesInFuture = [new Date('2022-03-01'), new Date('2023-01-01')]; 22 | const datesInPast = [new Date('2022-01-01'), new Date('2020-01-01')]; 23 | 24 | describe('lessThan', () => { 25 | const ltPredicate = s.date().lessThan(date, { message }); 26 | 27 | test.each(datesInPast)('GIVEN %j THEN returns given value', (value) => { 28 | expect(ltPredicate.parse(value)).toBe(value); 29 | }); 30 | 31 | test.each(datesInFuture)('GIVEN %j THEN throws ConstraintError', (value) => { 32 | expectError( 33 | () => ltPredicate.parse(value), 34 | new ExpectedConstraintError('s.date().lessThan()', invalidDateErrorMessage, value, 'expected < 2022-02-01T00:00:00.000Z') 35 | ); 36 | }); 37 | }); 38 | 39 | describe('lessThanOrEqual', () => { 40 | const lePredicate = s.date().lessThanOrEqual(date, { message }); 41 | 42 | test.each([...datesInPast, date])('GIVEN %j THEN returns given value', (value) => { 43 | expect(lePredicate.parse(value)).toBe(value); 44 | }); 45 | 46 | test.each(datesInFuture)('GIVEN %j THEN throws ConstraintError', (value) => { 47 | expectError( 48 | () => lePredicate.parse(value), 49 | new ExpectedConstraintError('s.date().lessThanOrEqual()', invalidDateErrorMessage, value, 'expected <= 2022-02-01T00:00:00.000Z') 50 | ); 51 | }); 52 | }); 53 | 54 | describe('greaterThan', () => { 55 | const gtPredicate = s.date().greaterThan(date, { message }); 56 | 57 | test.each(datesInFuture)('GIVEN %j THEN returns given value', (value) => { 58 | expect(gtPredicate.parse(value)).toBe(value); 59 | }); 60 | 61 | test.each(datesInPast)('GIVEN %j THEN throws ConstraintError', (value) => { 62 | expectError( 63 | () => gtPredicate.parse(value), 64 | new ExpectedConstraintError('s.date().greaterThan()', invalidDateErrorMessage, value, 'expected > 2022-02-01T00:00:00.000Z') 65 | ); 66 | }); 67 | }); 68 | 69 | describe('greaterThanOrEqual', () => { 70 | const gePredicate = s.date().greaterThanOrEqual(date, { message }); 71 | 72 | test.each([date, ...datesInFuture])('GIVEN %j THEN returns given value', (value) => { 73 | expect(gePredicate.parse(value)).toBe(value); 74 | }); 75 | 76 | test.each(datesInPast)('GIVEN %j THEN throws ConstraintError', (value) => { 77 | expectError( 78 | () => gePredicate.parse(value), 79 | new ExpectedConstraintError( 80 | 's.date().greaterThanOrEqual()', 81 | invalidDateErrorMessage, 82 | value, 83 | 'expected >= 2022-02-01T00:00:00.000Z' 84 | ) 85 | ); 86 | }); 87 | }); 88 | 89 | describe('equal', () => { 90 | const eqPredicate = s.date().equal(date, { message }); 91 | 92 | test('GIVEN date THEN returns given value', () => { 93 | expect(eqPredicate.parse(date)).toBe(date); 94 | }); 95 | 96 | test.each([...datesInPast, ...datesInFuture])('GIVEN %j THEN throws ConstraintError', (value) => { 97 | expectError( 98 | () => eqPredicate.parse(value), 99 | new ExpectedConstraintError('s.date().equal()', invalidDateErrorMessage, value, 'expected === 2022-02-01T00:00:00.000Z') 100 | ); 101 | }); 102 | 103 | describe('equal > NaN', () => { 104 | test.each(['not-a-date', NaN])('GIVEN %j THEN returns s.date().invalid', (value) => { 105 | expectClonedValidator(s.date().equal(value), s.date().invalid()); 106 | }); 107 | }); 108 | }); 109 | 110 | describe('notEqual', () => { 111 | const nePredicate = s.date().notEqual(date, { message }); 112 | 113 | test.each([...datesInPast, ...datesInFuture])('GIVEN %j THEN returns given value', (value) => { 114 | expect(nePredicate.parse(value)).toBe(value); 115 | }); 116 | 117 | test('GIVEN date THEN throws ConstraintError', () => { 118 | expectError( 119 | () => nePredicate.parse(date), 120 | new ExpectedConstraintError('s.date().notEqual()', invalidDateErrorMessage, date, 'expected !== 2022-02-01T00:00:00.000Z') 121 | ); 122 | }); 123 | 124 | describe('notEqual > NaN', () => { 125 | test.each(['not-a-date', NaN])('GIVEN %j THEN returns s.date().invalid', (value) => { 126 | expectClonedValidator(s.date().notEqual(value), s.date().invalid()); 127 | }); 128 | }); 129 | }); 130 | }); 131 | 132 | describe('valid', () => { 133 | const validPredicate = s.date().valid({ message }); 134 | 135 | test.each(['2022-03-13T11:19:13.698Z', 1647170353698])('GIVEN a valid date (%j) THEN returns the given value', (value) => { 136 | const date = new Date(value); 137 | expect(validPredicate.parse(date)).toBe(date); 138 | }); 139 | 140 | test.each([NaN, Infinity, -Infinity])('GIVEN an invalid date (%j) THEN throws ValidationError', (value) => { 141 | const date = new Date(value); 142 | 143 | expectError( 144 | () => validPredicate.parse(date), 145 | new ExpectedConstraintError('s.date().valid()', invalidDateErrorMessage, date, 'expected !== NaN') 146 | ); 147 | }); 148 | }); 149 | 150 | describe('invalid', () => { 151 | const invalidPredicate = s.date().invalid({ message }); 152 | 153 | test.each([NaN, Infinity, -Infinity])('GIVEN an invalid date (%j) THEN returns the given value', (value) => { 154 | const date = new Date(value); 155 | expect(invalidPredicate.parse(date)).toBe(date); 156 | }); 157 | 158 | test.each(['2022-03-13T11:19:13.698Z', 1647170353698])('GIVEN a valid date (%j) THEN throws ValidationError', (value) => { 159 | const date = new Date(value); 160 | expectError( 161 | () => invalidPredicate.parse(date), 162 | new ExpectedConstraintError('s.date().invalid()', invalidDateErrorMessage, date, 'expected === NaN') 163 | ); 164 | }); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /tests/unit/validators/enum.test.ts: -------------------------------------------------------------------------------- 1 | import { CombinedError, ExpectedValidationError, s } from '../../../src'; 2 | import { expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('EnumValidator', (message) => { 5 | const predicate = s.enum(['a', 'b', 'c'], { message }); 6 | 7 | test.each(['a', 'b', 'c'])('GIVEN a string (%j) THEN returns a string', (input) => { 8 | expect(predicate.parse(input)).toBe(input); 9 | }); 10 | 11 | test.each(['d', 'e', 'f', 1, null, true])('GIVEN a invalid value (%j) THEN throws CombinedError', (input) => { 12 | const errorMessage = message ?? 'Expected values to be equals'; 13 | expectError( 14 | () => predicate.parse(input), 15 | new CombinedError( 16 | [ 17 | new ExpectedValidationError('s.literal(V)', errorMessage, input, 'a'), 18 | new ExpectedValidationError('s.literal(V)', errorMessage, input, 'b'), 19 | new ExpectedValidationError('s.literal(V)', errorMessage, input, 'c') 20 | ], 21 | { 22 | message: message ?? 'Received one or more errors' 23 | } 24 | ) 25 | ); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/unit/validators/instance.test.ts: -------------------------------------------------------------------------------- 1 | import { ExpectedValidationError, s } from '../../../src'; 2 | import { expectClonedValidator, expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('InstanceValidator (%s)', (message) => { 5 | class User { 6 | public constructor(public name: string) {} 7 | } 8 | const predicate = s.instance(User, { message }); 9 | 10 | test('GIVEN an instance of User THEN returns the given value', () => { 11 | expect(predicate.parse(new User('Sapphire'))).toStrictEqual(new User('Sapphire')); 12 | }); 13 | 14 | test('GIVEN anything which is not and instance of User THEN throws ValidationError', () => { 15 | expectError(() => predicate.parse(123), new ExpectedValidationError('s.instance(V)', message ?? 'Expected', 123, User)); 16 | }); 17 | 18 | test('GIVEN clone THEN returns similar instance', () => { 19 | expectClonedValidator(predicate, predicate['clone']()); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/unit/validators/lazy.test.ts: -------------------------------------------------------------------------------- 1 | import { CombinedPropertyError, ExpectedConstraintError, MissingPropertyError, ValidationError, s, type SchemaOf } from '../../../src'; 2 | import { expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('LazyValidator (%s)', (message) => { 5 | const predicate = s.lazy((value) => { 6 | if (typeof value === 'boolean') return s.boolean().true(); 7 | return s.string({ message }); 8 | }); 9 | 10 | test.each([true, 'hello'])('GIVEN %j THEN returns the given value', (input) => { 11 | expect(predicate.parse(input)).toBe(input); 12 | }); 13 | 14 | test('GIVEN an invalid value THEN throw ValidationError', () => { 15 | const errorMessage = message ?? 'Expected a string primitive'; 16 | expectError(() => predicate.parse(123), new ValidationError('s.string()', errorMessage, 123)); 17 | }); 18 | }); 19 | 20 | describe.each(['custom message', undefined])('NestedLazyValidator (%s)', (message) => { 21 | const predicate = s.lazy((value) => { 22 | if (typeof value === 'boolean') return s.boolean().true(); 23 | return s.lazy((value) => { 24 | if (typeof value === 'string') return s.string().lengthEqual(5, { message }); 25 | return s.number({ message }); 26 | }); 27 | }); 28 | 29 | test.each([true, 'hello', 123])('GIVEN %j THEN returns the given value', (input) => { 30 | expect(predicate.parse(input)).toBe(input); 31 | }); 32 | 33 | test('GIVEN an invalid value THEN throw ValidationError', () => { 34 | const errorMessage = message ?? 'Invalid string length'; 35 | expectError( 36 | () => predicate.parse('Sapphire'), 37 | new ExpectedConstraintError('s.string().lengthEqual()', errorMessage, 'Sapphire', 'expected.length === 5') 38 | ); 39 | }); 40 | }); 41 | 42 | describe.each(['custom message', undefined])('CircularLazyValidator (%s)', (message) => { 43 | interface PredicateSchema { 44 | id: string; 45 | items: PredicateSchema; 46 | } 47 | 48 | const predicate: SchemaOf = s.object({ 49 | id: s.string({ message }), 50 | items: s.lazy>(() => predicate) 51 | }); 52 | 53 | test('GIVEN circular schema THEN throw ', () => { 54 | expectError( 55 | () => predicate.parse({ id: 'Hello', items: { id: 'Hello', items: { id: 'Hello' } } }), 56 | new CombinedPropertyError([ 57 | ['items', new CombinedPropertyError([['items', new CombinedPropertyError([['items', new MissingPropertyError('items')]])]])] 58 | ]) 59 | ); 60 | }); 61 | }); 62 | 63 | describe.each(['custom message', undefined])('PassingCircularLazyValidator (%s)', (message) => { 64 | interface PredicateSchema { 65 | id: string; 66 | items?: PredicateSchema; 67 | } 68 | 69 | const predicate: SchemaOf = s.object({ 70 | id: s.string({ message }), 71 | items: s.lazy>(() => predicate).optional({ message }) 72 | }); 73 | 74 | test('GIVEN circular schema THEN return given value', () => { 75 | expect(predicate.parse({ id: 'Sapphire', items: { id: 'Hello' } })).toStrictEqual({ id: 'Sapphire', items: { id: 'Hello' } }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /tests/unit/validators/literal.test.ts: -------------------------------------------------------------------------------- 1 | import { ExpectedValidationError, s } from '../../../src'; 2 | import { expectClonedValidator, expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('LiteralValidator (%s)', (message) => { 5 | const predicate = s.literal('sapphire', { 6 | dateOptions: { 7 | message 8 | }, 9 | equalsOptions: { 10 | message 11 | } 12 | }); 13 | 14 | test('GIVEN a literal THEN returns the given value', () => { 15 | expect(predicate.parse('sapphire')).toBe('sapphire'); 16 | }); 17 | 18 | test('GIVEN anything which is not the literal THEN throws ExpectedValidationError', () => { 19 | const errorMessage = message ?? 'Expected values to be equals'; 20 | expectError(() => predicate.parse('hello'), new ExpectedValidationError('s.literal(V)', errorMessage, 'hello', 'sapphire')); 21 | }); 22 | 23 | test('GIVEN date literal THEN returns s.date().equal(V)', () => { 24 | const date = new Date('2022-01-01'); 25 | expectClonedValidator(s.literal(date), s.date().equal(date)); 26 | }); 27 | 28 | test('GIVEN clone THEN returns similar instance', () => { 29 | expectClonedValidator(predicate, predicate['clone']()); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/unit/validators/map.test.ts: -------------------------------------------------------------------------------- 1 | import { CombinedPropertyError, s, ValidationError } from '../../../src'; 2 | import { expectClonedValidator, expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('MapValidator (%s)', (message) => { 5 | const value = new Map([ 6 | ['a', 1], 7 | ['b', 2] 8 | ]); 9 | const predicate = s.map(s.string({ message }), s.number({ message }), { message }); 10 | 11 | test('GIVEN a non-map THEN throws ValidationError', () => { 12 | const errorMessage = message ?? 'Expected a map'; 13 | expectError(() => predicate.parse(false), new ValidationError('s.map(K, V)', errorMessage, false)); 14 | }); 15 | 16 | test('GIVEN a matching map THEN returns a map', () => { 17 | expect(predicate.parse(value)).toStrictEqual(value); 18 | }); 19 | 20 | test('GIVEN a non-matching map THEN throws CombinedError', () => { 21 | const map = new Map([ 22 | ['fizz', 1], 23 | [2, 3], 24 | ['foo', 'bar'], 25 | [4, 'buzz'] 26 | ]); 27 | 28 | expectError( 29 | () => predicate.parse(map), 30 | new CombinedPropertyError( 31 | [ 32 | [2, new ValidationError('s.string()', message ?? 'Expected a string primitive', 2)], 33 | ['foo', new ValidationError('s.number()', message ?? 'Expected a number primitive', 'bar')], 34 | [4, new ValidationError('s.string()', message ?? 'Expected a string primitive', 4)], 35 | [4, new ValidationError('s.number()', message ?? 'Expected a number primitive', 'buzz')] 36 | ], 37 | { 38 | message: message ?? 'Received one or more errors' 39 | } 40 | ) 41 | ); 42 | }); 43 | 44 | test('GIVEN clone THEN returns similar instance', () => { 45 | expectClonedValidator(predicate, predicate['clone']()); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/unit/validators/nativeEnum.test.ts: -------------------------------------------------------------------------------- 1 | import { s, UnknownEnumValueError, ValidationError } from '../../../src'; 2 | import { expectClonedValidator, expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('NativeEnumValidator (%s)', (message) => { 5 | describe('invalid inputs', () => { 6 | const predicate = s.nativeEnum({ hello: 'world' }, { message }); 7 | 8 | test.each([true, null, undefined, {}])('GIVEN %j THEN throws ValidationError', (value) => { 9 | const errorMessage = message ?? 'Expected the value to be a string or number'; 10 | expectError(() => predicate.parse(value), new ValidationError('s.nativeEnum(T)', errorMessage, value)); 11 | }); 12 | }); 13 | 14 | describe('string enum', () => { 15 | enum StringEnum { 16 | Hi = 'hi' 17 | } 18 | 19 | const stringPredicate = s.nativeEnum(StringEnum, { message }); 20 | 21 | test.each([ 22 | ['Hi', StringEnum.Hi], 23 | [StringEnum.Hi, StringEnum.Hi] 24 | ])('GIVEN a key or value of a native enum (%j) THEN returns the value', (value, expected) => { 25 | expect(stringPredicate.parse(value)).toBe(expected); 26 | }); 27 | 28 | it('GIVEN a number input for a string enum THEN throws ValidationError', () => { 29 | const errorMessage = message ?? 'Expected the value to be a string'; 30 | expectError(() => stringPredicate.parse(1), new ValidationError('s.nativeEnum(T)', errorMessage, 1)); 31 | }); 32 | }); 33 | 34 | describe('number enum', () => { 35 | enum NumberEnum { 36 | Vladdy, 37 | Kyra, 38 | Favna 39 | } 40 | const numberPredicate = s.nativeEnum(NumberEnum, { message }); 41 | 42 | test.each([ 43 | ['Vladdy', NumberEnum.Vladdy], 44 | [NumberEnum.Vladdy, NumberEnum.Vladdy] 45 | ])('GIVEN a key or value of a native enum (%j) THEN returns the value', (input, expected) => { 46 | expect(numberPredicate.parse(input)).toBe(expected); 47 | }); 48 | }); 49 | 50 | describe('mixed enum', () => { 51 | enum MixedEnum { 52 | Sapphire = 'is awesome', 53 | Vladdy = 420 54 | } 55 | 56 | const mixedPredicate = s.nativeEnum(MixedEnum, { message }); 57 | 58 | test.each([ 59 | ['Sapphire', MixedEnum.Sapphire], 60 | [MixedEnum.Sapphire, MixedEnum.Sapphire], 61 | ['Vladdy', MixedEnum.Vladdy], 62 | [MixedEnum.Vladdy, MixedEnum.Vladdy] 63 | ])('GIVEN a key or value of a native enum (%j) THEN returns the value', (input, expected) => { 64 | expect(mixedPredicate.parse(input)).toBe(expected); 65 | }); 66 | }); 67 | 68 | describe('valid input but invalid enum value', () => { 69 | const predicate = s.nativeEnum({ owo: 42 }, { message }); 70 | 71 | test.each(['uwu', 69])('GIVEN valid type for input but not part of enum (%j) THEN throws ValidationError', (value) => { 72 | const errorMessage = message ?? 'Expected the value to be one of the following enum values:'; 73 | expectError(() => predicate.parse(value), new UnknownEnumValueError(value, ['owo'], new Map([['owo', 42]]), { message: errorMessage })); 74 | }); 75 | }); 76 | 77 | test('GIVEN clone THEN returns similar instance', () => { 78 | const predicate = s.nativeEnum({ Example: 69 }); 79 | // @ts-expect-error Test clone 80 | const clonePredicate = predicate.clone(); 81 | 82 | expectClonedValidator(predicate, clonePredicate); 83 | expect(predicate.parse('Example')).toBe(69); 84 | expect(predicate.parse(69)).toBe(69); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /tests/unit/validators/never.test.ts: -------------------------------------------------------------------------------- 1 | import { s, ValidationError } from '../../../src'; 2 | import { expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('NeverValidator (%s)', (message) => { 5 | const predicate = s.never({ message }); 6 | 7 | test.each([123, 'hello'])('GIVEN %j THEN throws ConstraintError', (input) => { 8 | expectError(() => predicate.parse(input), new ValidationError('s.never()', message ?? 'Expected a value to not be passed', input)); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/unit/validators/null.test.ts: -------------------------------------------------------------------------------- 1 | import { ExpectedValidationError, s } from '../../../src'; 2 | import { expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('NullValidator (%s)', (message) => { 5 | const predicate = s.null({ message }); 6 | 7 | test('GIVEN null THEN returns null', () => { 8 | expect(predicate.parse(null)).toBe(null); 9 | }); 10 | 11 | test.each([undefined, 123, 'Hello', {}])('GIVEN %j THEN throws ExpectedValidationError', (input) => { 12 | expectError( 13 | () => predicate.parse(input), 14 | new ExpectedValidationError('s.literal(V)', message ?? 'Expected values to be equals', input, null) 15 | ); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/unit/validators/nullish.test.ts: -------------------------------------------------------------------------------- 1 | import { s, ValidationError } from '../../../src'; 2 | import { expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('NullishValidator (%s)', (message) => { 5 | const predicate = s.nullish({ message }); 6 | 7 | test.each([null, undefined])('GIVEN %j THEN returns the given value', (input) => { 8 | expect(predicate.parse(input)).toBe(input); 9 | }); 10 | 11 | test.each([123, 'hello'])('GIVEN %j THEN throws ValidationError', (input) => { 12 | expectError(() => predicate.parse(input), new ValidationError('s.nullish()', message ?? 'Expected undefined or null', input)); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/unit/validators/passthrough.test.ts: -------------------------------------------------------------------------------- 1 | import { s } from '../../../src'; 2 | 3 | describe.each(['custom message', undefined])('AnyValidator (%s)', (message) => { 4 | const predicate = s.any({ message }); 5 | 6 | test.each([1, 'hello', null])('GIVEN anything (%j) THEN returns the given value', (input) => { 7 | expect(predicate.parse(input)).toBe(input); 8 | }); 9 | }); 10 | 11 | describe('UnknownValidator', () => { 12 | const predicate = s.unknown(); 13 | 14 | test.each([1, 'hello', null])('GIVEN anything (%j) THEN returns the given value', (input) => { 15 | expect(predicate.parse(input)).toBe(input); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/unit/validators/record.test.ts: -------------------------------------------------------------------------------- 1 | import { CombinedPropertyError, s, ValidationError } from '../../../src'; 2 | import { expectClonedValidator, expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('RecordValidator (%s)', (message) => { 5 | const value = { foo: 'bar', fizz: 'buzz' }; 6 | const predicate = s.record(s.string({ message }), { message }); 7 | 8 | test('GIVEN a non-record THEN throws ValidationError', () => { 9 | expectError(() => predicate.parse(false), new ValidationError('s.record(T)', message ?? 'Expected an object', false)); 10 | }); 11 | 12 | test('GIVEN null THEN throws ValidationError', () => { 13 | expectError(() => predicate.parse(null), new ValidationError('s.record(T)', message ?? 'Expected the value to not be null', null)); 14 | }); 15 | 16 | test('GIVEN a matching record THEN returns a record', () => { 17 | expect(predicate.parse(value)).toStrictEqual(value); 18 | }); 19 | 20 | test('GIVEN a non-matching record THEN throws CombinedError', () => { 21 | expectError( 22 | () => predicate.parse({ foo: 1, fizz: true }), 23 | new CombinedPropertyError( 24 | [ 25 | ['foo', new ValidationError('s.string()', message ?? 'Expected a string primitive', 1)], 26 | ['fizz', new ValidationError('s.string()', message ?? 'Expected a string primitive', true)] 27 | ], 28 | { 29 | message: message ?? 'Received one or more errors' 30 | } 31 | ) 32 | ); 33 | }); 34 | 35 | test('GIVEN clone THEN returns similar instance', () => { 36 | expectClonedValidator(predicate, predicate['clone']()); 37 | }); 38 | 39 | test('GIVEN an array THEN throws ValidationError', () => { 40 | expectError( 41 | () => predicate.parse([1, 2, 3]), 42 | new ValidationError('s.record(T)', message ?? 'Expected the value to not be an array', [1, 2, 3]) 43 | ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/unit/validators/set.test.ts: -------------------------------------------------------------------------------- 1 | import { CombinedError, s, ValidationError } from '../../../src'; 2 | import { expectClonedValidator, expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('SetValidator (%s)', (message) => { 5 | const predicate = s.set(s.string({ message }), { message }); 6 | 7 | test.each([123, 'foo', [], {}, new Map()])("GIVEN a value which isn't a set (%j) THEN throws ValidationError", (input) => { 8 | expectError(() => predicate.parse(input), new ValidationError('s.set(T)', message ?? 'Expected a set', input)); 9 | }); 10 | 11 | test.each(['1', 'a', 'foo'])('GIVEN a set with string value (%j) THEN returns the given set', (input) => { 12 | const set = new Set([input]); 13 | 14 | expect(predicate.parse(set)).toStrictEqual(set); 15 | }); 16 | 17 | test.each([123, [], {}])('GIVEN a set with non-string value (%j) THEN throw CombinedError', (input) => { 18 | const set = new Set([input]); 19 | 20 | expectError( 21 | () => predicate.parse(set), 22 | new CombinedError([new ValidationError('s.string()', message ?? 'Expected a string primitive', input)], { 23 | message: message ?? 'Received one or more errors' 24 | }) 25 | ); 26 | }); 27 | 28 | test('GIVEN clone THEN returns similar instance', () => { 29 | expectClonedValidator(predicate, predicate['clone']()); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/unit/validators/tuple.test.ts: -------------------------------------------------------------------------------- 1 | import { CombinedPropertyError, s, ValidationError } from '../../../src'; 2 | import { expectClonedValidator, expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('TupleValidator', (message) => { 5 | const predicate = s.tuple([s.string({ message }), s.number({ message })], { message }); 6 | 7 | test('GIVEN a matching tuple THEN returns a tuple', () => { 8 | expect<[string, number]>(predicate.parse(['foo', 1])).toStrictEqual(['foo', 1]); 9 | }); 10 | 11 | test.each([false, 1, 'Hello', null, undefined])('GIVEN %j THEN throws ValidationError', (input) => { 12 | expectError(() => predicate.parse(input), new ValidationError('s.tuple(T)', message ?? 'Expected an array', input)); 13 | }); 14 | 15 | test.each([ 16 | [1, 'foo'], 17 | [null, 'bar'], 18 | [undefined, {}], 19 | [{}, null] 20 | ])('GIVEN [%j, %j] tuple THEN throws CombinedError', (a, b) => { 21 | expectError( 22 | () => predicate.parse([a, b]), 23 | new CombinedPropertyError( 24 | [ 25 | [0, new ValidationError('s.string()', message ?? 'Expected a string primitive', a)], 26 | [1, new ValidationError('s.number()', message ?? 'Expected a number primitive', b)] 27 | ], 28 | { message } 29 | ) 30 | ); 31 | }); 32 | 33 | test('GIVEN a tuple with too few elements THEN throws ValidationError', () => { 34 | expectError(() => predicate.parse(['foo']), new ValidationError('s.tuple(T)', message ?? 'Expected an array of length 2', ['foo'])); 35 | }); 36 | 37 | test('GIVEN a tuple with too many elements THEN throws ValidationError', () => { 38 | expectError( 39 | () => predicate.parse(['foo', 1, 'bar']), 40 | new ValidationError('s.tuple(T)', message ?? 'Expected an array of length 2', ['foo', 1, 'bar']) 41 | ); 42 | }); 43 | 44 | test('GIVEN clone THEN returns similar instance', () => { 45 | expectClonedValidator(predicate, predicate['clone']()); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/unit/validators/undefined.test.ts: -------------------------------------------------------------------------------- 1 | import { ExpectedValidationError, s } from '../../../src'; 2 | import { expectError } from '../common/macros/comparators'; 3 | 4 | describe.each(['custom message', undefined])('UndefinedValidator (%s)', (message) => { 5 | const predicate = s.undefined({ message }); 6 | 7 | test('GIVEN undefined THEN returns undefined', () => { 8 | expect(predicate.parse(undefined)).toBe(undefined); 9 | }); 10 | 11 | test.each([null, 123, 'Hello'])('GIVEN %j THEN throws ExpectedValidationError', (input) => { 12 | expectError( 13 | () => predicate.parse(input), 14 | new ExpectedValidationError('s.literal(V)', message ?? 'Expected values to be equals', input, undefined) 15 | ); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@sapphire/ts-config", "@sapphire/ts-config/extra-strict", "@sapphire/ts-config/bundler", "@sapphire/ts-config/verbatim"], 3 | "compilerOptions": { 4 | "target": "ES2020", 5 | "useDefineForClassFields": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "incremental": false, 6 | "types": ["vitest/globals"], 7 | "lib": ["dom", "esnext"] 8 | }, 9 | "include": ["src", "tests", "scripts", "vitest.config.ts", "vitest.workspace.ts", "tsup.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.typecheck.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.eslint.json", 3 | "compilerOptions": { 4 | "skipLibCheck": true 5 | }, 6 | "exclude": ["tests/browser/browser.test.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { nodeModulesPolyfillPlugin } from 'esbuild-plugins-node-modules-polyfill'; 2 | import { defineConfig, type Options } from 'tsup'; 3 | import { dependencies } from './package.json'; 4 | 5 | const baseOptions: Options = { 6 | clean: true, 7 | dts: true, 8 | entry: ['src/index.ts'], 9 | minify: false, 10 | external: Object.keys(dependencies), 11 | sourcemap: true, 12 | target: 'es2020', 13 | tsconfig: 'src/tsconfig.json', 14 | keepNames: true, 15 | treeshake: true, 16 | esbuildPlugins: [nodeModulesPolyfillPlugin()] 17 | }; 18 | 19 | export default [ 20 | defineConfig({ 21 | ...baseOptions, 22 | outDir: 'dist/cjs', 23 | format: 'cjs', 24 | outExtension: () => ({ js: '.cjs' }) 25 | }), 26 | defineConfig({ 27 | ...baseOptions, 28 | outDir: 'dist/esm', 29 | format: 'esm' 30 | }), 31 | defineConfig({ 32 | ...baseOptions, 33 | globalName: 'SapphireShapeshift', 34 | dts: false, 35 | outDir: 'dist/iife', 36 | format: 'iife' 37 | }) 38 | ]; 39 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "entryPoints": ["src/index.ts"], 4 | "json": "docs/api.json", 5 | "tsconfig": "src/tsconfig.json", 6 | "excludePrivate": false 7 | } 8 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | includeTaskLocation: true, 6 | globals: true, 7 | coverage: { 8 | provider: 'v8', 9 | enabled: true, 10 | reporter: ['text', 'lcov'], 11 | include: ['src/**/*.ts'], 12 | exclude: ['src/constraints/base/IConstraint.ts', 'src/constraints/type-exports.ts'] 13 | } 14 | }, 15 | esbuild: { 16 | target: 'es2020' 17 | } 18 | }); 19 | --------------------------------------------------------------------------------