├── .eslintrc ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .npmignore ├── .snyk ├── API.md ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── package-lock.json ├── package.json ├── src ├── creators.js ├── index.js ├── lib │ ├── error.js │ ├── plugins.js │ └── validators.js ├── modifiers.js ├── rules.js ├── typeStrategies │ ├── README.md │ ├── any.js │ ├── array.js │ ├── boolean.js │ ├── email.js │ ├── index.js │ ├── ip.js │ ├── number.js │ ├── object.js │ ├── phone.js │ ├── string.js │ ├── url.js │ ├── uuid.js │ └── zip.js └── types.js └── test ├── fixtures ├── core.js ├── creators.js ├── modifiers.js └── validators.js ├── integration ├── core.spec.js ├── creators.spec.js ├── modifiers.spec.js └── validators.spec.js └── src ├── creators.spec.js ├── index.spec.js ├── lib ├── error.spec.js └── validators.spec.js ├── modifiers.spec.js ├── rules.spec.js ├── typeStrategies ├── README.md ├── any.spec.js ├── array.spec.js ├── boolean.spec.js ├── email.spec.js ├── ip.spec.js ├── number.spec.js ├── object.spec.js ├── phone.spec.js ├── string.spec.js ├── url.spec.js ├── uuid.spec.js └── zip.spec.js └── types.spec.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "ecmaFeatures": { 8 | "arrowFunctions": true, 9 | "blockBindings": true, 10 | "classes": true, 11 | "defaultParams": true, 12 | "destructuring": true, 13 | "forOf": true, 14 | "generators": false, 15 | "modules": true, 16 | "objectLiteralComputedProperties": true, 17 | "objectLiteralDuplicateProperties": false, 18 | "objectLiteralShorthandMethods": true, 19 | "objectLiteralShorthandProperties": true, 20 | "spread": true, 21 | "superInFunctions": true, 22 | "templateStrings": true, 23 | "jsx": true 24 | }, 25 | "rules": { 26 | /** 27 | * Strict mode 28 | */ 29 | // babel inserts "use strict"; for us 30 | // http://eslint.org/docs/rules/strict 31 | "strict": [2, "never"], 32 | 33 | /** 34 | * ES6 35 | */ 36 | "no-var": 2, // http://eslint.org/docs/rules/no-var 37 | 38 | /** 39 | * Variables 40 | */ 41 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 42 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 43 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars 44 | "vars": "local", 45 | "args": "after-used" 46 | }], 47 | "no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define 48 | 49 | /** 50 | * Possible errors 51 | */ 52 | "comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle 53 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign 54 | "no-console": 1, // http://eslint.org/docs/rules/no-console 55 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 56 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 57 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 58 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 59 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 60 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 61 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 62 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 63 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 64 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 65 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 66 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 67 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 68 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 69 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 70 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 71 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 72 | "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var 73 | 74 | /** 75 | * Best practices 76 | */ 77 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 78 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly 79 | "default-case": 2, // http://eslint.org/docs/rules/default-case 80 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation 81 | "allowKeywords": true 82 | }], 83 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 84 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in 85 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 86 | "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 87 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 88 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 89 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 90 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 91 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 92 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 93 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 94 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 95 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 96 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 97 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 98 | "no-new": 2, // http://eslint.org/docs/rules/no-new 99 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 100 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 101 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 102 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 103 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign 104 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 105 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 106 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 107 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 108 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 109 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 110 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 111 | "no-with": 2, // http://eslint.org/docs/rules/no-with 112 | "radix": 2, // http://eslint.org/docs/rules/radix 113 | "vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top 114 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 115 | "yoda": 2, // http://eslint.org/docs/rules/yoda 116 | 117 | /** 118 | * Style 119 | */ 120 | "indent": [2, 2], // http://eslint.org/docs/rules/ 121 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style 122 | "1tbs", { 123 | "allowSingleLine": true 124 | }], 125 | "quotes": [ 126 | 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes 127 | ], 128 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 129 | "properties": "never" 130 | }], 131 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 132 | "before": false, 133 | "after": true 134 | }], 135 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 136 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 137 | "func-names": 0, // http://eslint.org/docs/rules/func-names 138 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 139 | "beforeColon": false, 140 | "afterColon": true 141 | }], 142 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap 143 | "newIsCap": true 144 | }], 145 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 146 | "max": 2 147 | }], 148 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 149 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 150 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 151 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 152 | "no-extra-parens": 2, // http://eslint.org/docs/rules/no-wrap-func 153 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 154 | "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var 155 | "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks 156 | "semi": [ 2, "never" ], // http://eslint.org/docs/rules/semi 157 | "keyword-spacing": 2, // http://eslint.org/docs/rules/space-after-keywords 158 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 159 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren 160 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 161 | "spaced-comment": 2, // http://eslint.org/docs/rules/spaced-line-comment 162 | } 163 | } -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [14.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: Run `npm install` 21 | run: | 22 | npm install 23 | - name: Run `npm test` 24 | run: | 25 | npm run ci 26 | env: 27 | CI: true 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .c9/ 2 | build/ 3 | node_modules/ 4 | coverage/ 5 | **/*.log 6 | sandbox/ 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | coverage 3 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date 3 | ignore: 4 | 'npm:marked:20150520': 5 | - jsdoc > marked: 6 | reason: Dev Dependecy 7 | expires: '2016-05-28T00:25:33.252Z' 8 | SNYK-JS-LODASH-450202: 9 | - lodash: 10 | reason: None given 11 | expires: '2019-08-03T20:58:36.052Z' 12 | patch: {} 13 | version: v1.13.5 14 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | ## Objects 2 | 3 |
4 |
creators : object
5 |

Creators allow for methods which create values during validation when a 6 | value is not supplied

7 |
8 |
obey : object
9 |

The main object for Obey; exposes the core API methods for standard use as 10 | well as the API for all other modules

11 |
12 |
modifiers : object
13 |

Modifiers allow for coercion/modification of a value present in the object 14 | when validation occurs

15 |
16 |
rules : object
17 |

Rules is responsible for determining the execution of schema definition 18 | properties during validation

19 |
20 |
types : object
21 |

Types determine and execute the appropriate validation to be performed on the 22 | data during validation

23 |
24 |
25 | 26 | ## Functions 27 | 28 |
29 |
getMessages(msgObjs)Array.<string>
30 |

Compiles array items into string error messages

31 |
32 |
ValidationError(message)
33 |

Creates ValidationError object for throwing

34 |
35 |
validateByKeys(context, keyPrefix)Promise.<Object>
36 |

Validates an object using the definition's keys property

37 |
38 |
validateByValues(context, keyPrefix)Promise.<Object>
39 |

Validates an object using the definition's values property

40 |
41 |
42 | 43 | 44 | 45 | ## creators : object 46 | Creators allow for methods which create values during validation when a 47 | value is not supplied 48 | 49 | **Kind**: global namespace 50 | 51 | * [creators](#creators) : object 52 | * [.lib](#creators.lib) 53 | * [.execute(def, value)](#creators.execute) ⇒ function 54 | * [.add(name, fn)](#creators.add) 55 | 56 | 57 | 58 | ### creators.lib 59 | **Kind**: static property of [creators](#creators) 60 | **Properties** 61 | 62 | | Name | Type | Description | 63 | | --- | --- | --- | 64 | | Library | Object | of creators | 65 | 66 | 67 | 68 | ### creators.execute(def, value) ⇒ function 69 | Execute method calls the appropriate creator and returns the method or 70 | throws and error if the creator does not exist 71 | 72 | **Kind**: static method of [creators](#creators) 73 | **Returns**: function - The creator function 74 | 75 | | Param | Type | Description | 76 | | --- | --- | --- | 77 | | def | Object | The property configuration | 78 | | value | \* | The value being validated | 79 | 80 | 81 | 82 | ### creators.add(name, fn) 83 | Adds a creator to the library 84 | 85 | **Kind**: static method of [creators](#creators) 86 | 87 | | Param | Type | Description | 88 | | --- | --- | --- | 89 | | name | string | The name of the creator | 90 | | fn | function | The creator's method | 91 | 92 | 93 | 94 | ## obey : object 95 | The main object for Obey; exposes the core API methods for standard use as 96 | well as the API for all other modules 97 | 98 | **Kind**: global namespace 99 | 100 | * [obey](#obey) : object 101 | * [.rule(def)](#obey.rule) ⇒ Object 102 | * [.model(obj, [strict])](#obey.model) ⇒ Object 103 | * [.type(name, handler)](#obey.type) 104 | * [.modifier(name, fn)](#obey.modifier) 105 | * [.creator(name, fn)](#obey.creator) 106 | 107 | 108 | 109 | ### obey.rule(def) ⇒ Object 110 | Returns a composed rule from a definition object 111 | 112 | **Kind**: static method of [obey](#obey) 113 | 114 | | Param | Type | Description | 115 | | --- | --- | --- | 116 | | def | Object | The rule definition | 117 | 118 | 119 | 120 | ### obey.model(obj, [strict]) ⇒ Object 121 | Returns a composed model from a definition object 122 | 123 | **Kind**: static method of [obey](#obey) 124 | 125 | | Param | Type | Default | Description | 126 | | --- | --- | --- | --- | 127 | | obj | Object | | The definition object | 128 | | [strict] | boolean | true | Whether or not to enforce strict validation | 129 | 130 | 131 | 132 | ### obey.type(name, handler) 133 | Creates and stores (or replaces) a type 134 | 135 | **Kind**: static method of [obey](#obey) 136 | 137 | | Param | Type | Description | 138 | | --- | --- | --- | 139 | | name | string | The name of the type | 140 | | handler | Object | function | The type method or object of methods | 141 | 142 | 143 | 144 | ### obey.modifier(name, fn) 145 | Creates and stores a modifier 146 | 147 | **Kind**: static method of [obey](#obey) 148 | 149 | | Param | Type | Description | 150 | | --- | --- | --- | 151 | | name | string | The modifier's name | 152 | | fn | function | The method for the modifier | 153 | 154 | 155 | 156 | ### obey.creator(name, fn) 157 | Creates and stores a creator 158 | 159 | **Kind**: static method of [obey](#obey) 160 | 161 | | Param | Type | Description | 162 | | --- | --- | --- | 163 | | name | string | The creator's name | 164 | | fn | function | The method for the creator | 165 | 166 | 167 | 168 | ## modifiers : object 169 | Modifiers allow for coercion/modification of a value present in the object 170 | when validation occurs 171 | 172 | **Kind**: global namespace 173 | 174 | * [modifiers](#modifiers) : object 175 | * [.lib](#modifiers.lib) 176 | * [.execute(def, value)](#modifiers.execute) ⇒ function 177 | * [.add(name, fn)](#modifiers.add) 178 | 179 | 180 | 181 | ### modifiers.lib 182 | **Kind**: static property of [modifiers](#modifiers) 183 | **Properties** 184 | 185 | | Name | Type | Description | 186 | | --- | --- | --- | 187 | | Library | Object | of modifiers | 188 | 189 | 190 | 191 | ### modifiers.execute(def, value) ⇒ function 192 | Execute method calls the appropriate modifier and passes in the value or 193 | throws an error if the modifier does not exist 194 | 195 | **Kind**: static method of [modifiers](#modifiers) 196 | **Returns**: function - The modifier function 197 | 198 | | Param | Type | Description | 199 | | --- | --- | --- | 200 | | def | Object | The property configuration | 201 | | value | \* | The value being validated | 202 | 203 | 204 | 205 | ### modifiers.add(name, fn) 206 | Adds new modifier to the library 207 | 208 | **Kind**: static method of [modifiers](#modifiers) 209 | 210 | | Param | Type | Description | 211 | | --- | --- | --- | 212 | | name | string | The name of the modifier | 213 | | fn | function | The modifier's method | 214 | 215 | 216 | 217 | ## rules : object 218 | Rules is responsible for determining the execution of schema definition 219 | properties during validation 220 | 221 | **Kind**: global namespace 222 | 223 | * [rules](#rules) : object 224 | * [.props](#rules.props) 225 | * [.makeValidate(def)](#rules.makeValidate) 226 | * [.validate(def, data, [opts], [key], [errors], [rejectOnFail], [initData])](#rules.validate) ⇒ Promise.<\*> 227 | * [.build(def)](#rules.build) ⇒ Object 228 | * [.getProps(def, data)](#rules.getProps) ⇒ Array 229 | 230 | 231 | 232 | ### rules.props 233 | **Kind**: static property of [rules](#rules) 234 | **Properties** 235 | 236 | | Name | Type | Description | 237 | | --- | --- | --- | 238 | | Validation | Object | property setup and order of operations | 239 | 240 | 241 | 242 | ### rules.makeValidate(def) 243 | Binds rule definition in validate method 244 | 245 | **Kind**: static method of [rules](#rules) 246 | 247 | | Param | Type | Description | 248 | | --- | --- | --- | 249 | | def | Object | The rule definition object | 250 | 251 | 252 | 253 | ### rules.validate(def, data, [opts], [key], [errors], [rejectOnFail], [initData]) ⇒ Promise.<\*> 254 | Iterates over the properties present in the rule definition and sets the 255 | appropriate bindings to required methods 256 | 257 | **Kind**: static method of [rules](#rules) 258 | **Returns**: Promise.<\*> - Resolves with the resulting data, with any defaults, creators, and modifiers applied. 259 | Rejects with a ValidationError if applicable. 260 | 261 | | Param | Type | Default | Description | 262 | | --- | --- | --- | --- | 263 | | def | Object | | The rule definition object | 264 | | data | \* | | The data (value) to validate | 265 | | [opts] | Object | {partial: false} | Specific options for validation process | 266 | | [key] | string | null | null | Key for tracking parent in nested iterations | 267 | | [errors] | Array.<{type: string, sub: string, key: string, value: \*, message: string}> | [] | An error array to which any additional error objects will be added. If not specified, a new array will be created. | 268 | | [rejectOnFail] | boolean | true | If true, resulting promise will reject if the errors array is not empty; otherwise ValidationErrors will not cause a rejection | 269 | | [initData] | Object | null | | Initial data object | 270 | 271 | 272 | 273 | ### rules.build(def) ⇒ Object 274 | Adds new rule to the lib 275 | 276 | **Kind**: static method of [rules](#rules) 277 | 278 | | Param | Type | Description | 279 | | --- | --- | --- | 280 | | def | Object | The rule definition | 281 | 282 | 283 | 284 | ### rules.getProps(def, data) ⇒ Array 285 | Gets props list according to partial, required, and allowNull specifications 286 | 287 | **Kind**: static method of [rules](#rules) 288 | 289 | | Param | Type | Description | 290 | | --- | --- | --- | 291 | | def | Object | The rule definition | 292 | | data | \* | The value being evaluated | 293 | 294 | 295 | 296 | ## types : object 297 | Types determine and execute the appropriate validation to be performed on the 298 | data during validation 299 | 300 | **Kind**: global namespace 301 | 302 | * [types](#types) : object 303 | * [.strategies](#types.strategies) 304 | * [.checkSubType(def)](#types.checkSubType) ⇒ Object 305 | * [.validate(def, value, key, errors, initData)](#types.validate) ⇒ \* | Promise.<\*> 306 | * [.add(name, handler)](#types.add) 307 | * [.check(context)](#types.check) ⇒ Promise.<\*> 308 | 309 | 310 | 311 | ### types.strategies 312 | **Kind**: static property of [types](#types) 313 | **Properties** 314 | 315 | | Name | Type | Description | 316 | | --- | --- | --- | 317 | | Contains | Object | type strategies | 318 | 319 | 320 | 321 | ### types.checkSubType(def) ⇒ Object 322 | Checks for and applies sub-type to definition 323 | 324 | **Kind**: static method of [types](#types) 325 | 326 | | Param | Type | Description | 327 | | --- | --- | --- | 328 | | def | Object | The rule defintion | 329 | 330 | 331 | 332 | ### types.validate(def, value, key, errors, initData) ⇒ \* | Promise.<\*> 333 | Sets up the `fail` method and handles `empty` or `undefined` values. If neither 334 | empty or undefined, calls the appropriate `type` and executes validation 335 | 336 | **Kind**: static method of [types](#types) 337 | **Returns**: \* | Promise.<\*> - The value if empty or undefined, check method if value requires type validation 338 | 339 | | Param | Type | Description | 340 | | --- | --- | --- | 341 | | def | Object | The property configuration | 342 | | value | \* | The value being validated | 343 | | key | string | The key name of the property | 344 | | errors | Array.<{type: string, sub: (string\|number), key: string, value: \*, message: string}> | An error array to which any additional error objects will be added | 345 | | initData | Object | Initial data object | 346 | 347 | 348 | 349 | ### types.add(name, handler) 350 | Add (or override) a type in the library 351 | 352 | **Kind**: static method of [types](#types) 353 | 354 | | Param | Type | Description | 355 | | --- | --- | --- | 356 | | name | string | The name of the type | 357 | | handler | Object | function | The type strategy method | 358 | 359 | 360 | 361 | ### types.check(context) ⇒ Promise.<\*> 362 | Ensures that the strategy exists, loads if not already in memory, then ensures 363 | subtype and returns the applied type strategy 364 | 365 | **Kind**: static method of [types](#types) 366 | **Returns**: Promise.<\*> - Resolves with the provided data, possibly modified by the type strategy 367 | 368 | | Param | Type | Description | 369 | | --- | --- | --- | 370 | | context | Object | A type context | 371 | 372 | 373 | 374 | ## getMessages(msgObjs) ⇒ Array.<string> 375 | Compiles array items into string error messages 376 | 377 | **Kind**: global function 378 | 379 | | Param | Type | Description | 380 | | --- | --- | --- | 381 | | msgObjs | Array.<{type: string, sub: (string\|number), key: string, value: \*, message: string}> | Original array of error message objects | 382 | 383 | 384 | 385 | ## ValidationError(message) 386 | Creates ValidationError object for throwing 387 | 388 | **Kind**: global function 389 | 390 | | Param | Type | Description | 391 | | --- | --- | --- | 392 | | message | Array.<{type: string, sub: (string\|number), key: string, value: \*, message: string}> | Raw array of error objects | 393 | 394 | 395 | 396 | ## validateByKeys(context, keyPrefix) ⇒ Promise.<Object> 397 | Validates an object using the definition's `keys` property 398 | 399 | **Kind**: global function 400 | **Returns**: Promise.<Object> - Resolves with the final object 401 | 402 | | Param | Type | Description | 403 | | --- | --- | --- | 404 | | context | Object | An Obey type context | 405 | | keyPrefix | string | A prefix to include before the key in an error message | 406 | 407 | 408 | 409 | ## validateByValues(context, keyPrefix) ⇒ Promise.<Object> 410 | Validates an object using the definition's `values` property 411 | 412 | **Kind**: global function 413 | **Returns**: Promise.<Object> - Resolves with the final object 414 | 415 | | Param | Type | Description | 416 | | --- | --- | --- | 417 | | context | Object | An Obey type context | 418 | | keyPrefix | string | A prefix to include before the key in an error message | 419 | 420 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v5.0.1 (2022-10-10): 2 | 3 | * Fixes `min` and `max` validation and messaging 4 | 5 | ### v5.0.0 (2020-09-01): 6 | 7 | * Migrated tests from mocha to jest. 8 | * Update default `uuid` type to be case insensitive. 9 | * Replace travis with github actions for CI. 10 | 11 | ### v4.1.1 (2019-07-04): 12 | 13 | * Fixed bug around `allow` stomping on `empty` when validating strings. Originally using `allow` would require specifying `''` as an allowed value, even if `empty` was also enabled. 14 | 15 | ### v4.1.0 (2019-06-19): 16 | 17 | * Added `jexl` validator method for evaluating data against [Jexl](https://github.com/TomFrost/Jexl) expressions. 18 | 19 | * Added root `use` method for adding plugin references to external packages. (Currently only used by `jexl` validator.) 20 | 21 | ### v4.0.2 (2019-06-01): 22 | 23 | * Fixed bug in `zip` type that disallowed numeric values. Now values are coerced to strings before validation checks. 24 | 25 | ### v4.0.1 (2019-05-16): 26 | 27 | * Added Node 12 to Travis config. 28 | 29 | * Updated vulnerable dependencies. 30 | 31 | ### v4.0.0 (2019-05-03): 32 | 33 | * *BREAKING*: Fixed bug allowing empty string values for predefined types, even when required ([#76](https://github.com/psvet/obey/issues/76)). 34 | 35 | ### v3.0.5 (2019-04-19): 36 | 37 | * Conditional require rules (`requiredIf`, `requiredIfNot`) are now removed if a `creator` or `default` rule is defined. 38 | 39 | ### v3.0.0 (2019-01-05): 40 | 41 | * *BREAKING*: Removed Babel dependency. This effectively drops support for Node versions <6. 42 | 43 | * Fixed bug around `allowNull` and non-`null` falsey default values ([#71](https://github.com/psvet/obey/issues/71)). 44 | 45 | ### v2.0.0 (2018-03-28): 46 | 47 | * *BREAKING*: Changed default `zip` to US regex pattern. The original default pattern was discovered to be overly lenient and was removed. 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2015 TechnologyAdvice 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obey 2 | 3 | Asynchronous Data Modelling and Validation. 4 | 5 | [![Travis branch](https://img.shields.io/travis/psvet/obey/master.svg)](https://travis-ci.org/psvet/obey) 6 | ![Dependencies](https://img.shields.io/david/psvet/obey.svg) 7 | [![Known Vulnerabilities](https://snyk.io/test/npm/obey/badge.svg)](https://snyk.io/test/npm/obey) 8 | 9 | ## Contents 10 | 11 | - [Introduction](#introduction) 12 | - [Installation](#installation) 13 | - [API Documentation](#api-documentation) 14 | - [Rules](#rules) 15 | - [Models](#models) 16 | - [Validation](#validation) 17 | - [Validating Partials](#validating-partials) 18 | - [Validation Error Handling](#validation-error-handling) 19 | - [Definition Properties](#definition-properties) 20 | - [Types](#types) 21 | - [Adding New Types](#adding-new-types) 22 | - [Adding Single-Method Type](#adding-single-method-type) 23 | - [Adding Type with Subs](#adding-type-with-subs) 24 | - [Allow](#allow) 25 | - [Modifiers](#modifiers) 26 | - [Creating Modifiers](#creating-modifiers) 27 | - [Creators](#creators) 28 | - [Creating Creators](#creating-creators) 29 | - [Strict Mode](#strict-mode) 30 | - [Asynchronous Validation](#asynchronous-validation) 31 | - [Contributing](#contributing) 32 | - [License](#license) 33 | 34 | ## Introduction 35 | 36 | Obey is a library for creating asynchronous data models and rules. The core goal of the project is to provide methods for managing data models both through synchronous and asynchronous validation and alignment using [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). 37 | 38 | ## Installation 39 | 40 | Obey can be installed via [NPM](https://www.npmjs.com/package/obey): `npm install obey --save` 41 | 42 | ## API Documentation 43 | 44 | Detailed [API Documentation](API.md) is available for assistance in using, modifying, or [contributing](#contributing) to the Obey library. 45 | 46 | ## Rules 47 | 48 | > Rules are core definitions of how a value should be validated: 49 | 50 | ```javascript 51 | const obey = require('obey') 52 | 53 | const firstName = obey.rule({ type: 'string', min: 2, max: 45, required: true }) 54 | ``` 55 | 56 | ## Models 57 | 58 | > Models allow for creating validation rules for entire object schemas. The following demonstrates a model being created with Obey: 59 | 60 | ```javascript 61 | const obey = require('obey') 62 | 63 | const userModel = obey.model({ 64 | id: { type: 'uuid', creator: 'uuid', required: true }, 65 | email: { type: 'email', required: true }, 66 | password: { type: 'string', modifier: 'encryptPassword', required: true }, 67 | passwordConfirm: { type: 'string', equalTo: 'password' }, 68 | fname: { type: 'string', description: 'First Name' }, 69 | lname: { type: 'string', description: 'Last Name', empty: true }, 70 | suffix: { type: 'string', allowNull: true }, 71 | phone: { type: 'phone:numeric', min: 7, max: 10 }, 72 | phoneType: { type: 'string', requiredIf: 'phone' }, 73 | carrier: { type: 'string', requiredIf: { phoneType: 'mobile' }}, 74 | // Array 75 | labels: { type: 'array', values: { 76 | type: 'object', keys: { 77 | label: { type: 'string' } 78 | } 79 | }}, 80 | // Nested object 81 | address: { type: 'object', keys: { 82 | street: { type: 'string', max: 45 }, 83 | city: { type: 'string', max: 45 }, 84 | state: { type: 'string', max: 2, modifier: 'upperCase' }, 85 | zip: { type: 'number', min: 10000, max: 99999 } 86 | }}, 87 | // Key-independent object validation 88 | permissions: { type: 'object', values: { 89 | type: 'string' 90 | }}, 91 | account: { type: 'string', allow: [ 'user', 'admin' ], default: 'user' } 92 | }) 93 | ``` 94 | 95 | ## Validation 96 | 97 | Using the example above, validation is done by calling the `validate` method and supplying data. This applies to both individual rules and data models: 98 | 99 | ```javascript 100 | userModel.validate({ /* some data */ }) 101 | .then(data => { 102 | // Passes back `data` object, includes any defaults set, 103 | // generated, or modified data 104 | }) 105 | .catch(error => { 106 | // Returns instance of ValidationError 107 | // `error.message` => String format error messages 108 | // `error.collection` => Raw array of error objects 109 | }) 110 | ``` 111 | 112 | The validate method returns a promise (for more information see [Asynchronous Validation](#Asynchronous Validation)). A passing run will resolve with the data, any failures will reject and the `ValidationError` instance will be returned. 113 | 114 | ### Validating Partials 115 | 116 | The `validate` method has the ability to validate partial data objects: 117 | 118 | ```javascript 119 | // Allow partial validation by supplying second argument with `partial: true` 120 | userModel.validate({ /* some (partial) data */ }, { partial: true }) 121 | ``` 122 | 123 | The default for the partial option is `false`, but passing `true` will allow for validation of an object containing a subset (i.e. will not throw errors for `required` properties). 124 | 125 | The common use-case for validating partials is `PATCH` updates. 126 | 127 | _Note: Running a partial validation will prevent running `creator`'s on any properties_ 128 | 129 | ### Validation Error Handling 130 | 131 | Validation errors are collected and thrown after all validation has run. This is as opposed to blocking, or stopping, on the first failure. 132 | 133 | As shown in the example above, the `catch` will contain an instance of `ValidationError` with two properties; `message` and `collection`. The `message` simply contains the description of all errors. 134 | 135 | The `collection` is an array of objects containing details on each of the validation errors. For example, if a `type` evaluation for `phone:numeric` was performed and the value failed the following would be contained as an object in the array: 136 | 137 | ```javascript 138 | { 139 | type: 'phone', // The type evaluation performed 140 | sub: 'numeric', // The sub-type (if applicable) 141 | key: 'primaryPhone', // Name of the property in the model 142 | value: '(555) 123-4567', // The value evaluated 143 | message: 'Value must be a numeric phone number' // Message 144 | } 145 | ``` 146 | 147 | ## Definition Properties 148 | 149 | When setting definitions for rules or model properties, the following are supported: 150 | 151 | * `type`: The type of value with (optional) sub-type see [Types](#types) 152 | * `keys`: Property of `object` type, indicates nested object properties 153 | * `values`: Defines value specification for arrays or key-independent objects 154 | * `modifier`: uses a method and accepts a passed value to modify or transform data, see [Modifiers](#modifiers) 155 | * `creator`: uses a method to create a default value if no value is supplied, see [Creators](#creators) 156 | * `empty`: Set to `true` allows empty string or array, (default `false`) 157 | * `default`: The default value if no value specified 158 | * `min`: The minimum character length for a string, lowest number, or minimum items in array 159 | * `max`: The maximum character length for a string, highest number, or maximum items in array 160 | * `required`: Enforces the value cannot be `undefined` during validation (default `false`) 161 | * `requiredIf`: Enforces the value cannot be `undefined` if a value exists or matches a given value (`{ propertyName: 'requiredValue' }`), or any of a list of values (`{ propertyName: [ 'val1', 'val2' ] }`) 162 | * `requiredIfNot`: Enforces the value cannot be `undefined` if a value _does not_ exist or match a given value (`{ propertyName: 'requiredValue' }`), or any of a list of values (`{ propertyName: [ 'val1', 'val2' ] }`) 163 | * `equalTo`: Enforces the value to be the same as the corresponding field 164 | * `allow`: Object, array or single value representing allowed value(s), see [Allow](#allow) 165 | * `allowNull`: Accepts a null value or processes specified type 166 | * `strict`: Enable or disable strict checking of an object, see [Strict Mode](#strict-mode) 167 | * `description`: A description of the property 168 | * `jexl`: One or more objects containing an `expr` string for validating data against Jexl expressions. See [Jexl Validation](#jexl-validation). 169 | 170 | ## Types 171 | 172 | **Reference: [Type Documentation](/src/typeStrategies#types)** 173 | 174 | > Types are basic checks against native types, built-ins or customs. The library includes native types (`boolean`, `number`, `string`, `array`, and `object`) as well other common types. A [list of built-in types](/src/typeStrategies#types) is contained in the source. 175 | 176 | The `type` definition can also specify a sub-type, for example: 177 | 178 | ```javascript 179 | phone: { type: 'phone:numeric' } 180 | ``` 181 | 182 | The above would specify the general type `phone` with sub-type `numeric` (only allowing numbers). 183 | 184 | ### Adding New Types 185 | 186 | New types can be added to the Obey lib with the `obey.type` method. Types can be added as single methods or objects supporting sub-types: 187 | 188 | #### Adding Single-Method Type 189 | 190 | ```javascript 191 | obey.type('lowerCaseOnly', context => { 192 | if (!/[a-z]/.test(context.value)) { 193 | context.fail(`${context.key} must be lowercase`) 194 | } 195 | }) 196 | ``` 197 | 198 | #### Adding Type with Subs 199 | 200 | ```javascript 201 | obey.type('password', { 202 | default: context => { 203 | if (context.value.length < 6) { 204 | context.fail(`${context.key} must contain at least 6 characters`) 205 | } 206 | }, 207 | strong: context => { 208 | if (!context.value.test((/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])[0-9a-zA-Z]{8,}$/))) { 209 | context.fail(`${context.key} must contain a number, letter, and be at least 8 characters`) 210 | } 211 | } 212 | }) 213 | ``` 214 | 215 | The definition object contains keys that indicate the subtype (or `default` if no sub-type specified). 216 | 217 | Each method will be passed a `context` object at runtime. This object has the following properties: 218 | 219 | * `def`: The entire rule for the property in the model 220 | * `sub`: The sub-type (if provided) 221 | * `key`: The name of the property being tested (if an element in a model/object) 222 | * `value`: The value to test 223 | * `fail`: A function accepting a failure message as an argument 224 | 225 | The above would add a new type which would then be available for setting in the model configuration for any properties. 226 | 227 | ```javascript 228 | password: { type: 'password', /* ...additional config... */ } 229 | 230 | /* ...or... */ 231 | 232 | password: { type: 'password:strong', /* ...additional config... */ } 233 | ``` 234 | 235 | Types can be synchronous or asynchronous. For example, if a unique email is required the following could be used to define a `uniqueEmail` type: 236 | 237 | ```javascript 238 | obey.type('uniqueEmail', context => { 239 | return someDataSource.find({ email: context.value }) 240 | .then(record => { 241 | if (record.length >= 1) { 242 | context.fail(`${context.key} already exists`) 243 | } 244 | }) 245 | } 246 | ``` 247 | 248 | Types _can_ return/resolve a value, though it is not required and is recommended any coercion be handled with a modifier. 249 | 250 | Regardless of if a value is returned/resolved, asynchronous types must resolve. Errors should be handled with the `context.fail()` method. 251 | 252 | ## Allow 253 | 254 | The `allow` property in definition objects accepts three formats; `string`, `array` or `object` 255 | 256 | The `string` and `array` methods are straight-forward: 257 | 258 | ```javascript 259 | // Only allow 'bar' 260 | foo: { type: 'string', allow: 'bar' } 261 | // Allow 'buzz', 'bazz', 'bizz' 262 | fizz: { type: 'string', allow: [ 'buzz', 'bazz', 'bizz' ] } 263 | ``` 264 | 265 | The `object` representation of the `allow` property gives the ability to store enums alongside the model structure making sharing/reuse of the objects simplified: 266 | 267 | ```javascript 268 | const allowedStatuses = { 269 | 'prog': 'in progress', 270 | 'comp': 'completed', 271 | 'arch': 'archived' 272 | } 273 | 274 | // Allow statuses 275 | { status: { type: 'string', allow: allowedStatuses } } 276 | ``` 277 | 278 | In the above example, the model would only accept the keys (`prog`, `comp`, `arch`) during validation. 279 | 280 | ## Modifiers 281 | 282 | > Modifiers allow custom methods to return values which are modified/transformed versions of the received value. 283 | 284 | ### Creating Modifiers 285 | 286 | Modifiers can be added to the Obey lib with the `obey.modifier` method: 287 | 288 | ```javascript 289 | obey.modifier('upperCase', val => val.toUpperCase()) 290 | ``` 291 | 292 | When the model is validated, the value in any fields with the `upperCase` modifier will be transformed to uppercase. 293 | 294 | Similar to types, modifiers may be synchronous (returning a value) or asynchronous (returning a promise). 295 | 296 | ## Creators 297 | 298 | > Creators allow custom methods to return values which set the value similar to the `default` property. When validating, if a value is not provided the creator assigned will be used to set the value. 299 | 300 | ### Creating Creators 301 | 302 | Creators can be added to the Obey lib with the `obey.creator` method: 303 | 304 | ```javascript 305 | obey.creator('timestamp', () => new Date().getTime()) 306 | ``` 307 | 308 | The above example would add a creator named `timestamp` which could be assigned as shown below: 309 | 310 | ```javascript 311 | created: { type: 'number', creator: 'timestamp' } 312 | ``` 313 | 314 | When the model is validated, if no `created` property is provided the `timestamp` creator will assign the property a UTC timestamp. 315 | 316 | Similar to modifiers, creators may be synchronous (returning a value) or asynchronous (returning a promise). 317 | 318 | ## Strict Mode 319 | 320 | By default, Obey enforces strict matching on objects; meaning an object must define any keys that will be present in the data object being validated. 321 | 322 | To disable strict mode on a rule or object set the `strict` property to false: 323 | 324 | ```javascript 325 | foo: { type: 'object', strict: false, keys: { /* ... */ } } 326 | ``` 327 | 328 | To disable strict mode on a model pass the (optional) strict argument as `false`: 329 | 330 | ```javascript 331 | const model = obey.model({ /* definition */ }, false) 332 | ``` 333 | 334 | ## Jexl Validation 335 | 336 | Obey allows for validating data against [Jexl](https://github.com/TomFrost/Jexl) expressions via the `jexl` rule: 337 | 338 | ```javascript 339 | obey.model({ 340 | exprVal: { 341 | type: 'string', 342 | jexl: [{ 343 | expr: "value == root.testVal.nestedObjArray[.name == 'some specific name'].payload", 344 | message: "Do not seek the treasure" // Optional expression-specific error message 345 | }] 346 | }, 347 | testVal: { 348 | type: 'object', 349 | keys: { 350 | nestedObjArray: { 351 | type: 'array', 352 | values: { 353 | type: 'object', 354 | keys: { 355 | name: { type: 'string' }, 356 | payload: { type: 'string' } 357 | } 358 | } 359 | } 360 | } 361 | } 362 | }) 363 | ``` 364 | 365 | **Note**: the `jexl` validator uses `value` and `root` as its context keys for the specific value being validated and the corresponding data, respectively, so expression strings should be constructed accordingly. 366 | 367 | If necessary, a preconfigured Jexl instance can be passed in before the model is constructed, in order to utitlize user-defined transforms in validation: 368 | 369 | ```javascript 370 | const obey = require('obey') 371 | const jexl = require('jexl') 372 | jexl.addTransform('upper', val => val.toUpperCase(val)) 373 | obey.use('jexl', jexl) 374 | obey.model({/* ...definition... */}) 375 | ``` 376 | 377 | ## Asynchronous Validation 378 | 379 | The goal with Obey is to provide more than just standard type/regex checks against data to validate values and models. The ability to write both synchronous and asynchronous checks, creators, and modifiers, and include data coercion in the validation simplifies the process of validation and checking before moving onto data source interactions. 380 | 381 | Additionally, with the widespread use of promises, this structure fits well in the scheme of data processing in general: 382 | 383 | ```javascript 384 | // Define a model somewhere in your code... 385 | const user = obey.model(/* ...Model Definition... */) 386 | 387 | // Use it to validate before creating a record... 388 | user.validate(/* ...some data object... */) 389 | .then(createUser) 390 | .then(/* ...response or other action... */) 391 | .catch(/* ...handle errors... */) 392 | ``` 393 | 394 | ## Contributing 395 | 396 | Contibutions to Obey are welcomed and encouraged. If you would like to make a contribution please fork the repository and submit a PR with changes. Acceptance of PR's is based on a review by core contributors. To increase the likelihood of acceptance please ensure the following: 397 | 398 | * The PR states the reason for the modification/addition to the API **in detail** 399 | * All tests are passing and coverage is at, or near, 100% 400 | * The code submitted follows the conventions used throughout the library 401 | * [JSDoc](http://usejsdoc.org/) is in place and generates via `npm run doc` 402 | * Any needed documentation on the `README` is supplied 403 | 404 | ## License 405 | 406 | Obey was originally created at [TechnologyAdvice](http://www.technologyadvice.com) in Nashville, TN and released under the [MIT](LICENSE.txt) license. 407 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obey", 3 | "version": "5.0.1", 4 | "description": "Data modelling and validation library", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "clean": "rm -rf node_modules", 8 | "test": "snyk test && npm run lint && npm run jest", 9 | "jest": "NODE_PATH=./ RESOURCES_PATH=./src jest --coverage --forceExit --runInBand --colors", 10 | "jest:watch": "NODE_PATH=./ RESOURCES_PATH=./src jest --watchAll --runInBand --colors", 11 | "lint": "standard ./src ./test --fix", 12 | "ci": "npm run lint && npm run jest" 13 | }, 14 | "keywords": [ 15 | "obey", 16 | "valid", 17 | "validate", 18 | "validation", 19 | "model", 20 | "modelling" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git+ssh://git@github.com/psvet/obey.git" 25 | }, 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/psvet/obey/issues" 29 | }, 30 | "homepage": "https://github.com/psvet/obey#readme", 31 | "devDependencies": { 32 | "jest": "^26.4.2", 33 | "snyk": "^1.387.0", 34 | "standard": "^12.0.1" 35 | }, 36 | "dependencies": { 37 | "bluebird": "^3.5.3", 38 | "dot-object": "^2.1.3", 39 | "jexl": "^2.1.1", 40 | "lodash": "^4.17.20" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/creators.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 TechnologyAdvice 3 | */ 4 | 5 | /** 6 | * Creators allow for methods which create values during validation when a 7 | * value is not supplied 8 | * @namespace creators 9 | */ 10 | const creators = { 11 | /** 12 | * @memberof creators 13 | * @property {Object} Library of creators 14 | */ 15 | lib: {}, 16 | 17 | /** 18 | * Execute method calls the appropriate creator and returns the method or 19 | * throws and error if the creator does not exist 20 | * @memberof creators 21 | * @param {Object} def The property configuration 22 | * @param {*} value The value being validated 23 | * @returns {function} The return value of the creator function 24 | */ 25 | execute: function(def, value) { 26 | if (value !== undefined) return value 27 | if (creators.lib[def.creator]) return creators.lib[def.creator]() 28 | throw new Error(`creator '${def.creator}' does not exist`) 29 | }, 30 | 31 | /** 32 | * Adds a creator to the library 33 | * @memberof creators 34 | * @param {string} name The name of the creator 35 | * @param {function} fn The creator's method 36 | */ 37 | add: (name, fn) => { 38 | if (typeof name !== 'string') throw new Error('creator name should be a string') 39 | if (typeof fn !== 'function') throw new Error('creator method should be a function') 40 | creators.lib[name] = fn 41 | } 42 | } 43 | 44 | module.exports = creators 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 TechnologyAdvice 3 | */ 4 | 5 | const rules = require('./rules') 6 | const types = require('./types') 7 | const modifiers = require('./modifiers') 8 | const creators = require('./creators') 9 | const validators = require('./lib/validators') 10 | const ValidationError = require('./lib/error') 11 | const plugins = require('./lib/plugins') 12 | 13 | /** 14 | * The main object for Obey; exposes the core API methods for standard use as 15 | * well as the API for all other modules 16 | * @namespace obey 17 | */ 18 | module.exports = { 19 | /** 20 | * API, exposes modules to make lib API accessible 21 | */ 22 | rules, types, modifiers, creators, validators, ValidationError, 23 | 24 | /** 25 | * Returns a composed rule from a definition object 26 | * @memberof obey 27 | * @param {Object} def The rule definition 28 | * @returns {Object} 29 | */ 30 | rule: def => rules.build(def), 31 | 32 | /** 33 | * Returns a composed model from a definition object 34 | * @memberof obey 35 | * @param {Object} obj The definition object 36 | * @param {boolean} [strict=true] Whether or not to enforce strict validation 37 | * @returns {Object} 38 | */ 39 | model: (obj, strict = true) => rules.build({ type: 'object', keys: obj, strict }), 40 | 41 | /** 42 | * Creates and stores (or replaces) a type 43 | * @memberof obey 44 | * @param {string} name The name of the type 45 | * @param {Object|function} handler The type method or object of methods 46 | */ 47 | type: (name, handler) => types.add(name, handler), 48 | 49 | /** 50 | * Creates and stores a modifier 51 | * @memberof obey 52 | * @param {string} name The modifier's name 53 | * @param {function} fn The method for the modifier 54 | */ 55 | modifier: (name, fn) => modifiers.add(name, fn), 56 | 57 | /** 58 | * Creates and stores a creator 59 | * @memberof obey 60 | * @param {string} name The creator's name 61 | * @param {function} fn The method for the creator 62 | */ 63 | creator: (name, fn) => creators.add(name, fn), 64 | 65 | /** 66 | * Adds given package to plugins lib. 67 | * 68 | * @param {string} name The package name 69 | * @param {function|Object} pkg The package reference 70 | */ 71 | // TODO: make this actually useful 72 | use: (name, pkg) => plugins.add(name, pkg) 73 | } 74 | -------------------------------------------------------------------------------- /src/lib/error.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 TechnologyAdvice 3 | */ 4 | const util = require('util') 5 | 6 | /** 7 | * Compiles array items into string error messages 8 | * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} msgObjs Original array 9 | * of error message objects 10 | * @returns {Array} 11 | */ 12 | const getMessages = (msgObjs) => { 13 | const messages = [] 14 | msgObjs.forEach(obj => { 15 | messages.push((obj.key ? `${obj.key} (${obj.value}):` : `${obj.value}:`) + ` ${obj.message}`) 16 | }) 17 | return messages 18 | } 19 | 20 | /** 21 | * Creates ValidationError object for throwing 22 | * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} message Raw array of 23 | * error objects 24 | */ 25 | function ValidationError(message) { 26 | Object.defineProperty(this, 'name', { value: 'ValidationError' }) 27 | Object.defineProperty(this, 'message', { value: getMessages(message).join('\n') }) 28 | Object.defineProperty(this, 'collection', { value: message }) 29 | 30 | // Fixes captureStackTrace missing on Safari 31 | if (typeof Error.captureStackTrace === 'function') { 32 | Error.captureStackTrace(this, ValidationError) 33 | } else { 34 | this.stack = new Error().stack 35 | } 36 | } 37 | 38 | // Creates instance of ValidationError as Error object 39 | util.inherits(ValidationError, Error) 40 | 41 | module.exports = ValidationError 42 | -------------------------------------------------------------------------------- /src/lib/plugins.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @namespace plugins 3 | */ 4 | const plugins = { 5 | /** 6 | * @memberof plugins 7 | * @property {Object} Library of plugins 8 | */ 9 | lib: {}, 10 | /** 11 | * Adds a package to the library 12 | * @memberof creators 13 | * @param {string} name The name of the package 14 | * @param {function|Object} fn The package reference 15 | */ 16 | add: (name, pkg) => { 17 | if (typeof name !== 'string') throw new Error('plugin name should be a string') 18 | if (typeof pkg !== 'object' && typeof pkg !== 'function') { 19 | throw new Error('plugin package should be an object or function') 20 | } 21 | plugins.lib[name] = pkg 22 | } 23 | } 24 | 25 | module.exports = plugins 26 | -------------------------------------------------------------------------------- /src/lib/validators.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 TechnologyAdvice 3 | */ 4 | /* eslint no-console: 0, consistent-return: 0 */ 5 | const dot = require('dot-object') 6 | const jexl = require('jexl') 7 | const plugins = require('./plugins') 8 | const cloneDeep = require('lodash/cloneDeep') 9 | 10 | const validators = { 11 | /** 12 | * Validator default method, used by model 13 | * @param {Object} def The property configuration 14 | * @param {*} value The value being validated 15 | */ 16 | default: function(def, value) { 17 | if (/^(number|boolean)$/.test(def.type)) { 18 | if (value === 0 || value === false) return value 19 | } 20 | return value || cloneDeep(def.default) 21 | }, 22 | 23 | /** 24 | * Validator allowed method, used by model 25 | * @param {Object} def The property configuration 26 | * @param {*} value The value being validated 27 | * @param {string} key The key name of the property 28 | * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array 29 | * to which any additional error objects will be added 30 | */ 31 | allow: function(def, value, key, errors) { 32 | const type = 'allow' 33 | const sub = typeof def.allow === 'object' && !Array.isArray(def.allow) ? Object.keys(def.allow) : def.allow 34 | const subIsArray = Array.isArray(sub) 35 | if ((subIsArray && sub.indexOf(value) === -1) || (!subIsArray && sub !== value)) { 36 | if (value === '' && def.empty) return 37 | errors.push({ type, sub, key, value, message: `Value '${value}' is not allowed` }) 38 | } 39 | }, 40 | 41 | /** 42 | * Validator min method, used by model 43 | * @param {Object} def The property configuration 44 | * @param {*} value The value being validated 45 | * @param {string} key The key name of the property 46 | * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array 47 | * to which any additional error objects will be added 48 | */ 49 | min: function(def, value, key, errors) { 50 | const type = 'min' 51 | const sub = def.min 52 | if ((Array.isArray(value) || typeof value === 'string') && value.length < def.min) { 53 | errors.push({ type, sub, key, value, message: `Length must be greater than or equal to ${def.min}` }) 54 | } else if (typeof value === 'number' && value < def.min) { 55 | errors.push({ type, sub, key, value, message: `Value must be greater than or equal to ${def.min}` }) 56 | } 57 | }, 58 | 59 | /** 60 | * Validator max method, used by model 61 | * @param {Object} def The property configuration 62 | * @param {*} value The value being validated 63 | * @param {string} key The key name of the property 64 | * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array 65 | * to which any additional error objects will be added 66 | */ 67 | max: function(def, value, key, errors) { 68 | const type = 'max' 69 | const sub = def.max 70 | if ((Array.isArray(value) || typeof value === 'string') && value.length > def.max) { 71 | errors.push({ type, sub, key, value, message: `Length must be less than or equal to ${def.max}` }) 72 | } else if (typeof value === 'number' && value > def.max) { 73 | errors.push({ type, sub, key, value, message: `Value must be less than or equal to ${def.max}` }) 74 | } 75 | }, 76 | 77 | /** 78 | * Validator requiredIf method, used by model 79 | * @param {Object} def The property configuration 80 | * @param {*} value The value being validated 81 | * @param {string} key The key name of the property 82 | * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array 83 | * to which any additional error objects will be added 84 | * @param {Object} data The full initial data object 85 | */ 86 | requiredIf: function(def, value, key, errors, data) { 87 | const type = 'requiredIf' 88 | const sub = def.requiredIf 89 | if (typeof sub === 'object') { 90 | const field = Object.keys(sub)[0] 91 | const fieldArr = Array.isArray(sub[field]) ? sub[field] : [ sub[field] ] 92 | fieldArr.some(val => { 93 | /* istanbul ignore else */ 94 | if (dot.pick(field, data) === val && value === undefined) { 95 | errors.push({ type, sub, key, value, message: `Value required by existing '${field}' value` }) 96 | return true 97 | } 98 | }) 99 | } else if (dot.pick(sub, data) !== undefined && value === undefined) { 100 | errors.push({ type, sub, key, value, message: `Value required because '${sub}' exists` }) 101 | } 102 | }, 103 | /** 104 | * Alias for requiredIf 105 | */ 106 | requireIf: function(def, value, key, errors, data) { 107 | console.log('-----\nObey Warning: `requireIf` should be `requiredIf`\n-----') 108 | def.requiredIf = def.requireIf 109 | delete def.requireIf 110 | validators.requiredIf(def, value, key, errors, data) 111 | }, 112 | 113 | /** 114 | * Validator requiredIfNot method, used by model 115 | * @param {Object} def The property configuration 116 | * @param {*} value The value being validated 117 | * @param {string} key The key name of the property 118 | * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array 119 | * to which any additional error objects will be added 120 | * @param {Object} data The full initial data object 121 | */ 122 | requiredIfNot: function(def, value, key, errors, data) { 123 | const type = 'requiredIfNot' 124 | const sub = def.requiredIfNot 125 | if (typeof sub === 'object') { 126 | const field = Object.keys(sub)[0] 127 | const fieldArr = Array.isArray(sub[field]) ? sub[field] : [ sub[field] ] 128 | fieldArr.some(val => { 129 | /* istanbul ignore else */ 130 | if (dot.pick(field, data) !== val && value === undefined) { 131 | errors.push({ type, sub, key, value, message: `Value required because '${field}' value is not one specified` }) 132 | return true 133 | } 134 | }) 135 | } else if (dot.pick(sub, data) === undefined && value === undefined) { 136 | errors.push({ type, sub, key, value, message: `Value required because '${sub}' is undefined`}) 137 | } 138 | }, 139 | /** 140 | * Alias for requiredIfNot 141 | */ 142 | requireIfNot: function(def, value, key, errors, data) { 143 | console.log('-----\nObey Warning: `requireIfNot` should be `requiredIfNot`\n-----') 144 | def.requiredIfNot = def.requireIfNot 145 | delete def.requireIfNot 146 | validators.requiredIfNot(def, value, key, errors, data) 147 | }, 148 | 149 | /** 150 | * Validator equalTo method, used by model 151 | * @param {Object} def The property configuration 152 | * @param {*} value The value being validated 153 | * @param {string} key The key name of the property 154 | * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array 155 | * to which any additional error objects will be added 156 | * @param {Object} data The full initial data object 157 | */ 158 | equalTo: function(def, value, key, errors, data) { 159 | const type = 'equalTo' 160 | const sub = def.equalTo 161 | if (dot.pick(sub, data) !== value) { 162 | errors.push({ type, sub, key, value, message: `Value must match ${sub} value`}) 163 | } 164 | }, 165 | 166 | /** 167 | * Validator jexl method, used by model 168 | * @param {Object} def The property configuration 169 | * @param {*} value The value being validated 170 | * @param {string} key The key name of the property 171 | * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array 172 | * to which any additional error objects will be added 173 | * @param {Object} data The full initial data object 174 | */ 175 | jexl: (def, value, key, errors, data) => { 176 | const type = 'jexl' 177 | const sub = Array.isArray(def.jexl) ? def.jexl : [ def.jexl ] 178 | const promises = sub.map((obj) => { 179 | const { 180 | expr, 181 | message = 'Value failed Jexl evaluation' 182 | } = obj 183 | const instance = plugins.lib.jexl || jexl 184 | return instance.eval(expr, { root: data, value }) 185 | .then(val => { 186 | if (!val) errors.push({ type, sub: obj, key, value, message }) 187 | }) 188 | .catch(() => { 189 | errors.push({ type, sub: obj, key, value, message }) 190 | }) 191 | }) 192 | Promise.all(promises) 193 | } 194 | } 195 | 196 | module.exports = validators 197 | -------------------------------------------------------------------------------- /src/modifiers.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 TechnologyAdvice 3 | */ 4 | 5 | /** 6 | * Modifiers allow for coercion/modification of a value present in the object 7 | * when validation occurs 8 | * @namespace modifiers 9 | */ 10 | const modifiers = { 11 | /** 12 | * @memberof modifiers 13 | * @property {Object} Library of modifiers 14 | */ 15 | lib: {}, 16 | 17 | /** 18 | * Execute method calls the appropriate modifier and passes in the value or 19 | * throws an error if the modifier does not exist 20 | * @memberof modifiers 21 | * @param {Object} def The property configuration 22 | * @param {*} value The value being validated 23 | * @returns {function} The return value of the modifier function 24 | */ 25 | execute: function(def, value) { 26 | if (modifiers.lib[def.modifier]) return modifiers.lib[def.modifier](value) 27 | throw new Error(`Modifier '${def.modifier}' does not exist`) 28 | }, 29 | 30 | /** 31 | * Adds new modifier to the library 32 | * @memberof modifiers 33 | * @param {string} name The name of the modifier 34 | * @param {function} fn The modifier's method 35 | */ 36 | add: (name, fn) => { 37 | if (typeof name !== 'string') throw new Error('Modifier name should be a string') 38 | if (typeof fn !== 'function') throw new Error('Modifier method should be a function') 39 | modifiers.lib[name] = fn 40 | } 41 | } 42 | 43 | module.exports = modifiers 44 | -------------------------------------------------------------------------------- /src/rules.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 TechnologyAdvice 3 | */ 4 | /* eslint no-console: 0 */ 5 | const types = require('./types') 6 | const modifiers = require('./modifiers') 7 | const creators = require('./creators') 8 | const Promise = require('bluebird') 9 | const validators = require('./lib/validators') 10 | const ValidationError = require('./lib/error') 11 | 12 | /** 13 | * @memberof rules 14 | * Defines all definition property checks available 15 | */ 16 | const allProps = { 17 | creator: { name: 'creator', fn: creators.execute }, 18 | default: { name: 'default', fn: validators.default }, 19 | modifier: { name: 'modifier', fn: modifiers.execute }, 20 | allow: { name: 'allow', fn: validators.allow }, 21 | min: { name: 'min', fn: validators.min }, 22 | max: { name: 'max', fn: validators.max }, 23 | type: { name: 'type', fn: types.validate }, 24 | requiredIf: { name: 'requiredIf', fn: validators.requiredIf }, 25 | requiredIfNot: { name: 'requiredIfNot', fn: validators.requiredIfNot }, 26 | requireIf: { name: 'requireIf', fn: validators.requireIf }, 27 | requireIfNot: { name: 'requireIfNot', fn: validators.requireIfNot }, 28 | equalTo: { name: 'equalTo', fn: validators.equalTo }, 29 | jexl: { name: 'jexl', fn: validators.jexl } 30 | } 31 | 32 | /** 33 | * Rules is responsible for determining the execution of schema definition 34 | * properties during validation 35 | * @namespace rules 36 | */ 37 | const rules = { 38 | /** 39 | * @memberof rules 40 | * @property {Object} Validation property setup and order of operations 41 | */ 42 | props: { 43 | // Default props 44 | default: [ 45 | allProps.creator, 46 | allProps.default, 47 | allProps.modifier, 48 | allProps.allow, 49 | allProps.min, 50 | allProps.max, 51 | allProps.type, 52 | allProps.requiredIf, 53 | allProps.requiredIfNot, 54 | allProps.requireIf, 55 | allProps.requireIfNot, 56 | allProps.equalTo, 57 | allProps.jexl 58 | ], 59 | // No value/undefined 60 | noVal: [ 61 | allProps.creator, 62 | allProps.default, 63 | allProps.modifier, 64 | allProps.requiredIf, 65 | allProps.requiredIfNot, 66 | allProps.requireIf, 67 | allProps.requireIfNot, 68 | allProps.equalTo, 69 | allProps.jexl 70 | ], 71 | // No value, partial 72 | noValPartial: [] 73 | }, 74 | 75 | /** 76 | * Binds rule definition in validate method 77 | * @memberof rules 78 | * @param {Object} def The rule definition object 79 | */ 80 | makeValidate: def => rules.validate.bind(null, def), 81 | 82 | /** 83 | * Iterates over the properties present in the rule definition and sets the 84 | * appropriate bindings to required methods 85 | * @memberof rules 86 | * @param {Object} def The rule definition object 87 | * @param {*} data The data (value) to validate 88 | * @param {Object} [opts={partial: false}] Specific options for validation process 89 | * @param {string|null} [key=null] Key for tracking parent in nested iterations 90 | * @param {Array<{type: string, sub: string, key: string, value: *, message: string}>} [errors=[]] An error array 91 | * to which any additional error objects will be added. If not specified, a new array will be created. 92 | * @param {boolean} [rejectOnFail=true] If true, resulting promise will reject if the errors array is not empty; 93 | * otherwise ValidationErrors will not cause a rejection 94 | * @param {Object|null} [initData=null] Initial data object 95 | * @returns {Promise.<*>} Resolves with the resulting data, with any defaults, creators, and modifiers applied. 96 | * Rejects with a ValidationError if applicable. 97 | */ 98 | validate: (def, data, opts = { partial: false }, key = null, errors = [], rejectOnFail = true, initData = null) => { 99 | let passthruData = initData === null ? data : initData 100 | let curData = data 101 | def.opts = opts 102 | const props = rules.getProps(def, data) 103 | if (!def.type) throw new Error('Model properties must define a \'type\'') 104 | let chain = Promise.resolve(data) 105 | props.forEach(prop => { 106 | if (def.hasOwnProperty(prop.name)) { 107 | chain = chain 108 | .then(val => prop.fn(def, val, key, errors, passthruData)) 109 | .then(res => { 110 | if (res !== undefined) curData = res 111 | return curData 112 | }) 113 | } 114 | }) 115 | return chain.then(res => { 116 | if (rejectOnFail && errors.length > 0) throw new ValidationError(errors) 117 | return res 118 | }) 119 | }, 120 | 121 | /** 122 | * Adds new rule to the lib 123 | * @memberof rules 124 | * @param {Object} def The rule definition 125 | * @returns {Object} 126 | */ 127 | build: def => { 128 | return { 129 | def, 130 | validate: rules.makeValidate(def) 131 | } 132 | }, 133 | 134 | /** 135 | * Gets props list according to partial, required, and allowNull specifications 136 | * @memberof rules 137 | * @param {Object} def The rule definition 138 | * @param {*} val The value being evaluated 139 | * @returns {Array} 140 | */ 141 | getProps: (def, val) => { 142 | // Require(d) alias 143 | if (def.require) { 144 | def.required = def.require 145 | delete def.require 146 | console.log('-----\nObey Warning: `require` should be `required`\n-----') 147 | } 148 | 149 | 150 | // If default or creator defined, no need for conditional requires. 151 | if (def.default || def.creator) { 152 | const conditionalRequires = ['requireIf', 'requireIfNot', 'requiredIf', 'requiredIfNot'] 153 | const rules = [] 154 | conditionalRequires.forEach((key) => { 155 | if (def[key]) { 156 | rules.push(key) 157 | delete def[key] 158 | } 159 | }) 160 | if (rules.length) { 161 | const message = [ 162 | '-----\nObey Warning: removing conditional require', 163 | `rule(s) (${rules.join(', ')}) due to 'default' or`, 164 | '\'creator\' being defined\n-----' 165 | ].join(' ') 166 | console.log(message) 167 | } 168 | } 169 | 170 | // Partial and undefined 171 | if (def.opts.partial && val === undefined) return rules.props.noValPartial 172 | // Not required, undefined 173 | if (!def.required && val === undefined) return rules.props.noVal 174 | // AllowNull 175 | if (def.allowNull) { 176 | // val is null, look no further 177 | if (val === null) { 178 | return rules.props.noVal 179 | } 180 | // val is otherwise falsey, but not undefined, AND default is null 181 | if (!val && val !== undefined && def.default === null) { 182 | return rules.props.noVal 183 | } 184 | } 185 | // Use default 186 | return rules.props.default 187 | } 188 | } 189 | 190 | module.exports = rules 191 | -------------------------------------------------------------------------------- /src/typeStrategies/README.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | Below is a list of supported, built-in types for the Obey library 4 | 5 | ## Any 6 | 7 | Supports any data type or format 8 | 9 | ## Array 10 | 11 | Checks for native type `array` 12 | 13 | ## Boolean 14 | 15 | Checks for native type `boolean` 16 | 17 | ## Email 18 | 19 | Checks for valid email, includes valid characters, `@` separator for address and domain, and valid TLD. 20 | 21 | ## IP 22 | 23 | Checks for valid IP Address: 24 | 25 | * `ip`: Default, checks IPv4 format 26 | * `ip:v4`: Checks IPv4 format 27 | * `ip:v6`: Checks IPv6 format 28 | 29 | ## Number 30 | 31 | Checks for native type `number` 32 | 33 | ## Object 34 | 35 | Checks for native type `object` 36 | 37 | ## Phone 38 | 39 | Checks for valid phone numbers: 40 | 41 | * `phone`: Default, valid with or without separators 42 | * `phone:numeric`: Check value for numeric phone number, 7-10 digits 43 | 44 | ## String 45 | 46 | Checks for valid string types: 47 | 48 | * `string`: Default, `typeof` should be `string` 49 | * `string:alphanumeric`: Checks value contains only alpha-numeric characters 50 | 51 | ## URL 52 | 53 | Checks for valid URL 54 | 55 | ## UUID 56 | 57 | Checks for valid v4 UUID 58 | 59 | ## Zip 60 | 61 | Checks for valid zip/postal codes: 62 | 63 | * `zip`: Default, checks generic postal code 64 | * `zip:us`: Checks US zip code format 65 | * `zip:ca`: Checks Canadian zip code format -------------------------------------------------------------------------------- /src/typeStrategies/any.js: -------------------------------------------------------------------------------- 1 | const any = { 2 | default: context => context.value 3 | } 4 | 5 | module.exports = any 6 | -------------------------------------------------------------------------------- /src/typeStrategies/array.js: -------------------------------------------------------------------------------- 1 | const Promise = require('bluebird') 2 | let rules 3 | 4 | const loadRules = () => { 5 | if (!rules) rules = require('../rules') 6 | } 7 | 8 | const array = { 9 | default: context => { 10 | // Ensure array 11 | if (!Array.isArray(context.value)) { 12 | return context.fail('Value must be an array') 13 | } 14 | // If empty (and empty allowed), move forward 15 | if (context.def.empty && context.value.length === 0) { 16 | return context.value 17 | } 18 | // If empty (and not empty allowed), fail 19 | if (!context.def.empty && context.value.length === 0) { 20 | return context.fail('Value must not be empty array') 21 | } 22 | // Specific array sub-validation 23 | if (!context.def.values) return context.value 24 | 25 | loadRules() 26 | const promises = context.value.map((elem, idx) => { 27 | return rules.validate(context.def.values, elem, context.def.opts, `${context.key}[${idx}]`, context.errors, false, context.initData) 28 | }) 29 | return Promise.all(promises) 30 | } 31 | } 32 | 33 | module.exports = array 34 | -------------------------------------------------------------------------------- /src/typeStrategies/boolean.js: -------------------------------------------------------------------------------- 1 | const boolean = { 2 | default: context => { 3 | if (typeof context.value !== 'boolean') { 4 | context.fail('Value must be a boolean') 5 | } 6 | } 7 | } 8 | 9 | module.exports = boolean 10 | -------------------------------------------------------------------------------- /src/typeStrategies/email.js: -------------------------------------------------------------------------------- 1 | const email = { 2 | _regex: { 3 | default: /.+@.+\.\S+/ 4 | }, 5 | default: context => { 6 | if (context.value == null || !context.value || !context.value.toString().match(email._regex.default)) { 7 | context.fail('Value must be a valid email') 8 | } 9 | } 10 | } 11 | 12 | module.exports = email 13 | -------------------------------------------------------------------------------- /src/typeStrategies/index.js: -------------------------------------------------------------------------------- 1 | // Import all strategies 2 | const any = require('./any') 3 | const array = require('./array') 4 | const boolean = require('./boolean') 5 | const email = require('./email') 6 | const ip = require('./ip') 7 | const number = require('./number') 8 | const object = require('./object') 9 | const phone = require('./phone') 10 | const string = require('./string') 11 | const url = require('./url') 12 | const uuid = require('./uuid') 13 | const zip = require('./zip') 14 | 15 | // Export object with all built-in strategies 16 | module.exports = { 17 | any, 18 | array, 19 | boolean, 20 | email, 21 | ip, 22 | number, 23 | object, 24 | phone, 25 | string, 26 | url, 27 | uuid, 28 | zip 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/typeStrategies/ip.js: -------------------------------------------------------------------------------- 1 | const ip = { 2 | _regex: { 3 | v4: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, 4 | v6: /^((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$/ 5 | }, 6 | v4: context => { 7 | if (context.value == null || !context.value.length || !context.value.toString().match(ip._regex.v4)) { 8 | context.fail('Value must be a valid IPv4 address') 9 | } 10 | }, 11 | v6: context => { 12 | if (context.value == null || !context.value.length || !context.value.toString().match(ip._regex.v6)) { 13 | context.fail('Value must be a valid IPv6 address') 14 | } 15 | }, 16 | default: context => ip.v4(context) 17 | } 18 | 19 | module.exports = ip 20 | -------------------------------------------------------------------------------- /src/typeStrategies/number.js: -------------------------------------------------------------------------------- 1 | const number = { 2 | default: context => { 3 | if (typeof context.value !== 'number' || Number.isNaN(context.value)) { 4 | context.fail('Value must be a number') 5 | } 6 | } 7 | } 8 | 9 | module.exports = number 10 | -------------------------------------------------------------------------------- /src/typeStrategies/object.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const Promise = require('bluebird') 3 | let rules 4 | 5 | const loadRules = () => { 6 | if (!rules) rules = require('../rules') 7 | } 8 | 9 | /** 10 | * Validates an object using the definition's `keys` property 11 | * @param {Object} context An Obey type context 12 | * @param {string} keyPrefix A prefix to include before the key in an error message 13 | * @returns {Promise.} Resolves with the final object 14 | */ 15 | const validateByKeys = (context, keyPrefix) => { 16 | // Build validation checks 17 | const missingKeys = [] 18 | const promises = {} 19 | _.forOwn(context.def.keys, (keyDef, key) => { 20 | promises[key] = rules.validate(keyDef, context.value[key], context.def.opts, `${keyPrefix}${key}`, context.errors, false, context.initData) 21 | .then(val => { 22 | if (!context.value.hasOwnProperty(key) && val === undefined) missingKeys.push(key) 23 | return val 24 | }) 25 | }) 26 | // Check undefined keys 27 | const strictMode = !context.def.hasOwnProperty('strict') || context.def.strict 28 | _.forOwn(context.value, (val, key) => { 29 | if (!context.def.keys[key]) { 30 | if (strictMode) { 31 | context.fail(`'${key}' is not an allowed property`) 32 | } else { 33 | promises[key] = val 34 | } 35 | } 36 | }) 37 | return Promise.props(promises).then(obj => { 38 | missingKeys.forEach(key => delete obj[key]) 39 | return obj 40 | }) 41 | } 42 | 43 | /** 44 | * Validates an object using the definition's `values` property 45 | * @param {Object} context An Obey type context 46 | * @param {string} keyPrefix A prefix to include before the key in an error message 47 | * @returns {Promise.} Resolves with the final object 48 | */ 49 | const validateByValues = (context, keyPrefix) => { 50 | const promises = {} 51 | _.forOwn(context.value, (val, key) => { 52 | promises[key] = rules.validate(context.def.values, val, context.def.opts, `${keyPrefix}${key}`, context.errors, false, context.initData) 53 | }) 54 | return Promise.props(promises) 55 | } 56 | 57 | const object = { 58 | default: context => { 59 | if (!_.isObject(context.value) || context.value === null) { 60 | return context.fail('Value must be an object') 61 | } 62 | loadRules() 63 | const prefix = context.key ? `${context.key}.` : '' 64 | if (context.def.keys) return validateByKeys(context, prefix) 65 | if (context.def.values) return validateByValues(context, prefix) 66 | return context.value 67 | } 68 | } 69 | 70 | module.exports = object 71 | -------------------------------------------------------------------------------- /src/typeStrategies/phone.js: -------------------------------------------------------------------------------- 1 | const phone = { 2 | _regex: { 3 | default: /^[\(\)\s\-\+\d]{10,17}$/, 4 | numeric: /\d{7,10}/ 5 | }, 6 | default: context => { 7 | const stringified = context.value != null && context.value.toString() 8 | if (!stringified || !stringified.length || !stringified.match(phone._regex.default)) { 9 | context.fail('Value must be a valid phone number') 10 | } 11 | }, 12 | numeric: context => { 13 | const stringified = context.value != null && context.value.toString() 14 | if (!stringified || !stringified.length || !stringified.match(phone._regex.numeric)) { 15 | context.fail('Value must be a numeric phone number') 16 | } 17 | } 18 | } 19 | 20 | module.exports = phone 21 | -------------------------------------------------------------------------------- /src/typeStrategies/string.js: -------------------------------------------------------------------------------- 1 | const string = { 2 | _regex: { 3 | alphanumeric: /^[a-zA-Z0-9]*$/ 4 | }, 5 | default: context => { 6 | if (typeof context.value !== 'string' || context.value.length === 0) { 7 | context.fail('Value must be a string') 8 | } 9 | }, 10 | alphanumeric: context => { 11 | if (context.value == null || !context.value.length || !context.value.toString().match(string._regex.alphanumeric)) { 12 | context.fail('Value must contain only letters and/or numbers') 13 | } 14 | } 15 | } 16 | 17 | module.exports = string 18 | -------------------------------------------------------------------------------- /src/typeStrategies/url.js: -------------------------------------------------------------------------------- 1 | const url = { 2 | _regex: { 3 | default: /^(?:https?:\/\/)?[^\s\/\.]+(?:\.[a-z0-9-]{2,})+(?:\/\S*)?$/i 4 | }, 5 | default: context => { 6 | if (context.value == null || !context.value.length || !context.value.toString().match(url._regex.default)) { 7 | context.fail('Value must be a valid URL') 8 | } 9 | } 10 | } 11 | 12 | module.exports = url 13 | -------------------------------------------------------------------------------- /src/typeStrategies/uuid.js: -------------------------------------------------------------------------------- 1 | const uuid = { 2 | _regex: { 3 | default: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, 4 | upper: /^[0-9A-F]{8}-[0-9A-F]{4}-[1-5][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/, 5 | lower: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, 6 | }, 7 | default: context => { 8 | if (context.value == null || !context.value.length || !context.value.toString().match(uuid._regex.default)) { 9 | context.fail('Value must be a valid UUID') 10 | } 11 | }, 12 | upper: context => { 13 | if (context.value == null || !context.value.length || !context.value.toString().match(uuid._regex.upper)) { 14 | context.fail('Value must be a valid UUID with all uppercase letters') 15 | } 16 | }, 17 | lower: context => { 18 | if (context.value == null || !context.value.length || !context.value.toString().match(uuid._regex.lower)) { 19 | context.fail('Value must be a valid UUID with all lowercase letters') 20 | } 21 | } 22 | } 23 | 24 | module.exports = uuid 25 | -------------------------------------------------------------------------------- /src/typeStrategies/zip.js: -------------------------------------------------------------------------------- 1 | const zip = { 2 | _regex: { 3 | default: /^([0-9]{5})(?:[-\s]*([0-9]{4}))?$/, 4 | ca: /^([A-Z][0-9][A-Z])\s*([0-9][A-Z][0-9])$/ 5 | }, 6 | default: context => { 7 | const stringified = context.value != null && context.value.toString() 8 | if (!stringified || !stringified.length || !stringified.match(zip._regex.default)) { 9 | context.fail('Value must be a valid US zip code') 10 | } 11 | }, 12 | ca: context => { 13 | const stringified = context.value != null && context.value.toString() 14 | if (!stringified || !stringified.length || !stringified.match(zip._regex.ca)) { 15 | context.fail('Value must be a valid Canadian zip code') 16 | } 17 | } 18 | } 19 | 20 | module.exports = zip 21 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 TechnologyAdvice 3 | */ 4 | const _ = require('lodash') 5 | const strategies = require('./typeStrategies') 6 | 7 | /** 8 | * Types determine and execute the appropriate validation to be performed on the 9 | * data during validation 10 | * @namespace types 11 | */ 12 | const types = { 13 | /** 14 | * @memberof types 15 | * @property {Object} Contains type strategies 16 | */ 17 | strategies, 18 | 19 | /** 20 | * Checks for and applies sub-type to definition 21 | * @memberof types 22 | * @param {Object} def The rule defintion 23 | * @returns {Object} 24 | */ 25 | checkSubType: def => { 26 | const fullType = def.type.split(':') 27 | if (fullType.length === 2) { 28 | def.type = fullType[0] 29 | def.sub = fullType[1] 30 | } else { 31 | def.sub = 'default' 32 | } 33 | return def 34 | }, 35 | 36 | /** 37 | * Sets up the `fail` method and handles `empty` or `undefined` values. If neither 38 | * empty or undefined, calls the appropriate `type` and executes validation 39 | * @memberof types 40 | * @param {Object} def The property configuration 41 | * @param {*} value The value being validated 42 | * @param {string} key The key name of the property 43 | * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array 44 | * to which any additional error objects will be added 45 | * @param {Object} initData Initial data object 46 | * @returns {*|Promise.<*>} The value if empty or undefined, check method if value requires type validation 47 | */ 48 | validate: function(def, value, key, errors, initData) { 49 | const parsedDef = types.checkSubType(def) 50 | const fail = message => { 51 | errors.push({ type: def.type, sub: def.sub, key, value, message }) 52 | } 53 | // Handle `empty` prop for string values 54 | if (def.empty && typeof value === 'string' && def.type !== 'array' && value.length === 0) { 55 | return value 56 | } 57 | // Account for stray empties 58 | const isEmptyOrUndefined = value === undefined || value === '' 59 | // Don't run if undefined on required 60 | if (def.required && isEmptyOrUndefined && !def.opts.partial) { 61 | errors.push({ type: 'required', sub: 'default', key, value, message: `Property '${key}' is required` }) 62 | return value 63 | } 64 | // Execute check 65 | return types.check({ def: parsedDef, key, value, fail, errors, initData }) 66 | }, 67 | 68 | /** 69 | * Add (or override) a type in the library 70 | * @memberof types 71 | * @param {string} name The name of the type 72 | * @param {Object|function} handler The type strategy method 73 | */ 74 | add: (name, handler) => { 75 | types.strategies[name] = _.isFunction(handler) ? { default: handler } : handler 76 | }, 77 | 78 | /** 79 | * Ensures that the strategy exists, loads if not already in memory, then ensures 80 | * subtype and returns the applied type strategy 81 | * @memberof types 82 | * @param {{def: Object, key: string, value: *, fail: function, errors: Array<{Object}>}} context A type context 83 | * @returns {Promise.<*>} Resolves with the provided data, possibly modified by the type strategy 84 | */ 85 | check: context => { 86 | if (!types.strategies[context.def.type]) { 87 | if (context.def.type.match(/[\/\\]/)) { 88 | throw new Error(`Illegal type name: ${context.def.type}`) 89 | } 90 | } 91 | // Ensure type 92 | if (!types.strategies[context.def.type]) { 93 | throw new Error(`Type '${context.def.type}' does not exist`) 94 | } 95 | // Ensure subtype 96 | if (!types.strategies[context.def.type][context.def.sub]) { 97 | throw new Error(`Type '${context.def.type}:${context.def.sub}' does not exist`) 98 | } 99 | return Promise.resolve(types.strategies[context.def.type][context.def.sub](context)) 100 | .then(res => res === undefined ? context.value : res) 101 | } 102 | } 103 | 104 | module.exports = types 105 | -------------------------------------------------------------------------------- /test/fixtures/core.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | basic: { 3 | name: { type: 'string', required: true } 4 | }, 5 | missingType: { 6 | name: { required: true } 7 | }, 8 | basicRequired: { 9 | fname: { type: 'string' }, 10 | lname: { type: 'string', require: true } 11 | }, 12 | basicExtended: { 13 | fname: { type: 'string', required: true, min: 2, max: 20 }, 14 | lname: { type: 'string', min: 2, max: 20 }, 15 | type: { type: 'string', requiredIf: 'nested.foo', allowed: [ 'foo', 'bar' ] }, 16 | nested: { type: 'object', values: { type: 'string' } } 17 | }, 18 | basicCreator: { 19 | foo: { type: 'string', creator: 'testCreator' }, 20 | bar: { type: 'string' } 21 | }, 22 | basicNested: { 23 | name: { type: 'string' }, 24 | someobj: { type: 'object', keys: { 25 | foo: { type: 'string' } 26 | }} 27 | }, 28 | basicEmpty: { 29 | name: { type: 'string', empty: true } 30 | }, 31 | basicNoEmpty: { 32 | name: { type: 'string' } 33 | }, 34 | basicEmptyArray: { 35 | names: { type: 'array', empty: true } 36 | }, 37 | basicNoEmptyArray: { 38 | names: { type: 'array' } 39 | }, 40 | conditionalWithDefault: { 41 | fname: { type: 'string' }, 42 | lname: { type: 'string', requiredIfNot: { fname: 'Foo' }, default: 'Bar' } 43 | }, 44 | conditionalWithCreator: { 45 | fname: { type: 'string', requiredIf: { lname: 'Bar' }, creator: 'foo-namer' }, 46 | lname: { type: 'string' } 47 | }, 48 | requiredPredefined: { 49 | zip: { type: 'zip', required: true } 50 | }, 51 | notRequiredPredefined: { 52 | phone: { type: 'phone' } 53 | }, 54 | allowEmptyString: { 55 | foo: { type: 'string', allow: [ 'bar' ], empty: true } 56 | }, 57 | allowEmptyStringObject: { 58 | foo: { type: 'string', allow: { foo: 'FOO' }, empty: true } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/fixtures/creators.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | synchronous: { 3 | name: { type: 'string', creator: 'syncCreator' } 4 | }, 5 | asynchronous: { 6 | name: { type: 'string', creator: 'asyncCreator' } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/modifiers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | synchronous: { 3 | name: { type: 'string', modifier: 'syncModifier' } 4 | }, 5 | asynchronous: { 6 | name: { type: 'string', modifier: 'asyncModifier' } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/validators.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: { 3 | name: { type: 'string', default: 'foo' } 4 | }, 5 | allow: { 6 | name: { type: 'string', allow: [ 'foo', 'bar' ] } 7 | }, 8 | allowNull: { 9 | name: { type: 'string', allowNull: true }, 10 | email: { type: 'email', allowNull: true }, 11 | phone: { type: 'phone' } 12 | }, 13 | allowNullDefault: { 14 | name: { type: 'string', allowNull: true, default: null }, 15 | email: { type: 'email', allowNull: true }, 16 | phone: { type: 'phone' } 17 | }, 18 | min: { 19 | name: { type: 'string', min: 10 } 20 | }, 21 | max: { 22 | name: { type: 'string', max: 5 } 23 | }, 24 | required: { 25 | name: { type: 'string', required: true } 26 | }, 27 | requiredIf: { 28 | phone: { type: 'phone' }, 29 | phoneType: { type: 'string', requiredIf: 'phone' }, 30 | address: { type: 'object', keys: { 31 | street: { type: 'string' }, 32 | city: { type: 'string', requiredIf: 'address.street' } 33 | }} 34 | }, 35 | requiredIfNot: { 36 | address: { type: 'object', keys: { 37 | street: { type: 'string' }, 38 | state: { type: 'string' }, 39 | country: { type: 'string', requiredIfNot: 'address.state' } 40 | }} 41 | }, 42 | jexl: { 43 | exprVal: { type: 'string', jexl: [{ 44 | expr: "value == root.testVal.nestedObjArray[.name == 'theOne'].payload.treasure" 45 | }] }, 46 | testVal: { 47 | type: 'object', 48 | keys: { 49 | nestedObjArray: { 50 | type: 'array', 51 | values: { 52 | type: 'object', 53 | keys: { 54 | name: { type: 'string' }, 55 | payload: { 56 | type: 'object', 57 | keys: { 58 | treasure: { type: 'string' } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | }, 67 | jexlMessage: { 68 | exprVal: { type: 'string', jexl: [{ 69 | expr: "value == root.testVal.nestedObjArray[.name == 'theOne'].payload.treasure", 70 | message: "Do not seek the treasure" 71 | }] }, 72 | testVal: { 73 | type: 'object', 74 | keys: { 75 | nestedObjArray: { 76 | type: 'array', 77 | values: { 78 | type: 'object', 79 | keys: { 80 | name: { type: 'string' }, 81 | payload: { 82 | type: 'object', 83 | keys: { 84 | treasure: { type: 'string' } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | }, 93 | jexlTransform: { 94 | exprVal: { type: 'string', jexl: [{ 95 | expr: "value|upper == root.testVal.nestedObjArray[.name == 'theOne'].payload.treasure|upper" 96 | }] }, 97 | testVal: { 98 | type: 'object', 99 | keys: { 100 | nestedObjArray: { 101 | type: 'array', 102 | values: { 103 | type: 'object', 104 | keys: { 105 | name: { type: 'string' }, 106 | payload: { 107 | type: 'object', 108 | keys: { 109 | treasure: { type: 'string' } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /test/integration/core.spec.js: -------------------------------------------------------------------------------- 1 | const obey = require('src/index') 2 | const modelFixtures = require('test/fixtures/core') 3 | const ValidationError = require('src/lib/error') 4 | 5 | describe('integration:core', () => { 6 | let stub 7 | afterEach(() => { 8 | if (stub) stub.mockReset() 9 | }) 10 | it('builds a model and successfully validates passing object', () => { 11 | const testModel = obey.model(modelFixtures.basicExtended) 12 | const testData = { 13 | fname: 'John', 14 | lname: 'Smith', 15 | type: 'foo' 16 | } 17 | return testModel.validate(testData).then(res => { 18 | expect(res).toEqual(testData) 19 | }) 20 | }) 21 | it('builds a model and fails validation on type', () => { 22 | const testModel = obey.model(modelFixtures.basicExtended) 23 | const testData = { 24 | fname: 5, 25 | lname: 'Smith', 26 | type: 'foo', 27 | nested: { 28 | foo: 'bar' 29 | } 30 | } 31 | return testModel.validate(testData).catch(e => { 32 | expect(e).toBeInstanceOf(ValidationError) 33 | }) 34 | }) 35 | it('builds a model and passes when non-required field is undefined', () => { 36 | const testModel = obey.model(modelFixtures.basicExtended) 37 | const testData = { 38 | fname: 'John' 39 | } 40 | return testModel.validate(testData) 41 | .then((res) => { 42 | expect(res.fname).toEqual('John') 43 | expect(res.lname).toBeUndefined() 44 | expect(res.type).toBeUndefined() 45 | }) 46 | }) 47 | it('builds a models and passes with response including supplied undefined value', () => { 48 | const testModel = obey.model(modelFixtures.basicExtended) 49 | const testData = { 50 | fname: 'John', 51 | lname: undefined 52 | } 53 | return testModel.validate(testData) 54 | .then((res) => { 55 | expect(res.fname).toEqual('John') 56 | expect(res).toHaveProperty('lname') 57 | expect(res).not.toHaveProperty('type') 58 | }) 59 | }) 60 | it('builds a model and fails when required field is undefined', () => { 61 | stub = jest.spyOn(console, 'log') 62 | const testModel = obey.model(modelFixtures.basicRequired) 63 | const testData = { 64 | fname: 'John' 65 | } 66 | return testModel.validate(testData) 67 | .then(() => { throw new Error('Should fail') }) 68 | .catch((err) => { 69 | expect(err.message).toEqual('lname (undefined): Property \'lname\' is required') 70 | expect(stub).toHaveBeenCalledWith('-----\nObey Warning: `require` should be `required`\n-----') 71 | }) 72 | }) 73 | it('builds a model and successfully validates when nested object present', () => { 74 | const testModel = obey.model(modelFixtures.basicNested) 75 | const testData = { 76 | name: 'fizz', 77 | someobj: { 78 | foo: 'buzz' 79 | } 80 | } 81 | return testModel.validate(testData).then(res => { 82 | expect(res).toEqual(testData) 83 | }) 84 | }) 85 | it('builds a model and fails validates when nested object present', () => { 86 | const testModel = obey.model(modelFixtures.basicNested) 87 | const testData = { 88 | name: true, 89 | someobj: { 90 | foo: 5 91 | } 92 | } 93 | return testModel.validate(testData) 94 | .then(() => { throw new Error('Should fail') }) 95 | .catch(err => { 96 | expect(err.collection).toEqual([ 97 | { type: 'string', sub: 'default', key: 'name', value: true, message: 'Value must be a string' }, 98 | { type: 'string', sub: 'default', key: 'someobj.foo', value: 5, message: 'Value must be a string' } 99 | ]) 100 | }) 101 | }) 102 | it('builds a model and passes with empty string (allowed with flag)', () => { 103 | const testModel = obey.model(modelFixtures.basicEmpty) 104 | const testData = { 105 | name: '' 106 | } 107 | return testModel.validate(testData) 108 | .then(data => { 109 | expect(data.name).toEqual('') 110 | }) 111 | }) 112 | it('builds a model and fails with empty string (not allowed with flag)', () => { 113 | const testModel = obey.model(modelFixtures.basicNoEmpty) 114 | const testData = { 115 | name: '' 116 | } 117 | return testModel.validate(testData) 118 | .then(() => { 119 | throw new Error('Should have thrown') 120 | }) 121 | .catch(err => { 122 | expect(err.message).toEqual('name (): Value must be a string') 123 | }) 124 | }) 125 | it('builds a model and passes with empty array (allowed with flag)', () => { 126 | const testModel = obey.model(modelFixtures.basicEmptyArray) 127 | const testData = { 128 | names: [] 129 | } 130 | return testModel.validate(testData) 131 | .then(data => { 132 | expect(data.names).toEqual([]) 133 | }) 134 | }) 135 | it('builds a model and fails with empty array (not allowed with flag)', () => { 136 | const testModel = obey.model(modelFixtures.basicNoEmptyArray) 137 | const testData = { 138 | names: [] 139 | } 140 | return testModel.validate(testData) 141 | .then(() => { 142 | throw new Error('Should have thrown') 143 | }) 144 | .catch(err => { 145 | expect(err.message).toEqual('names (): Value must not be empty array') 146 | }) 147 | }) 148 | it('builds a model and passes validation when partial option is set to true', () => { 149 | const testModel = obey.model(modelFixtures.basicExtended) 150 | const testData = { 151 | lname: 'Smith' 152 | } 153 | return testModel.validate(testData, { partial: true }) 154 | .then(data => { 155 | expect(data).toEqual(testData) 156 | }) 157 | }) 158 | it('builds a model and passes validation when partial option is set to true, does not run creators', () => { 159 | obey.creator('testCreator', () => 'fizz') 160 | const testModel = obey.model(modelFixtures.basicCreator) 161 | const testData = { 162 | bar: 'buzz' 163 | } 164 | return testModel.validate(testData, { partial: true }) 165 | .then(data => { 166 | expect(data).toEqual(testData) 167 | }) 168 | }) 169 | it('builds a model and disregards conditional require because of default rule', () => { 170 | stub = jest.spyOn(console, 'log') 171 | const testModel = obey.model(modelFixtures.conditionalWithDefault) 172 | const testData = { 173 | fname: 'Test' 174 | } 175 | return testModel.validate(testData) 176 | .then(data => { 177 | expect(data).toEqual({ 178 | fname: 'Test', 179 | lname: 'Bar' 180 | }) 181 | expect(stub) 182 | .toHaveBeenCalledWith( 183 | "-----\nObey Warning: removing conditional require rule(s) (requiredIfNot) due to 'default' or 'creator' being defined\n-----" 184 | ) 185 | }) 186 | }) 187 | it('builds a model and disregards conditional require because of creator', () => { 188 | stub = jest.spyOn(console, 'log') 189 | obey.creator('foo-namer', () => 'FOO') 190 | const testModel = obey.model(modelFixtures.conditionalWithCreator) 191 | const testData = { 192 | lname: 'Bar' 193 | } 194 | return testModel.validate(testData) 195 | .then(data => { 196 | expect(data).toEqual({ 197 | fname: 'FOO', 198 | lname: 'Bar' 199 | }) 200 | expect(stub) 201 | .toHaveBeenCalledWith( 202 | "-----\nObey Warning: removing conditional require rule(s) (requiredIf) due to 'default' or 'creator' being defined\n-----" 203 | ) 204 | }) 205 | }) 206 | it('does not allow empty predefined type value without `empty` rule when required', () => { 207 | const testModel = obey.model(modelFixtures.requiredPredefined) 208 | const testData = { zip: '' } 209 | return testModel.validate(testData) 210 | .catch(err => { 211 | expect(err.message).toEqual('zip (): Property \'zip\' is required') 212 | }) 213 | }) 214 | it('does not allow empty predefined type value without `empty` rule when not required', () => { 215 | const testModel = obey.model(modelFixtures.notRequiredPredefined) 216 | const testData = { phone: '' } 217 | return testModel.validate(testData) 218 | .catch(err => { 219 | expect(err.message).toEqual('phone (): Value must be a valid phone number') 220 | }) 221 | }) 222 | it('builds a model correctly with `allow` (object) and `empty` rules', () => { 223 | const testModel = obey.model(modelFixtures.allowEmptyStringObject) 224 | const testData = { foo: '' } 225 | return testModel.validate(testData) 226 | .then(res => { 227 | expect(res).toEqual(testData) 228 | }) 229 | }) 230 | it('builds a model correctly with `allow` and `empty` rules', () => { 231 | const testModel = obey.model(modelFixtures.allowEmptyString) 232 | const testData = { foo: '' } 233 | return testModel.validate(testData) 234 | .then(res => { 235 | expect(res).toEqual(testData) 236 | }) 237 | }) 238 | }) 239 | -------------------------------------------------------------------------------- /test/integration/creators.spec.js: -------------------------------------------------------------------------------- 1 | const obey = require('src/index') 2 | const creators = require('src/creators') 3 | const modelFixtures = require('test/fixtures/creators') 4 | 5 | describe('integration:creators', () => { 6 | afterEach(() => { 7 | creators.lib = {} 8 | }) 9 | describe('synchronous', () => { 10 | it('creates a value synchronously when property has no value', () => { 11 | const testModel = obey.model(modelFixtures.synchronous) 12 | obey.creator('syncCreator', () => 'foo') 13 | return testModel.validate({}).then(res => { 14 | expect(res.name).toEqual('foo') 15 | }) 16 | }) 17 | }) 18 | describe('asynchronous', () => { 19 | it('creates a value asynchronously when property has no value', () => { 20 | const testModel = obey.model(modelFixtures.asynchronous) 21 | obey.creator('asyncCreator', () => { 22 | return new Promise((resolve) => { 23 | setTimeout(() => resolve('foo'), 300) 24 | }) 25 | }) 26 | return testModel.validate({}).then(res => { 27 | expect(res.name).toEqual('foo') 28 | }) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/integration/modifiers.spec.js: -------------------------------------------------------------------------------- 1 | const obey = require('src/index') 2 | const modifiers = require('src/modifiers') 3 | const modelFixtures = require('test/fixtures/modifiers') 4 | 5 | describe('integration:modifiers', () => { 6 | afterEach(() => { 7 | modifiers.lib = {} 8 | }) 9 | describe('synchronous', () => { 10 | it('modifies a value synchronously when property has no value', () => { 11 | const testModel = obey.model(modelFixtures.synchronous) 12 | obey.modifier('syncModifier', (val) => `${val}_CHANGED`) 13 | return testModel.validate({ name: 'foo' }).then(res => { 14 | expect(res.name).toEqual('foo_CHANGED') 15 | }) 16 | }) 17 | }) 18 | describe('asynchronous', () => { 19 | it('modifies a value asynchronously when property has no value', () => { 20 | const testModel = obey.model(modelFixtures.asynchronous) 21 | obey.modifier('asyncModifier', (val) => { 22 | return new Promise((resolve) => { 23 | setTimeout(() => resolve(`${val}_CHANGED`), 300) 24 | }) 25 | }) 26 | return testModel.validate({ name: 'foo' }).then(res => { 27 | expect(res.name).toEqual('foo_CHANGED') 28 | }) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/integration/validators.spec.js: -------------------------------------------------------------------------------- 1 | const obey = require('src/index') 2 | const modelFixtures = require('test/fixtures/validators') 3 | 4 | describe('integration:validators', () => { 5 | describe('default', () => { 6 | it('builds a model and returns object with a default value set', () => { 7 | const testModel = obey.model(modelFixtures.default) 8 | const testData = {} 9 | return testModel.validate(testData).then(res => { 10 | expect(res.name).toEqual('foo') 11 | }) 12 | }) 13 | }) 14 | describe('allow', () => { 15 | it('builds a model and fails validation due to value not being allowed', () => { 16 | const testModel = obey.model(modelFixtures.allow) 17 | const testData = { name: 'quz' } 18 | return testModel.validate(testData).catch(err => { 19 | expect(err.collection).toEqual([{ 20 | type: 'allow', 21 | sub: [ 'foo', 'bar' ], 22 | key: 'name', 23 | value: 'quz', 24 | message: 'Value \'quz\' is not allowed' 25 | }]) 26 | }) 27 | }) 28 | it('builds a model with allowed null value in string field', () => { 29 | const testModel = obey.model(modelFixtures.allowNull) 30 | const testData = { name: null, email: 'notNull@test.com', phone: '555-555-5555' } 31 | return testModel.validate(testData).then(res => { 32 | expect(res.name).toBeNull() 33 | expect(res.email).toEqual(testData.email) 34 | expect(res.phone).toEqual(testData.phone) 35 | }) 36 | }) 37 | it('builds a model with allowed null value in empty (falsey) field', () => { 38 | const testModel = obey.model(modelFixtures.allowNullDefault) 39 | const testData = { name: '', email: 'notNull@test.com', phone: '555-555-5555' } 40 | return testModel.validate(testData).then(res => { 41 | expect(res.name).toBeNull() 42 | expect(res.email).toEqual(testData.email) 43 | expect(res.phone).toEqual(testData.phone) 44 | }) 45 | }) 46 | it('builds a model and fails validation due to value of wrong type (allowNull)', () => { 47 | const testModel = obey.model(modelFixtures.allowNull) 48 | const testData = { name: 30, email: null, phone: null } 49 | return testModel.validate(testData).catch(err => { 50 | expect(err.collection).toEqual([{ 51 | type: 'string', 52 | sub: 'default', 53 | key: 'name', 54 | value: 30, 55 | message: 'Value must be a string' 56 | }, 57 | { 58 | type: 'phone', 59 | sub: 'default', 60 | key: 'phone', 61 | value: null, 62 | message: 'Value must be a valid phone number' 63 | }]) 64 | }) 65 | }) 66 | }) 67 | describe('min', () => { 68 | it('builds a model and fails validation because value is less than min', () => { 69 | const testModel = obey.model(modelFixtures.min) 70 | const testData = { name: 'foo' } 71 | return testModel.validate(testData).catch(err => { 72 | expect(err.collection).toEqual([{ 73 | type: 'min', 74 | sub: 10, 75 | key: 'name', 76 | value: 'foo', 77 | message: 'Length must be greater than or equal to 10' 78 | }]) 79 | }) 80 | }) 81 | }) 82 | describe('max', () => { 83 | it('builds a model and fails validation because value is greater than max', () => { 84 | const testModel = obey.model(modelFixtures.max) 85 | const testData = { name: 'foobarrrrr' } 86 | return testModel.validate(testData).catch(err => { 87 | expect(err.collection).toEqual([{ 88 | type: 'max', 89 | sub: 5, 90 | key: 'name', 91 | value: 'foobarrrrr', 92 | message: 'Length must be less than or equal to 5' 93 | }]) 94 | }) 95 | }) 96 | }) 97 | describe('requiredIf', () => { 98 | it('builds a model and fails validation because conditionally required value is undefined', () => { 99 | const testModel = obey.model(modelFixtures.requiredIf) 100 | const testData = { phone: 5551234567, address: { street: '123 test ave' } } 101 | return testModel.validate(testData).catch(err => { 102 | expect(err.collection).toEqual([{ 103 | type: 'requiredIf', 104 | sub: 'phone', 105 | key: 'phoneType', 106 | value: undefined, 107 | message: 'Value required because \'phone\' exists' 108 | }, 109 | { 110 | type: 'requiredIf', 111 | sub: 'address.street', 112 | key: 'address.city', 113 | value: undefined, 114 | message: 'Value required because \'address.street\' exists' 115 | }]) 116 | }) 117 | }) 118 | }) 119 | describe('requiredIfNot', () => { 120 | it('builds a model and fails validation because conditionally required value is undefined', () => { 121 | const testModel = obey.model(modelFixtures.requiredIfNot) 122 | const testData = { address: { street: '123 test ave' } } 123 | return testModel.validate(testData).catch(err => { 124 | expect(err.collection).toEqual([{ 125 | type: 'requiredIfNot', 126 | sub: 'address.state', 127 | key: 'address.country', 128 | value: undefined, 129 | message: 'Value required because \'address.state\' is undefined' 130 | }]) 131 | }) 132 | }) 133 | }) 134 | describe('jexl', () => { 135 | it('builds a models and validates based on jexl expression', () => { 136 | const testModel = obey.model(modelFixtures.jexl) 137 | const testData = { 138 | exprVal: 'Dapper Dan', 139 | testVal: { 140 | nestedObjArray: [ 141 | { name: 'wrong' }, 142 | { 143 | name: 'theOne', 144 | payload: { treasure: 'Dapper Dan' } 145 | } 146 | ] 147 | } 148 | } 149 | return testModel.validate(testData).then(res => { 150 | expect(res).toEqual(testData) 151 | }) 152 | }) 153 | it('builds a models and fails validation based on jexl expression', () => { 154 | const testModel = obey.model(modelFixtures.jexl) 155 | const testData = { 156 | exprVal: 'Dapper Dan', 157 | testVal: { 158 | nestedObjArray: [ 159 | { name: 'wrong' }, 160 | { 161 | name: 'theOne', 162 | payload: { treasure: 'Fop' } 163 | } 164 | ] 165 | } 166 | } 167 | return testModel.validate(testData).catch(err => { 168 | expect(err.collection).toEqual([{ 169 | type: 'jexl', 170 | sub: { 171 | expr: "value == root.testVal.nestedObjArray[.name == 'theOne'].payload.treasure" 172 | }, 173 | key: 'exprVal', 174 | value: 'Dapper Dan', 175 | message: 'Value failed Jexl evaluation' 176 | }]) 177 | }) 178 | }) 179 | it('builds a models and fails with custom message', () => { 180 | const testModel = obey.model(modelFixtures.jexlMessage) 181 | const testData = { 182 | exprVal: 'Dapper Dan', 183 | testVal: { 184 | nestedObjArray: [ 185 | { name: 'wrong' }, 186 | { 187 | name: 'theOne', 188 | payload: { treasure: 'Fop' } 189 | } 190 | ] 191 | } 192 | } 193 | return testModel.validate(testData).catch(err => { 194 | expect(err.collection).toEqual([{ 195 | type: 'jexl', 196 | sub: { 197 | expr: "value == root.testVal.nestedObjArray[.name == 'theOne'].payload.treasure", 198 | message: 'Do not seek the treasure' 199 | }, 200 | key: 'exprVal', 201 | value: 'Dapper Dan', 202 | message: 'Do not seek the treasure' 203 | }]) 204 | }) 205 | }) 206 | it('builds a models and validates using jexl plugin instance', () => { 207 | const jexl = require('jexl') 208 | jexl.addTransform('upper', (val) => val.toUpperCase()) 209 | obey.use('jexl', jexl) 210 | const testModel = obey.model(modelFixtures.jexlTransform) 211 | const testData = { 212 | exprVal: 'Dapper Dan', 213 | testVal: { 214 | nestedObjArray: [ 215 | { name: 'wrong' }, 216 | { 217 | name: 'theOne', 218 | payload: { treasure: 'Dapper Dan' } 219 | } 220 | ] 221 | } 222 | } 223 | return testModel.validate(testData).then(res => { 224 | expect(res).toEqual(testData) 225 | }) 226 | }) 227 | }) 228 | }) 229 | -------------------------------------------------------------------------------- /test/src/creators.spec.js: -------------------------------------------------------------------------------- 1 | const creators = require('src/creators') 2 | 3 | describe('creators', () => { 4 | afterEach(() => { 5 | creators.lib = {} 6 | }) 7 | describe('execute', () => { 8 | it('returns the original value if defined', () => { 9 | const actual = creators.execute({}, 'bar') 10 | expect(actual).toEqual('bar') 11 | }) 12 | it('runs creator and returns created value if exists', () => { 13 | creators.add('test', () => 'foo') 14 | const actual = creators.execute({ creator: 'test' }) 15 | expect(actual).toEqual('foo') 16 | }) 17 | it('throws an error if the creator does not exist', () => { 18 | expect(creators.execute.bind(null, { creator: 'nope'})).toThrow('creator \'nope\' does not exist') 19 | }) 20 | }) 21 | describe('add', () => { 22 | it('adds a new creator to the lib', () => { 23 | creators.add('test', () => 'foo') 24 | expect(creators.lib).toHaveProperty('test', expect.any(Function)) 25 | }) 26 | it('throws an error if the creator name is not a string', () => { 27 | expect(creators.add.bind(null, true, () => 'foo')).toThrow('creator name should be a string') 28 | }) 29 | it('throws an error if the creator method is not a function', () => { 30 | expect(creators.add.bind(null, 'foo', undefined)).toThrow('creator method should be a function') 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/src/index.spec.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const obey = require('src/index') 3 | const rules = require('src/rules') 4 | const types = require('src/types') 5 | const modifiers = require('src/modifiers') 6 | const creators = require('src/creators') 7 | 8 | describe('obey', () => { 9 | describe('rule', () => { 10 | beforeEach(() => jest.spyOn(rules, 'build')) 11 | afterEach(() => { rules.build.mockReset() }) 12 | it('creates a composed rule based on def configuration', () => { 13 | obey.rule({}) 14 | expect(rules.build).toHaveBeenCalledWith({}) 15 | }) 16 | }) 17 | describe('model', () => { 18 | it('creates a composed model based on def configuration', () => { 19 | obey.model({}) 20 | expect(rules.build).toHaveBeenCalledWith({ type: 'object', keys: {}, strict: true }) 21 | }) 22 | it('creates a composed model based on def config with strict set to false', () => { 23 | obey.model({}, false) 24 | expect(rules.build).toHaveBeenCalledWith({ type: 'object', keys: {}, strict: false }) 25 | }) 26 | }) 27 | describe('type', () => { 28 | beforeEach(() => jest.spyOn(types, 'add')) 29 | afterEach(() => { types.add.mockReset() }) 30 | it('adds or overrides a type definition in the obey library', () => { 31 | obey.type('tester', /^([a-z])*$/) 32 | expect(types.add).toHaveBeenCalled() 33 | }) 34 | }) 35 | describe('modifier', () => { 36 | beforeEach(() => jest.spyOn(modifiers, 'add')) 37 | afterEach(() => { modifiers.add.mockReset() }) 38 | it('adds a new modifier to the obey library', () => { 39 | obey.modifier('name', () => _.noop()) 40 | expect(modifiers.add).toHaveBeenCalled() 41 | }) 42 | }) 43 | describe('creator', () => { 44 | beforeEach(() => jest.spyOn(creators, 'add')) 45 | afterEach(() => { creators.add.mockReset() }) 46 | it('adds a new creator to the obey library', () => { 47 | obey.creator('name', () => _.noop()) 48 | expect(creators.add).toHaveBeenCalled() 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /test/src/lib/error.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, expect */ 2 | const ValidationError = require('src/lib/error') 3 | 4 | describe('ValidationError', () => { 5 | it('inherits from Error', () => { 6 | expect(new ValidationError([])).toBeInstanceOf(Error) 7 | }) 8 | it('creates a new instance of ValidationError', () => { 9 | expect(new ValidationError([])).toBeInstanceOf(ValidationError) 10 | }) 11 | it('contains stack property', () => { 12 | expect(new ValidationError([])).toHaveProperty('stack') 13 | }) 14 | it('creates an error without a key if key is not present', () => { 15 | const origError = [ 16 | { value: 'foo', message: 'Not cool, bro' } 17 | ] 18 | const err = new ValidationError(origError) 19 | expect(err.message).toEqual('foo: Not cool, bro') 20 | }) 21 | it('contains message and object (raw) properties', () => { 22 | const origError = [ 23 | { key: 'foo', value: 'bar', message: 'Not ok' }, 24 | { key: 'fizz', value: 'buzz', message: 'Nope' } 25 | ] 26 | const err = new ValidationError(origError) 27 | expect(err.message).toEqual('foo (bar): Not ok\nfizz (buzz): Nope') 28 | expect(err.collection).toEqual(origError) 29 | }) 30 | it('does not include ValidationError on stack', () => { 31 | const sampleError = [ 32 | { value: 'bar', message: 'Not ok'} 33 | ] 34 | const err = new ValidationError(sampleError) 35 | expect(err.stack).not.toContain('error.js') 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/src/lib/validators.spec.js: -------------------------------------------------------------------------------- 1 | const validators = require('src/lib/validators') 2 | 3 | describe('validators', () => { 4 | let mockErrors 5 | beforeEach(() => { 6 | mockErrors = [] 7 | }) 8 | describe('default', () => { 9 | it('sets a default value from the def if no value is set', () => { 10 | const def = { 11 | default: 'foo' 12 | } 13 | const actual = validators.default(def, undefined) 14 | expect(actual).toEqual('foo') 15 | }) 16 | it('uses the value if it is already set', () => { 17 | const def = { 18 | default: 'foo' 19 | } 20 | const actual = validators.default(def, 'bar') 21 | expect(actual).toEqual('bar') 22 | }) 23 | it('uses falsey value for boolean type if value is already set', () => { 24 | const def = { 25 | type: 'boolean', 26 | default: true 27 | } 28 | const actual = validators.default(def, false) 29 | expect(actual).toEqual(false) 30 | }) 31 | it('uses falsey value for number type if value is already set', () => { 32 | const def = { 33 | type: 'number', 34 | default: 3 35 | } 36 | const actual = validators.default(def, 0) 37 | expect(actual).toEqual(0) 38 | }) 39 | }) 40 | describe('allow', () => { 41 | it('passes if value is in allow (object)', () => { 42 | const def = { 43 | allow: { 'foo': 'fooey', 'bar': 'barey' } 44 | } 45 | validators.allow(def, 'foo', 'test', mockErrors) 46 | expect(mockErrors.length).toEqual(0) 47 | }) 48 | it('passes if value is in allow (array)', () => { 49 | const def = { 50 | allow: [ 'foo', 'bar' ] 51 | } 52 | validators.allow(def, 'foo', 'test', mockErrors) 53 | expect(mockErrors.length).toEqual(0) 54 | }) 55 | it('passes if value is in allow (single)', () => { 56 | const def = { 57 | allow: 'foo' 58 | } 59 | validators.allow(def, 'foo', 'test', mockErrors) 60 | expect(mockErrors.length).toEqual(0) 61 | }) 62 | it('creates an error object if value is not in allow (array)', () => { 63 | const def = { 64 | allow: [ 'foo', 'bar' ] 65 | } 66 | validators.allow(def, 'fizz', 'test', mockErrors) 67 | expect(mockErrors[0]).toEqual({ 68 | type: 'allow', 69 | sub: [ 'foo', 'bar' ], 70 | key: 'test', 71 | value: 'fizz', 72 | message: 'Value \'fizz\' is not allowed' 73 | }) 74 | }) 75 | it('creates an error object if value is not in allow (single)', () => { 76 | const def = { 77 | allow: 'foo' 78 | } 79 | validators.allow(def, 'bar', 'test', mockErrors) 80 | expect(mockErrors[0]).toEqual({ 81 | type: 'allow', 82 | sub: 'foo', 83 | key: 'test', 84 | value: 'bar', 85 | message: 'Value \'bar\' is not allowed' 86 | }) 87 | }) 88 | }) 89 | describe('min', () => { 90 | it('passes if array length is greater than or equal to def min', () => { 91 | const def = { min: 3 } 92 | validators.min(def, [ 'foo', 'bar', 'foobar' ], 'test', mockErrors) 93 | expect(mockErrors.length).toEqual(0) 94 | }) 95 | it('creates an error object if array length is less than def min', () => { 96 | const def = { min: 3 } 97 | validators.min(def, [ 'foo' ], 'test', mockErrors) 98 | expect(mockErrors[0]).toEqual({ 99 | type: 'min', 100 | sub: 3, 101 | key: 'test', 102 | value: [ 'foo' ], 103 | message: 'Length must be greater than or equal to 3' 104 | }) 105 | }) 106 | it('creates an error object if string length is less than def min', () => { 107 | const def = { min: 5 } 108 | validators.min(def, 'foo', 'test', mockErrors) 109 | expect(mockErrors[0]).toEqual({ 110 | type: 'min', 111 | sub: 5, 112 | key: 'test', 113 | value: 'foo', 114 | message: 'Length must be greater than or equal to 5' 115 | }) 116 | }) 117 | it('creates an error object if number is less than def min', () => { 118 | const def = { min: 10 } 119 | validators.min(def, 5, 'test', mockErrors) 120 | expect(mockErrors[0]).toEqual({ 121 | type: 'min', 122 | sub: 10, 123 | key: 'test', 124 | value: 5, 125 | message: 'Value must be greater than or equal to 10' 126 | }) 127 | }) 128 | }) 129 | describe('max', () => { 130 | it('passes if array length is less than or equal to def max', () => { 131 | const def = { max: 3 } 132 | validators.max(def, [ 'foo', 'bar'], 'test', mockErrors) 133 | expect(mockErrors.length).toEqual(0) 134 | }) 135 | it('creates an error object if array length is greater than def max', () => { 136 | const def = { max: 1 } 137 | validators.max(def, [ 'foo', 'bar' ], 'test', mockErrors) 138 | expect(mockErrors[0]).toEqual({ 139 | type: 'max', 140 | sub: 1, 141 | key: 'test', 142 | value: [ 'foo', 'bar' ], 143 | message: 'Length must be less than or equal to 1' 144 | }) 145 | }) 146 | it('creates an error object if string length is greater than def max', () => { 147 | const def = { max: 2 } 148 | validators.max(def, 'foo', 'test', mockErrors) 149 | expect(mockErrors[0]).toEqual({ 150 | type: 'max', 151 | sub: 2, 152 | key: 'test', 153 | value: 'foo', 154 | message: 'Length must be less than or equal to 2' 155 | }) 156 | }) 157 | it('creates an error object if number is greater than def max', () => { 158 | const def = { max: 5 } 159 | validators.max(def, 10, 'test', mockErrors) 160 | expect(mockErrors[0]).toEqual({ 161 | type: 'max', 162 | sub: 5, 163 | key: 'test', 164 | value: 10, 165 | message: 'Value must be less than or equal to 5' 166 | }) 167 | }) 168 | }) 169 | describe('requiredIf', () => { 170 | it('creates an error object if conditionally required value is undefined', () => { 171 | const data = { address: { street: '123 test ave' } } 172 | const def = { requiredIf: 'address.street' } 173 | validators.requiredIf(def, undefined, 'address.city', mockErrors, data) 174 | expect(mockErrors[0]).toEqual({ 175 | type: 'requiredIf', 176 | sub: 'address.street', 177 | key: 'address.city', 178 | value: undefined, 179 | message: 'Value required because \'address.street\' exists' 180 | }) 181 | }) 182 | it('creates an error object if conditionally required value is undefined when corresponding field HAS the given value', () => { 183 | const data = { address: { street: '123 test ave', country: 'US' } } 184 | const def = { requiredIf: { 'address.country': 'US' } } 185 | validators.requiredIf(def, undefined, 'address.zip', mockErrors, data) 186 | expect(mockErrors[0]).toEqual({ 187 | type: 'requiredIf', 188 | sub: { 'address.country': 'US' }, 189 | key: 'address.zip', 190 | value: undefined, 191 | message: 'Value required by existing \'address.country\' value' 192 | }) 193 | }) 194 | it('creates an error object if conditionally required value is undefined when corresponding field has any of the given values', () => { 195 | const data = { address: { street: '123 test ave', country: 'US' } } 196 | const def = { requiredIf: { 'address.country': [ 'US', 'Canada' ] } } 197 | validators.requiredIf(def, undefined, 'address.zip', mockErrors, data) 198 | expect(mockErrors[0]).toEqual({ 199 | type: 'requiredIf', 200 | sub: { 'address.country': [ 'US', 'Canada' ] }, 201 | key: 'address.zip', 202 | value: undefined, 203 | message: 'Value required by existing \'address.country\' value' 204 | }) 205 | }) 206 | }) 207 | describe('requireIf', () => { 208 | let stub 209 | afterEach(() => { 210 | stub.mockReset() 211 | }) 212 | it('logs a warning and calls requiredIf method', () => { 213 | stub = jest.spyOn(console, 'log') 214 | const data = { address: { street: '123 test ave' } } 215 | const def = { requireIf: 'address.street' } 216 | validators.requireIf(def, undefined, 'address.city', mockErrors, data) 217 | expect(mockErrors[0]).toEqual({ 218 | type: 'requiredIf', 219 | sub: 'address.street', 220 | key: 'address.city', 221 | value: undefined, 222 | message: 'Value required because \'address.street\' exists' 223 | }) 224 | expect(stub).toHaveBeenCalledWith('-----\nObey Warning: `requireIf` should be `requiredIf`\n-----') 225 | }) 226 | }) 227 | describe('requiredIfNot', () => { 228 | it('creates an error object if conditionally required value is undefined', () => { 229 | const data = { address: { street: '123 test ave' } } 230 | const def = { requiredIfNot: 'address.state' } 231 | validators.requiredIfNot(def, undefined, 'address.country', mockErrors, data) 232 | expect(mockErrors[0]).toEqual({ 233 | type: 'requiredIfNot', 234 | sub: 'address.state', 235 | key: 'address.country', 236 | value: undefined, 237 | message: 'Value required because \'address.state\' is undefined' 238 | }) 239 | }) 240 | it('creates an error object if conditionally required value is undefined when corresponding field does NOT have the given value', () => { 241 | const data = { testField: 'not what we want' } 242 | const def = { requiredIfNot: { testField: 'what we want' } } 243 | validators.requiredIfNot(def, undefined, 'conditionalField', mockErrors, data) 244 | expect(mockErrors[0]).toEqual({ 245 | type: 'requiredIfNot', 246 | sub: { testField: 'what we want' }, 247 | key: 'conditionalField', 248 | value: undefined, 249 | message: 'Value required because \'testField\' value is not one specified' 250 | }) 251 | }) 252 | it('creates an error object if conditionally required value is undefined when corresponding field does NOT have any of the given value', () => { 253 | const data = { testField: 'not what we want' } 254 | const def = { requiredIfNot: { testField: [ 'what we want', 'something else we want' ] } } 255 | validators.requiredIfNot(def, undefined, 'conditionalField', mockErrors, data) 256 | expect(mockErrors[0]).toEqual({ 257 | type: 'requiredIfNot', 258 | sub: { testField: [ 'what we want', 'something else we want' ] }, 259 | key: 'conditionalField', 260 | value: undefined, 261 | message: 'Value required because \'testField\' value is not one specified' 262 | }) 263 | }) 264 | }) 265 | describe('requireIfNot', () => { 266 | let stub 267 | afterEach(() => { 268 | stub.mockReset() 269 | }) 270 | it('logs a warning and calls requiredIfNot method', () => { 271 | stub = jest.spyOn(console, 'log') 272 | const data = { address: { street: '123 test ave' } } 273 | const def = { requireIfNot: 'address.state' } 274 | validators.requireIfNot(def, undefined, 'address.country', mockErrors, data) 275 | expect(mockErrors[0]).toEqual({ 276 | type: 'requiredIfNot', 277 | sub: 'address.state', 278 | key: 'address.country', 279 | value: undefined, 280 | message: 'Value required because \'address.state\' is undefined' 281 | }) 282 | expect(stub).toHaveBeenCalledWith('-----\nObey Warning: `requireIfNot` should be `requiredIfNot`\n-----') 283 | }) 284 | }) 285 | describe('equalTo', () => { 286 | it('creates an error object when fields are not equal', () => { 287 | const data = { password: 'Password' } 288 | const def = { equalTo: 'password' } 289 | validators.equalTo(def, 'Passwrod', 'passwordConfirm', mockErrors, data) 290 | expect(mockErrors[0]).toEqual({ 291 | type: 'equalTo', 292 | sub: 'password', 293 | key: 'passwordConfirm', 294 | value: 'Passwrod', 295 | message: 'Value must match password value' 296 | }) 297 | }) 298 | it('doesn\'t create an error object when fields are equal', () => { 299 | const data = { password: 'Password' } 300 | const def = { equalTo: 'password' } 301 | validators.equalTo(def, 'Password', 'passwordConfirm', mockErrors, data) 302 | expect(mockErrors).toEqual([]) 303 | }) 304 | }) 305 | }) 306 | -------------------------------------------------------------------------------- /test/src/modifiers.spec.js: -------------------------------------------------------------------------------- 1 | const modifiers = require('src/modifiers') 2 | 3 | describe('modifiers', () => { 4 | afterEach(() => { 5 | modifiers.lib = {} 6 | }) 7 | describe('execute', () => { 8 | it('runs modifier function and returns modified value if exists', () => { 9 | modifiers.add('test', (val) => val.toUpperCase()) 10 | const actual = modifiers.execute({ modifier: 'test' }, 'foo') 11 | expect(actual).toEqual('FOO') 12 | }) 13 | it('throws an error if the modifier does not exist', () => { 14 | expect(modifiers.execute.bind(null, { modifier: 'nope' })).toThrow('Modifier \'nope\' does not exist') 15 | }) 16 | }) 17 | describe('add', () => { 18 | it('adds a new modifier to the lib', () => { 19 | modifiers.add('test', () => 'foo') 20 | expect(modifiers.lib).toHaveProperty('test', expect.any(Function)) 21 | }) 22 | it('throws an error if the modifier name is not a string', () => { 23 | expect(modifiers.add.bind(null, true, () => 'foo')).toThrow('Modifier name should be a string') 24 | }) 25 | it('throws an error if the modifier method is not a function', () => { 26 | expect(modifiers.add.bind(null, 'foo', undefined)).toThrow('Modifier method should be a function') 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/src/rules.spec.js: -------------------------------------------------------------------------------- 1 | const rules = require('src/rules') 2 | const ValidationError = require('src/lib/error') 3 | 4 | describe('rules', () => { 5 | describe('makeValidate', () => { 6 | it('returns the bound validation method', () => { 7 | const actual = rules.makeValidate({ type: 'string' }) 8 | expect(actual).toEqual(expect.any(Function)) 9 | }) 10 | }) 11 | describe('validate', () => { 12 | it('throws an error if the definition does not contain a type', () => { 13 | expect(rules.validate.bind(null, {}, 'foo')).toThrow('Model properties must define a \'type\'') 14 | }) 15 | it('processes validation on a valid definition object', () => { 16 | return rules.validate({ type: 'string' }, 'foo') 17 | .then(data => { 18 | expect(data).toEqual('foo') 19 | }) 20 | }) 21 | it('processes global props even when falsey', () => { 22 | return rules.validate({ type: 'number', default: 0 }, undefined) 23 | .then(data => { 24 | expect(data).toEqual(0) 25 | }) 26 | }) 27 | it('should not persist init data forever', () => { 28 | if (rules.initData) { delete rules.initData } 29 | let def = { 30 | type: 'object', 31 | keys: { 32 | phone: { type: 'string' }, 33 | phoneType: { type: 'string', requiredIf: 'phone' } 34 | } 35 | } 36 | return rules.validate(def, { phone: '123' }) 37 | .then(() => { 38 | assert.fail(true, false, 'Data is invalid, marked as valid') 39 | }) 40 | .catch(() => expect(rules.validate(def, {})).resolves.toEqual({})) 41 | }) 42 | it('should recursively pass init data to objects', () => { 43 | let def = { 44 | type: 'object', 45 | keys: { 46 | person: { type: 'object', keys: { 47 | name: { type: 'string' } 48 | } 49 | }, 50 | contacts: { type: 'object', keys: { 51 | phone: { type: 'string', requiredIf: 'person.name' } 52 | }} 53 | } 54 | } 55 | let data = { 56 | person: { name: 'John Smith' }, 57 | contacts: {} 58 | } 59 | return expect(rules.validate(def, data)).rejects.toThrow(ValidationError) 60 | }) 61 | it('should recursively pass init data to arrays', () => { 62 | let def = { 63 | type: 'object', 64 | keys: { 65 | person: { type: 'object', keys: { 66 | name: { type: 'string' } 67 | }}, 68 | labels: { type: 'array', values: { 69 | type: 'object', keys: { 70 | label: { type: 'string' }, 71 | condRequired: { type: 'string', requiredIf: 'person.name' } 72 | } 73 | }} 74 | } 75 | } 76 | let data = { 77 | person: { name: 'John Smith' }, 78 | labels: [{label: 'awesome', condRequired: 'nice'}, {label: 'awful'}] 79 | } 80 | return expect(rules.validate(def, data)).rejects.toThrow(ValidationError) 81 | }) 82 | }) 83 | describe('build', () => { 84 | it('returns an object with the original definition and validate method', () => { 85 | const actual = rules.build({ type: 'string' }) 86 | expect(actual).toEqual(expect.any(Object)) 87 | expect(actual.def).toEqual({ type: 'string' }) 88 | expect(actual.validate).toEqual(expect.any(Function)) 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /test/src/typeStrategies/README.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | Below is a list of supported, built-in types for the Obey library. Types are loaded into Obey via the [index](./index.js) file in this directory. 4 | 5 | ## Any 6 | 7 | Supports any data type or format 8 | 9 | ## Array 10 | 11 | Checks for native type `array` 12 | 13 | ## Boolean 14 | 15 | Checks for native type `boolean` 16 | 17 | ## Email 18 | 19 | Checks for valid email, includes valid characters, `@` separator for address and domain, and valid TLD. 20 | 21 | ## IP 22 | 23 | Checks for valid IP Address: 24 | 25 | * `ip`: Default, checks IPv4 format 26 | * `ip:v4`: Checks IPv4 format 27 | * `ip:v6`: Checks IPv6 format 28 | 29 | ## Number 30 | 31 | Checks for native type `number` 32 | 33 | ## Object 34 | 35 | Checks for native type `object` 36 | 37 | ## Phone 38 | 39 | Checks for valid phone numbers: 40 | 41 | * `phone`: Default, valid with or without separators 42 | * `phone:numeric`: Check value for numeric phone number, 7-10 digits 43 | 44 | ## String 45 | 46 | Checks for valid string types: 47 | 48 | * `string`: Default, `typeof` should be `string` 49 | * `string:alphanumeric`: Checks value contains only alpha-numeric characters 50 | 51 | ## URL 52 | 53 | Checks for valid URL 54 | 55 | ## UUID 56 | 57 | Checks for valid v4 UUID 58 | 59 | ## Zip 60 | 61 | Checks for valid zip/postal codes: 62 | 63 | * `zip`: Default, checks generic postal code 64 | * `zip:us`: Checks US zip code format 65 | * `zip:ca`: Checks Canadian zip code format -------------------------------------------------------------------------------- /test/src/typeStrategies/any.spec.js: -------------------------------------------------------------------------------- 1 | const any = require('src/typeStrategies/any') 2 | 3 | describe('type:any', () => { 4 | it('passes through (via return) the value', () => { 5 | const context = { 6 | value: 'anything' 7 | } 8 | expect(any.default(context)).toEqual('anything') 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/src/typeStrategies/array.spec.js: -------------------------------------------------------------------------------- 1 | const array = require('src/typeStrategies/array') 2 | 3 | describe('type:array', () => { 4 | it('calls context.fail if type is not an array', () => { 5 | const context = { 6 | value: 'foo', 7 | fail: jest.fn() 8 | } 9 | array.default(context) 10 | expect(context.fail).toHaveBeenCalledWith('Value must be an array') 11 | }) 12 | it('does not call context fail if type is an array', () => { 13 | const context = { 14 | value: [ 'foo' ], 15 | fail: jest.fn(), 16 | def: {} 17 | } 18 | const actual = array.default(context) 19 | expect(context.fail).not.toHaveBeenCalled() 20 | expect(actual).toEqual(context.value) 21 | }) 22 | it('allows an empty array to pass if empty flag is set to true', () => { 23 | const context = { 24 | value: [], 25 | fail: jest.fn(), 26 | def: { 27 | empty: true 28 | } 29 | } 30 | array.default(context) 31 | expect(context.fail).not.toHaveBeenCalled() 32 | }) 33 | it('fails an empty array when empty flag is not set', () => { 34 | const context = { 35 | value: [], 36 | fail: jest.fn(), 37 | def: { 38 | values: { type: 'string' } 39 | }, 40 | errors: [] 41 | } 42 | array.default(context) 43 | expect(context.fail).toHaveBeenCalledWith('Value must not be empty array') 44 | }) 45 | it('passes when the elements of an array match the type specification', () => { 46 | const context = { 47 | value: [ 'foo', 'bar' ], 48 | fail: jest.fn(), 49 | def: { 50 | values: { type: 'string' } 51 | }, 52 | errors: [] 53 | } 54 | return array.default(context).then(() => { 55 | expect(context.errors.length).toEqual(0) 56 | }) 57 | }) 58 | it('fails when an element of an array does not match the type specification', () => { 59 | const context = { 60 | value: [ 'foo', 73, 34 ], 61 | fail: jest.fn(), 62 | def: { 63 | values: { type: 'string' } 64 | }, 65 | key: 'someKey', 66 | errors: [] 67 | } 68 | return array.default(context).then(() => { 69 | expect(context.errors.length).toEqual(2) 70 | }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /test/src/typeStrategies/boolean.spec.js: -------------------------------------------------------------------------------- 1 | const boolean = require('src/typeStrategies/boolean') 2 | 3 | describe('type:boolean', () => { 4 | it('calls context.fail if type is not a boolean', () => { 5 | const context = { 6 | value: 'foo', 7 | fail: jest.fn() 8 | } 9 | boolean.default(context) 10 | expect(context.fail).toHaveBeenCalledWith('Value must be a boolean') 11 | }) 12 | it('does not call context.fail if type is a boolean', () => { 13 | const context = { 14 | value: true, 15 | fail: jest.fn() 16 | } 17 | boolean.default(context) 18 | expect(context.fail).not.toHaveBeenCalled() 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/src/typeStrategies/email.spec.js: -------------------------------------------------------------------------------- 1 | const email = require('src/typeStrategies/email') 2 | 3 | describe('type:email', () => { 4 | it('calls context.fail if type is not a valid email', () => { 5 | const context = { 6 | value: 'foo', 7 | fail: jest.fn() 8 | } 9 | email.default(context) 10 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid email') 11 | }) 12 | it('calls context.fail if value is empty', () => { 13 | const context = { 14 | value: '', 15 | fail: jest.fn() 16 | } 17 | email.default(context) 18 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid email') 19 | }) 20 | it('does not call context.fail if type is a valid email (standard)', () => { 21 | const context = { 22 | value: 'jsmith@gmail.com', 23 | fail: jest.fn() 24 | } 25 | email.default(context) 26 | expect(context.fail).not.toHaveBeenCalled() 27 | }) 28 | it('does not call context.fail if type is a valid email (with symbol)', () => { 29 | const context = { 30 | value: 'jsmith+symbol@gmail.com', 31 | fail: jest.fn() 32 | } 33 | email.default(context) 34 | expect(context.fail).not.toHaveBeenCalled() 35 | }) 36 | it('does not call context.fail if type is a valid email (unusual; quotes, spaces on left-hand)', () => { 37 | const context = { 38 | value: '"this.is unusual"@example.com', 39 | fail: jest.fn() 40 | } 41 | email.default(context) 42 | expect(context.fail).not.toHaveBeenCalled() 43 | }) 44 | it('does not call context.fail if type is a valid email (REALLY friggin unusual)', () => { 45 | const context = { 46 | value: '#!$%&\'*+-/=?^_`{}|~@example.org', 47 | fail: jest.fn() 48 | } 49 | email.default(context) 50 | expect(context.fail).not.toHaveBeenCalled() 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /test/src/typeStrategies/ip.spec.js: -------------------------------------------------------------------------------- 1 | const ip = require('src/typeStrategies/ip') 2 | 3 | describe('type:ip', () => { 4 | it('calls context.fail if type is not a valid ipv4', () => { 5 | const context = { 6 | value: 'foo', 7 | fail: jest.fn() 8 | } 9 | ip.v4(context) 10 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid IPv4 address') 11 | }) 12 | it('calls context.fail if type is empty (ipv4)', () => { 13 | const context = { 14 | value: '', 15 | fail: jest.fn() 16 | } 17 | ip.v4(context) 18 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid IPv4 address') 19 | }) 20 | it('does not call context.fail if type is a valid ipv4', () => { 21 | const context = { 22 | value: '192.168.1.1', 23 | fail: jest.fn() 24 | } 25 | ip.v4(context) 26 | expect(context.fail).not.toHaveBeenCalled() 27 | }) 28 | it('calls context.fail if type is not a valid ipv6', () => { 29 | const context = { 30 | value: 'foo', 31 | fail: jest.fn() 32 | } 33 | ip.v6(context) 34 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid IPv6 address') 35 | }) 36 | it('calls context.fail if type is empty (ipv6)', () => { 37 | const context = { 38 | value: '', 39 | fail: jest.fn() 40 | } 41 | ip.v6(context) 42 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid IPv6 address') 43 | }) 44 | it('does not call context.fail if type is a valid ipv6', () => { 45 | const context = { 46 | value: '0000:0000:0000:0000:0000:0000:0000:0001', 47 | fail: jest.fn() 48 | } 49 | ip.v6(context) 50 | expect(context.fail).not.toHaveBeenCalled() 51 | }) 52 | it('defaults to ipv4', () => { 53 | const context = { 54 | value: '192.168.1.1', 55 | fail: jest.fn() 56 | } 57 | ip.default(context) 58 | expect(context.fail).not.toHaveBeenCalled() 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/src/typeStrategies/number.spec.js: -------------------------------------------------------------------------------- 1 | const number = require('src/typeStrategies/number') 2 | 3 | describe('type:number', () => { 4 | it('calls context.fail if type is not a number', () => { 5 | const context = { 6 | value: 'foo', 7 | fail: jest.fn() 8 | } 9 | number.default(context) 10 | expect(context.fail).toHaveBeenCalledWith('Value must be a number') 11 | }) 12 | it('call context.fail if type is NaN', () => { 13 | const context = { 14 | value: NaN, 15 | fail: jest.fn() 16 | } 17 | number.default(context) 18 | expect(context.fail).toHaveBeenCalledWith('Value must be a number') 19 | }) 20 | it('does not call context.fail if type is a number', () => { 21 | const context = { 22 | value: 73, 23 | fail: jest.fn() 24 | } 25 | number.default(context) 26 | expect(context.fail).not.toHaveBeenCalled() 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/src/typeStrategies/object.spec.js: -------------------------------------------------------------------------------- 1 | const object = require('src/typeStrategies/object') 2 | 3 | describe('type:object', () => { 4 | it('calls context.fail if type is not an object', () => { 5 | const context = { 6 | value: 'foo', 7 | fail: jest.fn(), 8 | def: {} 9 | } 10 | object.default(context) 11 | expect(context.fail).toHaveBeenCalledWith('Value must be an object') 12 | }) 13 | it('does not call context.fail if type is an object (with no keys prop)', () => { 14 | const context = { 15 | value: { foo: 'bar' }, 16 | fail: jest.fn(), 17 | def: {} 18 | } 19 | object.default(context) 20 | expect(context.fail).not.toHaveBeenCalled() 21 | }) 22 | it('creates no context errors for a passing object with values specification', () => { 23 | const context = { 24 | value: { 25 | bar: 'quz' 26 | }, 27 | fail: jest.fn(), 28 | def: { 29 | values: { type: 'string' } 30 | }, 31 | errors: [] 32 | } 33 | object.default(context).then(() => { 34 | expect(context.errors.length).toEqual(0) 35 | }) 36 | }) 37 | it('creates errors when an object with values specification fails', () => { 38 | const context = { 39 | key: 'someObj', 40 | value: { 41 | fizz: 'buzz', 42 | bar: 13, 43 | baz: true 44 | }, 45 | fail: jest.fn(), 46 | def: { 47 | type: 'object', 48 | values: { type: 'string' } 49 | }, 50 | errors: [] 51 | } 52 | object.default(context).then(() => { 53 | expect(context.errors.length).toEqual(2) 54 | }) 55 | }) 56 | it('creates an error if key in data is not present in definition (strict = true)', () => { 57 | const context = { 58 | key: 'someObj', 59 | value: { 60 | foo: 'bar', 61 | fizz: 'buzz' 62 | }, 63 | def: { 64 | type: 'object', 65 | keys: { 66 | fizz: { type: 'string' } 67 | } 68 | }, 69 | errors: [], 70 | fail: jest.fn() 71 | } 72 | object.default(context).then(() => { 73 | expect(context.fail).toHaveBeenCalledWith('\'foo\' is not an allowed property') 74 | }) 75 | }) 76 | it('allows non-defined properties to be passed (strict = false)', () => { 77 | const context = { 78 | key: 'someObj', 79 | value: { 80 | foo: 'bar', 81 | fizz: 'buzz' 82 | }, 83 | def: { 84 | type: 'object', 85 | keys: { 86 | fizz: { type: 'string' } 87 | }, 88 | strict: false 89 | }, 90 | errors: [], 91 | fail: jest.fn() 92 | } 93 | object.default(context).then(() => { 94 | expect(context.fail).not.toHaveBeenCalled() 95 | }) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /test/src/typeStrategies/phone.spec.js: -------------------------------------------------------------------------------- 1 | const phone = require('src/typeStrategies/phone') 2 | 3 | describe('type:phone', () => { 4 | it('calls context.fail if value is not a valid phone number', () => { 5 | const context = { 6 | value: 'foo', 7 | fail: jest.fn() 8 | } 9 | phone.default(context) 10 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid phone number') 11 | }) 12 | it('calls context.fail if value is empty', () => { 13 | const context = { 14 | value: '', 15 | fail: jest.fn() 16 | } 17 | phone.default(context) 18 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid phone number') 19 | }) 20 | it('does not call context fail if value is a valid phone number', () => { 21 | // Variation 22 | const contextOne = { value: '(555)-123-4567', fail: jest.fn() } 23 | phone.default(contextOne) 24 | expect(contextOne.fail).not.toHaveBeenCalled() 25 | // Variation 26 | const contextTwo = { value: '555 123-4567', fail: jest.fn() } 27 | phone.default(contextTwo) 28 | expect(contextTwo.fail).not.toHaveBeenCalled() 29 | // Variation 30 | const contextThree = { value: '5551234567', fail: jest.fn() } 31 | phone.default(contextThree) 32 | expect(contextThree.fail).not.toHaveBeenCalled() 33 | }) 34 | it('calls context.fail if value is not a valid numeric phone number', () => { 35 | const context = { 36 | value: 'foo', 37 | fail: jest.fn() 38 | } 39 | phone.numeric(context) 40 | expect(context.fail).toHaveBeenCalledWith('Value must be a numeric phone number') 41 | }) 42 | it('calls context.fail if value is empty (numeric)', () => { 43 | const context = { 44 | value: '', 45 | fail: jest.fn() 46 | } 47 | phone.numeric(context) 48 | expect(context.fail).toHaveBeenCalledWith('Value must be a numeric phone number') 49 | }) 50 | it('does not call context fail if value is a valid phone number', () => { 51 | const context = { 52 | value: '5551234567', 53 | fail: jest.fn() 54 | } 55 | phone.numeric(context) 56 | expect(context.fail).not.toHaveBeenCalled() 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /test/src/typeStrategies/string.spec.js: -------------------------------------------------------------------------------- 1 | const string = require('src/typeStrategies/string') 2 | 3 | describe('type:string', () => { 4 | it('calls context.fail if type is not a string', () => { 5 | const context = { 6 | value: true, 7 | fail: jest.fn() 8 | } 9 | string.default(context) 10 | expect(context.fail).toHaveBeenCalledWith('Value must be a string') 11 | }) 12 | it('does not call context.fail if type is a string', () => { 13 | const context = { 14 | value: 'foo', 15 | fail: jest.fn() 16 | } 17 | string.default(context) 18 | expect(context.fail).not.toHaveBeenCalled() 19 | }) 20 | it('calls context.fail if type is not an alphanumberic string', () => { 21 | const context = { 22 | value: 'abc$#', 23 | fail: jest.fn() 24 | } 25 | string.alphanumeric(context) 26 | expect(context.fail).toHaveBeenCalledWith('Value must contain only letters and/or numbers') 27 | }) 28 | it('does not call context.fail if type is an alphanumeric string', () => { 29 | const context = { 30 | value: 'abc123', 31 | fail: jest.fn() 32 | } 33 | string.alphanumeric(context) 34 | expect(context.fail).not.toHaveBeenCalled() 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /test/src/typeStrategies/url.spec.js: -------------------------------------------------------------------------------- 1 | const url = require('src/typeStrategies/url') 2 | 3 | describe('type:url', () => { 4 | it('calls context.fail if type is not a valid URL', () => { 5 | const context = { 6 | value: 'foo', 7 | fail: jest.fn() 8 | } 9 | url.default(context) 10 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid URL') 11 | }) 12 | it('calls context.fail if type is empty', () => { 13 | const context = { 14 | value: '', 15 | fail: jest.fn() 16 | } 17 | url.default(context) 18 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid URL') 19 | }) 20 | it('does not call context.fail if type is a valid URL', () => { 21 | const context = { 22 | value: 'https://www.google.com/test', 23 | fail: jest.fn() 24 | } 25 | url.default(context) 26 | expect(context.fail).not.toHaveBeenCalled() 27 | }) 28 | it('passes urls with number and hypens', () => { 29 | const context = { 30 | value: 'this-is.1weird.domain.com', 31 | fail: jest.fn() 32 | } 33 | url.default(context) 34 | expect(context.fail).not.toHaveBeenCalled() 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /test/src/typeStrategies/uuid.spec.js: -------------------------------------------------------------------------------- 1 | const uuid = require('src/typeStrategies/uuid') 2 | 3 | describe('type:uuid', () => { 4 | describe('default', () => { 5 | it('calls context.fail if type is not a valid UUID', () => { 6 | const context = { 7 | value: 'foo', 8 | fail: jest.fn() 9 | } 10 | uuid.default(context) 11 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid UUID') 12 | }) 13 | it('calls context.fail if type is empty', () => { 14 | const context = { 15 | value: '', 16 | fail: jest.fn() 17 | } 18 | uuid.default(context) 19 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid UUID') 20 | }) 21 | it('does not call context.fail if type is a valid UUID', () => { 22 | const context = { 23 | value: 'ef3d56e1-6046-4743-aa69-b94bc9e66181', 24 | fail: jest.fn() 25 | } 26 | uuid.default(context) 27 | expect(context.fail).not.toHaveBeenCalled() 28 | }) 29 | it('allows uppercase characters', () => { 30 | const context = { 31 | value: 'EF3D56E1-6046-4743-AA69-B94BC9E66181', 32 | fail: jest.fn() 33 | } 34 | uuid.default(context) 35 | expect(context.fail).not.toHaveBeenCalled() 36 | }) 37 | }) 38 | describe('upper', () => { 39 | it('calls context.fail if type is not a valid UUID', () => { 40 | const context = { 41 | value: 'foo', 42 | fail: jest.fn() 43 | } 44 | uuid.upper(context) 45 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid UUID with all uppercase letters') 46 | }) 47 | it('calls context.fail if type is empty', () => { 48 | const context = { 49 | value: '', 50 | fail: jest.fn() 51 | } 52 | uuid.upper(context) 53 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid UUID with all uppercase letters') 54 | }) 55 | it('does not call context.fail if type is a valid uppercase UUID', () => { 56 | const context = { 57 | value: 'EF3D56E1-6046-4743-AA69-B94BC9E66181', 58 | fail: jest.fn() 59 | } 60 | uuid.upper(context) 61 | expect(context.fail).not.toHaveBeenCalled() 62 | }) 63 | it('calls context.fail if type is not an uppercase UUID', () => { 64 | const context = { 65 | value: 'ef3d56e1-6046-4743-aa69-b94bc9e66181', 66 | fail: jest.fn() 67 | } 68 | uuid.upper(context) 69 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid UUID with all uppercase letters') 70 | }) 71 | }) 72 | describe('lower', () => { 73 | it('calls context.fail if type is not a valid UUID', () => { 74 | const context = { 75 | value: 'foo', 76 | fail: jest.fn() 77 | } 78 | uuid.lower(context) 79 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid UUID with all lowercase letters') 80 | }) 81 | it('calls context.fail if type is empty', () => { 82 | const context = { 83 | value: '', 84 | fail: jest.fn() 85 | } 86 | uuid.lower(context) 87 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid UUID with all lowercase letters') 88 | }) 89 | it('does not call context.fail if type is a valid lowercase UUID', () => { 90 | const context = { 91 | value: 'ef3d56e1-6046-4743-aa69-b94bc9e66181', 92 | fail: jest.fn() 93 | } 94 | uuid.lower(context) 95 | expect(context.fail).not.toHaveBeenCalled() 96 | }) 97 | it('calls context.fail if type is not a lowercase UUID', () => { 98 | const context = { 99 | value: 'EF3D56E1-6046-4743-AA69-B94BC9E66181', 100 | fail: jest.fn() 101 | } 102 | uuid.lower(context) 103 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid UUID with all lowercase letters') 104 | }) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /test/src/typeStrategies/zip.spec.js: -------------------------------------------------------------------------------- 1 | const zip = require('src/typeStrategies/zip') 2 | 3 | describe('type:zip', () => { 4 | it('calls context.fail if value is not a valid US zip code', () => { 5 | const context = { 6 | value: 'foo', 7 | fail: jest.fn() 8 | } 9 | zip.default(context) 10 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid US zip code') 11 | }) 12 | it('calls context.fail if value is empty (US)', () => { 13 | const context = { 14 | value: '', 15 | fail: jest.fn() 16 | } 17 | zip.default(context) 18 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid US zip code') 19 | }) 20 | it('does not call context fail if value is a valid zip code (numeric)', () => { 21 | const context = { 22 | value: 61029, 23 | fail: jest.fn() 24 | } 25 | zip.default(context) 26 | expect(context.fail).not.toHaveBeenCalled() 27 | }) 28 | it('does not call context fail if value is a valid zip code (string)', () => { 29 | const context = { 30 | value: '61029', 31 | fail: jest.fn() 32 | } 33 | zip.default(context) 34 | expect(context.fail).not.toHaveBeenCalled() 35 | }) 36 | it('calls context.fail if value is not a valid CA zip code', () => { 37 | const context = { 38 | value: 'foo', 39 | fail: jest.fn() 40 | } 41 | zip.ca(context) 42 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid Canadian zip code') 43 | }) 44 | it('calls context.fail if value is empty (CA)', () => { 45 | const context = { 46 | value: '', 47 | fail: jest.fn() 48 | } 49 | zip.ca(context) 50 | expect(context.fail).toHaveBeenCalledWith('Value must be a valid Canadian zip code') 51 | }) 52 | it('does not call context fail if value is a valid US zip code', () => { 53 | const context = { 54 | value: 'A1A 1A1', 55 | fail: jest.fn() 56 | } 57 | zip.ca(context) 58 | expect(context.fail).not.toHaveBeenCalled() 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/src/types.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, expect, beforeEach */ 2 | /* eslint no-unused-vars: 0 */ 3 | const types = require('src/types.js') 4 | 5 | describe('types', () => { 6 | describe('checkSubType', () => { 7 | it('returns the original definition if no sub-type specified', () => { 8 | const actual = types.checkSubType({ type: 'foo' }) 9 | expect(actual).toEqual({ type: 'foo', sub: 'default' }) 10 | }) 11 | it('returns the definition with specific type and sub if sub-type specified', () => { 12 | const actual = types.checkSubType({ type: 'foo:bar' }) 13 | expect(actual).toEqual({ type: 'foo', sub: 'bar' }) 14 | }) 15 | }) 16 | describe('validate', () => { 17 | it('builds a fail method and returns types.check', () => { 18 | const actual = types.validate.call({ errors: [] }, { type: 'string' }, 'foo', 'bar') 19 | expect(actual).toEqual(expect.any(Promise)) 20 | }) 21 | it('allows an empty string to pass (via return) when empty flag is set', () => { 22 | const actual = types.validate.call({ errors: [] }, { type: 'string', empty: true }, '', 'foo') 23 | expect(actual).toEqual('') 24 | }) 25 | }) 26 | describe('add', () => { 27 | it('adds a new type as a function to the strategies', () => { 28 | types.add('testFn', context => null) 29 | expect(types.strategies.testFn.default).toEqual(expect.any(Function)) 30 | }) 31 | it('adds a new type as an object to the strageties', () => { 32 | types.add('testObj', { foo: () => null }) 33 | expect(types.strategies.testObj.foo).toEqual(expect.any(Function)) 34 | }) 35 | }) 36 | describe('check', () => { 37 | let context = {} 38 | beforeEach(() => { 39 | context = { 40 | def: { 41 | type: 'string', 42 | sub: 'default' 43 | }, 44 | fail: () => null 45 | } 46 | }) 47 | it('loads a type strategy and returns a promise which calls the strategy', () => { 48 | const actual = types.check(context) 49 | expect(actual).toEqual(expect.any(Promise)) 50 | }) 51 | it('throws an error if the specified type does not exist', () => { 52 | context.def.type = 'nope' 53 | expect(types.check.bind(null, context)).toThrow('Type \'nope\' does not exist') 54 | }) 55 | it('throws an error if the specified subtype does not exist', () => { 56 | context.def.sub = 'nope' 57 | expect(types.check.bind(null, context)).toThrow('Type \'string:nope\' does not exist') 58 | }) 59 | it('throws an error if the type contains path characters', () => { 60 | context.def.type = '../creators' 61 | expect(types.check.bind(null, context)).toThrow('Illegal type name: ../creators') 62 | }) 63 | }) 64 | }) 65 | --------------------------------------------------------------------------------