├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── dictionaries │ └── project.xml ├── fluentvalidation.iml ├── inspectionProfiles │ └── Project_Default.xml ├── jsLinters │ └── eslint.xml ├── modules.xml ├── prettier.xml ├── runConfigurations │ └── All_Tests.xml └── vcs.xml ├── .prettierrc.json ├── .vscode └── settings.json ├── License.txt ├── README.md ├── docs ├── api │ ├── configuration │ │ ├── unless.md │ │ ├── when.md │ │ └── withMessage.md │ ├── core │ │ ├── asyncValidator.md │ │ ├── ruleFor.md │ │ ├── ruleForEach.md │ │ ├── ruleForEachTransformed.md │ │ ├── ruleForTransformed.md │ │ ├── validationErrors.md │ │ └── validator.md │ └── rules │ │ ├── emailAddress.md │ │ ├── equal.md │ │ ├── exclusiveBetween.md │ │ ├── greaterThan.md │ │ ├── greaterThanOrEqualTo.md │ │ ├── inclusiveBetween.md │ │ ├── length.md │ │ ├── lessThan.md │ │ ├── lessThanOrEqualTo.md │ │ ├── matches.md │ │ ├── maxLength.md │ │ ├── minLength.md │ │ ├── must.md │ │ ├── mustAsync.md │ │ ├── notEmpty.md │ │ ├── notEqual.md │ │ ├── notNull.md │ │ ├── notUndefined.md │ │ ├── null.md │ │ ├── precisionScale.md │ │ ├── setAsyncValidator.md │ │ ├── setValidator.md │ │ └── undefined.md ├── guides │ ├── ambientContext.md │ ├── arrayProperties.md │ ├── customRules.md │ ├── formik.md │ ├── objectProperties.md │ └── reactHookForm.md ├── overview.md └── tutorial.md ├── eslint.config.mjs ├── jest.config.ts ├── logo.png ├── package-lock.json ├── package.json ├── src ├── AsyncValidator.ts ├── IAsyncValidator.ts ├── IValidator.ts ├── SyncValidator.ts ├── ValidationErrors.ts ├── ValueValidationResult.ts ├── ValueValidator.ts ├── index.ts ├── numberHelpers.ts ├── rules │ ├── AsyncRule.ts │ ├── AsyncValidatorRule.ts │ ├── CoreRule.ts │ ├── EmailAddressRule.ts │ ├── EqualRule.ts │ ├── ExclusiveBetweenRule.ts │ ├── GreaterThanOrEqualToRule.ts │ ├── GreaterThanRule.ts │ ├── InclusiveBetweenRule.ts │ ├── LengthRule.ts │ ├── LessThanOrEqualToRule.ts │ ├── LessThanRule.ts │ ├── MatchesRule.ts │ ├── MaxLengthRule.ts │ ├── MinLengthRule.ts │ ├── MustAsyncRule.ts │ ├── MustRule.ts │ ├── NotEmptyRule.ts │ ├── NotEqualRule.ts │ ├── NotNullRule.ts │ ├── NotUndefinedRule.ts │ ├── NullRule.ts │ ├── PrecisionScaleRule.ts │ ├── Rule.ts │ ├── UndefinedRule.ts │ └── ValidatorRule.ts ├── types │ ├── AppliesTo.ts │ ├── ArrayType.ts │ ├── Constrain.ts │ ├── FlatType.ts │ ├── IfNotNeverThen.ts │ ├── IfType.ts │ ├── Message.ts │ ├── Optional.ts │ ├── Predicate.ts │ └── TransformedValue.ts └── valueValidator │ ├── ArrayValueValidatorBuilder.ts │ ├── AsyncArrayValueValidatorBuilder.ts │ ├── AsyncRuleValidators.ts │ ├── AsyncValueValidator.ts │ ├── AsyncValueValidatorBuilder.ts │ ├── CoreValueValidatorBuilder.ts │ ├── RuleValidators.ts │ ├── ValueTransformer.ts │ ├── ValueValidator.ts │ ├── ValueValidatorBuilder.ts │ └── ValueValidatorBuilderTypes.ts ├── test ├── baseValidatorsAsync.test.ts ├── baseValidatorsSync.test.ts ├── examplesAsync.test.ts ├── examplesSync.test.ts ├── index.test.ts ├── numberHelpers.test.ts ├── numberValidatorsAsync.test.ts ├── numberValidatorsSync.test.ts ├── ruleForEach.test.ts ├── ruleForEachTransformed.test.ts ├── ruleForTransformed.test.ts ├── stringValidators.test.ts ├── stringValidatorsAsync.test.ts ├── testHelpers.ts ├── unless.test.ts ├── when.test.ts └── withMessage.test.ts ├── tsconfig.json ├── tsconfig.test.json └── website ├── docusaurus.config.js ├── package-lock.json ├── package.json ├── sidebars.json ├── src ├── css │ └── customTheme.css ├── pages │ ├── help.js │ └── index.js └── theme │ └── Root.js └── static ├── css └── code-block-buttons.css ├── img ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── logo-outlined.svg ├── logo-text.svg ├── logo.svg └── site.webmanifest └── js └── code-block-buttons.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ['*'] 6 | pull_request: 7 | branches: ['*'] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [22.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | - run: npm ci 25 | - run: npm run typecheck 26 | - run: npm run prettier:check 27 | - run: npm run lint:check 28 | - run: npm test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .rts2_cache_cjs 5 | .rts2_cache_esm 6 | .rts2_cache_umd 7 | dist 8 | 9 | website/translated_docs 10 | website/build 11 | website/node_modules 12 | website/i18n 13 | website/.docusaurus 14 | 15 | coverage/ 16 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx --no-install pretty-quick 2 | npx pretty-quick --staged 3 | 4 | npm run lint 5 | 6 | npm run typecheck 7 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /../../../../../:\workspace\my\fluentvalidation\.idea/dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 17 | 18 | 26 | 27 | 34 | 35 | 42 | 43 | 50 | 51 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/dictionaries/project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tseslint 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/fluentvalidation.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/jsLinters/eslint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/All_Tests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "printWidth": 100, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "trailingComma": "all", 7 | "singleQuote": true 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "typescript.tsdk": "node_modules\\typescript\\lib" 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fluentvalidation-ts 2 | 3 | [![CI](https://github.com/AlexJPotter/fluentvalidation-ts/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/AlexJPotter/fluentvalidation-ts/actions/workflows/ci.yml) 4 | ![Coverage](https://badgen.net/badge/coverage/100%25/green?icon=codecov) 5 | ![Dependencies](https://badgen.net/badge/dependencies/none/green) 6 | [![GZIP Size](https://img.badgesize.io/https://unpkg.com/fluentvalidation-ts@latest/dist/index.js?compression=gzip)](https://unpkg.com/fluentvalidation-ts@latest/dist/index.js) 7 | 8 | [![NPM Version](https://badgen.net/npm/v/fluentvalidation-ts?icon=npm)](https://www.npmjs.com/package/fluentvalidation-ts) 9 | ![License](https://badgen.net/npm/license/fluentvalidation-ts) 10 | ![Last Commit](https://badgen.net/github/last-commit/alexjpotter/fluentvalidation-ts/main?icon=github) 11 | [![Open Issues](https://badgen.net/github/open-issues/alexjpotter/fluentvalidation-ts?icon=github)](https://github.com/AlexJPotter/fluentvalidation-ts/issues) 12 | 13 | ## Strong, simple, extensible validation. 14 | 15 | Visit [https://fluentvalidation-ts.alexpotter.dev](https://fluentvalidation-ts.alexpotter.dev) to get started. 16 | 17 | ## Overview 18 | 19 | Front-end validation is a must-have for any project that involves forms, but the requirements vary hugely. You might have a simple sign-up form with a few text fields, or a complex configuration page with collections and deeply nested fields. 20 | 21 | There are plenty of libraries out there which help you to solve the problem of front-end validation, but all the ones I've tried have felt lacking in one aspect or another - whether that's TypeScript support, their capacity to handle complex requirements, or the ability to define your own reusable validation logic. 22 | 23 | So I wrote **fluentvalidation-ts**, a tiny library that is: 24 | 25 | - Designed for **TypeScript** 26 | - Simple yet powerful 27 | - Fully extensible 28 | 29 | Whatever your validation needs, **fluentvalidation-ts** can handle them. 30 | 31 | ## Docs 32 | 33 | Full documentation, including a tutorial and a number of useful guides, is available on the [documentation website](https://fluentvalidation-ts.alexpotter.dev). 34 | 35 | - [Overview](https://fluentvalidation-ts.alexpotter.dev/docs/overview) 36 | - [Tutorial](https://fluentvalidation-ts.alexpotter.dev/docs/tutorial) 37 | - [Guides](https://fluentvalidation-ts.alexpotter.dev/docs/guides/customrules) 38 | - [Core API Reference](https://fluentvalidation-ts.alexpotter.dev/docs/api/core/validator) 39 | - [Validation Rules API Reference](https://fluentvalidation-ts.alexpotter.dev/docs/api/rules/emailaddress) 40 | - [Releases](https://github.com/AlexJPotter/fluentvalidation-ts/releases) 41 | 42 | ### Requirements 43 | 44 | This library has been written in, and for, **TypeScript**. You can still use **fluentvalidation-ts** without TypeScript, but the primary benefit of having strongly-typed validation rules is lost. 45 | 46 | If using TypeScript (strongly recommended), you must be on TypeScript version **`2.9`** or later. 47 | 48 | ### Installation 49 | 50 | Using NPM: 51 | 52 | ``` 53 | npm i --save fluentvalidation-ts 54 | ``` 55 | 56 | Using Yarn: 57 | 58 | ``` 59 | yarn add fluentvalidation-ts 60 | ``` 61 | 62 | > [!TIP] 63 | > There's no need to install types separately - **fluentvalidation-ts** has been written with first-class support for TypeScript! 64 | 65 | ### Example Usage 66 | 67 | ```typescript 68 | import { Validator } from 'fluentvalidation-ts'; 69 | 70 | type Person = { 71 | name: string; 72 | age: number; 73 | }; 74 | 75 | class PersonValidator extends Validator { 76 | constructor() { 77 | super(); 78 | 79 | this.ruleFor('name') // This is type-safe! (Argument is of type 'name' | 'age') 80 | .notEmpty() 81 | .withMessage('Please enter your name'); 82 | 83 | this.ruleFor('age').greaterThanOrEqualTo(0).withMessage('Age cannot be negative'); 84 | } 85 | } 86 | 87 | const validator = new PersonValidator(); 88 | 89 | validator.validate({ name: '', age: 25 }); 90 | // { name: 'Please enter your name' } 91 | 92 | validator.validate({ name: 'Alex', age: -1 }); 93 | // { age: 'Age cannot be negative' } 94 | 95 | validator.validate({ name: '', age: -1 }); 96 | // { name: 'Please enter your name', age: 'Age cannot be negative' } 97 | ``` 98 | 99 | ### Test Coverage 100 | 101 | **fluentvalidation-ts** has 100% test coverage via unit tests written with [Jest](https://jestjs.io/). 102 | 103 | > [!NOTE] 104 | > Some branches are incorrectly reported as uncovered due to the following issue: [https://github.com/gotwarlost/istanbul/issues/690](https://github.com/gotwarlost/istanbul/issues/690). 105 | 106 | ### Issues 107 | 108 | Please report issues via [GitHub](https://github.com/AlexJPotter/fluentvalidation-ts/issues). 109 | 110 | ### License 111 | 112 | **fluentvalidation-ts** is provided under the terms of an [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) license. 113 | 114 | ### Development 115 | 116 | Clone the repo and run `npm install`, then run `npm run watch` in the root of the project to start the TypeScript compiler in watch mode. You can run the tests with `npm test`. 117 | 118 | ### About the Author 119 | 120 | Alex Potter is a full-stack Software Engineer, currently working as a Technical Lead at [Ghyston](https://www.ghyston.com), an award-winning software development company based in Bristol. 121 | -------------------------------------------------------------------------------- /docs/api/configuration/unless.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: unless 3 | title: '.unless' 4 | --- 5 | 6 | The `.unless` option is used to control when a rule or chain of rules should **not** execute. 7 | 8 | By default, the `.unless` option will apply to all rules in the chain so far, but you can pass a second parameter to specify that it should only apply to the rule immediately preceding it. 9 | 10 | :::note 11 | 12 | In the case that there are multiple `.when` and/or `.unless` conditions in the rule chain, each condition applies only to the rules defined **between it and the previous condition**. 13 | 14 | ::: 15 | 16 | ## Examples 17 | 18 | ### Apply to all rules in the chain so far 19 | 20 | In this example we apply an `.unless` condition to an entire rule chain. 21 | 22 | In particular, we validate that the delivery note has been entered and is no more than 1,000 characters long unless it has been specified that a delivery note is not required. 23 | 24 | ```typescript 25 | import { Validator } from 'fluentvalidation-ts'; 26 | 27 | type FormModel = { 28 | doesNotRequireDeliveryNote: boolean; 29 | deliveryNote: string | null; 30 | }; 31 | 32 | class FormValidator extends Validator { 33 | constructor() { 34 | super(); 35 | 36 | this.ruleFor('deliveryNote') 37 | .notNull() 38 | .notEmpty() 39 | .maxLength(1000) 40 | // highlight-next-line 41 | .unless((formModel) => formModel.doesNotRequireDeliveryNote); 42 | } 43 | } 44 | 45 | const formValidator = new FormValidator(); 46 | 47 | formValidator.validate({ 48 | doesNotRequireDeliveryNote: true, 49 | deliveryNote: null, 50 | }); 51 | // ✔ {} 52 | 53 | formValidator.validate({ 54 | doesNotRequireDeliveryNote: false, 55 | deliveryNote: null, 56 | }); 57 | // ❌ { deliveryNote: 'Value cannot be null' } 58 | ``` 59 | 60 | ### Multiple calls within the same chain 61 | 62 | In this example we apply multiple `.unless` conditions within the same rule chain. 63 | 64 | In particular, we validate that the account balance is non-negative unless overdrafts are allowed, and also validate that the account balance is more than 100 unless the account is not subject to minimum balance requirements. 65 | 66 | ```typescript 67 | import { Validator } from 'fluentvalidation-ts'; 68 | 69 | type FormModel = { 70 | accountBalance: number; 71 | allowOverdrafts: boolean; 72 | subjectToMinimumBalance: boolean; 73 | }; 74 | 75 | class FormValidator extends Validator { 76 | constructor() { 77 | super(); 78 | 79 | this.ruleFor('accountBalance') 80 | .greaterThanOrEqualTo(0) 81 | // highlight-next-line 82 | .unless((formModel) => formModel.allowOverdrafts) 83 | .greaterThanOrEqualTo(100) 84 | // highlight-next-line 85 | .unless((formModel) => !formModel.subjectToMinimumBalance); 86 | } 87 | } 88 | 89 | const formValidator = new FormValidator(); 90 | 91 | formValidator.validate({ 92 | accountBalance: -50, 93 | allowOverdrafts: true, 94 | subjectToMinimumBalance: false, 95 | }); 96 | // ✔ {} 97 | 98 | formValidator.validate({ 99 | accountBalance: -50, 100 | allowOverdrafts: false, 101 | subjectToMinimumBalance: false, 102 | }); 103 | // ❌ { accountBalance: 'Value must be greater than or equal to 0' } 104 | 105 | formValidator.validate({ 106 | accountBalance: 50, 107 | allowOverdrafts: false, 108 | subjectToMinimumBalance: true, 109 | }); 110 | // ❌ { accountBalance: 'Value must be greater than or equal to 100' } 111 | ``` 112 | 113 | ### Apply to a specific rule in the chain 114 | 115 | In this example we apply an `.unless` condition to a specific rule in the chain. 116 | 117 | In particular, we validate that an age has been entered, and also validate that it is at least 18 unless no alcoholic drink has been chosen. 118 | 119 | ```typescript 120 | import { Validator } from 'fluentvalidation-ts'; 121 | 122 | type FormModel = { 123 | age: number | null; 124 | alcoholicDrink: string | null; 125 | }; 126 | 127 | class FormValidator extends Validator { 128 | constructor() { 129 | super(); 130 | 131 | this.ruleFor('age') 132 | .notNull() 133 | .greaterThanOrEqualTo(18) 134 | // highlight-start 135 | .unless((formModel) => formModel.alcoholicDrink == null, 'AppliesToCurrentValidator'); 136 | // highlight-end 137 | } 138 | } 139 | 140 | const formValidator = new FormValidator(); 141 | 142 | formValidator.validate({ 143 | age: 17, 144 | alcoholicDrink: null, 145 | }); 146 | // ✔ {} 147 | 148 | formValidator.validate({ 149 | age: 17, 150 | alcoholicDrink: 'Beer', 151 | }); 152 | // ❌ { age: 'Value must be greater than or equal to 18' } 153 | 154 | formValidator.validate({ 155 | age: null, 156 | alcoholicDrink: null, 157 | }); 158 | // ❌ { age: 'Value cannot be null' } 159 | ``` 160 | 161 | ## Reference 162 | 163 | ### `.unless(condition: (model: TModel) => boolean, appliesTo?: 'AppliesToAllValidators' | 'AppliesToCurrentValidator')` 164 | 165 | A configuration option which controls when a particular rule or chain of rules should not execute. 166 | 167 | ### `condition` 168 | 169 | This is a function which accepts the value of the base model and returns a `boolean` indicating whether the rule or chain of rules preceding it should not execute. 170 | 171 | A return value of `true` indicates that the rule or chain of rules **should not** execute. 172 | 173 | Conversely, a return value of `false` indicates that the rule or chain of rules **should** execute. 174 | 175 | ### `TModel` 176 | 177 | Matches the type of the base model. 178 | 179 | ### `appliesTo` 180 | 181 | This is an optional parameter which can be used to control which rules in the current rule chain the condition applies to. 182 | 183 | A value of `'AppliesToAllValidators'` means that the `.unless` condition applies to all rules in the current rule chain so far. If there are other calls to `.when` or `.unless` in the chain, only the rules defined since the most recent condition will have the condition applied to them. 184 | 185 | A value of `'AppliesToCurrentValidator'` specifies that the `.unless` condition only controls the execution of the rule immediately preceding it in the current rule chain. 186 | 187 | By default, the `appliesTo` parameter is set to `'AppliesToAllValidators'`. 188 | -------------------------------------------------------------------------------- /docs/api/configuration/when.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: when 3 | title: '.when' 4 | --- 5 | 6 | The `.when` option is used to control when a rule or chain of rules should execute. 7 | 8 | By default, the `.when` option will apply to all rules in the chain so far, but you can pass a second parameter to specify that it should only apply to the rule immediately preceding it. 9 | 10 | :::note 11 | 12 | In the case that there are multiple `.when` and/or `.unless` conditions in the rule chain, each condition applies only to the rules defined **between it and the previous condition**. 13 | 14 | ::: 15 | 16 | ## Examples 17 | 18 | ### Apply to all rules in the chain so far 19 | 20 | In this example we apply a `.when` condition to an entire rule chain. 21 | 22 | In particular, we validate that the delivery note has been entered and is no more than 1,000 characters long when it has been specified that a delivery note is required. 23 | 24 | ```typescript 25 | import { Validator } from 'fluentvalidation-ts'; 26 | 27 | type FormModel = { 28 | requiresDeliveryNote: boolean; 29 | deliveryNote: string | null; 30 | }; 31 | 32 | class FormValidator extends Validator { 33 | constructor() { 34 | super(); 35 | 36 | this.ruleFor('deliveryNote') 37 | .notNull() 38 | .notEmpty() 39 | .maxLength(1000) 40 | // highlight-next-line 41 | .when((formModel) => formModel.requiresDeliveryNote); 42 | } 43 | } 44 | 45 | const formValidator = new FormValidator(); 46 | 47 | formValidator.validate({ 48 | requiresDeliveryNote: false, 49 | deliveryNote: null, 50 | }); 51 | // ✔ {} 52 | 53 | formValidator.validate({ 54 | requiresDeliveryNote: true, 55 | deliveryNote: null, 56 | }); 57 | // ❌ { deliveryNote: 'Value cannot be null' } 58 | ``` 59 | 60 | ### Multiple calls within the same chain 61 | 62 | In this example we apply multiple `.when` conditions within the same rule chain. 63 | 64 | In particular, we validate that Sunday delivery rates are only applied when the delivery day is a Sunday. 65 | 66 | ```typescript 67 | import { Validator } from 'fluentvalidation-ts'; 68 | 69 | type FormModel = { 70 | deliveryDay: string; 71 | deliveryRate: number; 72 | }; 73 | 74 | class FormValidator extends Validator { 75 | constructor() { 76 | super(); 77 | 78 | this.ruleFor('deliveryRate') 79 | .equal(4.99) 80 | .withMessage('Sunday rates must apply if delivery day is Sunday') 81 | // highlight-next-line 82 | .when((formModel) => formModel.deliveryDay === 'Sunday') 83 | .equal(2.99) 84 | .withMessage('Standard rates must apply if delivery day is Monday to Saturday') 85 | // highlight-next-line 86 | .when((formModel) => formModel.deliveryDay !== 'Sunday'); 87 | } 88 | } 89 | 90 | const formValidator = new FormValidator(); 91 | 92 | formValidator.validate({ deliveryDay: 'Sunday', deliveryRate: 4.99 }); 93 | // ✔ {} 94 | 95 | formValidator.validate({ deliveryDay: 'Sunday', deliveryRate: 2.99 }); 96 | // ❌ { deliveryRate: 'Sunday rates must apply if delivery day is Sunday' } 97 | 98 | formValidator.validate({ deliveryDay: 'Monday', deliveryRate: 2.99 }); 99 | // ✔ {} 100 | 101 | formValidator.validate({ deliveryDay: 'Monday', deliveryRate: 4.99 }); 102 | // ❌ { deliveryRate: 'Standard rates must apply if delivery day is Monday to Saturday' } 103 | ``` 104 | 105 | ### Apply to a specific rule in the chain 106 | 107 | In this example we apply a `.when` condition to a specific rule in the chain. 108 | 109 | In particular, we validate that an age has been entered, and also validate that it is at least 18 when an alcoholic drink has been chosen. 110 | 111 | ```typescript 112 | import { Validator } from 'fluentvalidation-ts'; 113 | 114 | type FormModel = { 115 | age: number | null; 116 | alcoholicDrink: string | null; 117 | }; 118 | 119 | class FormValidator extends Validator { 120 | constructor() { 121 | super(); 122 | 123 | this.ruleFor('age') 124 | .notNull() 125 | .greaterThanOrEqualTo(18) 126 | // highlight-start 127 | .when((formModel) => formModel.alcoholicDrink != null, 'AppliesToCurrentValidator'); 128 | // highlight-end 129 | } 130 | } 131 | 132 | const formValidator = new FormValidator(); 133 | 134 | formValidator.validate({ 135 | age: 17, 136 | alcoholicDrink: null, 137 | }); 138 | // ✔ {} 139 | 140 | formValidator.validate({ 141 | age: 17, 142 | alcoholicDrink: 'Beer', 143 | }); 144 | // ❌ { age: 'Value must be greater than or equal to 18' } 145 | 146 | formValidator.validate({ 147 | age: null, 148 | alcoholicDrink: null, 149 | }); 150 | // ❌ { age: 'Value cannot be null' } 151 | ``` 152 | 153 | ## Reference 154 | 155 | ### `.when(condition: (model: TModel) => boolean, appliesTo?: 'AppliesToAllValidators' | 'AppliesToCurrentValidator')` 156 | 157 | A configuration option which controls when a particular rule or chain of rules should execute. 158 | 159 | ### `condition` 160 | 161 | This is a function which accepts the value of the base model and returns a `boolean` indicating whether the rule or chain of rules preceding it should execute. 162 | 163 | A return value of `true` indicates that the rule or chain of rules **should** execute. 164 | 165 | Conversely, a return value of `false` indicates that the rule or chain of rules **should not** execute. 166 | 167 | ### `TModel` 168 | 169 | Matches the type of the base model. 170 | 171 | ### `appliesTo` 172 | 173 | This is an optional parameter which can be used to control which rules in the current rule chain the condition applies to. 174 | 175 | A value of `'AppliesToAllValidators'` means that the `.when` condition applies to all rules in the current rule chain so far. If there are other calls to `.when` or `.unless` in the chain, only the rules defined since the most recent condition will have the condition applied to them. 176 | 177 | A value of `'AppliesToCurrentValidator'` specifies that the `.when` condition only controls the execution of the rule immediately preceding it in the current rule chain. 178 | 179 | By default, the `appliesTo` parameter is set to `'AppliesToAllValidators'`. 180 | -------------------------------------------------------------------------------- /docs/api/configuration/withMessage.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: withMessage 3 | title: '.withMessage' 4 | --- 5 | 6 | The `.withMessage` option is used to specify a custom error message that should be used when a given validation rule fails. 7 | 8 | All validation rules have a default error message associated with them, but sometimes you may wish to override these defaults and specify your own user-friendly error message. 9 | 10 | Note that `.withMessage` only applies to the rule immediately preceding it in the rule chain, not to all rules in the chain so far. 11 | 12 | ## Example 13 | 14 | ```typescript 15 | import { Validator } from 'fluentvalidation-ts'; 16 | 17 | type FormModel = { 18 | name: string; 19 | }; 20 | 21 | class FormValidator extends Validator { 22 | constructor() { 23 | super(); 24 | 25 | this.ruleFor('name') 26 | .notEmpty() 27 | // highlight-next-line 28 | .withMessage('Please enter your name') 29 | .maxLength(1000) 30 | // highlight-next-line 31 | .withMessage('Please enter no more than 1,000 characters'); 32 | } 33 | } 34 | 35 | const formValidator = new FormValidator(); 36 | 37 | formValidator.validate({ name: 'Alex' }); 38 | // ✔ {} 39 | 40 | formValidator.validate({ name: '' }); 41 | // ❌ { name: 'Please enter your name' } 42 | ``` 43 | 44 | ## Reference 45 | 46 | ### `.withMessage(customMessage: string)` 47 | 48 | A configuration option which takes a custom error message and uses that message in place of the default error message if the given validation rule fails. 49 | -------------------------------------------------------------------------------- /docs/api/core/asyncValidator.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: asyncValidator 3 | title: AsyncValidator 4 | --- 5 | 6 | The `AsyncValidator` generic class is an extension of [`Validator`](api/core/validator.md) that has additional async rules available (most notably [`.mustAsync`](api/rules/mustAsync.md) and [`.setAsyncValidator`](api/rules/setAsyncValidator.md)). 7 | 8 | ```typescript 9 | import { AsyncValidator } from 'fluentvalidation-ts'; 10 | ``` 11 | 12 | Defining an async validator for a model of type `TModel` works exactly the same as defining a standard validator - all you have to do is define a class which extends `AsyncValidator` (as opposed to `Validator`) and specify some rules in the constructor using the [`.ruleFor`](api/core/ruleFor.md) and [`.ruleForEach`](api/core/ruleForEach.md) methods. 13 | 14 | ```typescript 15 | type FormModel = { username: string }; 16 | 17 | class FormValidator extends AsyncValidator { 18 | constructor() { 19 | super(); 20 | 21 | this.ruleFor('username').mustAsync(async (username) => 22 | await api.usernameIsAvailable(username); 23 | ) 24 | .withMessage('This username is already taken'); 25 | } 26 | } 27 | ``` 28 | 29 | To actually validate an instance of your model, simply create an instance of your validator and pass your model to the `.validateAsync` method. As the name suggests this method is **asynchronous**, so be sure to `await` the result or use Promise callback methods (i.e. `.then` and `.catch`). 30 | 31 | Note that the synchronous `.validate` method is **not available** on an instance of `AsyncValidator`, you must always use the `.validateAsync` method. 32 | 33 | ```typescript 34 | const formValidator = new FormValidator(); 35 | 36 | const validResult = await formValidator.validateAsync({ 37 | username: 'ajp_dev123', 38 | }); 39 | // ✔ {} 40 | 41 | const invalidResult = await formValidator.validateAsync({ 42 | username: 'ajp_dev', 43 | }); 44 | // ❌ { username: 'This username is already taken' } 45 | ``` 46 | 47 | A call to `.validateAsync` returns a `Promise` that resolves to an object of type [`ValidationErrors`](api/core/validationErrors.md), which describes the validity of the given value. 48 | -------------------------------------------------------------------------------- /docs/api/core/ruleFor.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: ruleFor 3 | title: '.ruleFor' 4 | --- 5 | 6 | The `.ruleFor` method on the `Validator` class is used to build up rule chains for properties on your model. 7 | 8 | To get started, simply call `this.ruleFor` in the constructor of your validator and pass in the name of a property on your model (note that this is strongly typed, you'll get a compilation error if you pass the name of a property that doesn't exist on the model). 9 | 10 | The result of this call is a rule chain builder that exposes all the relevant built-in validation rules for the property you specified. 11 | 12 | ```typescript 13 | import { Validator } from 'fluentvalidation-ts'; 14 | 15 | type FormModel = { 16 | name: string; 17 | isEmployed: boolean; 18 | jobTitle: string | null; 19 | }; 20 | 21 | class FormValidator extends Validator { 22 | constructor() { 23 | super(); 24 | 25 | // Returns a rule chain builder for the 'name' property 26 | this.ruleFor('name'); 27 | } 28 | } 29 | ``` 30 | 31 | To add a validation rule to the target property, simply call the relevant method on the rule chain builder (passing in any parameters as necessary). The result of such a call is again the rule chain builder, so you can specify multiple rules in a single call to `.ruleFor`. 32 | 33 | ```typescript 34 | // The result of adding a rule is again the rule chain builder, 35 | // so you can add multiple rules in a single call 36 | this.ruleFor('name').notEmpty().maxLength(100); 37 | ``` 38 | 39 | After adding a rule to the chain you also gain access to a number of configuration methods which allow you to do things like specify what error should be used if the validation rule fails, and conditions under which the rules should/shouldn't run. 40 | 41 | ```typescript 42 | this.ruleFor('name').notEmpty().maxLength(100); 43 | 44 | // You can specify a custom error message for each rule in the chain, 45 | // and provide a condition to determine when the rules should run 46 | this.ruleFor('jobTitle') 47 | .notEmpty() 48 | .withMessage('Please enter a Job Title') 49 | .maxLength(100) 50 | .withMessage('Please enter no more than 100 characters') 51 | // highlight-next-line 52 | .when((formModel) => formModel.isEmployed); 53 | 54 | // You can also provide a condition to determine when certain rules 55 | // should not run 56 | this.ruleFor('jobTitle') 57 | .equal('') 58 | .withMessage('You cannot enter a Job Title if you are not employed') 59 | // highlight-next-line 60 | .unless((formModel) => formModel.isEmployed); 61 | ``` 62 | 63 | As the above example illustrates, you can make several calls to `.ruleFor` for the same property. It doesn't matter how many rule chains you define for a particular property, and you don't have to define any at all if you don't need to. 64 | -------------------------------------------------------------------------------- /docs/api/core/ruleForEach.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: ruleForEach 3 | title: '.ruleForEach' 4 | --- 5 | 6 | The `.ruleForEach` method on the `Validator` class is much like the [`.ruleFor`](api/core/ruleFor.md) method, except that is used to build up rule chains for **array** properties on your model. 7 | 8 | You can use `.ruleForEach` to specify a rule chain that should apply to **each element** of a particular array property. Aside from this, the `.ruleForEach` method works almost exactly the same as the [`.ruleFor`](api/core/ruleFor.md) method, with all the chaining and configuration available in exactly the same way. 9 | 10 | ```typescript 11 | import { Validator } from 'fluentvalidation-ts'; 12 | 13 | type FormModel = { scores: Array }; 14 | 15 | class FormValidator extends Validator { 16 | constructor() { 17 | super(); 18 | 19 | this.ruleForEach('scores') 20 | .greaterThan(0) 21 | .withMessage('Please enter a positive score') 22 | .lessThanOrEqualTo(5) 23 | .withMessage('Please enter a score no greater than 5'); 24 | } 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/api/core/ruleForEachTransformed.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: ruleForEachTransformed 3 | title: '.ruleForEachTransformed' 4 | --- 5 | 6 | The `.ruleForEachTransformed` method on the `Validator` class is identical to the [`.ruleForEach`](api/core/ruleForEach.md) method, except that it allows you to transform each item of the given array property on your model via a transformation function prior to building up the rule chain for it. 7 | 8 | The available validation rules will be based on the type of the **transformed** items, rather than the original type of the items. 9 | 10 | To get started, simply call `this.ruleForTransformed` in the constructor of your validator and pass in the name of an array property on your model, along with a transformation function. 11 | 12 | The result of this call is a rule chain builder, exactly the same as that returned by [`.ruleForEach`](api/core/ruleForEach.md), except that it exposes all the relevant built-in validation rules for the type of the **transformed** item values. 13 | 14 | ```typescript 15 | import { Validator } from 'fluentvalidation-ts'; 16 | 17 | type FormModel = { 18 | scores: Array; 19 | }; 20 | 21 | class FormValidator extends Validator { 22 | constructor() { 23 | super(); 24 | 25 | this.ruleForEachTransformed('scores', (s) => Number(s)) 26 | .must((numberScore) => !isNaN(numberScore)) 27 | .greaterThan(0) 28 | .lessThanOrEqualTo(100); 29 | } 30 | } 31 | ``` 32 | 33 | ## Limitations 34 | 35 | The same limitations that apply to the [`.ruleForTransformed`](api/core/ruleForTransformed.md) method apply also to the `.ruleForEachTransformed` method. 36 | -------------------------------------------------------------------------------- /docs/api/core/ruleForTransformed.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: ruleForTransformed 3 | title: '.ruleForTransformed' 4 | --- 5 | 6 | The `.ruleForTransformed` method on the `Validator` class is identical to the [`.ruleFor`](api/core/ruleFor.md) method, except that it allows you to transform the given property on your model via a transformation function prior to building up the rule chain for it. 7 | 8 | The available validation rules will be based on the type of the **transformed** value, rather than the original type of the property. 9 | 10 | To get started, simply call `this.ruleForTransformed` in the constructor of your validator and pass in the name of a property on your model, along with a transformation function. 11 | 12 | The result of this call is a rule chain builder, exactly the same as that returned by [`.ruleFor`](api/core/ruleFor.md), except that it exposes all the relevant built-in validation rules for the type of the **transformed** property value. 13 | 14 | ```typescript 15 | import { Validator } from 'fluentvalidation-ts'; 16 | 17 | type FormModel = { 18 | quantity: string; 19 | }; 20 | 21 | class FormValidator extends Validator { 22 | constructor() { 23 | super(); 24 | 25 | this.ruleForTransformed('quantity', (q) => Number(q)) 26 | .must((numberQuantity) => !isNaN(numberQuantity)) 27 | .greaterThan(0) 28 | .lessThanOrEqualTo(100); 29 | } 30 | } 31 | ``` 32 | 33 | ## Limitations 34 | 35 | Note that in order to preserve the shape of the [errors object](api/core/validationErrors.md) returned by the `.validate` and `.validateAsync` methods, the transformation function passed to `.ruleForTransformed` cannot map flat types into complex types. 36 | 37 | For example, a `string` property cannot be transformed into an `Array`. This is because the errors object could then contain an array of errors at the path of the `string` property, while the expected type at this path is a "flat" error (i.e. `string | null | undefined`). 38 | 39 | For the same reasons, complex types cannot be mapped to other complex types that look different. For example, if an `object` property is mapped to another `object` with different properties, then the errors object could contain nested errors at the path of the property with unexpected keys (i.e. keys not present on the original type of the property). 40 | 41 | It is possible to map complex types to flat types, or complex types to other complex types with some/all of the same properties. This is because the shape of the errors object is preserved in these cases. 42 | -------------------------------------------------------------------------------- /docs/api/core/validator.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: validator 3 | title: Validator 4 | --- 5 | 6 | ## Validator<TModel> 7 | 8 | The `Validator` generic class is the core component of the **fluentvalidation-ts** API. 9 | 10 | ```typescript 11 | import { Validator } from 'fluentvalidation-ts'; 12 | ``` 13 | 14 | To define a validator for a model of type `TModel` all you have to do is define a class which extends `Validator` and specify some rules in the constructor using the `.ruleFor` and `.ruleForEach` methods. 15 | 16 | ```typescript 17 | type FormModel = { name: string }; 18 | 19 | class FormValidator extends Validator { 20 | constructor() { 21 | super(); 22 | 23 | this.ruleFor('name').notEmpty().withMessage('Please enter your name'); 24 | } 25 | } 26 | ``` 27 | 28 | ## .validate 29 | 30 | To actually validate an instance of your model, simply create an instance of your validator and pass your model to the `.validate` method. 31 | 32 | ```typescript 33 | const formValidator = new FormValidator(); 34 | 35 | const validResult = formValidator.validate({ name: 'Alex' }); 36 | // ✔ {} 37 | 38 | const invalidResult = formValidator.validate({ name: '' }); 39 | // ❌ { name: 'Please enter your name' } 40 | ``` 41 | 42 | A call to `.validate` returns an object of type `ValidationErrors`, which describes the validity of the given value. 43 | -------------------------------------------------------------------------------- /docs/api/rules/emailAddress.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: emailAddress 3 | title: '.emailAddress' 4 | --- 5 | 6 | The `.emailAddress` rule is used to ensure that the value of a given `string` property is a valid email address. 7 | 8 | ## Example 9 | 10 | ```typescript 11 | import { Validator } from 'fluentvalidation-ts'; 12 | 13 | type FormModel = { 14 | contactEmail: string; 15 | }; 16 | 17 | class FormValidator extends Validator { 18 | constructor() { 19 | super(); 20 | 21 | this.ruleFor('contactEmail').emailAddress(); 22 | } 23 | } 24 | 25 | const formValidator = new FormValidator(); 26 | 27 | formValidator.validate({ contactEmail: 'foo@example.com' }); 28 | // ✔ {} 29 | 30 | formValidator.validate({ contactEmail: 'foo' }); 31 | // ❌ { contactEmail: 'Not a valid email address' } 32 | ``` 33 | 34 | ## Reference 35 | 36 | ### `.emailAddress()` 37 | 38 | A string validation rule which ensures that the given property is a valid email address. 39 | 40 | ## Example Message 41 | 42 | > Not a valid email address 43 | -------------------------------------------------------------------------------- /docs/api/rules/equal.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: equal 3 | title: '.equal' 4 | --- 5 | 6 | The `.equal` rule is used to ensure that the value of a given property is equal to a given value. 7 | 8 | Note that this rule uses **strict** equality (i.e. the `===` operator) and may not work as intended for object or array values. 9 | 10 | ## Example 11 | 12 | ```typescript 13 | import { Validator } from 'fluentvalidation-ts'; 14 | 15 | type FormModel = { 16 | acceptsTermsAndConditions: boolean; 17 | }; 18 | 19 | class FormValidator extends Validator { 20 | constructor() { 21 | super(); 22 | 23 | this.ruleFor('acceptsTermsAndConditions').equal(true); 24 | } 25 | } 26 | 27 | const formValidator = new FormValidator(); 28 | 29 | formValidator.validate({ acceptsTermsAndConditions: true }); 30 | // ✔ {} 31 | 32 | formValidator.validate({ acceptsTermsAndConditions: false }); 33 | // ❌ { acceptsTermsAndConditions: `Must equal 'true'` } 34 | ``` 35 | 36 | ## Reference 37 | 38 | ### `.equal(comparisonValue: TValue)` 39 | 40 | A base validation rule which takes in a value and ensures that the given property is equal to that value. 41 | 42 | ### `TValue` 43 | 44 | Matches the type of the property that the rule is applied to. 45 | 46 | ## Example Message 47 | 48 | > Must equal '`[comparisonValue]`' 49 | -------------------------------------------------------------------------------- /docs/api/rules/exclusiveBetween.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: exclusiveBetween 3 | title: '.exclusiveBetween' 4 | --- 5 | 6 | The `.exclusiveBetween` rule is used to ensure that the value of a given `number` property is exclusively between the given bounds (i.e. greater than the lower bound and less than the upper bound). 7 | 8 | ## Example 9 | 10 | ```typescript 11 | import { Validator } from 'fluentvalidation-ts'; 12 | 13 | type FormModel = { 14 | score: number; 15 | }; 16 | 17 | class FormValidator extends Validator { 18 | constructor() { 19 | super(); 20 | 21 | this.ruleFor('score').exclusiveBetween(0, 10); 22 | } 23 | } 24 | 25 | const formValidator = new FormValidator(); 26 | 27 | formValidator.validate({ score: 5 }); 28 | // ✔ {} 29 | 30 | formValidator.validate({ score: 0 }); 31 | // ❌ { score: 'Value must be between 0 and 10 (exclusive)' } 32 | ``` 33 | 34 | ## Reference 35 | 36 | ### `.exclusiveBetween(lowerBound: number, upperBound: number)` 37 | 38 | A number validation rule which takes in a lower bound and upper bound and ensures that the given property is exclusively between them (i.e. greater than the lower bound and less than the upper bound). 39 | 40 | ## Example Message 41 | 42 | > Value must be between `[lowerBound]` and `[upperBound]` (exclusive) 43 | -------------------------------------------------------------------------------- /docs/api/rules/greaterThan.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: greaterThan 3 | title: '.greaterThan' 4 | --- 5 | 6 | The `.greaterThan` rule is used to ensure that the value of a given `number` property is strictly greater than a given value. 7 | 8 | ## Example 9 | 10 | ```typescript 11 | import { Validator } from 'fluentvalidation-ts'; 12 | 13 | type FormModel = { 14 | quantity: number; 15 | }; 16 | 17 | class FormValidator extends Validator { 18 | constructor() { 19 | super(); 20 | 21 | this.ruleFor('quantity').greaterThan(0); 22 | } 23 | } 24 | 25 | const formValidator = new FormValidator(); 26 | 27 | formValidator.validate({ quantity: 2 }); 28 | // ✔ {} 29 | 30 | formValidator.validate({ quantity: 0 }); 31 | // ❌ { quantity: 'Value must be greater than 0' } 32 | ``` 33 | 34 | ## Reference 35 | 36 | ### `.greaterThan(threshold: number)` 37 | 38 | A number validation rule which takes in a threshold and ensures that the given property is strictly greater than it. 39 | 40 | ## Example Message 41 | 42 | > Value must be greater than `[threshold]` 43 | -------------------------------------------------------------------------------- /docs/api/rules/greaterThanOrEqualTo.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: greaterThanOrEqualTo 3 | title: '.greaterThanOrEqualTo' 4 | --- 5 | 6 | The `.greaterThanOrEqualTo` rule is used to ensure that the value of a given `number` property is greater than or equal to a given value. 7 | 8 | ## Example 9 | 10 | ```typescript 11 | import { Validator } from 'fluentvalidation-ts'; 12 | 13 | type FormModel = { 14 | age: number; 15 | }; 16 | 17 | class FormValidator extends Validator { 18 | constructor() { 19 | super(); 20 | 21 | this.ruleFor('age').greaterThanOrEqualTo(18); 22 | } 23 | } 24 | 25 | const formValidator = new FormValidator(); 26 | 27 | formValidator.validate({ age: 18 }); 28 | // ✔ {} 29 | 30 | formValidator.validate({ age: 16 }); 31 | // ❌ { age: 'Value must be greater than or equal to 18' } 32 | ``` 33 | 34 | ## Reference 35 | 36 | ### `.greaterThanOrEqualTo(threshold: number)` 37 | 38 | A number validation rule which takes in a threshold and ensures that the given property is greater than or equal to it. 39 | 40 | ## Example Message 41 | 42 | > Value must be greater than or equal to `[threshold]` 43 | -------------------------------------------------------------------------------- /docs/api/rules/inclusiveBetween.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: inclusiveBetween 3 | title: '.inclusiveBetween' 4 | --- 5 | 6 | The `.inclusiveBetween` rule is used to ensure that the value of a given `number` property is inclusively between the given bounds (i.e. greater than or equal to the lower bound and less than or equal to the upper bound). 7 | 8 | ## Example 9 | 10 | ```typescript 11 | import { Validator } from 'fluentvalidation-ts'; 12 | 13 | type FormModel = { 14 | percentageComplete: number; 15 | }; 16 | 17 | class FormValidator extends Validator { 18 | constructor() { 19 | super(); 20 | 21 | this.ruleFor('percentageComplete').inclusiveBetween(0, 100); 22 | } 23 | } 24 | 25 | const formValidator = new FormValidator(); 26 | 27 | formValidator.validate({ percentageComplete: 50 }); 28 | // ✔ {} 29 | 30 | formValidator.validate({ percentageComplete: 110 }); 31 | // ❌ { percentageComplete: 'Value must be between 0 and 100 (inclusive)' } 32 | ``` 33 | 34 | ## Reference 35 | 36 | ### `.inclusiveBetween(lowerBound: number, upperBound: number)` 37 | 38 | A number validation rule which takes in a lower bound and upper bound and ensures that the given property is inclusively between them (i.e. greater than or equal to the lower bound and less than or equal to the upper bound). 39 | 40 | ## Example Message 41 | 42 | > Value must be between `[lowerBound]` and `[upperBound]` (inclusive) 43 | -------------------------------------------------------------------------------- /docs/api/rules/length.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: length 3 | title: '.length' 4 | --- 5 | 6 | The `.length` rule is used to ensure that the length of a given `string` property is inclusively between the given bounds (i.e. greater than or equal to the lower bound and less than or equal to the upper bound). 7 | 8 | ## Example 9 | 10 | ```typescript 11 | import { Validator } from 'fluentvalidation-ts'; 12 | 13 | type FormModel = { 14 | voucherCode: string; 15 | }; 16 | 17 | class FormValidator extends Validator { 18 | constructor() { 19 | super(); 20 | 21 | this.ruleFor('voucherCode').length(5, 10); 22 | } 23 | } 24 | 25 | const formValidator = new FormValidator(); 26 | 27 | formValidator.validate({ voucherCode: 'ABC44' }); 28 | // ✔ {} 29 | 30 | formValidator.validate({ voucherCode: 'ZZ' }); 31 | // ❌ { voucherCode: 'Value must be between 5 and 10 characters long' } 32 | ``` 33 | 34 | ## Reference 35 | 36 | ### `.length(lowerBound: number, upperBound: number)` 37 | 38 | A string validation rule which takes in a lower bound and upper bound and ensures that the length of the given property is inclusively between them (i.e. greater than or equal to the lower bound and less than or equal to the upper bound). 39 | 40 | ## Example Message 41 | 42 | > Value must be between `[lowerBound]` and `[upperBound]` characters long 43 | -------------------------------------------------------------------------------- /docs/api/rules/lessThan.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: lessThan 3 | title: '.lessThan' 4 | --- 5 | 6 | The `.lessThan` rule is used to ensure that the value of a given `number` property is strictly less than a given value. 7 | 8 | ## Example 9 | 10 | ```typescript 11 | import { Validator } from 'fluentvalidation-ts'; 12 | 13 | type FormModel = { 14 | bagWeightInKilograms: number; 15 | }; 16 | 17 | class FormValidator extends Validator { 18 | constructor() { 19 | super(); 20 | 21 | this.ruleFor('bagWeightInKilograms').lessThan(20); 22 | } 23 | } 24 | 25 | const formValidator = new FormValidator(); 26 | 27 | formValidator.validate({ bagWeightInKilograms: 18.5 }); 28 | // ✔ {} 29 | 30 | formValidator.validate({ bagWeightInKilograms: 22.8 }); 31 | // ❌ { bagWeightInKilograms: 'Value must be less than 20' } 32 | ``` 33 | 34 | ## Reference 35 | 36 | ### `.lessThan(threshold: number)` 37 | 38 | A number validation rule which takes in a threshold and ensures that the given property is strictly less than it. 39 | 40 | ## Example Message 41 | 42 | > Value must be less than `[threshold]` 43 | -------------------------------------------------------------------------------- /docs/api/rules/lessThanOrEqualTo.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: lessThanOrEqualTo 3 | title: '.lessThanOrEqualTo' 4 | --- 5 | 6 | The `.lessThanOrEqualTo` rule is used to ensure that the value of a given `number` property is less than or equal to a given value. 7 | 8 | ## Example 9 | 10 | ```typescript 11 | import { Validator } from 'fluentvalidation-ts'; 12 | 13 | type FormModel = { 14 | passengers: number; 15 | }; 16 | 17 | class FormValidator extends Validator { 18 | constructor() { 19 | super(); 20 | 21 | this.ruleFor('passengers').lessThanOrEqualTo(4); 22 | } 23 | } 24 | 25 | const formValidator = new FormValidator(); 26 | 27 | formValidator.validate({ passengers: 4 }); 28 | // ✔ {} 29 | 30 | formValidator.validate({ passengers: 6 }); 31 | // ❌ { passengers: 'Value must be less than or equal to 4' } 32 | ``` 33 | 34 | ## Reference 35 | 36 | ### `.lessThanOrEqualTo(threshold: number)` 37 | 38 | A number validation rule which takes in a threshold and ensures that the given property is less than or equal to it. 39 | 40 | ## Example Message 41 | 42 | > Value must be less than or equal to `[threshold]` 43 | -------------------------------------------------------------------------------- /docs/api/rules/matches.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: matches 3 | title: '.matches' 4 | --- 5 | 6 | The `.matches` rule is used to ensure that the value of a given `string` property matches the given [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp). 7 | 8 | ## Example 9 | 10 | ```typescript 11 | import { Validator } from 'fluentvalidation-ts'; 12 | 13 | type FormModel = { 14 | price: string; 15 | }; 16 | 17 | class FormValidator extends Validator { 18 | constructor() { 19 | super(); 20 | 21 | this.ruleFor('price').matches(new RegExp('^([0-9])+.([0-9]){2}$')); 22 | } 23 | } 24 | 25 | const formValidator = new FormValidator(); 26 | 27 | formValidator.validate({ price: '249.99' }); 28 | // ✔ {} 29 | 30 | formValidator.validate({ price: '15' }); 31 | // ❌ { price: 'Value does not match the required pattern' } 32 | ``` 33 | 34 | ## Reference 35 | 36 | ### `.matches(pattern: RegExp)` 37 | 38 | A string validation rule which takes in a [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) and ensures that the given property matches it. 39 | 40 | ## Example Message 41 | 42 | > Value does not match the required pattern 43 | -------------------------------------------------------------------------------- /docs/api/rules/maxLength.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: maxLength 3 | title: '.maxLength' 4 | --- 5 | 6 | The `.maxLength` rule is used to ensure that the length of a given `string` property is less than or equal to a given value. 7 | 8 | ## Example 9 | 10 | ```typescript 11 | import { Validator } from 'fluentvalidation-ts'; 12 | 13 | type FormModel = { 14 | username: string; 15 | }; 16 | 17 | class FormValidator extends Validator { 18 | constructor() { 19 | super(); 20 | 21 | this.ruleFor('username').maxLength(20); 22 | } 23 | } 24 | 25 | const formValidator = new FormValidator(); 26 | 27 | formValidator.validate({ username: 'AlexPotter' }); 28 | // ✔ {} 29 | 30 | formValidator.validate({ username: 'ThisUsernameIsFarTooLong' }); 31 | // ❌ { username: 'Value must be no more than 20 characters long' } 32 | ``` 33 | 34 | ## Reference 35 | 36 | ### `.maxLength(upperBound: number)` 37 | 38 | A string validation rule which takes in an upper bound and ensures that the length of the given property is less than or equal to it. 39 | 40 | ## Example Message 41 | 42 | > Value must be no more than `[upperBound]` characters long 43 | -------------------------------------------------------------------------------- /docs/api/rules/minLength.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: minLength 3 | title: '.minLength' 4 | --- 5 | 6 | The `.minLength` rule is used to ensure that the length of a given `string` property is greater than or equal to a given value. 7 | 8 | ## Example 9 | 10 | ```typescript 11 | import { Validator } from 'fluentvalidation-ts'; 12 | 13 | type FormModel = { 14 | password: string; 15 | }; 16 | 17 | class FormValidator extends Validator { 18 | constructor() { 19 | super(); 20 | 21 | this.ruleFor('password').minLength(6); 22 | } 23 | } 24 | 25 | const formValidator = new FormValidator(); 26 | 27 | formValidator.validate({ password: 'supersecret' }); 28 | // ✔ {} 29 | 30 | formValidator.validate({ password: 'foo' }); 31 | // ❌ { password: 'Value must be at least 6 characters long' } 32 | ``` 33 | 34 | ## Reference 35 | 36 | ### `.minLength(lowerBound: number)` 37 | 38 | A string validation rule which takes in a lower bound and ensures that the length of the given property is greater than or equal to it. 39 | 40 | ## Example Message 41 | 42 | > Value must be at least `[lowerBound]` characters long 43 | -------------------------------------------------------------------------------- /docs/api/rules/mustAsync.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: mustAsync 3 | title: '.mustAsync' 4 | --- 5 | 6 | The `.mustAsync` rule is one of the special async rules that become available when you extend from [`AsyncValidator`](api/core/asyncValidator.md) as opposed to just [`Validator`](api/core/validator.md). 7 | 8 | This rule works exactly the same as the [`.must`](api/rules/must.md) rule, except that it takes an async predicate function. This allows you to do things like define custom validation rules which perform API requests (e.g. checking if a username is already taken). 9 | 10 | All the various overloads for the [`.must`](api/rules/must.md) rule are also available for the `.mustAsync` rule - the only difference is that your predicate function must be async (i.e. have a return type of `Promise` instead of `boolean`). 11 | 12 | ## Examples 13 | 14 | The documentation page for the [`.must`](api/rules/must.md) rule includes a full list of examples demonstrating the different overloads that are available. 15 | 16 | These are all relevant to the `.mustAsync` rule too, just replace `Validator` with `AsyncValidator`, `.must` with `.mustAsync`, and synchronous predicate functions with asynchronous ones. 17 | 18 | ### Predicate dependent on value 19 | 20 | In this example we specify an async predicate on its own, which is dependent only on the value of the property we're validating. 21 | 22 | ```typescript 23 | import { AsyncValidator } from 'fluentvalidation-ts'; 24 | 25 | type FormModel = { 26 | username: string; 27 | }; 28 | 29 | class FormValidator extends AsyncValidator { 30 | constructor() { 31 | super(); 32 | 33 | // highlight-start 34 | this.ruleFor('username').mustAsync( 35 | async (username) => await api.usernameIsAvailable(username) 36 | ); 37 | // highlight-end 38 | } 39 | } 40 | 41 | const formValidator = new FormValidator(); 42 | 43 | await formValidator.validateAsync({ username: 'ajp_dev123' }); 44 | // ✔ {} 45 | 46 | await formValidator.validateAsync({ username: 'ajp_dev' }); 47 | // ❌ { username: 'Value is not valid' } 48 | ``` 49 | 50 | ## Reference 51 | 52 | The `.mustAsync` rule is one of the more complex built-in rules. You may wish to refer to the examples on the documentation page for the [`.must`](api/rules/must.md) rule to help you understand the different variations of this rule. 53 | 54 | ### `.mustAsync(predicate: SimpleAsyncPredicate)` 55 | 56 | A validation rule which takes in a simple async predicate function and ensures that the given property is valid according to that predicate function. 57 | 58 | ### `.mustAsync(predicateAndMessage: SimpleAsyncPredicateWithMessage)` 59 | 60 | A validation rule which takes in a definition that specifies both an async predicate function and a message (or message generator), and ensures that the given property is valid according to the given predicate function (exposing the relevant message if validation fails). 61 | 62 | ### `.mustAsync(definitions: Array | SimpleAsyncPredicateWithMessage>)` 63 | 64 | A validation rule which takes in an array of async predicate functions and/or predicate function and message (or message generator) pairs, and ensures that the given property is valid according to each one (exposing a relevant message for the first failing predicate if validation fails). 65 | 66 | ### `SimpleAsyncPredicateWithMessage` 67 | 68 | Equivalent to `{ predicate: SimpleAsyncPredicate; message: string | MessageGenerator }` 69 | 70 | An object that specifies both an async predicate function and a message (or message generator). The predicate function is used to determine whether a given value is valid, and the message (either explicit or generated) is used in the validation errors object if validation fails. 71 | 72 | ### `SimpleAsyncPredicate` 73 | 74 | Equivalent to `(value: TValue, model: TModel) => Promise`. 75 | 76 | A simple predicate is an async function which accepts the value of the property being validated and the value of the model as a whole, and returns a `Promise` indicating whether the property is valid or not. 77 | 78 | A return value that resolves to `true` indicates that the property is valid ✔. 79 | 80 | Conversely, a return value that resolves to `false` indicates that the property is invalid ❌. 81 | 82 | ### `MessageGenerator` 83 | 84 | Equivalent to `(value: TValue, model: TModel) => string`. 85 | 86 | A function which accepts both the value being validated and the model as a whole, and returns an appropriate error message. 87 | 88 | ### `TValue` 89 | 90 | Matches the type of the property that the rule is applied to. 91 | 92 | ### `TModel` 93 | 94 | Matches the type of the base model. 95 | 96 | ## Example Message 97 | 98 | > Value is not valid 99 | -------------------------------------------------------------------------------- /docs/api/rules/notEmpty.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: notEmpty 3 | title: '.notEmpty' 4 | --- 5 | 6 | The `.notEmpty` rule is used to ensure that the value of a given `string` property is not the empty string, or formed entirely of whitespace. 7 | 8 | ## Example 9 | 10 | ```typescript 11 | import { Validator } from 'fluentvalidation-ts'; 12 | 13 | type FormModel = { 14 | name: string; 15 | }; 16 | 17 | class FormValidator extends Validator { 18 | constructor() { 19 | super(); 20 | 21 | this.ruleFor('name').notEmpty(); 22 | } 23 | } 24 | 25 | const formValidator = new FormValidator(); 26 | 27 | formValidator.validate({ name: 'Alex' }); 28 | // ✔ {} 29 | 30 | formValidator.validate({ name: ' ' }); 31 | // ❌ { name: 'Value cannot be empty' } 32 | ``` 33 | 34 | ## Reference 35 | 36 | ### `.notEmpty()` 37 | 38 | A string validation rule which ensures that the given property is not the empty string, or formed entirely of whitespace. 39 | 40 | ## Example Message 41 | 42 | > Value cannot be empty 43 | -------------------------------------------------------------------------------- /docs/api/rules/notEqual.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: notEqual 3 | title: '.notEqual' 4 | --- 5 | 6 | The `.notEqual` rule is used to ensure that the value of a given property is not equal to a given value. 7 | 8 | Note that this rule uses **strict** inequality (i.e. the `!==` operator) and may not work as intended for object or array values. 9 | 10 | ## Example 11 | 12 | ```typescript 13 | import { Validator } from 'fluentvalidation-ts'; 14 | 15 | type FormModel = { 16 | acceptsTermsAndConditions: boolean; 17 | }; 18 | 19 | class FormValidator extends Validator { 20 | constructor() { 21 | super(); 22 | 23 | this.ruleFor('acceptsTermsAndConditions').notEqual(false); 24 | } 25 | } 26 | 27 | const formValidator = new FormValidator(); 28 | 29 | formValidator.validate({ acceptsTermsAndConditions: true }); 30 | // ✔ {} 31 | 32 | formValidator.validate({ acceptsTermsAndConditions: false }); 33 | // ❌ { acceptsTermsAndConditions: `Value must not equal 'false'` } 34 | ``` 35 | 36 | ## Reference 37 | 38 | ### `.notEqual(comparisonValue: TValue)` 39 | 40 | A base validation rule which takes in a value and ensures that the given property is not equal to that value. 41 | 42 | ### `TValue` 43 | 44 | Matches the type of the property that the rule is applied to. 45 | 46 | ## Example Message 47 | 48 | > Value must not equal '`[comparisonValue]`' 49 | -------------------------------------------------------------------------------- /docs/api/rules/notNull.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: notNull 3 | title: '.notNull' 4 | --- 5 | 6 | The `.notNull` rule is used to ensure that the value of a given property is not `null` (including `undefined` by default, though this is configurable). 7 | 8 | :::tip 9 | 10 | If you only want to check for `undefined` values, you may use the [`.notUndefined`](./notUndefined.md) rule instead. 11 | 12 | ::: 13 | 14 | ## Examples 15 | 16 | ### Default Usage 17 | 18 | If you don't specify any options, the rule will check that the given value is not `null` or `undefined`. 19 | 20 | In other words, the `includeUndefined` option is defaulted to `true` - this decision was made to avoid introducing a breaking change. 21 | 22 | In this setup, both `null` and `undefined` values will be considered invalid. 23 | 24 | ```typescript 25 | import { Validator } from 'fluentvalidation-ts'; 26 | 27 | type FormModel = { 28 | customerId?: number | null; 29 | }; 30 | 31 | class FormValidator extends Validator { 32 | constructor() { 33 | super(); 34 | 35 | this.ruleFor('customerId').notNull(); 36 | } 37 | } 38 | 39 | const formValidator = new FormValidator(); 40 | 41 | formValidator.validate({ customerId: 100 }); 42 | // ✔ {} 43 | 44 | formValidator.validate({ customerId: null }); 45 | // ❌ { customerId: 'Value cannot be null' } 46 | 47 | formValidator.validate({ customerId: undefined }); 48 | // ❌ { customerId: 'Value cannot be null' } 49 | 50 | formValidator.validate({}); 51 | // ❌ { customerId: 'Value cannot be null' } 52 | ``` 53 | 54 | ### Excluding `undefined` 55 | 56 | The behaviour of the `.notNull` rule can be made "strict" (in the sense that it only checks for `null` and not `undefined`) by passing the `includeUndefined` option as `false`. 57 | 58 | In this setup, `undefined` values will be allowed, and only `null` values will be considered invalid. 59 | 60 | ```typescript 61 | import { Validator } from 'fluentvalidation-ts'; 62 | 63 | type FormModel = { 64 | customerId?: number | null; 65 | }; 66 | 67 | class FormValidator extends Validator { 68 | constructor() { 69 | super(); 70 | 71 | // highlight-next-line 72 | this.ruleFor('customerId').notNull({ includeUndefined: false }); 73 | } 74 | } 75 | 76 | const formValidator = new FormValidator(); 77 | 78 | formValidator.validate({ customerId: 100 }); 79 | // ✔ {} 80 | 81 | formValidator.validate({ customerId: null }); 82 | // ❌ { customerId: 'Value cannot be null' } 83 | 84 | // highlight-start 85 | formValidator.validate({ customerId: undefined }); 86 | // ✔ {} 87 | 88 | formValidator.validate({}); 89 | // ✔ {} 90 | // highlight-end 91 | ``` 92 | 93 | ## Reference 94 | 95 | ### `.notNull(ruleOptions?: NotNullRuleOptions)` 96 | 97 | A validation rule which ensures that the given property is not `null` (or `undefined`, depending on the value of `ruleOptions`). 98 | 99 | The default value of `ruleOptions` is `{ includeUndefined: true }`, meaning that both `null` and `undefined` values will be considered invalid. 100 | 101 | ### `NotNullRuleOptions` 102 | 103 | Equivalent to `{ includeUndefined: boolean }`, where the `includeUndefined` property determines whether `undefined` values should be considered invalid. 104 | 105 | When `includeUndefined` is `true`, both `null` and `undefined` values will be considered invalid. 106 | 107 | When `includeUndefined` is `false`, only `null` values will be considered invalid, and `undefined` values will be allowed. 108 | 109 | ## Example Message 110 | 111 | > Value cannot be null 112 | -------------------------------------------------------------------------------- /docs/api/rules/notUndefined.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: notUndefined 3 | title: '.notUndefined' 4 | --- 5 | 6 | The `.notUndefined` rule is used to ensure that the value of a given property is not `undefined`. 7 | 8 | :::note 9 | 10 | Note that this rule considers `null` values to be **valid**. If you need to disallow both `null` and `undefined` values (or just `null` values), you may use the [`.notNull`](./notNull.md) rule instead. 11 | 12 | ::: 13 | 14 | ## Example 15 | 16 | ```typescript 17 | import { Validator } from 'fluentvalidation-ts'; 18 | 19 | type FormModel = { 20 | customerId?: number | null; 21 | }; 22 | 23 | class FormValidator extends Validator { 24 | constructor() { 25 | super(); 26 | 27 | this.ruleFor('customerId').notUndefined(); 28 | } 29 | } 30 | 31 | const formValidator = new FormValidator(); 32 | 33 | formValidator.validate({ customerId: 100 }); 34 | // ✔ {} 35 | 36 | formValidator.validate({ customerId: null }); 37 | // ✔ {} 38 | 39 | formValidator.validate({}); 40 | // ❌ { customerId: 'Value cannot be undefined' } 41 | 42 | formValidator.validate({ customerId: undefined }); 43 | // ❌ { customerId: 'Value cannot be undefined' } 44 | ``` 45 | 46 | ## Reference 47 | 48 | ### `.notUndefined()` 49 | 50 | A validation rule which ensures that the given property is not `undefined`. 51 | 52 | ## Example Message 53 | 54 | > Value cannot be undefined 55 | -------------------------------------------------------------------------------- /docs/api/rules/null.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: nullRule 3 | title: '.null' 4 | --- 5 | 6 | The `.null` rule is used to ensure that the value of a given property is `null` (or `undefined` by default, though this is configurable). 7 | 8 | :::tip 9 | 10 | If you only want to check for `undefined` values, you may use the [`.undefined`](./undefined.md) rule instead. 11 | 12 | ::: 13 | 14 | ## Examples 15 | 16 | ### Default Usage 17 | 18 | If you don't specify any options, the rule will check that the given value is `null` or `undefined`. 19 | 20 | In other words, the `includeUndefined` option is defaulted to `true` - this decision was made to avoid introducing a breaking change. 21 | 22 | In this setup, both `null` and `undefined` values will be considered valid. 23 | 24 | ```typescript 25 | import { Validator } from 'fluentvalidation-ts'; 26 | 27 | type FormModel = { 28 | apiError?: string | null; 29 | }; 30 | 31 | class FormValidator extends Validator { 32 | constructor() { 33 | super(); 34 | 35 | this.ruleFor('apiError').null(); 36 | } 37 | } 38 | 39 | const formValidator = new FormValidator(); 40 | 41 | formValidator.validate({ apiError: null }); 42 | // ✔ {} 43 | 44 | formValidator.validate({ apiError: 'Failed to fetch data from the API' }); 45 | // ❌ { apiError: 'Value must be null' } 46 | 47 | formValidator.validate({ apiError: undefined }); 48 | // ✔ {} 49 | 50 | formValidator.validate({}); 51 | // ✔ {} 52 | ``` 53 | 54 | ### Excluding `undefined` 55 | 56 | The behaviour of the `.null` rule can be made "strict" (in the sense that it only checks for `null` and not `undefined`) by passing the `includeUndefined` option as `false`. 57 | 58 | In this setup, `undefined` values will be considered invalid, and only `null` values will be allowed. 59 | 60 | ```typescript 61 | import { Validator } from 'fluentvalidation-ts'; 62 | 63 | type FormModel = { 64 | apiError?: string | null; 65 | }; 66 | 67 | class FormValidator extends Validator { 68 | constructor() { 69 | super(); 70 | 71 | this.ruleFor('apiError').null({ includeUndefined: false }); 72 | } 73 | } 74 | 75 | const formValidator = new FormValidator(); 76 | 77 | formValidator.validate({ apiError: null }); 78 | // ✔ {} 79 | 80 | formValidator.validate({ apiError: 'Failed to fetch data from the API' }); 81 | // ❌ { apiError: 'Value must be null' } 82 | 83 | // highlight-start 84 | formValidator.validate({ apiError: undefined }); 85 | // ❌ { apiError: 'Value must be null' } 86 | 87 | formValidator.validate({}); 88 | // ❌ { apiError: 'Value must be null' } 89 | // highlight-end 90 | ``` 91 | 92 | ## Reference 93 | 94 | ### `.null(ruleOptions?: NullRuleOptions)` 95 | 96 | A validation rule which ensures that the given property is `null` (or `undefined`, depending on the value of `ruleOptions`). 97 | 98 | The default value of `ruleOptions` is `{ includeUndefined: true }`, meaning that both `null` and `undefined` values will be considered valid. 99 | 100 | ### `NullRuleOptions` 101 | 102 | Equivalent to `{ includeUndefined: boolean }`, where the `includeUndefined` property determines whether `undefined` values should be considered valid. 103 | 104 | When `includeUndefined` is `true`, both `null` and `undefined` values will be considered valid. 105 | 106 | When `includeUndefined` is `false`, only `null` values will be considered valid, and `undefined` values will be considered invalid. 107 | 108 | ## Example Message 109 | 110 | > Value must be null 111 | -------------------------------------------------------------------------------- /docs/api/rules/precisionScale.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: precisionScale 3 | title: '.precisionScale' 4 | --- 5 | 6 | The `.precisionScale` rule is used to ensure that the value of a given `number` property is permissible for the specified **precision** and **scale**. 7 | 8 | These terms are defined as follows: 9 | 10 | - **Precision** is the number of digits in a number. 11 | - **Scale** is the number of digits to the right of the decimal point in a number. 12 | 13 | :::warning 14 | 15 | Prior to `v5.0.0` the `.precisionScale` rule was called `.scalePrecision` and the parameter naming was incorrect! 16 | 17 | ::: 18 | 19 | ## Example 20 | 21 | ```typescript 22 | import { Validator } from 'fluentvalidation-ts'; 23 | 24 | type FormModel = { 25 | price: number; 26 | }; 27 | 28 | class FormValidator extends Validator { 29 | constructor() { 30 | super(); 31 | 32 | this.ruleFor('price').precisionScale(4, 2); 33 | } 34 | } 35 | 36 | const formValidator = new FormValidator(); 37 | 38 | formValidator.validate({ price: 10.01 }); 39 | // ✔ {} 40 | 41 | formValidator.validate({ price: 0.001 }); // Too many digits after the decimal point 42 | // ❌ { price: 'Value must be no more than 4 digits in total, with allowance for 2 decimals' } 43 | 44 | formValidator.validate({ price: 100.1 }); // Too many digits (when accounting for reserved digits after the decimal point) 45 | // ❌ { price: 'Value must be no more than 4 digits in total, with allowance for 2 decimals' } 46 | ``` 47 | 48 | ## Reference 49 | 50 | ### `.precisionScale(precision: number, scale: number)` 51 | 52 | A number validation rule which takes in an allowed precision and scale, and ensures that the value of the given property is permissible. 53 | 54 | :::danger 55 | 56 | Due to rounding issues with floating point numbers in JavaScript, this rule may not function as expected for large precisions/scales. 57 | 58 | ::: 59 | 60 | ### `precision` 61 | 62 | This is the total number of digits that the value may have (taking into account the number of digits "reserved" for after the decimal point). 63 | 64 | The maximum number of significant digits allowed before the decimal point (i.e. the integer part) can be calculated as `(precision - scale)`. 65 | 66 | ### `scale` 67 | 68 | This is the maximum number of digits after the decimal point that the value may have. 69 | 70 | :::note 71 | 72 | When `precision` and `scale` are equal, the "leading zero" to the left of the decimal point is **not** counted as a digit (e.g. a value of `0.01` would be viewed as `.01`). 73 | 74 | ::: 75 | 76 | ## Example Message 77 | 78 | > Value must not be more than `[precision]` digits in total, with allowance for `[scale]` decimals 79 | -------------------------------------------------------------------------------- /docs/api/rules/setAsyncValidator.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: setAsyncValidator 3 | title: '.setAsyncValidator' 4 | --- 5 | 6 | The `.setAsyncValidator` rule is one of the special async rules that become available when you extend from [`AsyncValidator`](api/core/asyncValidator.md) as opposed to just [`Validator`](api/core/validator.md). 7 | 8 | This rule works exactly the same as the [`.setValidator`](api/rules/setValidator.md) rule, except that you must pass an instance of [`AsyncValidator`](api/core/asyncValidator.md) as opposed to an instance of [`Validator`](api/core/validator.md). 9 | 10 | As with the [`.setValidator`](api/rules/setValidator.md) rule, the async validator to use is specified by way of a producer function, which takes in the value of the base model and returns an appropriate validator. 11 | 12 | ## Examples 13 | 14 | The documentation page for the [`.setValidator`](api/rules/setValidator.md) rule includes a full list of examples demonstrating the different overloads that are available. 15 | 16 | These are all relevant to the `.setAsyncValidator` rule too, just replace `Validator` with `AsyncValidator` and `.setValidator` with `.setAsyncValidator`. 17 | 18 | ### Nested validator does not depend on the base model 19 | 20 | In this example the nested validator has no dependency on the base model, so we can simply define an instance of the nested validator ahead of time and return that from the validator producer function. 21 | 22 | ```typescript 23 | import { AsyncValidator } from 'fluentvalidation-ts'; 24 | 25 | type ContactDetails = { 26 | name: string; 27 | emailAddress: string; 28 | }; 29 | 30 | // highlight-start 31 | class ContactDetailsValidator extends AsyncValidator { 32 | constructor() { 33 | super(); 34 | 35 | this.ruleFor('name').notEmpty(); 36 | 37 | this.ruleFor('emailAddress') 38 | .emailAddress() 39 | .mustAsync( 40 | async (emailAddress) => await api.emailAddressNotInUse(emailAddress) 41 | ) 42 | .withMessage('This email address is already in use'); 43 | } 44 | } 45 | 46 | const contactDetailsValidator = new ContactDetailsValidator(); 47 | // highlight-end 48 | 49 | type FormModel = { 50 | contactDetails: ContactDetails; 51 | }; 52 | 53 | class FormValidator extends AsyncValidator { 54 | constructor() { 55 | super(); 56 | 57 | // highlight-start 58 | this.ruleFor('contactDetails').setAsyncValidator( 59 | () => contactDetailsValidator 60 | ); 61 | // highlight-end 62 | } 63 | } 64 | 65 | const formValidator = new FormValidator(); 66 | 67 | await formValidator.validateAsync({ 68 | contactDetails: { name: 'Alex', emailAddress: 'alex123@example.com' }, 69 | }); 70 | // ✔ {} 71 | 72 | await formValidator.validateAsync({ 73 | contactDetails: { name: 'Alex', emailAddress: 'alex@example.com' }, 74 | }); 75 | // ❌ { contactDetails: { emailAddress: 'This email address is already in use' } } 76 | ``` 77 | 78 | ## Reference 79 | 80 | ### `.setAsyncValidator(asyncValidatorProducer: (model: TModel) => AsyncValidator)` 81 | 82 | A validation rule which takes in a validator producer function and ensures that the given property is valid according to the async validator produced by that function. 83 | 84 | ### `TModel` 85 | 86 | Matches the type of the base model. 87 | 88 | ### `TValue` 89 | 90 | Matches the type of the property that the rule is applied to. 91 | 92 | ### `AsyncValidator` 93 | 94 | The [`AsyncValidator`](api/core/asyncValidator.md) generic class provided by **fluentvalidation-ts**. 95 | -------------------------------------------------------------------------------- /docs/api/rules/setValidator.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: setValidator 3 | title: '.setValidator' 4 | --- 5 | 6 | The `.setValidator` rule is used to ensure that the value of a given `object` property is valid according to a given [`Validator`](api/core/validator.md). 7 | 8 | The validator to use is specified by way of a producer function, which takes in the value of the base model and returns an appropriate validator. 9 | 10 | This approach enables the nested validator to depend on the base model, and makes recursive validation possible. 11 | 12 | ## Examples 13 | 14 | ### Nested validator does not depend on the base model 15 | 16 | In this example the nested validator has no dependency on the base model, so we can simply define an instance of the nested validator ahead of time and return that from the validator producer function. 17 | 18 | ```typescript 19 | import { Validator } from 'fluentvalidation-ts'; 20 | 21 | // highlight-start 22 | type ContactDetails = { 23 | name: string; 24 | emailAddress: string; 25 | }; 26 | 27 | class ContactDetailsValidator extends Validator { 28 | constructor() { 29 | super(); 30 | 31 | this.ruleFor('name').notEmpty(); 32 | 33 | this.ruleFor('emailAddress').emailAddress(); 34 | } 35 | } 36 | 37 | const contactDetailsValidator = new ContactDetailsValidator(); 38 | // highlight-end 39 | 40 | type FormModel = { 41 | // highlight-next-line 42 | contactDetails: ContactDetails; 43 | }; 44 | 45 | class FormValidator extends Validator { 46 | constructor() { 47 | super(); 48 | 49 | // highlight-next-line 50 | this.ruleFor('contactDetails').setValidator(() => contactDetailsValidator); 51 | } 52 | } 53 | 54 | const formValidator = new FormValidator(); 55 | 56 | formValidator.validate({ 57 | contactDetails: { name: 'Alex', emailAddress: 'alex@example.com' }, 58 | }); 59 | // ✔ {} 60 | 61 | formValidator.validate({ 62 | contactDetails: { name: '', emailAddress: 'alex@example.com' }, 63 | }); 64 | // ❌ { contactDetails: { name: 'Value cannot be empty' } } 65 | ``` 66 | 67 | ### Nested validator depends on the base model 68 | 69 | In this example the nested validator has a constructor argument which changes its behaviour. 70 | 71 | In particular, we only require an email address to be given if the user has indicated that they wish to sign up to the mailing list. 72 | 73 | ```typescript 74 | import { Validator } from 'fluentvalidation-ts'; 75 | 76 | type ContactDetails = { 77 | name: string; 78 | emailAddress: string | null; 79 | }; 80 | 81 | class ContactDetailsValidator extends Validator { 82 | // highlight-next-line 83 | constructor(emailAddressIsRequired: boolean) { 84 | super(); 85 | 86 | this.ruleFor('name').notEmpty(); 87 | 88 | this.ruleFor('emailAddress') 89 | .notNull() 90 | // highlight-next-line 91 | .when(() => emailAddressIsRequired); 92 | 93 | this.ruleFor('emailAddress').emailAddress(); 94 | } 95 | } 96 | 97 | type FormModel = { 98 | signUpToMailingList: boolean; 99 | contactDetails: ContactDetails; 100 | }; 101 | 102 | class FormValidator extends Validator { 103 | constructor() { 104 | super(); 105 | 106 | this.ruleFor('contactDetails').setValidator( 107 | // highlight-next-line 108 | (formModel) => new ContactDetailsValidator(formModel.signUpToMailingList) 109 | ); 110 | } 111 | } 112 | 113 | const formValidator = new FormValidator(); 114 | 115 | formValidator.validate({ 116 | signUpToMailingList: false, 117 | contactDetails: { name: 'Alex', emailAddress: null }, 118 | }); 119 | // ✔ {} 120 | 121 | formValidator.validate({ 122 | signUpToMailingList: true, 123 | contactDetails: { name: 'Alex', emailAddress: null }, 124 | }); 125 | // ❌ { contactDetails: { emailAddress: 'Value cannot be null' } } 126 | ``` 127 | 128 | ### Recursive validators 129 | 130 | In this example we deal with validating a recursive (self-referencing) model. 131 | 132 | In particular, an employee might have a line manager, who is also an employee. This line manager might themselves have a line manager, and so on. 133 | 134 | ```typescript 135 | import { Validator } from 'fluentvalidation-ts'; 136 | 137 | type Employee = { 138 | name: string; 139 | lineManager: Employee | null; 140 | }; 141 | 142 | class EmployeeValidator extends Validator { 143 | constructor() { 144 | super(); 145 | 146 | this.ruleFor('name').notEmpty(); 147 | 148 | // highlight-next-line 149 | this.ruleFor('lineManager').setValidator(() => new EmployeeValidator()); 150 | } 151 | } 152 | 153 | const validator = new EmployeeValidator(); 154 | 155 | validator.validate({ 156 | name: 'Bob', 157 | lineManager: { 158 | name: 'Alice', 159 | lineManager: null, 160 | }, 161 | }); 162 | // ✔ {} 163 | 164 | validator.validate({ 165 | name: 'Alex', 166 | lineManager: { 167 | name: '', 168 | lineManager: null, 169 | }, 170 | }); 171 | // ❌ { lineManager: { name: 'Value cannot be empty' } } 172 | ``` 173 | 174 | ## Reference 175 | 176 | ### `.setValidator(validatorProducer: (model: TModel) => Validator)` 177 | 178 | A validation rule which takes in a validator producer function and ensures that the given property is valid according to the validator produced by that function. 179 | 180 | ### `TModel` 181 | 182 | Matches the type of the base model. 183 | 184 | ### `TValue` 185 | 186 | Matches the type of the property that the rule is applied to. 187 | 188 | ### `Validator` 189 | 190 | The [`Validator`](api/core/validator.md) generic class provided by **fluentvalidation-ts**. 191 | -------------------------------------------------------------------------------- /docs/api/rules/undefined.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: undefinedRule 3 | title: '.undefined' 4 | --- 5 | 6 | The `.undefined` rule is used to ensure that the value of a given property is `undefined`. 7 | 8 | :::note 9 | 10 | Note that this rule considers `null` values to be **invalid**. If you need to allow for both `null` and `undefined` values (or just `null` values), you may use the [`.null`](./null.md) rule instead. 11 | 12 | ::: 13 | 14 | ## Example 15 | 16 | ```typescript 17 | import { Validator } from 'fluentvalidation-ts'; 18 | 19 | type FormModel = { 20 | customerId?: number | null; 21 | }; 22 | 23 | class FormValidator extends Validator { 24 | constructor() { 25 | super(); 26 | 27 | this.ruleFor('customerId').undefined(); 28 | } 29 | } 30 | 31 | const formValidator = new FormValidator(); 32 | 33 | formValidator.validate({}); 34 | // ✔ {} 35 | 36 | formValidator.validate({ customerId: undefined }); 37 | // ✔ {} 38 | 39 | formValidator.validate({ customerId: 100 }); 40 | // ❌ { customerId: 'Value must be undefined' } 41 | 42 | formValidator.validate({ customerId: null }); 43 | // ❌ { customerId: 'Value must be undefined' } 44 | ``` 45 | 46 | ## Reference 47 | 48 | ### `.undefined()` 49 | 50 | A validation rule which ensures that the given property is `undefined`. 51 | 52 | ## Example Message 53 | 54 | > Value must be undefined 55 | -------------------------------------------------------------------------------- /docs/guides/ambientContext.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: ambientContext 3 | title: Ambient Context 4 | --- 5 | 6 | Sometimes your validation logic will need to depend on external, or "ambient", context that isn't part of your form model. With **fluentvalidation-ts** validators are just classes, so you can make use of constructor arguments to inject dependencies. 7 | 8 | ## The Gist 9 | 10 | You can inject external dependencies into your validators using constructor arguments: 11 | 12 | ```typescript 13 | type FormModel = { 14 | age: number; 15 | }; 16 | 17 | class FormValidator extends Validator { 18 | // highlight-next-line 19 | constructor(country: string) { 20 | super(); 21 | 22 | this.ruleFor('age') 23 | // highlight-next-line 24 | .greaterThanOrEqualTo(country === 'US' ? 21 : 18); 25 | } 26 | } 27 | ``` 28 | 29 | This approach means that you need to instantiate a new instance of your validator every time the ambient context changes, so there is potentially a performance cost involved. 30 | 31 | Usage of the example validator from above might look something like this: 32 | 33 | ```typescript 34 | const ukValidator = new FormValidator('UK'); 35 | const usValidator = new FormValidator('US'); 36 | 37 | const pubGoer = { age: 20 }; 38 | 39 | const ukResult = ukValidator.validate(pubGoer); // {} 40 | const usResult = usValidator.validate(pubGoer); // { age: 'Value must be greater than or equal to 21' } 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/guides/arrayProperties.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: arrayProperties 3 | title: Array Properties 4 | --- 5 | 6 | Validating array properties is made easy with the [`.ruleForEach`](api/core/ruleForEach.md) method. 7 | 8 | The `.ruleForEach` method works almost exactly the same as the [`.ruleFor`](api/core/ruleFor.md) method, so it's worth reading up on that first if you haven't already. 9 | 10 | ## The Gist 11 | 12 | You can validate an array property using the `.ruleFor` method: 13 | 14 | ```typescript 15 | this.ruleFor('scores').must( 16 | (scores) => scores.filter((score) => score < 0 || score > 100).length === 0 17 | ); 18 | ``` 19 | 20 | Alternatively, you can use the `.ruleForEach` method: 21 | 22 | ```typescript 23 | this.ruleForEach('scores').greaterThanOrEqualTo(0).lessThanOrEqualTo(100); 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/guides/customRules.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: customRules 3 | title: Custom Rules 4 | --- 5 | 6 | One of the main features of **fluentvalidation-ts** is that it is fully extensible, allowing you define your own custom validation logic and inject it via the [`.must`](api/rules/must.md) rule. 7 | 8 | The [documentation page](api/rules/must.md) for the `.must` rule contains several [examples](api/rules/must.md#examples) that demonstrate the different ways in which you can define and consume custom rules, as well as a full [API reference](api/rules/must.md#reference) which outlines everything in detail. 9 | 10 | ## The Gist 11 | 12 | Custom validation logic is defined by way of a **predicate** function, which takes a value and returns a boolean (true/false) value indicating whether or not the value is valid. 13 | 14 | You can pass custom validation logic directly into the `.must` rule with a predicate: 15 | 16 | ```typescript 17 | this.ruleFor('numberOfSocks').must((numberOfSocks) => numberOfSocks % 2 === 0); 18 | ``` 19 | 20 | If you want to reuse the logic, you could pull it out into a named function: 21 | 22 | ```typescript 23 | const beEven = (value: number) => value % 2 === 0; 24 | ``` 25 | 26 | Then you can just pass the named function into `.must`, like so: 27 | 28 | ```typescript 29 | this.ruleFor('numberOfSocks').must(beEven); 30 | ``` 31 | 32 | The predicate function can also depend on the value of the model as well as the value of the property: 33 | 34 | ```typescript 35 | this.ruleFor('numberOfSocks').must( 36 | // highlight-next-line 37 | (numberOfSocks, model) => numberOfSocks === 2 * model.numberOfPants 38 | ); 39 | ``` 40 | 41 | You can define groups of rules by forming arrays: 42 | 43 | ```typescript 44 | const beEven = (value: number) => value % 2 === 0; 45 | const bePositive = (value: number) => value > 0; 46 | 47 | // highlight-next-line 48 | const beEvenAndPositive = [beEven, bePositive]; 49 | ``` 50 | 51 | These arrays can be passed directly to the `.must` rule: 52 | 53 | ```typescript 54 | this.ruleFor('numberOfSocks').must(beEvenAndPositive); 55 | ``` 56 | 57 | You can also attach a custom message to your rule, alongside the predicate: 58 | 59 | ```typescript 60 | const beEven = { 61 | predicate: (value: number) => value % 2 === 0, 62 | // highlight-next-line 63 | message: 'Please enter an even number', 64 | }; 65 | ``` 66 | 67 | As before, you just pass this into the `.must` rule directly: 68 | 69 | ```typescript 70 | this.ruleFor('numberOfSocks').must(beEven); 71 | ``` 72 | 73 | Again, you can use arrays to compose rules together: 74 | 75 | ```typescript 76 | const beEven = { 77 | predicate: (value: number) => value % 2 === 0, 78 | message: 'Please enter an even number', 79 | }; 80 | 81 | const bePositive = { 82 | predicate: (value: number) => value > 0, 83 | message: 'Please enter a positive number', 84 | }; 85 | 86 | // highlight-next-line 87 | const beEvenAndPositive = [beEven, bePositive]; 88 | ``` 89 | 90 | You can even compose groups of rules together by spreading or concatenating the arrays: 91 | 92 | ```typescript 93 | const newRuleGroup = [...ruleGroup, ...otherRuleGroup]; 94 | ``` 95 | -------------------------------------------------------------------------------- /docs/guides/formik.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: formik 3 | title: Formik 4 | --- 5 | 6 | When I first wrote **fluentvalidation-ts**, I had seamless integration with [Formik](https://formik.org/) in mind. 7 | 8 | The [`ValidationErrors`](/docs/api/core/ValidationErrors) object returned by the [`.validate`](/docs/api/core/validator#validate) function has been designed to "just work" with Formik, so you can start using the two together with minimal effort. 9 | 10 | If you're not familiar with Formik, it's a fantastic library for writing forms in [React](https://react.dev/). 11 | 12 | ## Usage 13 | 14 | To use **fluentvalidation-ts** with Formik, simply define a `Validator` for your form model, instantiate an instance of your validator, then pass the validator's [`.validate`](https://formik.org/docs/guides/validation#validate) method to Formik's `validate` prop: 15 | 16 | ```tsx 17 | import { Formik } from 'formik'; 18 | import { Validator } from 'fluentvalidation-ts'; 19 | 20 | type FormModel = { username: string }; 21 | 22 | // highlight-start 23 | class MyFormValidator extends Validator { 24 | constructor() { 25 | super(); 26 | this.ruleFor('username').notEmpty().withMessage('Please enter your username'); 27 | } 28 | } 29 | 30 | const formValidator = new MyFormValidator(); 31 | // highlight-end 32 | 33 | export const MyForm = () => ( 34 | 35 | // highlight-next-line 36 | validate={formValidator.validate} 37 | ... 38 | > 39 | ... 40 | 41 | ); 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/guides/objectProperties.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: objectProperties 3 | title: Object Properties 4 | --- 5 | 6 | Object properties can be validated by way of the [`.setValidator`](api/rules/setValidator.md) rule. 7 | 8 | The [documentation page](api/rules/setValidator.md) for the `.setValidator` rule contains several [examples](api/rules/setValidator.md#examples) that demonstrate the different ways in which you can use it, as well as a full [API reference](api/rules/setValidator.md#reference) which outlines everything in detail. 9 | 10 | ## The Gist 11 | 12 | You can validate an object property using the built-in rules: 13 | 14 | ```typescript 15 | this.ruleFor('pet') 16 | .notNull() 17 | .must((pet) => pet.age >= 0) 18 | .must((pet) => pet.name !== ''); 19 | ``` 20 | 21 | Alternatively, you can define a validator for the type of the object property: 22 | 23 | ```typescript 24 | class PetValidator extends Validator { 25 | constructor() { 26 | super(); 27 | this.ruleFor('age').greaterThanOrEqualTo(0); 28 | this.ruleFor('name').notEmpty(); 29 | } 30 | } 31 | 32 | const petValidator = new PetValidator(); 33 | ``` 34 | 35 | This can then be passed in with the `.setValidator` rule: 36 | 37 | ```typescript 38 | this.ruleFor('pet') 39 | .notNull() 40 | // highlight-next-line 41 | .setValidator(() => petValidator); 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/guides/reactHookForm.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: reactHookForm 3 | title: React Hook Form 4 | --- 5 | 6 | While **fluentvalidation-ts** was originally developed with Formik integration in mind, [React Hook Form](https://react-hook-form.com/) has become increasingly popular in the React community. Thankfully, wonderful members of the community have contributed a [fluentvalidation-ts resolver](https://github.com/react-hook-form/resolvers?tab=readme-ov-file#fluentvalidation-ts) for React Hook Form, allowing you to integrate it seamlessly with no effort required on your part! 7 | -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: overview 3 | title: Overview 4 | --- 5 | 6 | Front-end validation is a must-have for any project that involves forms, but the requirements vary hugely. You might have a simple sign-up form with a few text fields, or a complex configuration page with collections and deeply nested fields. 7 | 8 | There are plenty of libraries out there which help you to solve the problem of front-end validation, but all the ones I've tried have felt lacking in one aspect or another - whether that's TypeScript support, their capacity to handle complex requirements, the ability to define your own reusable validation logic, or just the expressiveness of the API. 9 | 10 | So I wrote **fluentvalidation-ts**, a tiny library that is: 11 | 12 | - Designed for TypeScript 13 | - Simple yet powerful 14 | - Fully extensible 15 | 16 | Whatever your validation needs, **fluentvalidation-ts** can handle them. 17 | 18 | ## Compatibility 19 | 20 | **fluentvalidation-ts** is completely framework-agnostic, so you can use it with any front-end framework or library. It has no dependencies, and is designed to be as lightweight as possible. Having said that, it has primarily been designed to integrate seamlessly with popular form libraries for React - see the guides on [Formik](/docs/guides/formik) and [React Hook Form](/docs/guides/reactHookForm) for more information. 21 | 22 | ## Influences 23 | 24 | If you've ever worked on a .NET API, you might have heard of a library called [FluentValidation](https://fluentvalidation.net/). It has a really nice API for building up validation rules, and that made me wonder whether I could achieve something similar in TypeScript. While **fluentvalidation-ts** is not a direct port, it will still feel very familiar to anyone who's used FluentValidation before. 25 | 26 | ## Installation 27 | 28 | You can install **fluentvalidation-ts** with NPM/Yarn, or include it directly via a ` 50 | ``` 51 | 52 | Or, to target a specific version (e.g. `4.0.0`), add the following: 53 | 54 | ```html 55 | 56 | ``` 57 | 58 | Once you've done this, all you need is the `Validator` class which can be accessed via: 59 | 60 | ```js 61 | window['fluentvalidation'].Validator; 62 | ``` 63 | 64 | ## The Gist 65 | 66 | To use **fluentvalidation-ts** simply import the `Validator` generic class, and define your own class which extends it using the appropriate generic type argument. Build up the rules for your various properties in the constructor of your derived class, then create an instance of your class to get hold of a validator. Finally, pass an instance of your model into the `.validate` function of your validator to obtain a validation errors object. 67 | 68 | ```typescript 69 | import { Validator } from 'fluentvalidation-ts'; 70 | 71 | type FormModel = { 72 | name: string; 73 | age: number; 74 | }; 75 | 76 | class FormValidator extends Validator { 77 | constructor() { 78 | super(); 79 | 80 | this.ruleFor('name').notEmpty().withMessage('Please enter your name'); 81 | 82 | this.ruleFor('age') 83 | .greaterThanOrEqualTo(0) 84 | .withMessage('Please enter a non-negative number'); 85 | } 86 | } 87 | 88 | const formValidator = new FormValidator(); 89 | 90 | const valid: FormModel = { 91 | name: 'Alex', 92 | age: 26, 93 | }; 94 | formValidator.validate(valid); 95 | // {} 96 | 97 | const invalid: FormModel = { 98 | name: '', 99 | age: -1, 100 | }; 101 | formValidator.validate(invalid); 102 | // { name: 'Please enter your name', age: 'Please enter a non-negative number' } 103 | ``` 104 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import { defineConfig } from 'eslint/config'; 4 | import importPlugin from 'eslint-plugin-import'; 5 | import tseslint from 'typescript-eslint'; 6 | 7 | const ignores = ['website/', 'coverage/', 'dist/']; 8 | 9 | export default defineConfig([ 10 | { 11 | ignores, 12 | }, 13 | { 14 | plugins: { js }, 15 | extends: ['js/recommended'], 16 | }, 17 | { 18 | languageOptions: { 19 | globals: globals.browser, 20 | }, 21 | }, 22 | tseslint.configs.recommended, 23 | { 24 | extends: [ 25 | importPlugin.flatConfigs.recommended, 26 | importPlugin.flatConfigs.typescript, 27 | ], 28 | rules: { 29 | '@typescript-eslint/no-explicit-any': 'error', 30 | 'import/order': 'error', 31 | 'import/no-unresolved': 'off', 32 | }, 33 | }, 34 | { 35 | files: ['test/**/*.ts'], 36 | rules: { 37 | '@typescript-eslint/no-explicit-any': 'off', 38 | '@typescript-eslint/no-unused-vars': 'off', 39 | '@typescript-eslint/ban-ts-comment': 'off', 40 | }, 41 | }, 42 | ]); 43 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | collectCoverage: true, 5 | coverageDirectory: 'coverage', 6 | preset: 'ts-jest', 7 | transform: { 8 | '^.+\\.ts$': [ 9 | 'ts-jest', 10 | { 11 | tsconfig: 'tsconfig.test.json', 12 | }, 13 | ], 14 | }, 15 | moduleNameMapper: { 16 | '^@/(.*)$': '/src/$1', 17 | }, 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexJPotter/fluentvalidation-ts/5a9f064835ea910aed9ab04bf3c0e89b6c04553c/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fluentvalidation-ts", 3 | "version": "5.0.0", 4 | "description": "A TypeScript-first library for building strongly-typed validation rules", 5 | "keywords": [ 6 | "fluent", 7 | "validation", 8 | "validator", 9 | "typescript", 10 | "form", 11 | "formik", 12 | "react-hook-form", 13 | "fluentvalidation" 14 | ], 15 | "homepage": "https://github.com/AlexJPotter/fluentvalidation-ts", 16 | "main": "dist/index.js", 17 | "author": "Alex Potter ", 18 | "license": "Apache-2.0", 19 | "private": false, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/AlexJPotter/fluentvalidation-ts.git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/AlexJPotter/fluentvalidation-ts/issues" 26 | }, 27 | "umd:main": "dist/index.global.js", 28 | "module": "dist/index.mjs", 29 | "typings": "dist/index.d.ts", 30 | "files": [ 31 | "dist" 32 | ], 33 | "scripts": { 34 | "watch": "tsc --noEmit --watch", 35 | "build": "cross-env NODE_ENV=production tsup src/index.ts --dts --minify --treeshake --format cjs,esm,iife --global-name fluentvalidation --sourcemap", 36 | "test": "jest", 37 | "test:watch": "jest --watch", 38 | "lint": "eslint --fix", 39 | "lint:check": "eslint", 40 | "prettier": "prettier --write \"{src,test}/**/*.ts\"", 41 | "prettier:check": "prettier --check \"{src,test}/**/*.ts\"", 42 | "typecheck": "tsc --noEmit && tsc --noEmit --project tsconfig.test.json", 43 | "prepare": "husky" 44 | }, 45 | "devDependencies": { 46 | "@eslint/js": "^9.26.0", 47 | "@swc/core": "^1.3.56", 48 | "@types/jest": "^29.5.14", 49 | "@typescript-eslint/parser": "^8.32.0", 50 | "cross-env": "^7.0.3", 51 | "eslint": "^9.26.0", 52 | "eslint-import-resolver-typescript": "^4.3.4", 53 | "eslint-plugin-import": "^2.31.0", 54 | "globals": "^16.1.0", 55 | "husky": "^9.1.7", 56 | "jest": "^29.7.0", 57 | "prettier": "^3.5.3", 58 | "pretty-quick": "^4.1.1", 59 | "ts-jest": "^29.3.2", 60 | "ts-node": "^10.9.2", 61 | "tslib": "^2.3.0", 62 | "tsup": "^8.4.0", 63 | "typescript": "^5.8.3", 64 | "typescript-eslint": "^8.32.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/AsyncValidator.ts: -------------------------------------------------------------------------------- 1 | import { AsyncArrayValueValidatorBuilder } from '@/valueValidator/AsyncArrayValueValidatorBuilder'; 2 | import { AsyncValueValidatorBuilder } from '@/valueValidator/AsyncValueValidatorBuilder'; 3 | import { ValidationErrors } from '@/ValidationErrors'; 4 | import { AsyncRuleValidators } from '@/valueValidator/AsyncRuleValidators'; 5 | import { hasError } from '@/valueValidator/ValueValidator'; 6 | import { Constrain } from '@/types/Constrain'; 7 | import { ValueValidationResult } from '@/ValueValidationResult'; 8 | import { ValueTransformer } from '@/valueValidator/ValueTransformer'; 9 | import { ArrayType } from '@/types/ArrayType'; 10 | import { TransformedValue } from '@/types/TransformedValue'; 11 | import { AsyncValueValidator } from '@/valueValidator/AsyncValueValidator'; 12 | import { Optional } from '@/types/Optional'; 13 | import { IfNotNeverThen } from '@/types/IfNotNeverThen'; 14 | 15 | interface IAsyncValueValidatorBuilder { 16 | build: () => AsyncValueValidator; 17 | } 18 | 19 | export class AsyncValidator { 20 | private asyncValueValidatorBuildersByPropertyName: { 21 | [propertyName in keyof TModel]?: Array>; 22 | } = {}; 23 | 24 | protected _validateAsync: (value: TModel) => Promise> = async () => { 25 | return Promise.resolve({}); 26 | }; 27 | 28 | public validateAsync = (value: TModel): Promise> => 29 | this._validateAsync(value); 30 | 31 | private rebuildValidateAsync = () => { 32 | this._validateAsync = async (value: TModel): Promise> => { 33 | const errors: ValidationErrors = {}; 34 | 35 | for (const propertyName of Object.keys(this.asyncValueValidatorBuildersByPropertyName)) { 36 | const asyncValueValidatorBuilders = 37 | this.asyncValueValidatorBuildersByPropertyName[propertyName as keyof TModel]; 38 | 39 | for (const asyncValueValidatorBuilder of asyncValueValidatorBuilders!) { 40 | const asyncValueValidator = asyncValueValidatorBuilder.build(); 41 | 42 | const result = (await asyncValueValidator( 43 | value[propertyName as keyof TModel], 44 | value, 45 | )) as ValueValidationResult; 46 | 47 | if (hasError(result)) { 48 | errors[propertyName as keyof TModel] = result; 49 | } 50 | } 51 | } 52 | 53 | return errors; 54 | }; 55 | }; 56 | 57 | protected ruleFor = ( 58 | propertyName: TPropertyName, 59 | ): AsyncRuleValidators => { 60 | const asyncValueValidatorBuilder = new AsyncValueValidatorBuilder< 61 | TModel, 62 | TModel[TPropertyName], 63 | TransformedValue 64 | >(this.rebuildValidateAsync, (value) => value); 65 | 66 | this.asyncValueValidatorBuildersByPropertyName[propertyName] = 67 | this.asyncValueValidatorBuildersByPropertyName[propertyName] || []; 68 | 69 | this.asyncValueValidatorBuildersByPropertyName[propertyName]!.push( 70 | asyncValueValidatorBuilder as IAsyncValueValidatorBuilder, 71 | ); 72 | 73 | return asyncValueValidatorBuilder.getAllRules() as AsyncRuleValidators; 74 | }; 75 | 76 | protected ruleForTransformed = < 77 | TPropertyName extends keyof TModel, 78 | TValue extends TModel[TPropertyName], 79 | TTransformedValue extends TransformedValue, 80 | >( 81 | propertyName: TPropertyName, 82 | transformValue: ( 83 | value: TValue, 84 | ) => TTransformedValue extends object 85 | ? Constrain 86 | : TTransformedValue, 87 | ): AsyncRuleValidators => { 88 | const asyncValueValidatorBuilder = new AsyncValueValidatorBuilder< 89 | TModel, 90 | TValue, 91 | TTransformedValue 92 | >(this.rebuildValidateAsync, transformValue as ValueTransformer); 93 | 94 | this.asyncValueValidatorBuildersByPropertyName[propertyName] = 95 | this.asyncValueValidatorBuildersByPropertyName[propertyName] || []; 96 | 97 | this.asyncValueValidatorBuildersByPropertyName[propertyName]!.push( 98 | asyncValueValidatorBuilder as IAsyncValueValidatorBuilder, 99 | ); 100 | 101 | return asyncValueValidatorBuilder.getAllRules() as AsyncRuleValidators< 102 | TModel, 103 | TTransformedValue 104 | >; 105 | }; 106 | 107 | protected ruleForEach = < 108 | TPropertyName extends keyof TModel, 109 | TEachValue extends TModel[TPropertyName] extends Optional> 110 | ? TEachValueInferred 111 | : never, 112 | TValue extends TModel[TPropertyName] & ArrayType, 113 | >( 114 | propertyName: IfNotNeverThen, 115 | ): IfNotNeverThen> => { 116 | const asyncArrayValueValidatorBuilder = new AsyncArrayValueValidatorBuilder< 117 | TModel, 118 | TValue, 119 | TEachValue, 120 | TEachValue 121 | >(this.rebuildValidateAsync, (value) => value); 122 | 123 | if (this.asyncValueValidatorBuildersByPropertyName[propertyName] == null) { 124 | this.asyncValueValidatorBuildersByPropertyName[propertyName] = []; 125 | } 126 | 127 | this.asyncValueValidatorBuildersByPropertyName[propertyName]!.push( 128 | asyncArrayValueValidatorBuilder as IAsyncValueValidatorBuilder, 129 | ); 130 | 131 | return asyncArrayValueValidatorBuilder.getAllRules() as IfNotNeverThen< 132 | TEachValue, 133 | AsyncRuleValidators 134 | >; 135 | }; 136 | 137 | protected ruleForEachTransformed = < 138 | TPropertyName extends keyof TModel, 139 | TEachValue extends TModel[TPropertyName] extends Optional> 140 | ? TEachValueInferred 141 | : never, 142 | TValue extends TModel[TPropertyName] & ArrayType, 143 | TEachTransformedValue extends TransformedValue, 144 | >( 145 | propertyName: IfNotNeverThen, 146 | transformValue: ( 147 | value: TEachValue, 148 | ) => TEachTransformedValue extends object 149 | ? Constrain 150 | : TEachTransformedValue, 151 | ): IfNotNeverThen> => { 152 | const asyncArrayValueValidatorBuilder = new AsyncArrayValueValidatorBuilder< 153 | TModel, 154 | TValue, 155 | TEachValue, 156 | TEachTransformedValue 157 | >(this.rebuildValidateAsync, transformValue); 158 | 159 | if (this.asyncValueValidatorBuildersByPropertyName[propertyName] == null) { 160 | this.asyncValueValidatorBuildersByPropertyName[propertyName] = []; 161 | } 162 | 163 | this.asyncValueValidatorBuildersByPropertyName[propertyName]!.push( 164 | asyncArrayValueValidatorBuilder as IAsyncValueValidatorBuilder, 165 | ); 166 | 167 | return asyncArrayValueValidatorBuilder.getAllRules() as IfNotNeverThen< 168 | TEachValue, 169 | AsyncRuleValidators 170 | >; 171 | }; 172 | } 173 | -------------------------------------------------------------------------------- /src/IAsyncValidator.ts: -------------------------------------------------------------------------------- 1 | import { ValidationErrors } from './ValidationErrors'; 2 | 3 | export interface IAsyncValidator { 4 | validateAsync: (model: TModel) => Promise>; 5 | } 6 | -------------------------------------------------------------------------------- /src/IValidator.ts: -------------------------------------------------------------------------------- 1 | import { ValidationErrors } from './ValidationErrors'; 2 | 3 | export interface IValidator { 4 | validate: (model: TModel) => ValidationErrors; 5 | } 6 | -------------------------------------------------------------------------------- /src/SyncValidator.ts: -------------------------------------------------------------------------------- 1 | import { ValidationErrors } from '@/ValidationErrors'; 2 | import { ArrayValueValidatorBuilder } from '@/valueValidator/ArrayValueValidatorBuilder'; 3 | import { RuleValidators } from '@/valueValidator/RuleValidators'; 4 | import { hasError } from '@/valueValidator/ValueValidator'; 5 | import { ValueValidatorBuilder } from '@/valueValidator/ValueValidatorBuilder'; 6 | import { Constrain } from '@/types/Constrain'; 7 | import { ArrayType } from '@/types/ArrayType'; 8 | import { TransformedValue } from '@/types/TransformedValue'; 9 | import { ValueValidator } from '@/ValueValidator'; 10 | import { Optional } from '@/types/Optional'; 11 | import { IfNotNeverThen } from '@/types/IfNotNeverThen'; 12 | 13 | interface IValueValidatorBuilder { 14 | build: () => ValueValidator; 15 | } 16 | 17 | export class SyncValidator { 18 | private valueValidatorBuildersByPropertyName: { 19 | [propertyName in keyof TModel]?: Array>; 20 | } = {}; 21 | 22 | protected _validate: (value: TModel) => ValidationErrors = () => { 23 | return {}; 24 | }; 25 | 26 | public validate = (value: TModel): ValidationErrors => this._validate(value); 27 | 28 | private rebuildValidate = () => { 29 | this._validate = (value: TModel): ValidationErrors => { 30 | const errors: ValidationErrors = {}; 31 | 32 | for (const propertyName of Object.keys(this.valueValidatorBuildersByPropertyName)) { 33 | const valueValidatorBuilders = 34 | this.valueValidatorBuildersByPropertyName[propertyName as keyof TModel]; 35 | 36 | for (const valueValidatorBuilder of valueValidatorBuilders!) { 37 | const valueValidator = valueValidatorBuilder.build(); 38 | 39 | const result = valueValidator(value[propertyName as keyof TModel], value); 40 | 41 | if (hasError(result)) { 42 | errors[propertyName as keyof TModel] = result; 43 | } 44 | } 45 | } 46 | 47 | return errors; 48 | }; 49 | }; 50 | 51 | protected ruleFor = ( 52 | propertyName: TPropertyName, 53 | ): RuleValidators => { 54 | const valueValidatorBuilder = new ValueValidatorBuilder( 55 | this.rebuildValidate, 56 | (value) => value, 57 | ); 58 | 59 | this.valueValidatorBuildersByPropertyName[propertyName] = 60 | this.valueValidatorBuildersByPropertyName[propertyName] || []; 61 | 62 | this.valueValidatorBuildersByPropertyName[propertyName]!.push( 63 | valueValidatorBuilder as IValueValidatorBuilder, 64 | ); 65 | 66 | return valueValidatorBuilder.getAllRules(); 67 | }; 68 | 69 | protected ruleForTransformed = < 70 | TPropertyName extends keyof TModel, 71 | TValue extends TModel[TPropertyName], 72 | TTransformedValue extends TransformedValue, 73 | >( 74 | propertyName: TPropertyName, 75 | transformValue: ( 76 | value: TValue, 77 | ) => TTransformedValue extends object 78 | ? Constrain 79 | : TTransformedValue, 80 | ): RuleValidators => { 81 | const valueValidatorBuilder = new ValueValidatorBuilder( 82 | this.rebuildValidate, 83 | transformValue, 84 | ); 85 | 86 | this.valueValidatorBuildersByPropertyName[propertyName] = 87 | this.valueValidatorBuildersByPropertyName[propertyName] || []; 88 | 89 | this.valueValidatorBuildersByPropertyName[propertyName]!.push( 90 | valueValidatorBuilder as IValueValidatorBuilder, 91 | ); 92 | 93 | return valueValidatorBuilder.getAllRules(); 94 | }; 95 | 96 | protected ruleForEach = < 97 | TPropertyName extends keyof TModel, 98 | TEachValue extends TModel[TPropertyName] extends Optional> 99 | ? TEachValueInferred 100 | : never, 101 | TValue extends TModel[TPropertyName] & ArrayType, 102 | >( 103 | propertyName: IfNotNeverThen, 104 | ): IfNotNeverThen> => { 105 | const arrayValueValidatorBuilder = new ArrayValueValidatorBuilder< 106 | TModel, 107 | TValue, 108 | TEachValue, 109 | TEachValue 110 | >(this.rebuildValidate, (value) => value); 111 | 112 | if (this.valueValidatorBuildersByPropertyName[propertyName] == null) { 113 | this.valueValidatorBuildersByPropertyName[propertyName] = []; 114 | } 115 | 116 | this.valueValidatorBuildersByPropertyName[propertyName]!.push( 117 | arrayValueValidatorBuilder as IValueValidatorBuilder, 118 | ); 119 | 120 | return arrayValueValidatorBuilder.getAllRules() as IfNotNeverThen< 121 | TEachValue, 122 | RuleValidators 123 | >; 124 | }; 125 | 126 | protected ruleForEachTransformed = < 127 | TPropertyName extends keyof TModel, 128 | TEachValue extends TModel[TPropertyName] extends Optional> 129 | ? TEachValueInferred 130 | : never, 131 | TValue extends TModel[TPropertyName] & ArrayType, 132 | TEachTransformedValue extends TransformedValue, 133 | >( 134 | propertyName: IfNotNeverThen, 135 | transformValue: ( 136 | value: TEachValue, 137 | ) => TEachTransformedValue extends object 138 | ? Constrain 139 | : TEachTransformedValue, 140 | ): IfNotNeverThen> => { 141 | const arrayValueValidatorBuilder = new ArrayValueValidatorBuilder< 142 | TModel, 143 | TValue, 144 | TEachValue, 145 | TEachTransformedValue 146 | >(this.rebuildValidate, transformValue); 147 | 148 | if (this.valueValidatorBuildersByPropertyName[propertyName] == null) { 149 | this.valueValidatorBuildersByPropertyName[propertyName] = []; 150 | } 151 | 152 | this.valueValidatorBuildersByPropertyName[propertyName]!.push( 153 | arrayValueValidatorBuilder as IValueValidatorBuilder, 154 | ); 155 | 156 | return arrayValueValidatorBuilder.getAllRules() as IfNotNeverThen< 157 | TEachValue, 158 | RuleValidators 159 | >; 160 | }; 161 | } 162 | -------------------------------------------------------------------------------- /src/ValidationErrors.ts: -------------------------------------------------------------------------------- 1 | import { ValueValidationResult } from './ValueValidationResult'; 2 | 3 | export type ValidationErrors = { 4 | [propertyName in keyof TModel]?: ValueValidationResult; 5 | }; 6 | -------------------------------------------------------------------------------- /src/ValueValidationResult.ts: -------------------------------------------------------------------------------- 1 | import { ArrayType } from '@/types/ArrayType'; 2 | 3 | export type ValueValidationResult = 4 | | (TValue extends ArrayType 5 | ? Array> | string | null 6 | : TValue extends object 7 | ? 8 | | { 9 | [propertyName in keyof TValue]?: ValueValidationResult; 10 | } 11 | | string 12 | | null 13 | : string | null) 14 | | string 15 | | null; 16 | -------------------------------------------------------------------------------- /src/ValueValidator.ts: -------------------------------------------------------------------------------- 1 | import { ValueValidationResult } from './ValueValidationResult'; 2 | 3 | export type ValueValidator = ( 4 | value: TValue, 5 | model: TModel, 6 | ) => ValueValidationResult; 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { SyncValidator as Validator } from './SyncValidator'; 2 | export { AsyncValidator } from './AsyncValidator'; 3 | export { ValidationErrors } from '@/ValidationErrors'; 4 | export { ValueValidationResult } from '@/ValueValidationResult'; 5 | export { RuleValidators } from '@/valueValidator/RuleValidators'; 6 | export { AsyncRuleValidators } from '@/valueValidator/AsyncRuleValidators'; 7 | -------------------------------------------------------------------------------- /src/numberHelpers.ts: -------------------------------------------------------------------------------- 1 | export const formatNumber = (value: number) => 2 | value.toLocaleString(undefined, { maximumFractionDigits: 20 }); 3 | -------------------------------------------------------------------------------- /src/rules/AsyncRule.ts: -------------------------------------------------------------------------------- 1 | import { CoreRule } from './CoreRule'; 2 | import { AsyncValueValidator } from '@/valueValidator/AsyncValueValidator'; 3 | import { ValueValidationResult } from '@/ValueValidationResult'; 4 | 5 | export class AsyncRule extends CoreRule { 6 | private readonly asyncValueValidator: AsyncValueValidator; 7 | 8 | constructor(asyncValueValidator: AsyncValueValidator) { 9 | super(); 10 | this.asyncValueValidator = asyncValueValidator; 11 | } 12 | 13 | public validateAsync = async ( 14 | value: TValue, 15 | model: TModel, 16 | ): Promise> => { 17 | if (this.whenCondition != null && !this.whenCondition(model)) { 18 | return null; 19 | } 20 | 21 | if (this.unlessCondition != null && this.unlessCondition(model)) { 22 | return null; 23 | } 24 | 25 | const errorOrNull = await this.asyncValueValidator(value, model); 26 | return errorOrNull != null ? this.customErrorMessage || errorOrNull : null; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/rules/AsyncValidatorRule.ts: -------------------------------------------------------------------------------- 1 | import { AsyncRule } from './AsyncRule'; 2 | import { ValueValidationResult } from '@/ValueValidationResult'; 3 | import { IAsyncValidator } from '@/IAsyncValidator'; 4 | 5 | export class AsyncValidatorRule extends AsyncRule { 6 | constructor(validatorProducer: (model: TModel) => IAsyncValidator) { 7 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 8 | super(async (value: TValue, model: TModel) => 9 | value == null 10 | ? Promise.resolve(null) 11 | : ((await validatorProducer(model).validateAsync(value)) as ValueValidationResult), 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/rules/CoreRule.ts: -------------------------------------------------------------------------------- 1 | export class CoreRule { 2 | protected customErrorMessage?: string; 3 | protected whenCondition?: (model: TModel) => boolean; 4 | protected unlessCondition?: (model: TModel) => boolean; 5 | 6 | public setCustomErrorMessage = (customErrorMessage: string): void => { 7 | this.customErrorMessage = customErrorMessage; 8 | }; 9 | 10 | public setWhenCondition = (condition: (model: TModel) => boolean) => { 11 | this.whenCondition = condition; 12 | }; 13 | 14 | public setUnlessCondition = (condition: (model: TModel) => boolean) => { 15 | this.unlessCondition = condition; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/rules/EmailAddressRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | 3 | const emailAddressPattern = /^[a-zA-Z0-9.!#$%&’"*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/; 4 | 5 | export class EmailAddressRule extends Rule { 6 | constructor() { 7 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 8 | super((value: TValue) => { 9 | if (value == null) { 10 | return null; 11 | } 12 | if (typeof value !== 'string') { 13 | throw new TypeError('A non-string value was passed to the emailAddress rule'); 14 | } 15 | return emailAddressPattern.test(value) ? null : 'Not a valid email address'; 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/rules/EqualRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | 3 | export class EqualRule extends Rule { 4 | constructor(requiredValue: TValue) { 5 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 6 | super((value: TValue) => (value === requiredValue ? null : `Must equal '${requiredValue}'`)); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/rules/ExclusiveBetweenRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | import { formatNumber } from '@/numberHelpers'; 3 | 4 | export class ExclusiveBetweenRule extends Rule { 5 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 6 | constructor(lowerBound: number, upperBound: number) { 7 | super((value: TValue) => { 8 | if (value == null) { 9 | return null; 10 | } 11 | if (typeof value !== 'number') { 12 | throw new TypeError('A non-number value was passed to the exclusiveBetween rule'); 13 | } 14 | return value > lowerBound && value < upperBound 15 | ? null 16 | : `Value must be between ${formatNumber(lowerBound)} and ${formatNumber(upperBound)} (exclusive)`; 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/rules/GreaterThanOrEqualToRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | import { formatNumber } from '@/numberHelpers'; 3 | 4 | export class GreaterThanOrEqualToRule extends Rule { 5 | constructor(threshold: number) { 6 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 7 | super((value: TValue) => { 8 | if (value == null) { 9 | return null; 10 | } 11 | if (typeof value !== 'number') { 12 | throw new TypeError('A non-number value was passed to the greaterThanOrEqualTo rule'); 13 | } 14 | return value >= threshold 15 | ? null 16 | : `Value must be greater than or equal to ${formatNumber(threshold)}`; 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/rules/GreaterThanRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | import { formatNumber } from '@/numberHelpers'; 3 | 4 | export class GreaterThanRule extends Rule { 5 | constructor(threshold: number) { 6 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 7 | super((value: TValue) => { 8 | if (value == null) { 9 | return null; 10 | } 11 | if (typeof value !== 'number') { 12 | throw new TypeError('A non-number value was passed to the greaterThan rule'); 13 | } 14 | return value > threshold ? null : `Value must be greater than ${formatNumber(threshold)}`; 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/rules/InclusiveBetweenRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | import { formatNumber } from '@/numberHelpers'; 3 | 4 | export class InclusiveBetweenRule extends Rule { 5 | constructor(lowerBound: number, upperBound: number) { 6 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 7 | super((value: TValue) => { 8 | if (value == null) { 9 | return null; 10 | } 11 | if (typeof value !== 'number') { 12 | throw new TypeError('A non-number value was passed to the inclusiveBetween rule'); 13 | } 14 | return value >= lowerBound && value <= upperBound 15 | ? null 16 | : `Value must be between ${formatNumber(lowerBound)} and ${formatNumber(upperBound)} (inclusive)`; 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/rules/LengthRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | import { formatNumber } from '@/numberHelpers'; 3 | 4 | export class LengthRule extends Rule { 5 | constructor(minLength: number, maxLength: number) { 6 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 7 | super((value: TValue) => { 8 | if (value == null) { 9 | return null; 10 | } 11 | if (typeof value !== 'string') { 12 | throw new TypeError('A non-string value was passed to the length rule'); 13 | } 14 | return value.length >= minLength && value.length <= maxLength 15 | ? null 16 | : `Value must be between ${formatNumber(minLength)} and ${formatNumber(maxLength)} characters long`; 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/rules/LessThanOrEqualToRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | import { formatNumber } from '@/numberHelpers'; 3 | 4 | export class LessThanOrEqualToRule extends Rule { 5 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 6 | constructor(threshold: number) { 7 | super((value: TValue) => { 8 | if (value == null) { 9 | return null; 10 | } 11 | if (typeof value !== 'number') { 12 | throw new TypeError('A non-number value was passed to the lessThanOrEqualTo rule'); 13 | } 14 | return value <= threshold 15 | ? null 16 | : `Value must be less than or equal to ${formatNumber(threshold)}`; 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/rules/LessThanRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | import { formatNumber } from '@/numberHelpers'; 3 | 4 | export class LessThanRule extends Rule { 5 | constructor(threshold: number) { 6 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 7 | super((value: TValue) => { 8 | if (value == null) { 9 | return null; 10 | } 11 | if (typeof value !== 'number') { 12 | throw new TypeError('A non-number value was passed to the lessThan rule'); 13 | } 14 | return value < threshold ? null : `Value must be less than ${formatNumber(threshold)}`; 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/rules/MatchesRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | 3 | export class MatchesRule extends Rule { 4 | constructor(pattern: RegExp) { 5 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 6 | super((value: TValue) => { 7 | if (value == null) { 8 | return null; 9 | } 10 | if (typeof value !== 'string') { 11 | throw new TypeError('A non-string value was passed to the matches rule'); 12 | } 13 | return pattern.test(value) ? null : 'Value does not match the required pattern'; 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/rules/MaxLengthRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | import { formatNumber } from '@/numberHelpers'; 3 | 4 | export class MaxLengthRule extends Rule { 5 | constructor(maxLength: number) { 6 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 7 | super((value: TValue) => { 8 | if (value == null) { 9 | return null; 10 | } 11 | if (typeof value !== 'string') { 12 | throw new TypeError('A non-string value was passed to the maxLength rule'); 13 | } 14 | return value.length <= maxLength 15 | ? null 16 | : `Value must be no more than ${formatNumber(maxLength)} characters long`; 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/rules/MinLengthRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | import { formatNumber } from '@/numberHelpers'; 3 | 4 | export class MinLengthRule extends Rule { 5 | constructor(minLength: number) { 6 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 7 | super((value: TValue) => { 8 | if (value == null) { 9 | return null; 10 | } 11 | if (typeof value !== 'string') { 12 | throw new TypeError('A non-string value was passed to the minLength rule'); 13 | } 14 | return value.length >= minLength 15 | ? null 16 | : `Value must be at least ${formatNumber(minLength)} characters long`; 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/rules/MustAsyncRule.ts: -------------------------------------------------------------------------------- 1 | import { AsyncRule } from './AsyncRule'; 2 | import { AsyncPredicate } from '@/types/Predicate'; 3 | 4 | export class MustAsyncRule extends AsyncRule { 5 | constructor(definition: AsyncPredicate) { 6 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 7 | super(async (value: TValue, model: TModel) => { 8 | if (Array.isArray(definition)) { 9 | for (const eachDefinition of definition) { 10 | if (typeof eachDefinition === 'function') { 11 | const isValid = await eachDefinition(value, model); 12 | if (!isValid) { 13 | return 'Value is not valid'; 14 | } 15 | } else { 16 | const isValid = await eachDefinition.predicate(value, model); 17 | if (!isValid) { 18 | return typeof eachDefinition.message === 'function' 19 | ? eachDefinition.message(value, model) 20 | : eachDefinition.message; 21 | } 22 | } 23 | } 24 | return null; 25 | } 26 | 27 | if (typeof definition === 'function') { 28 | return (await definition(value, model)) ? null : 'Value is not valid'; 29 | } 30 | 31 | const { predicate, message } = definition; 32 | 33 | return (await predicate(value, model)) 34 | ? null 35 | : typeof message === 'function' 36 | ? message(value, model) 37 | : message; 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/rules/MustRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | import { Predicate } from '@/types/Predicate'; 3 | 4 | export class MustRule extends Rule { 5 | constructor(definition: Predicate) { 6 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 7 | super((value: TValue, model: TModel) => { 8 | if (Array.isArray(definition)) { 9 | for (const eachDefinition of definition) { 10 | if (typeof eachDefinition === 'function') { 11 | const isValid = eachDefinition(value, model); 12 | if (!isValid) { 13 | return 'Value is not valid'; 14 | } 15 | } else { 16 | const isValid = eachDefinition.predicate(value, model); 17 | if (!isValid) { 18 | return typeof eachDefinition.message === 'function' 19 | ? eachDefinition.message(value, model) 20 | : eachDefinition.message; 21 | } 22 | } 23 | } 24 | return null; 25 | } 26 | 27 | if (typeof definition === 'function') { 28 | return definition(value, model) ? null : 'Value is not valid'; 29 | } 30 | 31 | const { predicate, message } = definition; 32 | 33 | return predicate(value, model) 34 | ? null 35 | : typeof message === 'function' 36 | ? message(value, model) 37 | : message; 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/rules/NotEmptyRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | 3 | export class NotEmptyRule extends Rule { 4 | constructor() { 5 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 6 | super((value: TValue) => { 7 | if (typeof value !== 'string') { 8 | if (value == null) { 9 | return null; 10 | } 11 | throw new TypeError('A non-string value was passed to the notEmpty rule'); 12 | } 13 | return value.trim().length > 0 ? null : 'Value cannot be empty'; 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/rules/NotEqualRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | 3 | export class NotEqualRule extends Rule { 4 | constructor(forbiddenValue: TValue) { 5 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 6 | super((value: TValue) => 7 | value !== forbiddenValue ? null : `Must not equal '${forbiddenValue}'`, 8 | ); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/rules/NotNullRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | 3 | export type NotNullRuleOptions = { 4 | includeUndefined?: boolean; 5 | }; 6 | 7 | export class NotNullRule extends Rule { 8 | constructor({ includeUndefined }: NotNullRuleOptions) { 9 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 10 | super((value: TValue) => { 11 | if (includeUndefined) { 12 | return value == null ? 'Value cannot be null' : null; 13 | } 14 | 15 | return value === null ? 'Value cannot be null' : null; 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/rules/NotUndefinedRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | 3 | export class NotUndefinedRule extends Rule { 4 | constructor() { 5 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 6 | super((value: TValue) => (value === undefined ? 'Value cannot be undefined' : null)); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/rules/NullRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | 3 | export type NullRuleOptions = { 4 | includeUndefined: boolean; 5 | }; 6 | 7 | export class NullRule extends Rule { 8 | constructor({ includeUndefined }: NullRuleOptions) { 9 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 10 | super((value: TValue) => { 11 | if (includeUndefined) { 12 | return value == null ? null : 'Value must be null'; 13 | } 14 | 15 | return value === null ? null : 'Value must be null'; 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/rules/PrecisionScaleRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | 3 | export class PrecisionScaleRule extends Rule { 4 | constructor(precision: number, scale: number) { 5 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 6 | super((value: TValue) => { 7 | if (value == null || value === 0) { 8 | return null; 9 | } 10 | if (typeof value !== 'number') { 11 | throw new TypeError('A non-number value was passed to the precisionScale rule'); 12 | } 13 | 14 | const regex = precisionScaleRegex({ precision, scale }); 15 | 16 | if (!regex.test(value.toString())) { 17 | return `Value must not be more than ${precision} digits in total, with allowance for ${scale} decimals`; 18 | } 19 | 20 | return null; 21 | }); 22 | } 23 | } 24 | 25 | const precisionScaleRegex = ({ precision, scale }: { precision: number; scale: number }) => { 26 | const integerDigits = precision - scale; 27 | 28 | return integerDigits === 0 29 | ? new RegExp(`^-?0?\\.\\d{0,${scale}}$`) // The leading 0 to the left of the decimal point does not count towards precision 30 | : new RegExp(`^-?\\d{1,${integerDigits}}(\\.\\d{1,${scale}})?$`); 31 | }; 32 | -------------------------------------------------------------------------------- /src/rules/Rule.ts: -------------------------------------------------------------------------------- 1 | import { CoreRule } from './CoreRule'; 2 | import { ValueValidationResult } from '@/ValueValidationResult'; 3 | import { ValueValidator } from '@/ValueValidator'; 4 | 5 | export class Rule extends CoreRule { 6 | private readonly valueValidator: ValueValidator; 7 | 8 | constructor(valueValidator: ValueValidator) { 9 | super(); 10 | this.valueValidator = valueValidator; 11 | } 12 | 13 | public validate = (value: TValue, model: TModel): ValueValidationResult => { 14 | if (this.whenCondition != null && !this.whenCondition(model)) { 15 | return null; 16 | } 17 | 18 | if (this.unlessCondition != null && this.unlessCondition(model)) { 19 | return null; 20 | } 21 | 22 | const errorOrNull = this.valueValidator(value, model); 23 | return errorOrNull != null ? this.customErrorMessage || errorOrNull : null; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/rules/UndefinedRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | 3 | export class UndefinedRule extends Rule { 4 | constructor() { 5 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 6 | super((value: TValue) => (value === undefined ? null : 'Value must be undefined')); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/rules/ValidatorRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | import { IValidator } from '@/IValidator'; 3 | import { ValueValidationResult } from '@/ValueValidationResult'; 4 | 5 | export class ValidatorRule extends Rule { 6 | constructor(validatorProducer: (model: TModel) => IValidator) { 7 | // istanbul ignore next - https://github.com/gotwarlost/istanbul/issues/690 8 | super((value: TValue, model: TModel) => 9 | value == null 10 | ? null 11 | : (validatorProducer(model).validate(value) as ValueValidationResult), 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/types/AppliesTo.ts: -------------------------------------------------------------------------------- 1 | export type AppliesTo = 'AppliesToAllValidators' | 'AppliesToCurrentValidator'; 2 | -------------------------------------------------------------------------------- /src/types/ArrayType.ts: -------------------------------------------------------------------------------- 1 | export type ArrayType = 2 | | Array 3 | | ReadonlyArray 4 | | Readonly>; 5 | -------------------------------------------------------------------------------- /src/types/Constrain.ts: -------------------------------------------------------------------------------- 1 | // Credit: https://github.com/microsoft/TypeScript/issues/9252#issuecomment-472881853 2 | // Slightly hacky workaround for the fact that a generic type parameter 3 | // does not limit values of T to having **only** the properties of U, but rather requires 4 | // that they have **at least** the properties specified in U. This is currently used for 5 | // getting the right typing on the `.ruleForTransformed` methods, so that an object property 6 | // can't be mapped to an object property of a different type (in particular, one that has 7 | // more properties, since these won't be present in the `ValidationErrors` object). 8 | /** 9 | * Constrain 10 | * @desc Constrains type `T` to only those properties that exist in type `U` 11 | */ 12 | export type Constrain = { 13 | [key in keyof T]: key extends keyof U ? T[key] : never; 14 | }; 15 | -------------------------------------------------------------------------------- /src/types/FlatType.ts: -------------------------------------------------------------------------------- 1 | export type FlatType = string | number | boolean | symbol; 2 | -------------------------------------------------------------------------------- /src/types/IfNotNeverThen.ts: -------------------------------------------------------------------------------- 1 | export type IfNotNeverThen = TValue extends never ? never : TOut; 2 | -------------------------------------------------------------------------------- /src/types/IfType.ts: -------------------------------------------------------------------------------- 1 | export type IfString = TValue extends string ? TOut : never; 2 | export type IfNumber = TValue extends number ? TOut : never; 3 | export type IfObject = TValue extends object ? TOut : never; 4 | -------------------------------------------------------------------------------- /src/types/Message.ts: -------------------------------------------------------------------------------- 1 | type MessageGenerator = ( 2 | value: TTransformedValue, 3 | model: TModel, 4 | ) => string; 5 | 6 | export type Message = 7 | | string 8 | | MessageGenerator; 9 | -------------------------------------------------------------------------------- /src/types/Optional.ts: -------------------------------------------------------------------------------- 1 | export type Optional = T | null | undefined; 2 | -------------------------------------------------------------------------------- /src/types/Predicate.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '@/types/Message'; 2 | 3 | export type SimplePredicate = ( 4 | value: TTransformedValue, 5 | model: TModel, 6 | ) => boolean; 7 | 8 | export type SimpleAsyncPredicate = ( 9 | value: TTransformedValue, 10 | model: TModel, 11 | ) => Promise; 12 | 13 | export type SimplePredicateWithMessage = { 14 | predicate: SimplePredicate; 15 | message: Message; 16 | }; 17 | 18 | export type SimpleAsyncPredicateWithMessage = { 19 | predicate: SimpleAsyncPredicate; 20 | message: Message; 21 | }; 22 | 23 | export type Predicate = 24 | | SimplePredicate 25 | | SimplePredicateWithMessage 26 | | Array< 27 | | SimplePredicate 28 | | SimplePredicateWithMessage 29 | >; 30 | 31 | export type AsyncPredicate = 32 | | SimpleAsyncPredicate 33 | | SimpleAsyncPredicateWithMessage 34 | | Array< 35 | | SimpleAsyncPredicate 36 | | SimpleAsyncPredicateWithMessage 37 | >; 38 | -------------------------------------------------------------------------------- /src/types/TransformedValue.ts: -------------------------------------------------------------------------------- 1 | import { FlatType } from '@/types/FlatType'; 2 | 3 | // We restrict the type to flat types, otherwise it would be possible to map a flat type 4 | // to a complex type and force an object/array property in the validation errors, when only 5 | // a flat error (`string | null`) is expected. `TValue` is also obviously accepted, since 6 | // the errors object will have the same shape in that case. 7 | export type TransformedValue = TValue | FlatType | null | undefined; 8 | 9 | // TODO: Should we allow Partial here given that a subset of the 10 | // properties being in the errors object is a valid use case? 11 | -------------------------------------------------------------------------------- /src/valueValidator/ArrayValueValidatorBuilder.ts: -------------------------------------------------------------------------------- 1 | import { ValueTransformer } from './ValueTransformer'; 2 | import { hasError } from './ValueValidator'; 3 | import { ValueValidatorBuilder } from './ValueValidatorBuilder'; 4 | import { ValueValidator } from '@/ValueValidator'; 5 | import { ValueValidationResult } from '@/ValueValidationResult'; 6 | import { ArrayType } from '@/types/ArrayType'; 7 | 8 | export class ArrayValueValidatorBuilder< 9 | TModel, 10 | TValue extends ArrayType, 11 | TEachValue, 12 | TEachTransformedValue, 13 | > { 14 | private eachValueValidatorBuilder: ValueValidatorBuilder< 15 | TModel, 16 | TEachValue, 17 | TEachTransformedValue 18 | >; 19 | 20 | constructor( 21 | rebuildValidate: () => void, 22 | transformValue: ValueTransformer, 23 | ) { 24 | this.eachValueValidatorBuilder = new ValueValidatorBuilder< 25 | TModel, 26 | TEachValue, 27 | TEachTransformedValue 28 | >(rebuildValidate, transformValue); 29 | } 30 | 31 | public build = (): ValueValidator => { 32 | return (value: TValue, model: TModel) => { 33 | if (value == null) { 34 | return null; 35 | } 36 | 37 | const valueValidator = this.eachValueValidatorBuilder.build(); 38 | 39 | const errors = value.map((element) => { 40 | const errorOrNull = valueValidator(element, model); 41 | return hasError(errorOrNull) ? errorOrNull : null; 42 | }) as ValueValidationResult; 43 | 44 | return hasError(errors) ? errors : null; 45 | }; 46 | }; 47 | 48 | public getAllRules = () => this.eachValueValidatorBuilder.getAllRules(); 49 | } 50 | -------------------------------------------------------------------------------- /src/valueValidator/AsyncArrayValueValidatorBuilder.ts: -------------------------------------------------------------------------------- 1 | import { AsyncValueValidator } from './AsyncValueValidator'; 2 | import { AsyncValueValidatorBuilder } from './AsyncValueValidatorBuilder'; 3 | import { ValueTransformer } from './ValueTransformer'; 4 | import { ValueValidationResult } from '@/ValueValidationResult'; 5 | import { hasError } from '@/valueValidator/ValueValidator'; 6 | import { ArrayType } from '@/types/ArrayType'; 7 | 8 | export class AsyncArrayValueValidatorBuilder< 9 | TModel, 10 | TValue extends ArrayType, 11 | TEachValue, 12 | TEachTransformedValue, 13 | > { 14 | private eachAsyncValueValidatorBuilder: AsyncValueValidatorBuilder< 15 | TModel, 16 | TEachValue, 17 | TEachTransformedValue 18 | >; 19 | 20 | constructor( 21 | rebuildValidateAsync: () => void, 22 | transformValue: ValueTransformer, 23 | ) { 24 | this.eachAsyncValueValidatorBuilder = new AsyncValueValidatorBuilder< 25 | TModel, 26 | TEachValue, 27 | TEachTransformedValue 28 | >(rebuildValidateAsync, transformValue); 29 | } 30 | 31 | public build = (): AsyncValueValidator => { 32 | return async (value: TValue, model: TModel) => { 33 | if (value == null) { 34 | return null; 35 | } 36 | 37 | const asyncValueValidator = this.eachAsyncValueValidatorBuilder.build(); 38 | 39 | const errors = [] as Array>; 40 | 41 | for (const element of value) { 42 | const errorOrNull = await asyncValueValidator(element, model); 43 | const valueValidationResult = hasError(errorOrNull) ? errorOrNull : null; 44 | errors.push(valueValidationResult); 45 | } 46 | 47 | return hasError(errors as ValueValidationResult) 48 | ? (errors as ValueValidationResult) 49 | : null; 50 | }; 51 | }; 52 | 53 | public getAllRules = () => this.eachAsyncValueValidatorBuilder.getAllRules(); 54 | } 55 | -------------------------------------------------------------------------------- /src/valueValidator/AsyncRuleValidators.ts: -------------------------------------------------------------------------------- 1 | import { AppliesTo } from '@/types/AppliesTo'; 2 | import { AsyncPredicate, Predicate } from '@/types/Predicate'; 3 | import { IValidator } from '@/IValidator'; 4 | import { IAsyncValidator } from '@/IAsyncValidator'; 5 | import { IfNumber, IfObject, IfString } from '@/types/IfType'; 6 | import { NotNullRuleOptions } from '@/rules/NotNullRule'; 7 | import { NullRuleOptions } from '@/rules/NullRule'; 8 | 9 | export type AsyncRuleValidators = { 10 | notEqual: (forbiddenValue: TValue) => AsyncRuleValidatorsAndExtensions; 11 | equal: (requiredValue: TValue) => AsyncRuleValidatorsAndExtensions; 12 | must: (predicate: Predicate) => AsyncRuleValidatorsAndExtensions; 13 | mustAsync: ( 14 | predicate: AsyncPredicate, 15 | ) => AsyncRuleValidatorsAndExtensions; 16 | notNull: (ruleOptions?: NotNullRuleOptions) => AsyncRuleValidatorsAndExtensions; 17 | notUndefined: () => AsyncRuleValidatorsAndExtensions; 18 | null: (ruleOptions?: NullRuleOptions) => AsyncRuleValidatorsAndExtensions; 19 | undefined: () => AsyncRuleValidatorsAndExtensions; 20 | notEmpty: IfString AsyncRuleValidatorsAndExtensions>; 21 | length: IfString< 22 | TValue, 23 | (minLength: number, maxLength: number) => AsyncRuleValidatorsAndExtensions 24 | >; 25 | maxLength: IfString< 26 | TValue, 27 | (maxLength: number) => AsyncRuleValidatorsAndExtensions 28 | >; 29 | minLength: IfString< 30 | TValue, 31 | (minLength: number) => AsyncRuleValidatorsAndExtensions 32 | >; 33 | matches: IfString AsyncRuleValidatorsAndExtensions>; 34 | emailAddress: IfString AsyncRuleValidatorsAndExtensions>; 35 | lessThan: IfNumber< 36 | TValue, 37 | (threshold: number) => AsyncRuleValidatorsAndExtensions 38 | >; 39 | lessThanOrEqualTo: IfNumber< 40 | TValue, 41 | (threshold: number) => AsyncRuleValidatorsAndExtensions 42 | >; 43 | greaterThan: IfNumber< 44 | TValue, 45 | (threshold: number) => AsyncRuleValidatorsAndExtensions 46 | >; 47 | greaterThanOrEqualTo: IfNumber< 48 | TValue, 49 | (threshold: number) => AsyncRuleValidatorsAndExtensions 50 | >; 51 | exclusiveBetween: IfNumber< 52 | TValue, 53 | (lowerBound: number, upperBound: number) => AsyncRuleValidatorsAndExtensions 54 | >; 55 | inclusiveBetween: IfNumber< 56 | TValue, 57 | (lowerBound: number, upperBound: number) => AsyncRuleValidatorsAndExtensions 58 | >; 59 | precisionScale: IfNumber< 60 | TValue, 61 | (precision: number, scale: number) => AsyncRuleValidatorsAndExtensions 62 | >; 63 | setValidator: IfObject< 64 | TValue, 65 | ( 66 | validatorProducer: (model: TModel) => IValidator>, 67 | ) => AsyncRuleValidatorsAndExtensions 68 | >; 69 | setAsyncValidator: IfObject< 70 | TValue, 71 | ( 72 | validatorProducer: (model: TModel) => IAsyncValidator>, 73 | ) => AsyncRuleValidatorsAndExtensions 74 | >; 75 | }; 76 | 77 | export type AsyncWhenCondition = ( 78 | condition: (model: TModel) => boolean, 79 | appliesTo?: AppliesTo, 80 | ) => AsyncRuleValidators; 81 | 82 | export type AsyncUnlessCondition = ( 83 | condition: (model: TModel) => boolean, 84 | appliesTo?: AppliesTo, 85 | ) => AsyncRuleValidators; 86 | 87 | export type AsyncRuleValidatorsAndConditionExtensions = AsyncRuleValidators< 88 | TModel, 89 | TValue 90 | > & { 91 | when: AsyncWhenCondition; 92 | unless: AsyncUnlessCondition; 93 | }; 94 | 95 | export type AsyncWithMessage = ( 96 | message: string, 97 | ) => AsyncRuleValidatorsAndConditionExtensions; 98 | 99 | export type AsyncRuleValidatorsAndExtensions = 100 | AsyncRuleValidatorsAndConditionExtensions & { 101 | withMessage: AsyncWithMessage; 102 | }; 103 | -------------------------------------------------------------------------------- /src/valueValidator/AsyncValueValidator.ts: -------------------------------------------------------------------------------- 1 | import { ValueValidationResult } from '@/ValueValidationResult'; 2 | 3 | export type AsyncValueValidator = ( 4 | value: TValue, 5 | model: TModel, 6 | ) => Promise>; 7 | -------------------------------------------------------------------------------- /src/valueValidator/AsyncValueValidatorBuilder.ts: -------------------------------------------------------------------------------- 1 | import { hasError } from '@/valueValidator/ValueValidator'; 2 | import { AsyncPredicate } from '@/types/Predicate'; 3 | import { CoreValueValidatorBuilder } from '@/valueValidator/CoreValueValidatorBuilder'; 4 | import { ValueTransformer } from '@/valueValidator/ValueTransformer'; 5 | import { AsyncValueValidator } from '@/valueValidator/AsyncValueValidator'; 6 | import { AsyncRule } from '@/rules/AsyncRule'; 7 | import { MustAsyncRule } from '@/rules/MustAsyncRule'; 8 | import { Rule } from '@/rules/Rule'; 9 | import { ValueValidationResult } from '@/ValueValidationResult'; 10 | import { IAsyncValidator } from '@/IAsyncValidator'; 11 | import { AsyncValidatorRule } from '@/rules/AsyncValidatorRule'; 12 | import { MustAsync, SetValidatorAsync } from '@/valueValidator/ValueValidatorBuilderTypes'; 13 | import { 14 | AsyncRuleValidators, 15 | AsyncRuleValidatorsAndConditionExtensions, 16 | AsyncRuleValidatorsAndExtensions, 17 | } from '@/valueValidator/AsyncRuleValidators'; 18 | 19 | export class AsyncValueValidatorBuilder< 20 | TModel, 21 | TValue, 22 | TTransformedValue, 23 | > extends CoreValueValidatorBuilder< 24 | TModel, 25 | TValue, 26 | TTransformedValue, 27 | AsyncRuleValidators, 28 | AsyncRuleValidatorsAndConditionExtensions, 29 | AsyncRuleValidatorsAndExtensions 30 | > { 31 | constructor( 32 | rebuildValidateAsync: () => void, 33 | transformValue: ValueTransformer, 34 | ) { 35 | super(rebuildValidateAsync, transformValue); 36 | } 37 | 38 | public build = (): AsyncValueValidator => { 39 | return async (value: TValue, model: TModel): Promise> => { 40 | const transformedValue = this.transformValue(value); 41 | 42 | for (const rule of this.rules) { 43 | const validationResult = rule.isAsync 44 | ? ((await (rule.rule as AsyncRule).validateAsync( 45 | transformedValue, 46 | model, 47 | )) as ValueValidationResult) 48 | : ((rule.rule as Rule).validate( 49 | transformedValue, 50 | model, 51 | ) as ValueValidationResult); 52 | 53 | if (hasError(validationResult)) { 54 | return validationResult; 55 | } 56 | } 57 | 58 | return null; 59 | }; 60 | }; 61 | 62 | public mustAsync: MustAsync = ( 63 | predicate: AsyncPredicate, 64 | ) => { 65 | const asyncMustRule = new MustAsyncRule(predicate); 66 | this.pushAsyncRule(asyncMustRule); 67 | return this.getAllRulesAndExtensions(); 68 | }; 69 | 70 | public setAsyncValidator: SetValidatorAsync = ( 71 | validatorProducer: (model: TModel) => IAsyncValidator, 72 | ) => { 73 | const asyncValidatorRule = new AsyncValidatorRule( 74 | validatorProducer as (model: TModel) => IAsyncValidator, 75 | ); 76 | this.pushAsyncRule(asyncValidatorRule); 77 | return this.getAllRulesAndExtensions(); 78 | }; 79 | 80 | public getAllRules = () => 81 | ({ 82 | ...this._getAllRules(), 83 | mustAsync: this.mustAsync, 84 | setAsyncValidator: this.setAsyncValidator, 85 | }) as AsyncRuleValidators; 86 | 87 | public getAllRulesAndConditionExtensions = () => 88 | ({ 89 | ...this.getAllRules(), 90 | when: this.when, 91 | unless: this.unless, 92 | }) as AsyncRuleValidatorsAndConditionExtensions; 93 | 94 | public getAllRulesAndExtensions = () => 95 | ({ 96 | ...this.getAllRulesAndConditionExtensions(), 97 | withMessage: this.withMessage, 98 | }) as AsyncRuleValidatorsAndExtensions; 99 | } 100 | -------------------------------------------------------------------------------- /src/valueValidator/RuleValidators.ts: -------------------------------------------------------------------------------- 1 | import { AppliesTo } from '@/types/AppliesTo'; 2 | import { Predicate } from '@/types/Predicate'; 3 | import { IValidator } from '@/IValidator'; 4 | import { IfNumber, IfObject, IfString } from '@/types/IfType'; 5 | import { NotNullRuleOptions } from '@/rules/NotNullRule'; 6 | import { NullRuleOptions } from '@/rules/NullRule'; 7 | 8 | export type RuleValidators = { 9 | notEqual: (forbiddenValue: TValue) => RuleValidatorsAndExtensions; 10 | equal: (requiredValue: TValue) => RuleValidatorsAndExtensions; 11 | must: (predicate: Predicate) => RuleValidatorsAndExtensions; 12 | notNull: (options?: NotNullRuleOptions) => RuleValidatorsAndExtensions; 13 | notUndefined: () => RuleValidatorsAndExtensions; 14 | null: (ruleOptions?: NullRuleOptions) => RuleValidatorsAndExtensions; 15 | undefined: () => RuleValidatorsAndExtensions; 16 | notEmpty: IfString RuleValidatorsAndExtensions>; 17 | length: IfString< 18 | TValue, 19 | (minLength: number, maxLength: number) => RuleValidatorsAndExtensions 20 | >; 21 | maxLength: IfString RuleValidatorsAndExtensions>; 22 | minLength: IfString RuleValidatorsAndExtensions>; 23 | matches: IfString RuleValidatorsAndExtensions>; 24 | emailAddress: IfString RuleValidatorsAndExtensions>; 25 | lessThan: IfNumber RuleValidatorsAndExtensions>; 26 | lessThanOrEqualTo: IfNumber< 27 | TValue, 28 | (threshold: number) => RuleValidatorsAndExtensions 29 | >; 30 | greaterThan: IfNumber RuleValidatorsAndExtensions>; 31 | greaterThanOrEqualTo: IfNumber< 32 | TValue, 33 | (threshold: number) => RuleValidatorsAndExtensions 34 | >; 35 | exclusiveBetween: IfNumber< 36 | TValue, 37 | (lowerBound: number, upperBound: number) => RuleValidatorsAndExtensions 38 | >; 39 | inclusiveBetween: IfNumber< 40 | TValue, 41 | (lowerBound: number, upperBound: number) => RuleValidatorsAndExtensions 42 | >; 43 | precisionScale: IfNumber< 44 | TValue, 45 | (precision: number, scale: number) => RuleValidatorsAndExtensions 46 | >; 47 | setValidator: IfObject< 48 | TValue, 49 | ( 50 | validatorProducer: (model: TModel) => IValidator>, 51 | ) => RuleValidatorsAndExtensions 52 | >; 53 | }; 54 | 55 | export type WhenCondition = ( 56 | condition: (model: TModel) => boolean, 57 | appliesTo?: AppliesTo, 58 | ) => RuleValidators; 59 | 60 | export type UnlessCondition = ( 61 | condition: (model: TModel) => boolean, 62 | appliesTo?: AppliesTo, 63 | ) => RuleValidators; 64 | 65 | export type RuleValidatorsAndConditionExtensions = RuleValidators< 66 | TModel, 67 | TValue 68 | > & { 69 | when: WhenCondition; 70 | unless: UnlessCondition; 71 | }; 72 | 73 | export type WithMessage = ( 74 | message: string, 75 | ) => RuleValidatorsAndConditionExtensions; 76 | 77 | export type RuleValidatorsAndExtensions = RuleValidatorsAndConditionExtensions< 78 | TModel, 79 | TValue 80 | > & { 81 | withMessage: WithMessage; 82 | }; 83 | -------------------------------------------------------------------------------- /src/valueValidator/ValueTransformer.ts: -------------------------------------------------------------------------------- 1 | export type ValueTransformer = (value: TValue) => TTransformedValue; 2 | -------------------------------------------------------------------------------- /src/valueValidator/ValueValidator.ts: -------------------------------------------------------------------------------- 1 | import { ValueValidationResult } from '@/ValueValidationResult'; 2 | 3 | export const hasError = (valueValidationResult: ValueValidationResult): boolean => { 4 | if (valueValidationResult == null) { 5 | return false; 6 | } 7 | 8 | if (Array.isArray(valueValidationResult)) { 9 | return valueValidationResult.filter((eachResult) => hasError(eachResult)).length > 0; 10 | } 11 | 12 | if (typeof valueValidationResult === 'object') { 13 | return Object.keys(valueValidationResult as object).length > 0; 14 | } 15 | 16 | return true; 17 | }; 18 | -------------------------------------------------------------------------------- /src/valueValidator/ValueValidatorBuilder.ts: -------------------------------------------------------------------------------- 1 | import { hasError } from './ValueValidator'; 2 | import { CoreValueValidatorBuilder } from './CoreValueValidatorBuilder'; 3 | import { ValueTransformer } from './ValueTransformer'; 4 | import { Rule } from '@/rules/Rule'; 5 | import { ValueValidationResult } from '@/ValueValidationResult'; 6 | import { ValueValidator } from '@/ValueValidator'; 7 | import { 8 | RuleValidators, 9 | RuleValidatorsAndConditionExtensions, 10 | RuleValidatorsAndExtensions, 11 | } from '@/valueValidator/RuleValidators'; 12 | 13 | export class ValueValidatorBuilder< 14 | TModel, 15 | TValue, 16 | TTransformedValue, 17 | > extends CoreValueValidatorBuilder< 18 | TModel, 19 | TValue, 20 | TTransformedValue, 21 | RuleValidators, 22 | RuleValidatorsAndConditionExtensions, 23 | RuleValidatorsAndExtensions 24 | > { 25 | constructor( 26 | rebuildValidate: () => void, 27 | transformValue: ValueTransformer, 28 | ) { 29 | super(rebuildValidate, transformValue); 30 | } 31 | 32 | public build = (): ValueValidator => { 33 | return (value: TValue, model: TModel): ValueValidationResult => { 34 | const transformedValue = this.transformValue(value); 35 | 36 | for (const rule of this.rules) { 37 | const validationResult = (rule.rule as Rule).validate( 38 | transformedValue, 39 | model, 40 | ) as ValueValidationResult; 41 | 42 | if (hasError(validationResult)) { 43 | return validationResult; 44 | } 45 | } 46 | 47 | return null; 48 | }; 49 | }; 50 | 51 | public getAllRules = () => this._getAllRules() as RuleValidators; 52 | 53 | public getAllRulesAndConditionExtensions = () => 54 | ({ 55 | ...this.getAllRules(), 56 | when: this.when, 57 | unless: this.unless, 58 | }) as RuleValidatorsAndConditionExtensions; 59 | 60 | public getAllRulesAndExtensions = () => 61 | ({ 62 | ...this.getAllRulesAndConditionExtensions(), 63 | withMessage: this.withMessage, 64 | }) as RuleValidatorsAndExtensions; 65 | } 66 | -------------------------------------------------------------------------------- /src/valueValidator/ValueValidatorBuilderTypes.ts: -------------------------------------------------------------------------------- 1 | import { AsyncPredicate, Predicate } from '@/types/Predicate'; 2 | import { IAsyncValidator } from '@/IAsyncValidator'; 3 | import { RuleValidatorsAndExtensions } from '@/valueValidator/RuleValidators'; 4 | 5 | export type Must = ( 6 | predicate: Predicate, 7 | ) => RuleValidatorsAndExtensions; 8 | 9 | export type Matches = ( 10 | pattern: RegExp, 11 | ) => RuleValidatorsAndExtensions; 12 | 13 | export type MustAsync = ( 14 | predicate: AsyncPredicate, 15 | ) => RuleValidatorsAndExtensions; 16 | 17 | export type SetValidatorAsync = ( 18 | validatorProducer: (model: TModel) => IAsyncValidator, 19 | ) => RuleValidatorsAndExtensions; 20 | -------------------------------------------------------------------------------- /test/examplesAsync.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncValidator } from '@/index'; 2 | 3 | describe('examples (async)', () => { 4 | it('custom rules', async () => { 5 | type ClothesPile = { 6 | numberOfSocks: number; 7 | }; 8 | 9 | const beAnEvenInteger = (value: number) => value % 2 === 0; 10 | 11 | class ClothesPileValidator extends AsyncValidator { 12 | constructor() { 13 | super(); 14 | 15 | this.ruleFor('numberOfSocks').must(beAnEvenInteger).withMessage(`Can't have odd socks!`); 16 | } 17 | } 18 | 19 | const validator = new ClothesPileValidator(); 20 | 21 | expect(await validator.validateAsync({ numberOfSocks: 3 })).toEqual({ 22 | numberOfSocks: `Can't have odd socks!`, 23 | }); 24 | }); 25 | 26 | it('deeply nested types', async () => { 27 | type Person = { 28 | name: string; 29 | age: number; 30 | favouriteBand: Band; 31 | leastFavouriteBand?: Band; 32 | }; 33 | 34 | type Band = { 35 | name: string; 36 | debutAlbum: Album; 37 | currentAlbum: Album | null; 38 | allAlbumNames: Array; 39 | }; 40 | 41 | type Album = { 42 | name: string; 43 | numberOfTracks: number; 44 | rating?: number; 45 | }; 46 | 47 | class AlbumValidator extends AsyncValidator { 48 | constructor() { 49 | super(); 50 | this.ruleFor('name').notEmpty(); 51 | this.ruleFor('numberOfTracks').greaterThan(0); 52 | this.ruleFor('rating') 53 | .inclusiveBetween(1, 5) 54 | .must((rating: number | undefined) => rating == null || rating % 1 === 0) 55 | .withMessage('Rating must be an integer') 56 | .when((c) => c.rating != null); 57 | } 58 | } 59 | 60 | class BandValidator extends AsyncValidator { 61 | constructor() { 62 | super(); 63 | this.ruleFor('name').notEmpty(); 64 | this.ruleFor('debutAlbum') 65 | .setAsyncValidator(() => new AlbumValidator()) 66 | .must((value: Album, model: Band) => model.allAlbumNames.indexOf(value.name) !== -1) 67 | .withMessage('Name of debut album was not present in list of all album names'); 68 | this.ruleForEach('allAlbumNames').notEmpty(); 69 | this.ruleFor('currentAlbum') 70 | .setAsyncValidator(() => new AlbumValidator()) 71 | .must( 72 | (value: Album | null, model: Band) => 73 | value == null || model.allAlbumNames.indexOf(value.name) !== -1, 74 | ) 75 | .withMessage('Name of current album was not present in list of all album names') 76 | .unless((c) => c.currentAlbum == null); 77 | } 78 | } 79 | 80 | class PersonValidator extends AsyncValidator { 81 | constructor() { 82 | super(); 83 | this.ruleFor('name').notEmpty(); 84 | this.ruleFor('age').greaterThanOrEqualTo(18); 85 | this.ruleFor('favouriteBand').setAsyncValidator(() => new BandValidator()); 86 | this.ruleFor('leastFavouriteBand') 87 | .setAsyncValidator(() => new BandValidator()) 88 | .unless((c) => c.leastFavouriteBand == null); 89 | } 90 | } 91 | 92 | const validator = new PersonValidator(); 93 | 94 | const person: Person = { 95 | name: 'Alex', 96 | age: 25, 97 | favouriteBand: { 98 | name: 'Arctic Monkeys', 99 | debutAlbum: { 100 | name: `Whatever People Say I Am, That's What I'm Not`, 101 | numberOfTracks: 13, 102 | rating: 9, 103 | }, 104 | currentAlbum: { 105 | name: `Tranquility Base Hotel and Casino`, 106 | numberOfTracks: 11, 107 | rating: 5, 108 | }, 109 | allAlbumNames: [ 110 | `Whatever People Say I Am, That's What I'm Not`, 111 | `Favourite Worst Nightmare`, 112 | `Humbug`, 113 | `Suck It and See`, 114 | `AM`, 115 | `Tranquility Base Hotel & Casino`, 116 | ], 117 | }, 118 | }; 119 | 120 | expect(await validator.validateAsync(person)).toEqual({ 121 | favouriteBand: { 122 | debutAlbum: { 123 | rating: 'Value must be between 1 and 5 (inclusive)', 124 | }, 125 | currentAlbum: 'Name of current album was not present in list of all album names', 126 | }, 127 | }); 128 | }); 129 | 130 | it('recursive types', async () => { 131 | type Employee = { 132 | name: string; 133 | age: number; 134 | lineManager: Employee | null; 135 | }; 136 | 137 | class EmployeeValidator extends AsyncValidator { 138 | constructor() { 139 | super(); 140 | this.ruleFor('name').notEmpty(); 141 | this.ruleFor('age').inclusiveBetween(18, 80); 142 | this.ruleFor('lineManager') 143 | .setAsyncValidator(() => new EmployeeValidator()) 144 | .when((employee) => employee.lineManager != null); 145 | } 146 | } 147 | 148 | const validator = new EmployeeValidator(); 149 | 150 | const boss: Employee = { 151 | name: 'The Boss', 152 | age: -1, 153 | lineManager: null, 154 | }; 155 | 156 | const teamLeader: Employee = { 157 | name: 'Team Leader', 158 | age: 28, 159 | lineManager: boss, 160 | }; 161 | 162 | const grunt: Employee = { 163 | name: 'Grunt', 164 | age: 25, 165 | lineManager: teamLeader, 166 | }; 167 | 168 | expect(await validator.validateAsync(grunt)).toEqual({ 169 | lineManager: { 170 | lineManager: { 171 | age: 'Value must be between 18 and 80 (inclusive)', 172 | }, 173 | }, 174 | }); 175 | }); 176 | 177 | it('complex array types', async () => { 178 | type Employee = { 179 | name: string; 180 | employeeNumber: number; 181 | lmees: Array; 182 | }; 183 | 184 | class EmployeeValidator extends AsyncValidator { 185 | constructor() { 186 | super(); 187 | this.ruleFor('name').notEmpty().withMessage('Employees have names!'); 188 | this.ruleForEach('lmees').setAsyncValidator(() => new EmployeeValidator()); 189 | } 190 | } 191 | 192 | const validator = new EmployeeValidator(); 193 | 194 | const employee: Employee = { 195 | name: 'Alex', 196 | employeeNumber: 1, 197 | lmees: [ 198 | { 199 | name: 'Bob', 200 | employeeNumber: 2, 201 | lmees: [], 202 | }, 203 | { 204 | name: 'Charlie', 205 | employeeNumber: 3, 206 | lmees: [ 207 | { 208 | name: '', 209 | employeeNumber: 4, 210 | lmees: [], 211 | }, 212 | ], 213 | }, 214 | ], 215 | }; 216 | 217 | const result = await validator.validateAsync(employee); 218 | 219 | expect(result).toEqual({ 220 | lmees: [ 221 | null, 222 | { 223 | lmees: [{ name: 'Employees have names!' }], 224 | }, 225 | ], 226 | }); 227 | }); 228 | }); 229 | -------------------------------------------------------------------------------- /test/examplesSync.test.ts: -------------------------------------------------------------------------------- 1 | import { Validator } from '@/index'; 2 | 3 | describe('examples (sync)', () => { 4 | it('custom rules', () => { 5 | type ClothesPile = { 6 | numberOfSocks: number; 7 | }; 8 | 9 | const beAnEvenInteger = (value: number) => value % 2 === 0; 10 | 11 | class ClothesPileValidator extends Validator { 12 | constructor() { 13 | super(); 14 | 15 | this.ruleFor('numberOfSocks').must(beAnEvenInteger).withMessage(`Can't have odd socks!`); 16 | } 17 | } 18 | 19 | const validator = new ClothesPileValidator(); 20 | 21 | expect(validator.validate({ numberOfSocks: 3 })).toEqual({ 22 | numberOfSocks: `Can't have odd socks!`, 23 | }); 24 | }); 25 | 26 | it('deeply nested types', () => { 27 | type Person = { 28 | name: string; 29 | age: number; 30 | favouriteBand: Band; 31 | leastFavouriteBand?: Band; 32 | }; 33 | 34 | type Band = { 35 | name: string; 36 | debutAlbum: Album; 37 | currentAlbum: Album | null; 38 | allAlbumNames: Array; 39 | }; 40 | 41 | type Album = { 42 | name: string; 43 | numberOfTracks: number; 44 | rating?: number; 45 | }; 46 | 47 | class AlbumValidator extends Validator { 48 | constructor() { 49 | super(); 50 | this.ruleFor('name').notEmpty(); 51 | this.ruleFor('numberOfTracks').greaterThan(0); 52 | this.ruleFor('rating') 53 | .inclusiveBetween(1, 5) 54 | .must((rating: number | undefined) => rating == null || rating % 1 === 0) 55 | .withMessage('Rating must be an integer') 56 | .when((c) => c.rating != null); 57 | } 58 | } 59 | 60 | class BandValidator extends Validator { 61 | constructor() { 62 | super(); 63 | this.ruleFor('name').notEmpty(); 64 | this.ruleFor('debutAlbum') 65 | .setValidator(() => new AlbumValidator()) 66 | .must((value: Album, model: Band) => model.allAlbumNames.indexOf(value.name) !== -1) 67 | .withMessage('Name of debut album was not present in list of all album names'); 68 | this.ruleForEach('allAlbumNames').notEmpty(); 69 | this.ruleFor('currentAlbum') 70 | .setValidator(() => new AlbumValidator()) 71 | .must( 72 | (value: Album | null, model: Band) => 73 | value == null || model.allAlbumNames.indexOf(value.name) !== -1, 74 | ) 75 | .withMessage('Name of current album was not present in list of all album names') 76 | .unless((c) => c.currentAlbum == null); 77 | } 78 | } 79 | 80 | class PersonValidator extends Validator { 81 | constructor() { 82 | super(); 83 | this.ruleFor('name').notEmpty(); 84 | this.ruleFor('age').greaterThanOrEqualTo(18); 85 | this.ruleFor('favouriteBand').setValidator(() => new BandValidator()); 86 | this.ruleFor('leastFavouriteBand') 87 | .setValidator(() => new BandValidator()) 88 | .unless((c) => c.leastFavouriteBand == null); 89 | } 90 | } 91 | 92 | const validator = new PersonValidator(); 93 | 94 | const person: Person = { 95 | name: 'Alex', 96 | age: 25, 97 | favouriteBand: { 98 | name: 'Arctic Monkeys', 99 | debutAlbum: { 100 | name: `Whatever People Say I Am, That's What I'm Not`, 101 | numberOfTracks: 13, 102 | rating: 9, 103 | }, 104 | currentAlbum: { 105 | name: `Tranquility Base Hotel and Casino`, 106 | numberOfTracks: 11, 107 | rating: 5, 108 | }, 109 | allAlbumNames: [ 110 | `Whatever People Say I Am, That's What I'm Not`, 111 | `Favourite Worst Nightmare`, 112 | `Humbug`, 113 | `Suck It and See`, 114 | `AM`, 115 | `Tranquility Base Hotel & Casino`, 116 | ], 117 | }, 118 | }; 119 | 120 | expect(validator.validate(person)).toEqual({ 121 | favouriteBand: { 122 | debutAlbum: { 123 | rating: 'Value must be between 1 and 5 (inclusive)', 124 | }, 125 | currentAlbum: 'Name of current album was not present in list of all album names', 126 | }, 127 | }); 128 | }); 129 | 130 | it('recursive types', () => { 131 | type Employee = { 132 | name: string; 133 | age: number; 134 | lineManager: Employee | null; 135 | }; 136 | 137 | class EmployeeValidator extends Validator { 138 | constructor() { 139 | super(); 140 | this.ruleFor('name').notEmpty(); 141 | this.ruleFor('age').inclusiveBetween(18, 80); 142 | this.ruleFor('lineManager') 143 | .setValidator(() => new EmployeeValidator()) 144 | .when((employee) => employee.lineManager != null); 145 | } 146 | } 147 | 148 | const validator = new EmployeeValidator(); 149 | 150 | const boss: Employee = { 151 | name: 'The Boss', 152 | age: -1, 153 | lineManager: null, 154 | }; 155 | 156 | const teamLeader: Employee = { 157 | name: 'Team Leader', 158 | age: 28, 159 | lineManager: boss, 160 | }; 161 | 162 | const grunt: Employee = { 163 | name: 'Grunt', 164 | age: 25, 165 | lineManager: teamLeader, 166 | }; 167 | 168 | expect(validator.validate(grunt)).toEqual({ 169 | lineManager: { 170 | lineManager: { 171 | age: 'Value must be between 18 and 80 (inclusive)', 172 | }, 173 | }, 174 | }); 175 | }); 176 | 177 | it('complex array types', () => { 178 | type Employee = { 179 | name: string; 180 | employeeNumber: number; 181 | lmees: Array; 182 | }; 183 | 184 | class EmployeeValidator extends Validator { 185 | constructor() { 186 | super(); 187 | this.ruleFor('name').notEmpty().withMessage('Employees have names!'); 188 | this.ruleForEach('lmees').setValidator(() => new EmployeeValidator()); 189 | } 190 | } 191 | 192 | const validator = new EmployeeValidator(); 193 | 194 | const employee: Employee = { 195 | name: 'Alex', 196 | employeeNumber: 1, 197 | lmees: [ 198 | { 199 | name: 'Bob', 200 | employeeNumber: 2, 201 | lmees: [], 202 | }, 203 | { 204 | name: 'Charlie', 205 | employeeNumber: 3, 206 | lmees: [ 207 | { 208 | name: '', 209 | employeeNumber: 4, 210 | lmees: [], 211 | }, 212 | ], 213 | }, 214 | ], 215 | }; 216 | 217 | const result = validator.validate(employee); 218 | 219 | expect(result).toEqual({ 220 | lmees: [ 221 | null, 222 | { 223 | lmees: [{ name: 'Employees have names!' }], 224 | }, 225 | ], 226 | }); 227 | }); 228 | }); 229 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncValidator, Validator } from '@/index'; 2 | 3 | describe('index', () => { 4 | it('no rules (sync)', () => { 5 | type TestType = { name: string }; 6 | class TestTypeValidator extends Validator { 7 | constructor() { 8 | super(); 9 | } 10 | } 11 | const validator = new TestTypeValidator(); 12 | 13 | validator.validate({ name: 'Alex' }); 14 | 15 | // @ts-expect-error 16 | validator.validate({ nonsense: true }); 17 | }); 18 | 19 | it('no rules (async)', async () => { 20 | type TestType = { name: string }; 21 | class TestTypeValidator extends AsyncValidator { 22 | constructor() { 23 | super(); 24 | } 25 | } 26 | const validator = new TestTypeValidator(); 27 | 28 | await validator.validateAsync({ name: 'Alex' }); 29 | 30 | // @ts-expect-error 31 | await validator.validateAsync({ nonsense: true }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/numberHelpers.test.ts: -------------------------------------------------------------------------------- 1 | import { formatNumber } from '@/numberHelpers'; 2 | 3 | describe('numberHelpers', () => { 4 | describe('formatNumber', () => { 5 | it('formats large numbers', () => { 6 | const value = 999_999_999_999; 7 | const result = formatNumber(value); 8 | expect(result).toBe('999,999,999,999'); 9 | }); 10 | 11 | it('formats small numbers', () => { 12 | const value = 0.00000000000000000001; 13 | const result = formatNumber(value); 14 | expect(result).toBe('0.00000000000000000001'); 15 | }); 16 | 17 | it('formats long numbers', () => { 18 | const value = 123_456_789.123_456_78; 19 | const result = formatNumber(value); 20 | expect(result).toBe('123,456,789.12345678'); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/ruleForEach.test.ts: -------------------------------------------------------------------------------- 1 | import { delay } from './testHelpers'; 2 | import { AsyncValidator, Validator } from '@/index'; 3 | 4 | describe('ruleForEach', () => { 5 | describe('sync', () => { 6 | class TestValidator extends Validator { 7 | constructor() { 8 | super(); 9 | 10 | this.ruleFor('scores') 11 | .must((scores) => scores == null || scores.length > 0) 12 | .withMessage('Must not be empty'); 13 | 14 | this.ruleForEach('scores') 15 | .inclusiveBetween(0, 100) 16 | .withMessage('Must be between 0 and 100'); 17 | } 18 | } 19 | const validator = new TestValidator(); 20 | 21 | it('does not give a validation error if the value is null and passes top level validation', () => { 22 | const valid: TestTypeWithArrayProperty = { 23 | scores: null, 24 | otherProperty: 1, 25 | }; 26 | const result = validator.validate(valid); 27 | expect(result.scores).toBeUndefined(); 28 | }); 29 | 30 | it('does not give a validation error if the value is undefined and passes top level validation', () => { 31 | const valid: TestTypeWithArrayProperty = { 32 | otherProperty: 1, 33 | }; 34 | const result = validator.validate(valid); 35 | expect(result.scores).toBeUndefined(); 36 | }); 37 | 38 | it('does not give a validation error if the value passes top level validation and each element is valid', () => { 39 | const valid: TestTypeWithArrayProperty = { 40 | scores: [0, 50, 100], 41 | otherProperty: 1, 42 | }; 43 | const result = validator.validate(valid); 44 | expect(result.scores).toBeUndefined(); 45 | }); 46 | 47 | it('gives a validation error if the value does not pass top level validation', () => { 48 | const invalid: TestTypeWithArrayProperty = { 49 | scores: [], 50 | otherProperty: 3, 51 | }; 52 | const result = validator.validate(invalid); 53 | expect(result.scores).toBe('Must not be empty'); 54 | }); 55 | 56 | it('gives a validation error at each appropriate index if the value passes top level validation but some elements are invalid', () => { 57 | const invalid: TestTypeWithArrayProperty = { 58 | scores: [0, 20, 100, -10, 120], 59 | otherProperty: 5, 60 | }; 61 | const result = validator.validate(invalid); 62 | expect(result).toEqual({ 63 | scores: [null, null, null, 'Must be between 0 and 100', 'Must be between 0 and 100'], 64 | }); 65 | }); 66 | 67 | it('gives a type error if used on a non-array property', () => { 68 | // @ts-ignore 69 | class AnotherValidator extends Validator { 70 | constructor() { 71 | super(); 72 | // @ts-expect-error 73 | this.ruleForEach('otherProperty').null(); 74 | } 75 | } 76 | }); 77 | 78 | describe('when nested rules depend on the base model', () => { 79 | class DependentValidator extends Validator { 80 | constructor() { 81 | super(); 82 | 83 | this.ruleForEach('scores') 84 | .inclusiveBetween(0, 100) 85 | .withMessage('Must be between 0 and 100') 86 | .unless((model) => model.otherProperty === -1); 87 | } 88 | } 89 | const dependentValidator = new DependentValidator(); 90 | 91 | it('gives a validation error if the base model is in an appropriate state', () => { 92 | const result = dependentValidator.validate({ 93 | scores: [0, 10, -44, 100], 94 | otherProperty: 1, 95 | }); 96 | 97 | expect(result).toEqual({ 98 | scores: [null, null, 'Must be between 0 and 100', null], 99 | }); 100 | }); 101 | 102 | it('does not give a validation error if the base model is in an appropriate state', () => { 103 | const result = dependentValidator.validate({ 104 | scores: [0, 10, -44, 100], 105 | otherProperty: -1, 106 | }); 107 | 108 | expect(result).toEqual({}); 109 | }); 110 | }); 111 | }); 112 | 113 | describe('async', () => { 114 | class TestValidator extends AsyncValidator { 115 | constructor() { 116 | super(); 117 | 118 | this.ruleFor('scores') 119 | .must((scores) => scores == null || scores.length > 0) 120 | .withMessage('Must not be empty'); 121 | 122 | this.ruleForEach('scores') 123 | .mustAsync(async (score) => { 124 | await delay(1); 125 | return score >= 0 && score <= 100; 126 | }) 127 | .withMessage('Must be between 0 and 100'); 128 | } 129 | } 130 | const validator = new TestValidator(); 131 | 132 | it('does not give a validation error if the value is null and passes top level validation', async () => { 133 | const valid: TestTypeWithArrayProperty = { 134 | scores: null, 135 | otherProperty: 1, 136 | }; 137 | const result = await validator.validateAsync(valid); 138 | expect(result.scores).toBeUndefined(); 139 | }); 140 | 141 | it('does not give a validation error if the value is undefined and passes top level validation', async () => { 142 | const valid: TestTypeWithArrayProperty = { 143 | otherProperty: 1, 144 | }; 145 | const result = await validator.validateAsync(valid); 146 | expect(result.scores).toBeUndefined(); 147 | }); 148 | 149 | it('does not give a validation error if the value passes top level validation and each element is valid', async () => { 150 | const valid: TestTypeWithArrayProperty = { 151 | scores: [0, 50, 100], 152 | otherProperty: 1, 153 | }; 154 | const result = await validator.validateAsync(valid); 155 | expect(result.scores).toBeUndefined(); 156 | }); 157 | 158 | it('gives a validation error if the value does not pass top level validation', async () => { 159 | const invalid: TestTypeWithArrayProperty = { 160 | scores: [], 161 | otherProperty: 3, 162 | }; 163 | const result = await validator.validateAsync(invalid); 164 | expect(result.scores).toBe('Must not be empty'); 165 | }); 166 | 167 | it('gives a validation error at each appropriate index if the value passes top level validation but some elements are invalid', async () => { 168 | const invalid: TestTypeWithArrayProperty = { 169 | scores: [0, 20, 100, -10, 120], 170 | otherProperty: 5, 171 | }; 172 | const result = await validator.validateAsync(invalid); 173 | expect(result).toEqual({ 174 | scores: [null, null, null, 'Must be between 0 and 100', 'Must be between 0 and 100'], 175 | }); 176 | }); 177 | 178 | it('gives a type error if used on a non-array property', () => { 179 | // @ts-ignore 180 | class AnotherValidator extends AsyncValidator { 181 | constructor() { 182 | super(); 183 | // @ts-expect-error 184 | this.ruleForEach('otherProperty').null(); 185 | } 186 | } 187 | }); 188 | 189 | describe('when nested rules depend on the base model', () => { 190 | class DependentValidator extends AsyncValidator { 191 | constructor() { 192 | super(); 193 | 194 | this.ruleForEach('scores') 195 | .inclusiveBetween(0, 100) 196 | .withMessage('Must be between 0 and 100') 197 | .unless((model) => model.otherProperty === -1); 198 | } 199 | } 200 | const dependentValidator = new DependentValidator(); 201 | 202 | it('gives a validation error if the base model is in an appropriate state', async () => { 203 | const result = await dependentValidator.validateAsync({ 204 | scores: [0, 10, -44, 100], 205 | otherProperty: 1, 206 | }); 207 | 208 | expect(result).toEqual({ 209 | scores: [null, null, 'Must be between 0 and 100', null], 210 | }); 211 | }); 212 | 213 | it('does not give a validation error if the base model is in an appropriate state', async () => { 214 | const result = await dependentValidator.validateAsync({ 215 | scores: [0, 10, -44, 100], 216 | otherProperty: -1, 217 | }); 218 | 219 | expect(result).toEqual({}); 220 | }); 221 | }); 222 | }); 223 | }); 224 | 225 | type TestTypeWithArrayProperty = { 226 | scores?: Array | null; 227 | otherProperty: number; 228 | }; 229 | -------------------------------------------------------------------------------- /test/testHelpers.ts: -------------------------------------------------------------------------------- 1 | export const delay = (delayInMilliseconds: number) => 2 | new Promise((resolve) => setTimeout(resolve, delayInMilliseconds)); 3 | -------------------------------------------------------------------------------- /test/withMessage.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncValidator, Validator } from '@/index'; 2 | 3 | describe('withMessage', () => { 4 | const beAtLeastFiveCharactersLong = { 5 | predicate: (value: string) => value.length >= 5, 6 | message: 'Please enter at least 5 characters', 7 | }; 8 | 9 | describe('sync', () => { 10 | class TestValidator extends Validator { 11 | constructor() { 12 | super(); 13 | this.ruleFor('numberProperty') 14 | .must((numberProperty) => numberProperty % 2 === 0) 15 | .withMessage('Must be even'); 16 | this.ruleFor('stringProperty').must(beAtLeastFiveCharactersLong).withMessage('Too short!'); 17 | } 18 | } 19 | const validator = new TestValidator(); 20 | 21 | it('gives the provided validation error if the value is invalid', () => { 22 | const invalid: TestType = { 23 | ...testInstance, 24 | numberProperty: 13, 25 | }; 26 | const result = validator.validate(invalid); 27 | expect(result.numberProperty).toBe('Must be even'); 28 | }); 29 | 30 | it('overrides custom messages defined for `must` rules', () => { 31 | const invalid: TestType = { 32 | ...testInstance, 33 | stringProperty: 'foo', 34 | }; 35 | const result = validator.validate(invalid); 36 | expect(result.stringProperty).toBe('Too short!'); 37 | }); 38 | 39 | it('gives no validation error if the value is valid', () => { 40 | const valid: TestType = { 41 | ...testInstance, 42 | numberProperty: 12, 43 | }; 44 | const result = validator.validate(valid); 45 | expect(result.numberProperty).toBeUndefined(); 46 | }); 47 | 48 | it('only applies to the latest rule in the chain', () => { 49 | class TestValidatorAlt extends Validator { 50 | constructor() { 51 | super(); 52 | this.ruleFor('nullableStringProperty') 53 | .notNull() 54 | .notEmpty() 55 | .withMessage('Enter something!'); 56 | } 57 | } 58 | const validatorAlt = new TestValidatorAlt(); 59 | const invalidNull: TestType = { 60 | ...testInstance, 61 | nullableStringProperty: null, 62 | }; 63 | const result = validatorAlt.validate(invalidNull); 64 | expect(result.nullableStringProperty).toBe('Value cannot be null'); 65 | const invalidEmpty: TestType = { 66 | ...testInstance, 67 | nullableStringProperty: '', 68 | }; 69 | const otherResult = validatorAlt.validate(invalidEmpty); 70 | expect(otherResult.nullableStringProperty).toBe('Enter something!'); 71 | }); 72 | }); 73 | 74 | describe('async', () => { 75 | class TestValidator extends AsyncValidator { 76 | constructor() { 77 | super(); 78 | this.ruleFor('numberProperty') 79 | .must((numberProperty) => numberProperty % 2 === 0) 80 | .withMessage('Must be even'); 81 | this.ruleFor('stringProperty').must(beAtLeastFiveCharactersLong).withMessage('Too short!'); 82 | } 83 | } 84 | const validator = new TestValidator(); 85 | 86 | it('gives the provided validation error if the value is invalid', async () => { 87 | const invalid: TestType = { 88 | ...testInstance, 89 | numberProperty: 13, 90 | }; 91 | const result = await validator.validateAsync(invalid); 92 | expect(result.numberProperty).toBe('Must be even'); 93 | }); 94 | 95 | it('overrides custom messages defined for `must` rules', async () => { 96 | const invalid: TestType = { 97 | ...testInstance, 98 | stringProperty: 'foo', 99 | }; 100 | const result = await validator.validateAsync(invalid); 101 | expect(result.stringProperty).toBe('Too short!'); 102 | }); 103 | 104 | it('gives no validation error if the value is valid', async () => { 105 | const valid: TestType = { 106 | ...testInstance, 107 | numberProperty: 12, 108 | }; 109 | const result = await validator.validateAsync(valid); 110 | expect(result.numberProperty).toBeUndefined(); 111 | }); 112 | 113 | it('only applies to the latest rule in the chain', async () => { 114 | class TestValidatorAlt extends AsyncValidator { 115 | constructor() { 116 | super(); 117 | this.ruleFor('nullableStringProperty') 118 | .notNull() 119 | .notEmpty() 120 | .withMessage('Enter something!'); 121 | } 122 | } 123 | const validatorAlt = new TestValidatorAlt(); 124 | const invalidNull: TestType = { 125 | ...testInstance, 126 | nullableStringProperty: null, 127 | }; 128 | const result = await validatorAlt.validateAsync(invalidNull); 129 | expect(result.nullableStringProperty).toBe('Value cannot be null'); 130 | const invalidEmpty: TestType = { 131 | ...testInstance, 132 | nullableStringProperty: '', 133 | }; 134 | const otherResult = await validatorAlt.validateAsync(invalidEmpty); 135 | expect(otherResult.nullableStringProperty).toBe('Enter something!'); 136 | }); 137 | }); 138 | }); 139 | 140 | type TestType = { 141 | stringProperty: string; 142 | nullableStringProperty: string | null; 143 | numberProperty: number; 144 | nullableNumberProperty: number | null; 145 | booleanProperty: boolean; 146 | nullableBooleanProperty: boolean | null; 147 | objectProperty: { nestedProperty: string }; 148 | nullableObjectProperty: { nestedProperty: string } | null; 149 | }; 150 | 151 | const testInstance: TestType = { 152 | stringProperty: '', 153 | nullableStringProperty: null, 154 | numberProperty: 0, 155 | nullableNumberProperty: null, 156 | booleanProperty: false, 157 | nullableBooleanProperty: null, 158 | objectProperty: { nestedProperty: '' }, 159 | nullableObjectProperty: null, 160 | }; 161 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./src", 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "moduleResolution": "node", 23 | "baseUrl": "./", 24 | "paths": { 25 | "*": ["node_modules/*"], 26 | "@/*": ["src/*"] 27 | }, 28 | "jsx": "react", 29 | "esModuleInterop": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["test"], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": ".", 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "moduleResolution": "node", 23 | "baseUrl": ".", 24 | "paths": { 25 | "*": ["node_modules/*"], 26 | "@/*": ["src/*"] 27 | }, 28 | "jsx": "react", 29 | "esModuleInterop": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | import { themes as prismThemes } from 'prism-react-renderer'; 2 | 3 | export default { 4 | title: 'fluentvalidation-ts', 5 | titleDelimiter: '·', 6 | tagline: 'Strong, simple, extensible validation.', 7 | url: 'https://fluentvalidation-ts.alexpotter.dev', 8 | baseUrl: '/', 9 | organizationName: 'AlexJPotter', 10 | projectName: 'fluentvalidation-ts', 11 | noIndex: false, 12 | scripts: [ 13 | 'https://buttons.github.io/buttons.js', 14 | 'https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js', 15 | '/js/code-block-buttons.js', 16 | ], 17 | stylesheets: ['/css/code-block-buttons.css'], 18 | favicon: 'img/favicon.ico', 19 | customFields: { 20 | search: true, 21 | users: [], 22 | repoUrl: 'https://github.com/alexjpotter/fluentvalidation-ts', 23 | disableHeaderTitle: true, 24 | }, 25 | onBrokenLinks: 'log', 26 | onBrokenMarkdownLinks: 'log', 27 | presets: [ 28 | [ 29 | '@docusaurus/preset-classic', 30 | { 31 | docs: { 32 | showLastUpdateAuthor: false, 33 | showLastUpdateTime: true, 34 | path: '../docs', 35 | sidebarPath: './sidebars.json', 36 | }, 37 | blog: false, 38 | theme: { 39 | customCss: './src/css/customTheme.css', 40 | }, 41 | }, 42 | ], 43 | ], 44 | plugins: [], 45 | themeConfig: { 46 | algolia: { 47 | appId: 'GQN7MVBZD5', 48 | apiKey: 'abd6e3438533f74ae866e825467bf51f', // NOTE: This is a public API key, not a secret key - it is safe to commit this :) 49 | indexName: 'fluentvalidation-ts', 50 | searchPagePath: 'search', 51 | contextualSearch: true, 52 | insights: true, 53 | }, 54 | prism: { 55 | theme: prismThemes.vsLight, 56 | darkTheme: prismThemes.nightOwl, 57 | }, 58 | navbar: { 59 | title: 'fluentvalidation-ts', 60 | logo: { 61 | src: 'img/logo.svg', 62 | }, 63 | items: [ 64 | { 65 | type: 'html', 66 | position: 'left', 67 | value: '
', 68 | }, 69 | { 70 | to: 'docs/overview', 71 | label: 'Docs', 72 | position: 'left', 73 | }, 74 | { 75 | to: '/help', 76 | label: 'Help', 77 | position: 'left', 78 | }, 79 | { 80 | href: 'https://github.com/alexjpotter/fluentvalidation-ts', 81 | label: 'GitHub', 82 | position: 'right', 83 | }, 84 | { 85 | type: 'html', 86 | position: 'right', 87 | value: '
', 88 | }, 89 | ], 90 | }, 91 | image: 'img/logo-outlined.svg', 92 | footer: { 93 | links: [], 94 | copyright: 'Copyright © 2025 Alex Potter. All rights reserved.', 95 | }, 96 | }, 97 | }; 98 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "examples": "docusaurus-examples", 4 | "start": "docusaurus start", 5 | "build": "docusaurus build", 6 | "publish-gh-pages": "docusaurus-publish", 7 | "write-translations": "docusaurus-write-translations", 8 | "version": "docusaurus-version", 9 | "rename-version": "docusaurus-rename-version", 10 | "swizzle": "docusaurus swizzle", 11 | "deploy": "docusaurus deploy", 12 | "docusaurus": "docusaurus" 13 | }, 14 | "dependencies": { 15 | "@docusaurus/core": "3.7.0", 16 | "@docusaurus/preset-classic": "3.7.0", 17 | "@mdx-js/react": "^3.0.0", 18 | "prism-react-renderer": "^2.1.0", 19 | "react": "^18.0.0", 20 | "react-dom": "^18.2.0" 21 | }, 22 | "devDependencies": { 23 | "@docusaurus/module-type-aliases": "3.0.0", 24 | "@docusaurus/types": "3.0.0" 25 | }, 26 | "engines": { 27 | "node": ">=18.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /website/sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": { 3 | "Getting Started": ["overview", "tutorial"], 4 | "Guides": [ 5 | "guides/customRules", 6 | "guides/objectProperties", 7 | "guides/arrayProperties", 8 | "guides/ambientContext" 9 | ], 10 | "Core API": [ 11 | "api/core/validator", 12 | "api/core/asyncValidator", 13 | "api/core/validationErrors", 14 | "api/core/ruleFor", 15 | "api/core/ruleForTransformed", 16 | "api/core/ruleForEach", 17 | "api/core/ruleForEachTransformed" 18 | ], 19 | "Rule Configuration": [ 20 | "api/configuration/withMessage", 21 | "api/configuration/when", 22 | "api/configuration/unless" 23 | ], 24 | "Validation Rules": { 25 | "Base Rules": [ 26 | "api/rules/equal", 27 | "api/rules/notEqual", 28 | "api/rules/must", 29 | "api/rules/mustAsync", 30 | "api/rules/notNull", 31 | "api/rules/nullRule", 32 | "api/rules/notUndefined", 33 | "api/rules/undefinedRule" 34 | ], 35 | "String Rules": [ 36 | "api/rules/emailAddress", 37 | "api/rules/matches", 38 | "api/rules/length", 39 | "api/rules/maxLength", 40 | "api/rules/minLength", 41 | "api/rules/notEmpty" 42 | ], 43 | "Number Rules": [ 44 | "api/rules/exclusiveBetween", 45 | "api/rules/inclusiveBetween", 46 | "api/rules/greaterThan", 47 | "api/rules/greaterThanOrEqualTo", 48 | "api/rules/lessThan", 49 | "api/rules/lessThanOrEqualTo", 50 | "api/rules/precisionScale" 51 | ], 52 | "Object Rules": ["api/rules/setValidator", "api/rules/setAsyncValidator"] 53 | }, 54 | "Form Libraries": ["guides/formik", "guides/reactHookForm"] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /website/src/css/customTheme.css: -------------------------------------------------------------------------------- 1 | .navbar__logo { 2 | height: 2.5rem; 3 | margin-right: 0.75rem; 4 | } 5 | 6 | .footer { 7 | font-size: 0.85rem; 8 | } 9 | 10 | :root { 11 | --docusaurus-highlighted-code-line-bg: rgb(244, 241, 253); 12 | 13 | --ifm-color-primary: #02735b; 14 | --ifm-color-primary-dark: #026752; 15 | --ifm-color-primary-darker: #02624d; 16 | --ifm-color-primary-darkest: #015040; 17 | --ifm-color-primary-light: #027e64; 18 | --ifm-color-primary-lighter: #028166; 19 | --ifm-color-primary-lightest: #028469; 20 | 21 | --ifm-font-family-base: 22 | 'Montserrat', system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, 23 | Noto Sans, sans-serif, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, 24 | sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 25 | } 26 | 27 | [data-theme='dark'] { 28 | --docusaurus-highlighted-code-line-bg: rgb(13, 43, 73); 29 | --ifm-code-background: #111 !important; 30 | 31 | span.token.comment { 32 | color: rgb(255, 131, 201) !important; 33 | } 34 | 35 | --ifm-color-primary: #03f4be; 36 | --ifm-color-primary-dark: #03dcab; 37 | --ifm-color-primary-darker: #03cfa2; 38 | --ifm-color-primary-darkest: #02ab85; 39 | --ifm-color-primary-light: #14fcc8; 40 | --ifm-color-primary-lighter: #20fccb; 41 | --ifm-color-primary-lightest: #44fdd3; 42 | } 43 | -------------------------------------------------------------------------------- /website/src/pages/help.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 3 | import Link from '@docusaurus/Link'; 4 | import Layout from '@theme/Layout'; 5 | 6 | export default function Help() { 7 | const { siteConfig } = useDocusaurusContext(); 8 | 9 | return ( 10 | 11 |
12 |
13 |

Need help?

14 |

15 | fluentvalidation-ts has been designed with 16 | simplicity and ease of use in mind. 17 |
18 | If you're struggling, check out the following resources. 19 |

20 |
21 |
22 |

📖 Browse the docs

23 |

24 | Whether you're just looking to get started, or are stuck on an 25 | advanced feature, the extensive{' '} 26 | documentation on this site 27 | should be your first port of call. There you'll find a 28 | straightforward introduction to the library, detailed guides for 29 | common use-cases, and a full API reference. 30 |

31 |
32 |
33 |

✨ Stay up to date

34 |

35 | The best way to keep up to date on all the latest features and 36 | bug fixes is to head over to GitHub and check out the{' '} 37 | 38 | releases 39 | {' '} 40 | page. There you'll find detailed release notes for each new 41 | package version, with new features, bug fixes, and breaking 42 | changes all clearly explained. 43 |

44 |
45 |
46 |

🐛 Raise an issue

47 |

48 | Can't find what you're looking for in the documentation or 49 | release notes? Think you've found a bug? Have a great idea for a 50 | new feature? Head over to GitHub and take a look at the{' '} 51 | 52 | open issues 53 | {' '} 54 | - if you need to raise a new issue please include as much detail 55 | as possible. 56 |

57 |
58 |
59 |
60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /website/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 3 | import Link from '@docusaurus/Link'; 4 | import Head from '@docusaurus/Head'; 5 | import Layout from '@theme/Layout'; 6 | 7 | export default function Index() { 8 | const { siteConfig } = useDocusaurusContext(); 9 | 10 | return ( 11 | 12 | 13 | 14 | fluentvalidation-ts · Strong, simple, extensible validation. 15 | 16 | 17 |
18 |
19 |

20 | Strong, simple, extensible validation. 21 |

22 |

23 | fluentvalidation-ts is a strongly-typed, 24 | framework-agnostic validation library with an intuitive fluent API 25 | for building simple or complex validation rules for your TypeScript 26 | models. 27 |

28 |
29 | 33 | Get Started 34 | 35 | 39 | GitHub 40 | 41 |
42 |
43 |
44 |

💪 Strong

45 |

46 | Written in and specifically designed for TypeScript, allowing 47 | you to build up strongly-typed validation rules for your models. 48 | Avoid mistakes, work faster thanks to code completion, and feel 49 | completely confident when refactoring. 50 |

51 |
52 |
53 |

✅ Simple

54 |

55 | Super simple to use thanks to its fluent API and built-in rules. 56 | Get up and running in no time with how-to guides and a full API 57 | reference. Boasting zero dependencies and a tiny bundle size, 58 | fluentvalidation-ts is also incredibly lightweight. 59 |

60 |
61 |
62 |

⚙️ Extensible

63 |

64 | Write your own reusable rules and drop them in with ease when 65 | the built-in validation rules aren't enough. By leveraging this 66 | powerful extensibility, you can handle almost any validation 67 | requirement imaginable. 68 |

69 |
70 |
71 |
72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /website/src/theme/Root.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from '@docusaurus/Head'; 3 | 4 | // Default implementation, that you can customize 5 | export default function Root({ children }) { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | 15 | 16 | {children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /website/static/css/code-block-buttons.css: -------------------------------------------------------------------------------- 1 | /* "Copy" code block button */ 2 | pre { 3 | position: relative; 4 | } 5 | 6 | pre .btnIcon { 7 | position: absolute; 8 | top: 4px; 9 | z-index: 2; 10 | cursor: pointer; 11 | border: 1px solid transparent; 12 | padding: 0; 13 | color: #555555; 14 | background-color: transparent; 15 | height: 30px; 16 | transition: all 0.25s ease-out; 17 | } 18 | 19 | pre .btnIcon:hover { 20 | text-decoration: none; 21 | } 22 | 23 | .btnIcon__body { 24 | align-items: center; 25 | display: flex; 26 | } 27 | 28 | .btnIcon svg { 29 | fill: currentColor; 30 | margin-right: 0.4em; 31 | } 32 | 33 | .btnIcon__label { 34 | font-size: 11px; 35 | } 36 | 37 | .btnClipboard { 38 | right: 10px; 39 | } 40 | -------------------------------------------------------------------------------- /website/static/img/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexJPotter/fluentvalidation-ts/5a9f064835ea910aed9ab04bf3c0e89b6c04553c/website/static/img/android-chrome-192x192.png -------------------------------------------------------------------------------- /website/static/img/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexJPotter/fluentvalidation-ts/5a9f064835ea910aed9ab04bf3c0e89b6c04553c/website/static/img/android-chrome-512x512.png -------------------------------------------------------------------------------- /website/static/img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexJPotter/fluentvalidation-ts/5a9f064835ea910aed9ab04bf3c0e89b6c04553c/website/static/img/apple-touch-icon.png -------------------------------------------------------------------------------- /website/static/img/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexJPotter/fluentvalidation-ts/5a9f064835ea910aed9ab04bf3c0e89b6c04553c/website/static/img/favicon-16x16.png -------------------------------------------------------------------------------- /website/static/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexJPotter/fluentvalidation-ts/5a9f064835ea910aed9ab04bf3c0e89b6c04553c/website/static/img/favicon-32x32.png -------------------------------------------------------------------------------- /website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexJPotter/fluentvalidation-ts/5a9f064835ea910aed9ab04bf3c0e89b6c04553c/website/static/img/favicon.ico -------------------------------------------------------------------------------- /website/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 32 | 36 | 37 | 38 | 61 | 68 | 69 | 71 | 72 | 74 | image/svg+xml 75 | 77 | 78 | 79 | 80 | 81 | 87 | 93 | 99 | 100 | 106 | 112 | 118 | 124 | 125 | 131 | 137 | 144 | 150 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /website/static/img/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /website/static/js/code-block-buttons.js: -------------------------------------------------------------------------------- 1 | // Turn off ESLint for this file because it's sent down to users as-is. 2 | /* eslint-disable */ 3 | window.addEventListener('load', function () { 4 | function button(label, ariaLabel, icon, className) { 5 | const btn = document.createElement('button'); 6 | btn.classList.add('btnIcon', className); 7 | btn.setAttribute('type', 'button'); 8 | btn.setAttribute('aria-label', ariaLabel); 9 | btn.innerHTML = 10 | '
' + 11 | icon + 12 | '' + 13 | label + 14 | '' + 15 | '
'; 16 | return btn; 17 | } 18 | 19 | function addButtons(codeBlockSelector, btn) { 20 | document.querySelectorAll(codeBlockSelector).forEach(function (code) { 21 | code.parentNode.appendChild(btn.cloneNode(true)); 22 | }); 23 | } 24 | 25 | const copyIcon = 26 | ''; 27 | 28 | addButtons( 29 | '.hljs', 30 | button('Copy', 'Copy code to clipboard', copyIcon, 'btnClipboard') 31 | ); 32 | 33 | const clipboard = new ClipboardJS('.btnClipboard', { 34 | target: function (trigger) { 35 | return trigger.parentNode.querySelector('code'); 36 | }, 37 | }); 38 | 39 | clipboard.on('success', function (event) { 40 | event.clearSelection(); 41 | const textEl = event.trigger.querySelector('.btnIcon__label'); 42 | textEl.textContent = 'Copied'; 43 | setTimeout(function () { 44 | textEl.textContent = 'Copy'; 45 | }, 2000); 46 | }); 47 | }); 48 | --------------------------------------------------------------------------------