├── .npmignore ├── .gitignore ├── .babelrc ├── src ├── index.js ├── rules.js ├── asyncValidation.js └── validation.js ├── .eslintrc ├── .travis.yml ├── LICENSE ├── spec ├── rules.test.js ├── asyncValidation.test.js └── validation.test.js ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | .eslintrc 3 | .babelrc 4 | coverage/ 5 | spec/ 6 | src/ 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.sw* 3 | .nyc_output/ 4 | *.log 5 | coverage/ 6 | lib/ 7 | ~/ 8 | pkg/ 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env"], 3 | "plugins": ["@babel/plugin-proposal-export-default-from"] 4 | } 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export validator, { defineValidator } from "./validation"; 2 | export { validator as asyncValidator } from "./asyncValidation"; 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier"], 3 | "extends": ["plugin:prettier/recommended"], 4 | "parser": "babel-eslint", 5 | "rules": { 6 | "no-debugger": "error", 7 | "import/prefer-default-export": ["off"], 8 | "prettier/prettier": "error" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/rules.js: -------------------------------------------------------------------------------- 1 | export function required(value) { 2 | if (!value) { 3 | return "required"; 4 | } 5 | } 6 | 7 | export function matches(fieldName) { 8 | return (val, values) => { 9 | if (val !== values[fieldName]) { 10 | return "mismatch"; 11 | } 12 | }; 13 | } 14 | 15 | export function numeric(n) { 16 | if (isNaN(parseFloat(n)) || !isFinite(n)) { 17 | return "expected_numeric"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | stages: 5 | - test 6 | - build 7 | jobs: 8 | include: 9 | - stage: test 10 | name: "Linters" 11 | script: npm run lint 12 | - stage: test 13 | name: "Unit tests" 14 | script: npm run spec-coverage 15 | - stage: build 16 | name: "Compile the library" 17 | script: npm run compile 18 | after_success: 19 | - npx codecov 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Sam Slotsky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/rules.test.js: -------------------------------------------------------------------------------- 1 | import expect from "expect"; 2 | import * as rules from "../src/rules"; 3 | 4 | describe("required", () => { 5 | describe("with no value", () => { 6 | it("returns an error", () => { 7 | expect(rules.required()).toEqual("required"); 8 | }); 9 | }); 10 | 11 | describe("with a value", () => { 12 | it("returns nothing", () => { 13 | expect(rules.required("any value")).toBe(undefined); 14 | }); 15 | }); 16 | }); 17 | 18 | describe("matches", () => { 19 | const rule = rules.matches("password"); 20 | const values = { password: "foo" }; 21 | 22 | describe("when values do not match", () => { 23 | it("returns an error", () => { 24 | expect(rule("bar", values)).toEqual("mismatch"); 25 | }); 26 | }); 27 | 28 | describe("when values match", () => { 29 | it("returns nothing", () => { 30 | expect(rule(values.password, values)).toBe(undefined); 31 | }); 32 | }); 33 | }); 34 | 35 | describe("numeric", () => { 36 | describe("when value is an integer", () => { 37 | it("returns nothing", () => { 38 | expect(rules.numeric("4")).toBe(undefined); 39 | }); 40 | }); 41 | 42 | describe("when value is a float", () => { 43 | it("returns nothing", () => { 44 | expect(rules.numeric("4.0")).toBe(undefined); 45 | }); 46 | }); 47 | 48 | describe("when value is not numeric", () => { 49 | it("returns an error", () => { 50 | expect(rules.numeric("a4")).toEqual("expected_numeric"); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/asyncValidation.js: -------------------------------------------------------------------------------- 1 | export function validator(model, validations) { 2 | const errors = {}; 3 | 4 | const applyError = (f, message) => { 5 | errors[f] = (errors[f] || []).concat(message); 6 | }; 7 | 8 | const validateField = rule => f => 9 | rule(f, model).then(message => { 10 | if (message) { 11 | applyError(f, message); 12 | } 13 | }); 14 | 15 | const rules = fields => ({ 16 | satisfies: (...rules) => { 17 | return Promise.all( 18 | rules.reduce( 19 | (all, rule) => all.concat(fields.map(validateField(rule))), 20 | [] 21 | ) 22 | ); 23 | } 24 | }); 25 | 26 | const v = (...facts) => Promise.all(facts); 27 | v.validate = (...fields) => rules(fields); 28 | v.validateChild = ( 29 | { field, drill = () => model[field] }, 30 | childValidations 31 | ) => { 32 | return validator(drill(model, field) || {}, childValidations).then( 33 | childErrors => { 34 | if (Object.keys(childErrors).length > 0) { 35 | errors[field] = childErrors; 36 | } 37 | } 38 | ); 39 | }; 40 | v.validateChildren = ( 41 | { field, drill = () => model[field] }, 42 | childValidations 43 | ) => { 44 | const children = (drill(model, field) || []).map(v => 45 | validator(v, childValidations) 46 | ); 47 | return Promise.all(children).then(childErrors => { 48 | if (childErrors.some(e => Object.keys(e).length > 0)) { 49 | errors[field] = childErrors; 50 | } 51 | }); 52 | }; 53 | 54 | return validations(v).then(() => errors); 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "validate-this", 3 | "version": "1.5.2", 4 | "description": "Easily validate deep form structures using both premade and custom validation rules.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "spec-coverage": "jest --collect-coverage", 8 | "lint": "eslint ./src/** ./spec/**/*", 9 | "test": "npm run lint && npm run spec-coverage", 10 | "compile": "pack build" 11 | }, 12 | "author": "Sam Slotsky", 13 | "license": "MIT", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/sslotsky/validate-this.git" 17 | }, 18 | "@pika/pack": { 19 | "pipeline": [ 20 | [ 21 | "@pika/plugin-standard-pkg", 22 | { 23 | "exclude": [ 24 | "__spec/**/*" 25 | ] 26 | } 27 | ], 28 | [ 29 | "@pika/plugin-build-node" 30 | ], 31 | [ 32 | "@pika/plugin-build-web" 33 | ] 34 | ] 35 | }, 36 | "keywords": [ 37 | "form", 38 | "forms", 39 | "validation", 40 | "validator", 41 | "validate", 42 | "extensible", 43 | "extendable", 44 | "custom", 45 | "customizable" 46 | ], 47 | "devDependencies": { 48 | "@babel/plugin-proposal-export-default-from": "^7.2.0", 49 | "@babel/preset-stage-0": "^7.0.0", 50 | "@pika/pack": "^0.3.7", 51 | "@pika/plugin-build-node": "^0.7.1", 52 | "@pika/plugin-build-web": "^0.8.1", 53 | "@pika/plugin-standard-pkg": "^0.8.1", 54 | "babel-cli": "^6.18.0", 55 | "babel-core": "^6.21.0", 56 | "babel-eslint": "^10.0.1", 57 | "babel-loader": "^8.0.6", 58 | "babel-preset-env": "^1.6.0", 59 | "babel-register": "^6.18.0", 60 | "eslint": "^6.0.1", 61 | "eslint-config-prettier": "^6.5.0", 62 | "eslint-plugin-import": "^2.2.0", 63 | "eslint-plugin-prettier": "^3.1.0", 64 | "jest": "^24.8.0", 65 | "prettier": "^1.18.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/validation.js: -------------------------------------------------------------------------------- 1 | import * as rules from "./rules"; 2 | 3 | const customRules = {}; 4 | 5 | export function defineValidator({ name, rule }) { 6 | customRules[name] = { rule }; 7 | } 8 | 9 | defineValidator({ name: "required", rule: rules.required }); 10 | defineValidator({ name: "isNumeric", rule: rules.numeric }); 11 | 12 | defineValidator({ 13 | name: "matches", 14 | rule: rules.matches 15 | }); 16 | 17 | export default function validator( 18 | values = {}, 19 | validations, 20 | translator = message => message 21 | ) { 22 | const errors = {}; 23 | 24 | function validateFields(rules, fields) { 25 | fields.forEach(f => { 26 | rules.forEach(rule => { 27 | const error = rule(values[f], values); 28 | if (error) { 29 | errors[f] = (errors[f] || []).concat(translator(error, f)); 30 | } 31 | }); 32 | }); 33 | } 34 | 35 | function customValidator(fields) { 36 | return Object.keys(customRules).reduce( 37 | (v, name) => { 38 | const config = customRules[name]; 39 | const validation = (...args) => { 40 | const rule = args.length ? config.rule(...args) : config.rule; 41 | return validateFields([rule], fields); 42 | }; 43 | 44 | return { 45 | [name]: validation, 46 | ...v 47 | }; 48 | }, 49 | { 50 | satisfies: (...rules) => validateFields(rules, fields) 51 | } 52 | ); 53 | } 54 | 55 | const v = { 56 | validateChild: (field, childValidations, childTranslator = translator) => { 57 | const childErrors = validator( 58 | values[field] || {}, 59 | childValidations, 60 | childTranslator 61 | ); 62 | 63 | if (Object.keys(childErrors).length > 0) { 64 | errors[field] = childErrors; 65 | } 66 | }, 67 | validateChildren: ( 68 | field, 69 | childValidations, 70 | childTranslator = translator 71 | ) => { 72 | const childErrors = (values[field] || []).map(v => 73 | validator(v, childValidations, childTranslator) 74 | ); 75 | if (childErrors.some(e => Object.keys(e).length > 0)) { 76 | errors[field] = childErrors; 77 | } 78 | }, 79 | validate: (...fields) => customValidator(fields) 80 | }; 81 | 82 | validations(v, values); 83 | 84 | return errors; 85 | } 86 | -------------------------------------------------------------------------------- /spec/asyncValidation.test.js: -------------------------------------------------------------------------------- 1 | import expect from "expect"; 2 | import { validator } from "../src/asyncValidation"; 3 | 4 | const required = (field, model) => { 5 | if (!model[field]) { 6 | return Promise.resolve("Required"); 7 | } 8 | 9 | return Promise.resolve(); 10 | }; 11 | 12 | describe("asyncValidation", () => { 13 | describe("satisfies", () => { 14 | it("populates errors based on a custom rule", () => { 15 | const rule = () => Promise.resolve("error"); 16 | 17 | return validator({ name: "invalid" }, v => 18 | v(v.validate("name").satisfies(rule)) 19 | ).then(errors => { 20 | expect(errors.name).toContain("error"); 21 | }); 22 | }); 23 | 24 | it("accepts multiple rules", () => { 25 | const rule1 = () => Promise.resolve("error1"); 26 | const rule2 = () => Promise.resolve("error2"); 27 | 28 | return validator({ name: "invalid" }, v => 29 | v(v.validate("name").satisfies(rule1, rule2)) 30 | ).then(errors => { 31 | expect(errors.name).toEqual(["error1", "error2"]); 32 | }); 33 | }); 34 | }); 35 | 36 | describe("validateChild", () => { 37 | describe("when a child object does not exist", () => { 38 | const values = {}; 39 | 40 | it("populates child errors", () => { 41 | return validator(values, v => 42 | v( 43 | v.validateChild({ field: "user" }, user => 44 | user(user.validate("name").satisfies(required)) 45 | ) 46 | ) 47 | ).then(errors => { 48 | expect(errors.user.name).toBeDefined(); 49 | }); 50 | }); 51 | }); 52 | 53 | describe("when a child object has no errors", () => { 54 | const values = { 55 | user: { 56 | name: "samo" 57 | } 58 | }; 59 | 60 | it("does not populate errors for the child", () => { 61 | return validator(values, v => 62 | v( 63 | v.validateChild("user", user => 64 | user(user.validate("name").satisfies(required)) 65 | ) 66 | ) 67 | ).then(errors => { 68 | expect(errors.user).toBeUndefined(); 69 | }); 70 | }); 71 | }); 72 | 73 | describe("when a child object has errors", () => { 74 | const values = { 75 | user: { 76 | name: null 77 | } 78 | }; 79 | 80 | it("populates the errors object correctly", () => { 81 | return validator(values, v => 82 | v( 83 | v.validateChild({ field: "user" }, user => 84 | user(user.validate("name").satisfies(required)) 85 | ) 86 | ) 87 | ).then(errors => { 88 | expect(errors.user.name).toBeDefined(); 89 | }); 90 | }); 91 | }); 92 | }); 93 | 94 | describe("validateChildren", () => { 95 | describe("when the child array does not exist", () => { 96 | const values = {}; 97 | 98 | it("does not populate errors", () => { 99 | return validator(values, v => 100 | v( 101 | v.validateChildren({ field: "contacts" }, contacts => 102 | contacts(contacts.validate("name").satisfies(required)) 103 | ) 104 | ) 105 | ).then(errors => { 106 | expect(errors.contacts).toBeUndefined(); 107 | }); 108 | }); 109 | }); 110 | 111 | describe("when a child array has no errors", () => { 112 | const values = { 113 | contacts: [ 114 | { 115 | name: "samo" 116 | } 117 | ] 118 | }; 119 | 120 | it("does not populate errors for the child array", () => { 121 | return validator(values, v => 122 | v( 123 | v.validateChildren("contacts", contacts => 124 | contacts(contacts.validate("name").satisfies(required)) 125 | ) 126 | ) 127 | ).then(errors => { 128 | expect(errors.contacts).toBeUndefined(); 129 | }); 130 | }); 131 | }); 132 | 133 | describe("when a child array item has errors", () => { 134 | const values = { 135 | contacts: [ 136 | { 137 | name: null 138 | } 139 | ] 140 | }; 141 | 142 | it("populates the errors object correctly", () => { 143 | return validator(values, v => 144 | v( 145 | v.validateChildren({ field: "contacts" }, contact => 146 | contact(contact.validate("name").satisfies(required)) 147 | ) 148 | ) 149 | ).then(errors => { 150 | expect(errors.contacts[0].name).toBeDefined(); 151 | }); 152 | }); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/sslotsky/validate-this.svg?branch=master)](https://travis-ci.org/sslotsky/validate-this) 2 | [![codecov](https://codecov.io/gh/sslotsky/validate-this/branch/master/graph/badge.svg)](https://codecov.io/gh/sslotsky/validate-this) 3 | 4 | 5 | # validate-this 6 | 7 | `validate-this` is a validation library that applies validation rules to structured form data. It also allows you to define your own validation rules. 8 | 9 | ## Validating Form Data 10 | 11 | Imagining that we have structured form data that looks like: 12 | 13 | ```javascript 14 | const formData = { 15 | username: '', 16 | email: 'bob' 17 | } 18 | ``` 19 | 20 | Then we could pass that object into the function below: 21 | 22 | ```javascript 23 | import { validator } from 'validate-this' 24 | 25 | function validate(values) { 26 | return validator(values, v => { 27 | v.validate('username', 'email').required() // the required() validation is built into the package 28 | v.validate('email').isValidEmail() // the email() validation is defined below as a custom validation, read on! 29 | }) 30 | } 31 | ``` 32 | 33 | Calling the function with the `formData` we defined previously will return an `errors` object like this: 34 | 35 | ```javascript 36 | { 37 | username: ['required'], 38 | email: ['email_invalid'] 39 | } 40 | ``` 41 | ## Defining a Custom Validation 42 | 43 | There are two ways to do your own validations: by using the `satisfies` validation, 44 | or by using `defineValidator`. 45 | 46 | ### .satisfies(...rules) 47 | 48 | Call this with your own validation rule(s). Example: 49 | 50 | ```javascript 51 | import email from 'email-validator' 52 | 53 | function isValidEmail(value) { 54 | if (value && !email.validate(value)) { 55 | return 'email_invalid' 56 | } 57 | } 58 | 59 | function validate(values) { 60 | return validator(values, v => { 61 | v.validate('email').satisfies(isValidEmail) 62 | }) 63 | } 64 | ``` 65 | 66 | Or with multiple rules: 67 | 68 | ```javascript 69 | function greaterThan(n) { 70 | return value => { 71 | if (value <= n) { 72 | return 'too_small' 73 | } 74 | } 75 | } 76 | 77 | function lessThan(n) { 78 | return value => { 79 | if (value >= n) { 80 | return 'too_big' 81 | } 82 | } 83 | } 84 | 85 | function validate(values) { 86 | return validator(values, v => { 87 | v.validate('age').satisfies(greaterThan(17), lessThan(26)) 88 | }) 89 | } 90 | ``` 91 | 92 | ### defineValidator(config) 93 | 94 | In the most simple case, a rule accepts a `value` and returns a string if and only if the `value` is invalid. 95 | 96 | ```javascript 97 | import email from 'email-validator' 98 | import { defineValidator } from 'validate-this' 99 | 100 | defineValidator({ 101 | name: 'isValidEmail', 102 | rule: value => { 103 | if (value && !email.validate(value)) { 104 | return 'email_invalid' 105 | } 106 | } 107 | }) 108 | ``` 109 | 110 | For more complex cases, a higher order rule can be defined. The example below is built into 111 | `validate-this` but makes a good demonstration. 112 | 113 | ```javascript 114 | defineValidator({ 115 | name: 'matches', 116 | rule: fieldName => (val, values) => { 117 | if (val !== values[fieldName]) { 118 | return 'mismatch' 119 | } 120 | } 121 | }) 122 | ``` 123 | 124 | This will validate that one field matches another. Here's a validation function that uses this 125 | validator: 126 | 127 | ```javascript 128 | function validate(values) { 129 | return validator(values, v => { 130 | v.validate('username', 'email', 'password', 'confirm').required() 131 | v.validate('email').isValidEmail() 132 | v.validate('confirm').matches('password') 133 | }) 134 | } 135 | ``` 136 | 137 | ## Deep Validation 138 | 139 | If your form data is like this instead: 140 | 141 | ```javascript 142 | const formData = { 143 | name: 'Bob', 144 | address: { 145 | street: '123 Fake St' 146 | } 147 | } 148 | ``` 149 | 150 | Then you can validate the `address` property like this: 151 | 152 | 153 | ```javascript 154 | import { validator } from 'validate-this' 155 | 156 | function validate(values) { 157 | return validator(values, v => { 158 | v.validateChild('address', av => { 159 | av.validate('street').required() 160 | }) 161 | }) 162 | } 163 | ``` 164 | 165 | Or if there's a nested array: 166 | 167 | ```javascript 168 | const formData = { 169 | contacts: [{ 170 | name: 'bob', 171 | email: 'bob@example.com' 172 | }] 173 | } 174 | ``` 175 | 176 | Then you can validate those like this: 177 | 178 | ```javascript 179 | import { validator } from 'validate-this' 180 | 181 | function validate(values) { 182 | return validator(values, v => { 183 | v.validateChildren('contacts', cv => { 184 | cv.validate('name', 'email').required() 185 | }) 186 | }) 187 | } 188 | ``` 189 | 190 | ## Message translation 191 | 192 | A third argument can be provided to the `validator` function that will allow you to 193 | translate error messages using something like `I18n`. Example: 194 | 195 | ```javascript 196 | function validate(values) { 197 | return validator(values, v => { 198 | v.validate('username', 'password', 'confirm').required() 199 | v.validate('confirm').matches('password') 200 | }, (message, field) => I18n.t(`forms.newUser.${field}.${message}`)) 201 | } 202 | ``` 203 | -------------------------------------------------------------------------------- /spec/validation.test.js: -------------------------------------------------------------------------------- 1 | import expect from "expect"; 2 | import validator, { defineValidator } from "../src/validation"; 3 | 4 | describe("validation", () => { 5 | describe("defineValidator", () => { 6 | it("registers a validator with the given name", () => { 7 | defineValidator({ 8 | name: "custom", 9 | rule: () => {} 10 | }); 11 | 12 | validator({}, v => { 13 | expect(v.validate().custom).toBeDefined(); 14 | }); 15 | }); 16 | }); 17 | 18 | describe("validator", () => { 19 | it("contains default validators", () => { 20 | validator({}, v => { 21 | expect(v.validate().required).toBeDefined(); 22 | expect(v.validate().isNumeric).toBeDefined(); 23 | expect(v.validate().matches).toBeDefined(); 24 | }); 25 | }); 26 | 27 | describe("with a custom translator", () => { 28 | const translator = (message, field) => `${field}: ${message}`; 29 | const errors = validator( 30 | { name: null }, 31 | v => { 32 | v.validate("name").required(); 33 | }, 34 | translator 35 | ); 36 | 37 | it("returns a translated message", () => { 38 | expect(errors.name).toContain(translator("required", "name")); 39 | }); 40 | }); 41 | 42 | describe("satisfies", () => { 43 | it("populates errors based on a custom rule", () => { 44 | const rule = () => "error"; 45 | const errors = validator({ name: "invalid" }, v => { 46 | v.validate("name").satisfies(rule); 47 | }); 48 | 49 | expect(errors.name).toContain("error"); 50 | }); 51 | 52 | it("accepts multiple rules", () => { 53 | const rule1 = () => "error1"; 54 | const rule2 = () => "error2"; 55 | const errors = validator({ name: "invalid" }, v => { 56 | v.validate("name").satisfies(rule1, rule2); 57 | }); 58 | 59 | expect(errors.name).toEqual(["error1", "error2"]); 60 | }); 61 | }); 62 | 63 | describe("matches", () => { 64 | describe("when values match", () => { 65 | const errors = validator({ password: "foo", confirm: "foo" }, v => { 66 | v.validate("confirm").matches("password"); 67 | }); 68 | 69 | it("raises no errors", () => { 70 | expect(errors.confirm).toBeUndefined(); 71 | }); 72 | }); 73 | 74 | describe("when values do not match", () => { 75 | const errors = validator({ password: "foo", confirm: "bar" }, v => { 76 | v.validate("confirm").matches("password"); 77 | }); 78 | 79 | it("populates the errors object correctly", () => { 80 | expect(errors.confirm).toEqual(["mismatch"]); 81 | }); 82 | }); 83 | }); 84 | 85 | describe("validateChild", () => { 86 | describe("when a child object does not exist", () => { 87 | const values = {}; 88 | 89 | it("populates child errors", () => { 90 | const errors = validator(values, v => { 91 | v.validateChild("user", cv => { 92 | cv.validate("name").required(); 93 | }); 94 | }); 95 | 96 | expect(errors.user.name).toBeDefined(); 97 | }); 98 | }); 99 | 100 | describe("when a child object has no errors", () => { 101 | const values = { 102 | user: { 103 | name: "samo" 104 | } 105 | }; 106 | 107 | it("does not populate errors for the child", () => { 108 | const errors = validator(values, v => { 109 | v.validateChild("user", cv => { 110 | cv.validate("name").required(); 111 | }); 112 | }); 113 | 114 | expect(errors.user).toBeUndefined(); 115 | }); 116 | }); 117 | 118 | describe("when a child object has errors", () => { 119 | const values = { 120 | user: { 121 | name: null 122 | } 123 | }; 124 | 125 | it("populates the errors object correctly", () => { 126 | const errors = validator(values, v => { 127 | v.validateChild("user", cv => { 128 | cv.validate("name").required(); 129 | }); 130 | }); 131 | 132 | expect(errors.user.name).toBeDefined(); 133 | }); 134 | }); 135 | }); 136 | 137 | describe("validateChildren", () => { 138 | describe("when the child array does not exist", () => { 139 | const values = {}; 140 | 141 | it("does not populate errors", () => { 142 | const errors = validator(values, v => { 143 | v.validateChildren("contacts", cv => { 144 | cv.validate("name").required(); 145 | }); 146 | }); 147 | 148 | expect(errors.contacts).toBeUndefined(); 149 | }); 150 | }); 151 | 152 | describe("when a child array has no errors", () => { 153 | const values = { 154 | contacts: [ 155 | { 156 | name: "samo" 157 | } 158 | ] 159 | }; 160 | 161 | it("does not populate errors for the child array", () => { 162 | const errors = validator(values, v => { 163 | v.validateChildren("contacts", cv => { 164 | cv.validate("name").required(); 165 | }); 166 | }); 167 | 168 | expect(errors.contacts).toBeUndefined(); 169 | }); 170 | }); 171 | 172 | describe("when a child array item has errors", () => { 173 | const values = { 174 | contacts: [ 175 | { 176 | name: null 177 | } 178 | ] 179 | }; 180 | 181 | it("populates the errors object correctly", () => { 182 | const errors = validator(values, v => { 183 | v.validateChildren("contacts", cv => { 184 | cv.validate("name").required(); 185 | }); 186 | }); 187 | 188 | expect(errors.contacts[0].name).toBeDefined(); 189 | }); 190 | }); 191 | }); 192 | }); 193 | }); 194 | --------------------------------------------------------------------------------