├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG.markdown ├── LICENSE ├── README.markdown ├── index.js ├── lib ├── context.js ├── index.js ├── registry.js ├── schema.js └── types.js ├── package.json └── test ├── break-on-error.test.js ├── mocha.opts ├── test-array.js ├── test-function.js ├── test-mixed.js ├── test-number.js ├── test-object-default-properties.js ├── test-object-property-count.js ├── test-object-wildcard-properties.js ├── test-optional.js ├── test-primitive.js ├── test-recursive.js ├── test-schema-errors.js ├── test-schema.js └── test-string.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-cond-assign": 2, 4 | "no-console": 1, 5 | "no-constant-condition": 2, 6 | "no-delete-var": 2, 7 | "no-dupe-args": 2, 8 | "no-dupe-keys": 2, 9 | "no-duplicate-case": 2, 10 | "no-empty": 1, 11 | "no-empty-character-class": 2, 12 | "no-extra-bind": 2, 13 | "no-extra-semi": 2, 14 | "no-extra-parens": 1, 15 | "no-fallthrough": 2, 16 | "no-func-assign": 2, 17 | "no-invalid-regexp": 2, 18 | "no-irregular-whitespace": 2, 19 | "no-multi-spaces": 2, 20 | "no-new-func": 2, 21 | "no-new-object": 2, 22 | "no-path-concat": 2, 23 | "callback-return": 1, 24 | "no-process-exit": 2, 25 | "no-redeclare": 2, 26 | "no-shadow": 1, 27 | "no-spaced-func": 2, 28 | "no-trailing-spaces": 2, 29 | "no-undef": 2, 30 | "no-unused-vars": [1, {"vars": "local", "args": "after-used"}], 31 | "no-underscore-dangle": 0, 32 | "no-use-before-define": [2, "nofunc"], 33 | "arrow-parens": [2, "always"], 34 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 35 | "camelcase": 0, 36 | "comma-spacing": [2, {"before": false, "after": true}], 37 | "comma-dangle": [2, "never"], 38 | "consistent-return": 0, 39 | "consistent-this": [2, "self"], 40 | "curly": [2, "all"], 41 | "dot-notation": [2, {"allowKeywords": true}], 42 | "eol-last": 2, 43 | "eqeqeq": [2, "smart"], 44 | "indent": [2, 2, {"SwitchCase": 1}], 45 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 46 | "keyword-spacing": 2, 47 | "new-parens": 2, 48 | "newline-per-chained-call": 0, 49 | "no-extra-label": 2, 50 | "no-unused-labels": 2, 51 | "no-new-symbol": 2, 52 | "no-restricted-imports": [2, "lodash", "underscore"], 53 | "no-useless-constructor": 2, 54 | "template-curly-spacing": [2, "never"], 55 | "operator-linebreak": [2, "after"], 56 | "quotes": [2, "single"], 57 | "semi": [2, "always"], 58 | "space-before-blocks": [2, "always"], 59 | "space-before-function-paren": [2, "never"], 60 | "object-curly-spacing": [2, "never"], 61 | "array-bracket-spacing": [2, "never"], 62 | "computed-property-spacing": [2, "never"], 63 | "space-infix-ops": 2, 64 | "space-unary-ops": [2, {"words": true, "nonwords": false}], 65 | "spaced-comment": [2, "always"], 66 | "strict": [2, "global"], 67 | "valid-typeof": 2, 68 | "yoda": [2, "never"], 69 | "no-const-assign": "error", 70 | "valid-jsdoc": [1, { 71 | "requireReturn": false, 72 | "requireReturnDescription": false, 73 | "requireParamDescription": false, 74 | "prefer": { 75 | "returns": "return" 76 | } 77 | }] 78 | }, 79 | "env": { 80 | "node": true, 81 | "es6": true 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | *.log 5 | *.heapsnapshot 6 | .DS_Store 7 | .tags 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | tools 5 | *.log 6 | *.heapsnapshot 7 | .DS_Store 8 | .tags 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "6" 5 | - "7" -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # ordered by first contribution 2 | # maintainer is the package.json author 3 | 4 | Christopher Blasnik 5 | Clemens Solar 6 | Malte-Thorben Bruns 7 | Christian Steininger 8 | -------------------------------------------------------------------------------- /CHANGELOG.markdown: -------------------------------------------------------------------------------- 1 | # mgl-validate ChangeLog 2 | 3 | ## 2016-10-03, [v2.1.0](https://github.com/magora-labs/mgl-validate/tree/v2.1.0) **_stable_** 4 | 5 | ### Commits 6 | 7 | - [[`85f9f4a974`](https://github.com/magora-labs/mgl-validate/commit/85f9f4a9743dbe662e477f39c29302deae991889)] - **deps**: eslint v3.6.1, nyc v8.3.0, mocha v3.1.0 8 | - [[`9f2e0466a2`](https://github.com/magora-labs/mgl-validate/commit/9f2e0466a2a046c996d18f494e598d40cb1b8372)] - **package**: update scripts 9 | - [[`a3e31b7338`](https://github.com/magora-labs/mgl-validate/commit/a3e31b733896e18f812aadb313c4f38316f43b28)] - **schema**: add `allowNullValue` property 10 | 11 | 12 | ## 2016-07-12, [v2.0.1](https://github.com/magora-labs/mgl-validate/tree/v2.0.1) **_stable_** 13 | 14 | ### Commits 15 | 16 | - [[`bac74ddb7e`](https://github.com/magora-labs/mgl-validate/commit/bac74ddb7e8de314ba66061885fe7e8b583fcb2c)] - **deps**: mocha 2.5.3 17 | - [[`afddc25b31`](https://github.com/magora-labs/mgl-validate/commit/afddc25b318db66379d662cb090c0cd12514c5ba)] - **deps**: istanbul 0.4.3 18 | - [[`97f335920d`](https://github.com/magora-labs/mgl-validate/commit/97f335920d7aebc2004ce1d94383091e5607c4aa)] - **deps**: eslint 2.11.1 19 | - [[`611a93f91c`](https://github.com/magora-labs/mgl-validate/commit/611a93f91c7cc8fbf82acd346888e9093da6a3a4)] - **doc**: add Christian Steininger to AUTHORS 20 | - [[`fcd7bcaa82`](https://github.com/magora-labs/mgl-validate/commit/fcd7bcaa828a37c872dce1c417118d1defd11918)] - **schema**: fix breakOnError option for array & mixed types 21 | 22 | 23 | ## 2016-04-04, [v2.0.0](https://github.com/magora-labs/mgl-validate/tree/v2.0.0) **_stable_** 24 | 25 | ### Notable Changes 26 | 27 | * Validation errors are returned without an error object, **all other errors are immediately thrown!** 28 | * The **enum** option has been refactored and introduces a new **ordered** option flag for more versatile **array** validation 29 | * Validation errors for the `enum` option now state `'value'` instead of `'enum'` as reason 30 | * Primitives only allow primitives of the the same type as elements of an array 31 | * Support for **recursive & self-registering schemas** has been added 32 | 33 | ## 2015-08-14, [v1.0.2](https://github.com/magora-labs/mgl-validate/tree/v1.0.2) **_stable_** 34 | 35 | ### Commits 36 | 37 | - [[`34f873351e`](https://github.com/magora-labs/mgl-validate/commit/34f873351ee326692ad0ba492fe32b3ddc570250)] - **config**: travis uses iojs v2.5.0 38 | - [[`a9a0032f0b`](https://github.com/magora-labs/mgl-validate/commit/a9a0032f0bcfa4367313ba166eb55a9d2632a914)] - **deps**: update dev dependencies 39 | - [[`a4322c9c18`](https://github.com/magora-labs/mgl-validate/commit/a4322c9c18b00d2ef5cf54d9557fcc6d3347a873)] - **schema**: extend definition type validation 40 | 41 | 42 | ## 2015-04-16, [v1.0.1](https://github.com/magora-labs/mgl-validate/tree/v1.0.1) **_stable_** 43 | 44 | ### Commits 45 | 46 | - [[`36bf33a8cc`](https://github.com/magora-labs/mgl-validate/commit/36bf33a8cc101a31b797656223df8adaef3c9a10)] - **deps**: changelog42: v0.5.0 47 | - [[`acefe7a051`](https://github.com/magora-labs/mgl-validate/commit/acefe7a051e862c48cc1284152f1574a6dd43030)] - **deps**: eslint v0.20.0 48 | - [[`0abef4526c`](https://github.com/magora-labs/mgl-validate/commit/0abef4526cc0581215fea1af029db9e88ea21757)] - **doc**: add introductory text 49 | - [[`37530b2b44`](https://github.com/magora-labs/mgl-validate/commit/37530b2b4430b63225644d5b0944028f0f750bca)] - **doc/README**: add dependency badge 50 | - [[`6e6358bee9`](https://github.com/magora-labs/mgl-validate/commit/6e6358bee9c332c4f0fb5f32d9f642811046dd72)] - **eslint**: update config (operator linebreak) 51 | - [[`1ec1e34c74`](https://github.com/magora-labs/mgl-validate/commit/1ec1e34c7453d801b3d4ace52d123ca8d82e0349)] - **package**: add changelog script 52 | - [[`8fbba192ac`](https://github.com/magora-labs/mgl-validate/commit/8fbba192ace3a3c6a01694f8cda92458eb8c3efe)] - **package**: update script calls to be less verbose 53 | - [[`ec02a2e63d`](https://github.com/magora-labs/mgl-validate/commit/ec02a2e63d9b18332a909437126a21d3494e826f)] - **package**: npm engine version v2.8.3 54 | - [[`3e1dc16a0b`](https://github.com/magora-labs/mgl-validate/commit/3e1dc16a0b2f80f4c95096f63d5f2d2c2fdfc35c)] - **package**: add keywords for search 55 | - [[`1e77beb766`](https://github.com/magora-labs/mgl-validate/commit/1e77beb76687d35d0c8c6b80936d222898fa8607)] - **package**: add license: "MIT" 56 | 57 | 58 | ## 2015-04-16, [v1.0.0](https://github.com/magora-labs/mgl-validate/tree/v1.0.0) **_stable_** 59 | 60 | **first public release** 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Magora Group GmbH, Austria (www.magora.at) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # mgl-validate 2 | 3 | [![NPM version](https://img.shields.io/npm/v/mgl-validate.svg?style=flat-square)](https://www.npmjs.com/package/mgl-validate) 4 | [![downloads](https://img.shields.io/npm/dm/mgl-validate.svg?style=flat-square)](https://www.npmjs.com/package/mgl-validate) 5 | [![dependencies](https://david-dm.org/magora-labs/mgl-validate.svg)](https://github.com/magora-labs/mgl-validate) 6 | [![devDependencies](https://david-dm.org/magora-labs/mgl-validate/dev-status.svg)](https://github.com/magora-labs/mgl-validate) 7 | [![Build Status](https://secure.travis-ci.org/magora-labs/mgl-validate.png)](http://travis-ci.org/magora-labs/mgl-validate) 8 | 9 | **_data object validation_** 10 | 11 | *mgl-validate* is a library for validating any variable type, including objects, arrays and primitives, as well as *mixed type* definitions. 12 | 13 | Moreover, *mgl-validate* enables the programmer to verify the validity of complex & deeply nested object structures. 14 | The library features defining validation **schemas** to be used as **references**, hence facilitating shared and reusable definitions upon which complex and possibly nested validation schemas can be built. 15 | 16 | Schemas are compiled at time of definition to keep performance penalties of actual validations at a minimum. 17 | 18 | 19 | ```js 20 | Stability: 3 - Stable 21 | ``` 22 | 23 | ## Table of Contents 24 | 25 | - [Schema](#schema) 26 | - [API](#api) 27 | - [Tests](#tests) 28 | - [License](#license) 29 | - [ChangeLog](./CHANGELOG.markdown) 30 | 31 | ## Testimonials 32 | 33 | _I really like the notion of adding a schema and referencing it in others - that's just super-cool_ 34 | 35 | ## Example 36 | 37 | ```js 38 | var registry = require('mgl-validate')({ 39 | breakOnError: true 40 | }); 41 | 42 | try { 43 | // create a nested schema 44 | registry.addSchema({ 45 | id: 'doc', 46 | type: 'object', 47 | properties: { 48 | ref: { 49 | id: 'uuid', 50 | type: 'string', 51 | pattern: '[a-f\\d]{8}(-[a-f\\d]{4}){3}-[a-f\\d]{12}' 52 | } 53 | } 54 | }); 55 | } catch (err) { 56 | return console.log(err.message); 57 | } 58 | 59 | var errors; 60 | 61 | // validate against 'doc' 62 | errors = registry.test('doc', {ref: 'THIS-IS-NOT-AN-UUID'}); 63 | if (errors) { 64 | console.log(errors); 65 | } 66 | 67 | // ... or validate directly against 'uuid' 68 | errors = registry.test('uuid', 'THIS-IS-NOT-AN-UUID'); 69 | if (errors) { 70 | console.log(errors); 71 | } 72 | ``` 73 | 74 | [back to top](#table-of-contents) 75 | 76 | ## Types 77 | 78 | - `null` 79 | - `boolean` 80 | - `number` 81 | - `integer` - validates as `number` too 82 | - `string` 83 | - `array` 84 | - `object` 85 | - `buffer` 86 | - `function` 87 | - `mixed` 88 | 89 | ## Schema 90 | See [`tests`](./test/) for examples covering all use cases. 91 | 92 | The options are processed in the following order; `type optional value min < max < enum < pattern properties`, where `A < B` means that `B` is only tested when `A` didn't fail. 93 | 94 | **properties** 95 | 96 | * `{?string=} id` - The schema id, when given the schema can be referenced 97 | * `{string} type` - The data type; See [Types](#types) 98 | * `{?number=} depth` - Absolute nesting limit for validated data, defaults to `10` 99 | * `{?boolean=} optional` - When `true`, the value may be `undefined` 100 | * `{?*=} default` - A default value, implies `optional: true` 101 | * `{?string=} pattern` - An encoded regular-expression for string validation 102 | * `{?string=} flags` - Regular-expression flags 103 | * `{?number=} min` - Minimum number _of chars, elements, properties, arguments_ 104 | * `{?number=} max` - Maximum number _of chars, elements, properties, arguments_ 105 | * `{?Object=} properties` - A map with schemas for each property of an object 106 | * `{?boolean=} allowUnknownProperties` - When `true`, an object may contain properties that don't have a schema 107 | * `{?(Array|Object|string)=} enum` - Validate values against given primitives and/or schemas 108 | * `{?boolean=} ordered` - When `true`, an array is matched against `enum` in order 109 | * `{?boolean=} allowNullValue` - When `true`, the affected schema's value may be `null` 110 | 111 | ### enum 112 | Validate values against given values and/or schemas. 113 | 114 | NOTE: The most likely values should be placed at the top of the enum array to improve validation performance. 115 | 116 | **Types:** `number`, `integer`, `string`, `array`, `mixed` 117 | 118 | #### Primitives 119 | Primitive data types can be validated against a list of static values of their own type; 120 | 121 | ```js 122 | { type: 'number', // or 'integer' or 'string' 123 | enum: [1, 2, 3] 124 | } 125 | ``` 126 | 127 | #### `type: array` 128 | Arrays support multiple combinations for `enum`; 129 | 130 | _... a single schema_, either as reference (`$id:`) or object. All elements in the array now have to comply to the given schema. 131 | 132 | ```js 133 | { type: 'array', 134 | enum: '$id:uuid' 135 | } 136 | ``` 137 | 138 | _... an array of schemas and/or primitives_. Any element in the array has to comply to any of the supplied schemas. When `ordered: true`, the elements of the array have to comply to the given schemas in order. 139 | 140 | ```js 141 | { type: 'array', 142 | ordered: true, 143 | enum: [ 144 | '$id:uuid', 145 | false, 146 | {type: 'integer'} 147 | ] 148 | } 149 | ``` 150 | 151 | _In the above example the 2nd, 5th, 8th, ... element of any array has to be `false` in order to pass the test_ 152 | 153 | Validation restarts at the first element of the given `enum` if the number of elements t.b. validated exceeds the given schemas. 154 | This behaviour can be further controlled with the `min` and `max` options. 155 | 156 | #### `type: mixed` 157 | `enum` for `type: mixed` works exactly like for `type: array` with an array of schemas and/or primitives, but with respect to a single value. 158 | 159 | ### properties 160 | An object with properties where each value is a primitive value, schema object or reference. 161 | 162 | **Types:** `object`, `function` 163 | 164 | ```js 165 | { type: 'object', 166 | properties: { 167 | xyz: { 168 | type: 'number' 169 | }, 170 | abc: '$id:other', // has to match "other" schema 171 | wtf: 'abc', // primitive equals match 172 | ... 173 | } 174 | } 175 | ``` 176 | 177 | #### wildcard 178 | 179 | **Types:** `object` 180 | 181 | ```js 182 | { type: 'object', 183 | properties: { 184 | '*': { // all unknown properties have to match this schema 185 | type: 'string', 186 | ... 187 | }, 188 | ... 189 | } 190 | } 191 | ``` 192 | 193 | 194 | ### min & max 195 | A number that describes the minimum ... 196 | 197 | - value for a `number` or an `integer` 198 | - length for a `string` 199 | - length for an `array` 200 | - number of arguments for a `function` 201 | - number of properties on an `object` 202 | 203 | **Types:** `number`, `integer`, `string`, `array`, `object`, `function` 204 | 205 | _Note: When `min` fails, the test for `max` is omitted for logical reasons._ 206 | 207 | 208 | ### pattern 209 | A string to be used for `new RegExp()`. 210 | 211 | **Types:** `string` 212 | 213 | #### flags 214 | 215 | **Types:** `string` 216 | 217 | 218 | ### optional 219 | When `true` the value may be `undefined`, type violations other than `undefined` produce an error. 220 | 221 | **Types: ALL** 222 | 223 | 224 | ### default 225 | _**Works ONLY for object properties!**_ 226 | 227 | Apply a default value if `undefined`. 228 | 229 | **Types:** `number`, `integer`, `string`, `array`, `function` 230 | 231 | 232 | ### allowUnknownProperties 233 | Allows an object to have properties not specified in the schema. 234 | Defaults to `true` for `function` and to `false` for `object`. 235 | 236 | **Types:** `object`, `function` 237 | 238 | 239 | [back to top](#table-of-contents) 240 | 241 | ## API 242 | 243 | ### Class: Registry 244 | #### new Registry(opt_options) 245 | 246 | * `{?Object=} opt_options` 247 | * `{?boolean=} breakOnError` - defaults to `false` 248 | * `{?number=} depth` - Global nesting limit, defaults to `10`. Can be overridden by each schema 249 | 250 | #### registry.breakOnError 251 | 252 | #### registry.getSchemas() 253 | 254 | #### registry.addSchema(definition) 255 | 256 | #### registry.removeSchema(schema) 257 | 258 | #### registry.test(schema, data); 259 | See `schema.test()`. 260 | 261 | ### Class: Schema 262 | #### new Schema(registry, definition) 263 | 264 | #### schema.test(data) 265 | Validate given data, returns validation errors as array, `null` otherwise. 266 | 267 | An annotated example; 268 | 269 | ```js 270 | [ 271 | [, , , ], 272 | 273 | // property b of object a is undefined 274 | ['a', 'object', 'undefined', 'b'], 275 | 276 | // property c of object a is not of type string 277 | ['a.c', 'string', 'type', 2], 278 | 279 | // property e of the 1st element of the array at property d of object a is too small 280 | ['a.d.0.e', 'number', 'min', -1], 281 | ... 282 | ] 283 | ``` 284 | 285 | #### schema.typeOf(value) 286 | 287 | [back to top](#table-of-contents) 288 | 289 | ## Tests 290 | 291 | ```bash 292 | npm test 293 | firefox coverage/lcov-report/index.html 294 | ``` 295 | 296 | ### Coverage 297 | 298 | ``` 299 | Statements : 99.37% ( 317/319 ) 300 | Branches : 97.74% ( 260/266 ) 301 | Functions : 100% ( 21/21 ) 302 | Lines : 99.37% ( 317/319 ) 303 | ``` 304 | 305 | [back to top](#table-of-contents) 306 | 307 | ## License 308 | 309 | (The MIT license) 310 | 311 | Copyright (c) 2015 Magora Group GmbH, Austria (www.magora.at) 312 | 313 | Permission is hereby granted, free of charge, to any person obtaining a copy 314 | of this software and associated documentation files (the "Software"), to deal 315 | in the Software without restriction, including without limitation the rights 316 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 317 | copies of the Software, and to permit persons to whom the Software is 318 | furnished to do so, subject to the following conditions: 319 | 320 | The above copyright notice and this permission notice shall be included in 321 | all copies or substantial portions of the Software. 322 | 323 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 324 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 325 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 326 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 327 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 328 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 329 | THE SOFTWARE. 330 | 331 | [back to top](#table-of-contents) 332 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib'); 4 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var TYPES = require('./types'); 3 | 4 | 5 | 6 | /** 7 | * @constructor 8 | */ 9 | function Context() { 10 | this.level = -1; 11 | this.schemas = []; 12 | this.keys = []; 13 | this.errors = []; 14 | } 15 | module.exports = Context; 16 | 17 | 18 | /** 19 | * @param {Schema} schema The current schema 20 | * @param {(string|number)} opt_key The property name or array index 21 | */ 22 | Context.prototype.up = function(schema, opt_key) { 23 | ++this.level; 24 | this.schemas.push(schema); 25 | 26 | if (opt_key !== undefined) { 27 | this.keys.push('' + opt_key); 28 | } 29 | }; 30 | 31 | 32 | Context.prototype.down = function() { 33 | --this.level; 34 | this.schemas.pop(); 35 | this.keys.pop(); 36 | }; 37 | 38 | 39 | /** 40 | * @param {string} reason Why is is an error 41 | * @param {*} offendingValue The value that did not match 42 | */ 43 | Context.prototype.error = function(reason, offendingValue) { 44 | this.errors.push([ 45 | this.keys.join('.') || null, 46 | TYPES.BY_ID[this.schemas[this.level].type], 47 | reason, 48 | offendingValue 49 | ]); 50 | }; 51 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = require('./registry'); 4 | exports.Schema = require('./schema'); 5 | exports.TYPES = require('./types'); 6 | -------------------------------------------------------------------------------- /lib/registry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Schema = require('./schema'); 3 | 4 | 5 | 6 | /** 7 | * The schema registry 8 | * 9 | * @param {?Object=} opt_options The registry configuration 10 | * @constructor 11 | */ 12 | function Registry(opt_options) { 13 | if (!(this instanceof Registry)) { 14 | return new Registry(opt_options); 15 | } 16 | opt_options = opt_options || {}; 17 | 18 | this.breakOnError = opt_options.breakOnError || false; 19 | this.depth = opt_options.depth || 10; 20 | 21 | // regex to match "$schema.*" 22 | this._match = /"\$id:[a-z0-9_-]*"/ig; 23 | // regex to escape strings for usage as regex 24 | this._escape = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g; 25 | 26 | this.schemas = {}; 27 | } 28 | module.exports = Registry; 29 | 30 | 31 | Registry.prototype.getSchemas = function() { 32 | return Object.keys(this.schemas); 33 | }; 34 | 35 | 36 | /** 37 | * @param {string} name Name of the schema to remove 38 | */ 39 | Registry.prototype.removeSchema = function(name) { 40 | if (!this.schemas[name]) { 41 | throw new Error('Unknown schema "' + name + '"'); 42 | } 43 | delete this.schemas[name]; 44 | }; 45 | 46 | 47 | /** 48 | * @param {Object} definition The schema definition 49 | * @return {Schema} 50 | */ 51 | Registry.prototype.addSchema = function(definition) { 52 | if (!definition.id) { 53 | throw new Error('Missing schema id'); 54 | } 55 | return new Schema(this, definition); 56 | }; 57 | 58 | 59 | /** 60 | * @param {string} name Name of the schema to validate data against 61 | * @param {*} data The data to validate 62 | * @return {Array|null} 63 | */ 64 | Registry.prototype.validate = Registry.prototype.test = function(name, data) { 65 | if (!this.schemas[name]) { 66 | throw new Error('Unknown schema "' + name + '"'); 67 | } 68 | return this.schemas[name].test(data); 69 | }; 70 | -------------------------------------------------------------------------------- /lib/schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var TYPES = require('./types'); 3 | var Context = require('./context'); 4 | 5 | 6 | 7 | /** 8 | * @param {Registry} registry The registry object 9 | * @param {Object} definition The schema definition 10 | * @param {?number=} opt_depth (internal) Nesting limit inheritance 11 | * 12 | * @constructor 13 | */ 14 | function Schema(registry, definition, opt_depth) { 15 | this._allowUnknownProperties = false; 16 | this._registry = registry; 17 | 18 | this.id = undefined; 19 | this.type = TYPES.UNKNOWN; 20 | this.depth = opt_depth || registry.depth; 21 | 22 | // possible primitive values or array elements 23 | this.enum = undefined; 24 | this.ordered = false; 25 | 26 | // object property schemas 27 | this.properties = undefined; 28 | this.propertiesList = []; 29 | this.wildcard = undefined; 30 | 31 | // single schema for all array elements 32 | this.content = undefined; 33 | 34 | this.value = undefined; 35 | this.min = undefined; 36 | this.max = undefined; 37 | this.pattern = undefined; 38 | 39 | // schema value can be null 40 | this.allowNullValue = false; 41 | 42 | return this._configure(definition); 43 | } 44 | module.exports = Schema; 45 | 46 | 47 | /** 48 | * @param {Object} definition The schema definition 49 | * @throws {Error} 50 | * @return {Schema} this 51 | */ 52 | Schema.prototype._configure = function(definition) { 53 | var i, keys, tmp; 54 | tmp = this.typeOf(definition); 55 | 56 | if (tmp === TYPES.ARRAY) { 57 | throw new Error('Invalid schema definition'); 58 | } 59 | 60 | if (definition) { 61 | this.allowNullValue = definition.allowNullValue === true; 62 | } 63 | 64 | // handle schema references 65 | if (tmp === TYPES.STRING && definition.substr(0, 4) === '$id:') { 66 | tmp = definition.substr(4); 67 | 68 | if (!this._registry.schemas[tmp]) { 69 | throw new Error('Unknown reference schema "' + tmp + '"'); 70 | } 71 | return this._registry.schemas[tmp]; 72 | 73 | // handle primitive matching 74 | } else if (tmp !== TYPES.OBJECT) { 75 | this.type = tmp; 76 | this.value = definition; 77 | return this; 78 | 79 | } else if (!definition.type || 80 | typeof TYPES.BY_NAME[definition.type] !== 'number' || 81 | TYPES.BY_NAME[definition.type] < 1) { 82 | throw new Error('Invalid type definition'); 83 | } 84 | 85 | if (definition.id && 86 | typeof definition.id !== 'string') { 87 | throw new Error('Invalid schema id'); 88 | } 89 | this.id = definition.id; 90 | 91 | if (this.id) { 92 | this._registry.schemas[this.id] = this; 93 | } 94 | 95 | this.type = TYPES.BY_NAME[definition.type]; 96 | 97 | if (typeof definition.depth === 'number') { 98 | this.depth = definition.depth; 99 | } 100 | 101 | this.default = definition.default !== undefined ? 102 | definition.default : undefined; 103 | this.optional = definition.optional ? true : false; 104 | 105 | // PRIMITIVE 106 | if (this.type === TYPES.NUMBER || 107 | this.type === TYPES.INTEGER || 108 | this.type === TYPES.STRING) { 109 | 110 | if (definition.enum !== undefined) { 111 | if (Array.isArray(definition.enum)) { 112 | 113 | if (definition.enum.length === 1) { 114 | tmp = this.typeOf(definition.enum[0]); 115 | 116 | if (this.type === tmp || 117 | this.type === TYPES.NUMBER && tmp === TYPES.INTEGER) { 118 | this.value = definition.enum[0]; 119 | } else { 120 | throw new Error('Invalid enum element type'); 121 | } 122 | return this; 123 | } 124 | 125 | this.enum = new Array(definition.enum.length); 126 | 127 | for (i = 0; i < definition.enum.length; ++i) { 128 | tmp = this.typeOf(definition.enum[i]); 129 | 130 | if (this.type === tmp || 131 | this.type === TYPES.NUMBER && tmp === TYPES.INTEGER) { 132 | this.enum[i] = definition.enum[i]; 133 | } else { 134 | throw new Error('Invalid enum element type'); 135 | } 136 | } 137 | 138 | } else { 139 | throw new Error('Invalid enum definition'); 140 | } 141 | 142 | } else { 143 | if (typeof definition.min === 'number') { 144 | this.min = definition.min; 145 | } 146 | if (typeof definition.max === 'number') { 147 | this.max = definition.max; 148 | } 149 | if (this.type === TYPES.STRING && definition.pattern) { 150 | this.pattern = new RegExp(definition.pattern, definition.flags); 151 | } 152 | } 153 | 154 | // ARRAY 155 | } else if (this.type === TYPES.ARRAY) { 156 | if (typeof definition.min === 'number') { 157 | this.min = definition.min; 158 | } 159 | if (typeof definition.max === 'number') { 160 | this.max = definition.max; 161 | } 162 | 163 | if (definition.enum !== undefined) { 164 | if (Array.isArray(definition.enum)) { 165 | this.enum = new Array(definition.enum.length); 166 | 167 | for (i = 0; i < definition.enum.length; ++i) { 168 | this.enum[i] = new Schema(this._registry, 169 | definition.enum[i], this.depth); 170 | } 171 | 172 | if (definition.ordered) { 173 | this.ordered = true; 174 | } 175 | 176 | } else if (typeof definition.enum === 'object' || 177 | typeof definition.enum === 'string') { 178 | this.content = new Schema(this._registry, definition.enum, this.depth); 179 | 180 | } else { 181 | throw new Error('Invalid enum definition'); 182 | } 183 | } 184 | 185 | // OBJECT or FUNCTION 186 | } else if (this.type === TYPES.OBJECT || this.type === TYPES.FUNCTION) { 187 | this._allowUnknownProperties = 188 | definition.allowUnknownProperties || this.type === TYPES.FUNCTION; 189 | 190 | // number of arguments 191 | if (typeof definition.min === 'number') { 192 | this.min = definition.min; 193 | } 194 | if (typeof definition.max === 'number') { 195 | this.max = definition.max; 196 | } 197 | 198 | if (definition.properties) { 199 | if (this.typeOf(definition.properties) !== TYPES.OBJECT) { 200 | throw new Error('Invalid properties definition'); 201 | } 202 | 203 | this.propertiesList = keys = Object.keys(definition.properties); 204 | this.properties = {}; 205 | 206 | for (i = 0; i < keys.length; ++i) { 207 | if (keys[i] === '*' && this.type === TYPES.OBJECT) { 208 | this.wildcard = new Schema(this._registry, 209 | definition.properties[keys[i]], this.depth); 210 | 211 | this.propertiesList.splice(i, 1); 212 | --i; 213 | } else { 214 | this.properties[keys[i]] = new Schema(this._registry, 215 | definition.properties[keys[i]], this.depth); 216 | } 217 | } 218 | } 219 | 220 | // MIXED 221 | } else if (this.type === TYPES.MIXED) { 222 | if (!Array.isArray(definition.enum)) { 223 | throw new Error('Invalid enum definition'); 224 | } 225 | this.enum = new Array(definition.enum.length); 226 | 227 | for (i = 0; i < definition.enum.length; ++i) { 228 | this.enum[i] = new Schema(this._registry, definition.enum[i], this.depth); 229 | } 230 | } 231 | }; 232 | 233 | 234 | /** 235 | * @param {*} value The data to validate 236 | * @return {?Error} 237 | */ 238 | Schema.prototype.validate = Schema.prototype.test = function(value) { 239 | var context = new Context(); 240 | 241 | this._validate(value, context); 242 | 243 | if (context.errors.length > 0) { 244 | return context.errors; 245 | } 246 | return null; 247 | }; 248 | 249 | 250 | /** 251 | * @param {*} value The data to validate 252 | * @param {Context} context The validation context 253 | * @param {?string=} opt_key Optional data key 254 | * @return {void} 255 | */ 256 | Schema.prototype._validate = function(value, context, opt_key) { 257 | var type; 258 | 259 | if (this._registry.breakOnError && context.errors.length > 0) { 260 | return; 261 | } 262 | 263 | type = this.typeOf(value); 264 | context.up(this, opt_key); 265 | 266 | if (context.level > this.depth) { 267 | context.error('depth', value); 268 | return; 269 | } 270 | 271 | // console.log(); 272 | // console.log(this); 273 | // console.log(); 274 | // if (this.allowNullValue) { 275 | // console.log('\n\n\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>'); 276 | // console.log(`type: ${type}`); 277 | // console.log(`valueL ${value}`); 278 | // console.log(`this ${this}`); 279 | // } 280 | 281 | if (type === TYPES.UNDEFINED) { 282 | if (!this.optional) { 283 | context.error('type', value); 284 | } 285 | 286 | } else if (type === TYPES.NULL && this.allowNullValue) { 287 | context.down(); 288 | return; 289 | 290 | } else if (this.type === TYPES.MIXED) { 291 | this._validateMixed(value, context); 292 | 293 | // handle type mismatches 294 | } else if (this.type !== type && 295 | !(this.type === TYPES.NUMBER && type === TYPES.INTEGER)) { 296 | context.error('type', value); 297 | 298 | // extended validation 299 | } else { 300 | switch (type) { 301 | case TYPES.BOOLEAN: 302 | if (this.value !== undefined && this.value !== value) { 303 | context.error('value', value); 304 | } 305 | break; 306 | 307 | case TYPES.STRING: 308 | this._validateString(value, context); 309 | break; 310 | 311 | case TYPES.INTEGER: 312 | /* falls through */ 313 | 314 | case TYPES.NUMBER: 315 | this._validateNumber(value, context); 316 | break; 317 | 318 | case TYPES.ARRAY: 319 | this._validateArray(value, context); 320 | break; 321 | 322 | case TYPES.FUNCTION: 323 | this._validateFunction(value, context); 324 | break; 325 | 326 | case TYPES.OBJECT: 327 | this._validateObject(value, context); 328 | break; 329 | } 330 | } 331 | 332 | context.down(); 333 | }; 334 | 335 | 336 | /** 337 | * Validate a mixed value 338 | * 339 | * @param {*} value The data to validate 340 | * @param {Context} context The validation context 341 | */ 342 | Schema.prototype._validateMixed = function(value, context) { 343 | var i, j, k; 344 | 345 | // memorize error count (total, round) 346 | k = j = context.errors.length; 347 | 348 | for (i = 0; i < this.enum.length; ++i) { 349 | this.enum[i]._validate(value, context, '*'); 350 | 351 | if (this._registry.breakOnError && context.errors.length > j) { 352 | break; 353 | } 354 | 355 | // no errors; matching schema found 356 | if (context.errors.length === j) { 357 | // remove all errors 358 | context.errors = context.errors.slice(0, k); 359 | break; 360 | } 361 | 362 | // memorize error count for next round 363 | j = context.errors.length; 364 | } 365 | }; 366 | 367 | 368 | /** 369 | * Validate a string 370 | * 371 | * @param {Array} value The data to validate 372 | * @param {Context} context The validation context 373 | */ 374 | Schema.prototype._validateString = function(value, context) { 375 | if (this.value && this.value !== value) { 376 | context.error('value', value); 377 | 378 | } else if (this.min !== undefined && value.length < this.min) { 379 | context.error('min', value.length); 380 | 381 | } else if (this.max !== undefined && value.length > this.max) { 382 | context.error('max', value.length); 383 | 384 | } else if (this.enum && this.enum.indexOf(value) === -1) { 385 | context.error('value', value); 386 | 387 | } else if (this.pattern && !this.pattern.test(value)) { 388 | context.error('pattern', value); 389 | } 390 | }; 391 | 392 | 393 | /** 394 | * Validate a number 395 | * 396 | * @param {number} value The data to validate 397 | * @param {Context} context The validation context 398 | */ 399 | Schema.prototype._validateNumber = function(value, context) { 400 | if (this.value !== undefined && this.value !== value) { 401 | context.error('value', value); 402 | 403 | } else if (this.min !== undefined && value < this.min) { 404 | context.error('min', value); 405 | 406 | } else if (this.max !== undefined && value > this.max) { 407 | context.error('max', value); 408 | 409 | } else if (this.enum && this.enum.indexOf(value) === -1) { 410 | context.error('value', value); 411 | } 412 | }; 413 | 414 | 415 | /** 416 | * Validate an array 417 | * 418 | * @param {Array} value The data to validate 419 | * @param {Context} context The validation context 420 | */ 421 | Schema.prototype._validateArray = function(value, context) { 422 | var i, j; 423 | var eTotal; 424 | 425 | if (this.min !== undefined && value.length < this.min) { 426 | context.error('min', value.length); 427 | 428 | } else if (this.max !== undefined && value.length > this.max) { 429 | context.error('max', value.length); 430 | 431 | /* istanbul ignore else */ 432 | } else if (value.length > 0) { 433 | if (this.content) { 434 | for (i = 0; i < value.length; ++i) { 435 | this.content._validate(value[i], context, i); 436 | } 437 | 438 | } else if (this.enum) { 439 | if (this.ordered) { 440 | for (i = 0; i < value.length; ++i) { 441 | this.enum[i % this.enum.length]._validate(value[i], context, i); 442 | } 443 | 444 | } else { 445 | for (i = 0; i < value.length; ++i) { 446 | eTotal = context.errors.length; 447 | 448 | for (j = 0; j < this.enum.length; ++j) { 449 | this.enum[j]._validate(value[i], context, i); 450 | } 451 | 452 | if (this._registry.breakOnError && context.errors.length > eTotal) { 453 | break; 454 | } 455 | 456 | // less errors than tested schemas 457 | if (context.errors.length < this.enum.length + eTotal) { 458 | // remove all errors for this element 459 | context.errors = context.errors.slice(0, eTotal); 460 | } else { 461 | // leave only one error for this element 462 | context.errors = context.errors.slice(0, eTotal + 1); 463 | } 464 | } 465 | } 466 | } 467 | } 468 | }; 469 | 470 | 471 | /** 472 | * Validate an object 473 | * 474 | * @param {Object|Function} value The data to validate 475 | * @param {Context} context The validation context 476 | */ 477 | Schema.prototype._validateObject = function(value, context) { 478 | var i, keys; 479 | 480 | if (this.wildcard) { 481 | keys = Object.keys(value); 482 | 483 | for (i = 0; i < keys.length; ++i) { 484 | if (this.propertiesList.indexOf(keys[i]) === -1) { 485 | this.wildcard._validate(value[keys[i]], context, keys[i]); 486 | } 487 | } 488 | 489 | } else if (!this._allowUnknownProperties) { 490 | keys = Object.keys(value); 491 | 492 | for (i = 0; i < keys.length; ++i) { 493 | if (this.propertiesList.indexOf(keys[i]) === -1) { 494 | context.error('unknown', keys[i]); 495 | } 496 | } 497 | } 498 | 499 | // validate object property count 500 | if (this.min !== undefined || this.max !== undefined) { 501 | keys = keys || Object.keys(value); 502 | 503 | if (this.min !== undefined && keys.length < this.min) { 504 | context.error('min', keys.length); 505 | 506 | } else if (this.max !== undefined && keys.length > this.max) { 507 | context.error('max', keys.length); 508 | } 509 | } 510 | 511 | this._validateProperties(value, context); 512 | }; 513 | 514 | 515 | /** 516 | * Validate a function 517 | * 518 | * @param {Function} value The data to validate 519 | * @param {Context} context The validation context 520 | */ 521 | Schema.prototype._validateFunction = function(value, context) { 522 | if (this.min !== undefined && value.length < this.min) { 523 | context.error('min', value.length); 524 | } else if (this.max !== undefined && value.length > this.max) { 525 | context.error('max', value.length); 526 | } 527 | 528 | this._validateProperties(value, context); 529 | }; 530 | 531 | 532 | /** 533 | * Validate object properties 534 | * 535 | * @param {Object|Function} value The data to validate 536 | * @param {Context} context The validation context 537 | */ 538 | Schema.prototype._validateProperties = function(value, context) { 539 | var i; 540 | 541 | // validate object property definitions 542 | for (i = 0; i < this.propertiesList.length; ++i) { 543 | if (value[this.propertiesList[i]] === undefined && 544 | !this.properties[this.propertiesList[i]].optional) { 545 | if (this.properties[this.propertiesList[i]].default !== undefined) { 546 | value[this.propertiesList[i]] = 547 | this.properties[this.propertiesList[i]].default; 548 | } else { 549 | context.error('undefined', this.propertiesList[i]); 550 | } 551 | 552 | } else { 553 | this.properties[this.propertiesList[i]]._validate( 554 | value[this.propertiesList[i]], 555 | context, 556 | this.propertiesList[i]); 557 | } 558 | } 559 | }; 560 | 561 | 562 | Schema.prototype.typeOf = function(value) { 563 | var type; 564 | 565 | if (value === undefined) { 566 | return TYPES.UNDEFINED; 567 | 568 | // FACT: (typeof null) === 'object' 569 | } else if (value === null) { 570 | return TYPES.NULL; 571 | } 572 | 573 | type = typeof value; 574 | 575 | switch (type) { 576 | case 'boolean': 577 | return TYPES.BOOLEAN; 578 | 579 | case 'string': 580 | return TYPES.STRING; 581 | 582 | case 'number': 583 | if (value === Math.floor(value)) { 584 | return TYPES.INTEGER; 585 | } 586 | return TYPES.NUMBER; 587 | 588 | case 'object': 589 | if (Array.isArray(value)) { 590 | return TYPES.ARRAY; 591 | } else if (Buffer.isBuffer(value)) { 592 | return TYPES.BUFFER; 593 | } 594 | return TYPES.OBJECT; 595 | 596 | case 'function': 597 | return TYPES.FUNCTION; 598 | 599 | /* istanbul ignore next */ 600 | default: 601 | return TYPES.UNKNOWN; 602 | } 603 | }; 604 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var types = module.exports = exports; 3 | 4 | types.UNKNOWN = 0; 5 | types.UNDEFINED = 1; 6 | types.NULL = 2; 7 | 8 | // Primitives 9 | types.BOOLEAN = 4; 10 | types.NUMBER = 8; 11 | types.STRING = 16; 12 | 13 | // Objects 14 | types.OBJECT = 32; 15 | types.ARRAY = 64; 16 | types.FUNCTION = 128; 17 | 18 | // SSJS specific 19 | types.BUFFER = 256; 20 | 21 | // Artificial 22 | types.INTEGER = 512; 23 | 24 | // Special Case 25 | types.MIXED = 65536; 26 | 27 | 28 | types.BY_NAME = { 29 | 'unknown': types.UNKNOWN, 30 | 'undefined': types.UNDEFINED, 31 | 'null': types.NULL, 32 | 'boolean': types.BOOLEAN, 33 | 'number': types.NUMBER, 34 | 'string': types.STRING, 35 | 'object': types.OBJECT, 36 | 'array': types.ARRAY, 37 | 'function': types.FUNCTION, 38 | 'buffer': types.BUFFER, 39 | 'integer': types.INTEGER, 40 | 'mixed': types.MIXED 41 | }; 42 | 43 | types.BY_ID = {}; 44 | 45 | types.BY_ID[types.UNKNOWN] = 'unknown'; 46 | types.BY_ID[types.UNDEFINED] = 'undefined'; 47 | types.BY_ID[types.NULL] = 'null'; 48 | types.BY_ID[types.BOOLEAN] = 'boolean'; 49 | types.BY_ID[types.NUMBER] = 'number'; 50 | types.BY_ID[types.STRING] = 'string'; 51 | types.BY_ID[types.OBJECT] = 'object'; 52 | types.BY_ID[types.ARRAY] = 'array'; 53 | types.BY_ID[types.FUNCTION] = 'function'; 54 | types.BY_ID[types.BUFFER] = 'buffer'; 55 | types.BY_ID[types.INTEGER] = 'integer'; 56 | types.BY_ID[types.MIXED] = 'mixed'; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mgl-validate", 3 | "version": "2.1.0", 4 | "description": "data object validation", 5 | "keyswords": [ 6 | "json", 7 | "schema", 8 | "validate", 9 | "property", 10 | "validation", 11 | "object", 12 | "type" 13 | ], 14 | "license": "MIT", 15 | "author": { 16 | "name": "Malte-Thorben Bruns", 17 | "email": "malte.bruns+labs@magora.com" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/magora-labs/mgl-validate.git" 22 | }, 23 | "devDependencies": { 24 | "changelog42": "0.9.0", 25 | "eslint": "3.6.1", 26 | "nyc": "8.3.0", 27 | "mocha": "3.1.0" 28 | }, 29 | "scripts": { 30 | "lint": "eslint ./", 31 | "test": "npm run lint && nyc --reporter=lcov --reporter=text --reporter=text-summary _mocha ./test/test-*.js", 32 | "log": "changelog42 --no-author --commit-url=https://github.com/magora-labs/mgl-validate/commit", 33 | "clean": "rm -rf node_modules/ && rm -rf coverage/ && rm -rf .nyc_output/ && rm -f npm-debug.log" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/break-on-error.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global suite: false, setup: false, test: false, 3 | teardown: false, suiteSetup: false, suiteTeardown: false */ 4 | var assert = require('assert'); 5 | var Registry = require('../'); 6 | 7 | 8 | suite('breakOnError', function() { 9 | var registry; 10 | 11 | 12 | setup(function() { 13 | registry = new Registry({breakOnError: true}); 14 | }); 15 | 16 | 17 | test('mixed', function() { 18 | registry.addSchema({ 19 | id: 'break-on-error:mixed', 20 | type: 'mixed', 21 | enum: [{type: 'string'}, {type: 'number'}] 22 | }); 23 | 24 | var errors = registry.test('break-on-error:mixed', true); 25 | assert.deepEqual(errors, [['*', 'string', 'type', true]]); 26 | }); 27 | 28 | 29 | test('array', function() { 30 | registry.addSchema({ 31 | id: 'break-on-error:array', 32 | type: 'array', 33 | enum: [{type: 'string'}, {type: 'number'}] 34 | }); 35 | 36 | var errors = registry.test('break-on-error:array', [true, false, 1]); 37 | assert.deepEqual(errors, [['0', 'string', 'type', true]]); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --ui tdd 3 | -------------------------------------------------------------------------------- /test/test-array.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global suite: false, setup: false, test: false, 3 | teardown: false, suiteSetup: false, suiteTeardown: false */ 4 | var assert = require('assert'); 5 | var Registry = require('../'); 6 | 7 | 8 | suite('array', function() { 9 | var registry; 10 | 11 | setup(function() { 12 | registry = new Registry(); 13 | }); 14 | 15 | 16 | test('min/max', function() { 17 | registry.addSchema({ 18 | id: 'meArray', 19 | type: 'array', 20 | min: 1, 21 | max: 2 22 | }); 23 | 24 | var errors = registry.test('meArray', [1, 2]); 25 | assert(!errors); 26 | }); 27 | 28 | 29 | test('min', function() { 30 | registry.addSchema({ 31 | id: 'meArray', 32 | type: 'array', 33 | min: 2 34 | }); 35 | 36 | var errors = registry.test('meArray', [1]); 37 | 38 | assert.deepEqual(errors, [[ 39 | null, 40 | 'array', 41 | 'min', 42 | 1 43 | ]]); 44 | }); 45 | 46 | 47 | test('max', function() { 48 | registry.addSchema({ 49 | id: 'meArray', 50 | type: 'array', 51 | max: 2 52 | }); 53 | 54 | var errors = registry.test('meArray', [1, 2, 3]); 55 | 56 | assert.deepEqual(errors, [[ 57 | null, 58 | 'array', 59 | 'max', 60 | 3 61 | ]]); 62 | }); 63 | 64 | 65 | test('enum', function() { 66 | registry.addSchema({ 67 | id: 'meArray', 68 | type: 'array', 69 | enum: [1, 2, 3] 70 | }); 71 | 72 | var errors = registry.test('meArray', [3, 1, 2]); 73 | assert(!errors); 74 | }); 75 | 76 | 77 | test('enum/ordered', function() { 78 | registry.addSchema({ 79 | id: 'meArray', 80 | type: 'array', 81 | enum: [1, 2, 3], 82 | ordered: true 83 | }); 84 | 85 | registry.test('meArray', [1, 2, 3]); 86 | 87 | var errors = registry.test('meArray', [3, 1, '2']); 88 | 89 | assert.deepEqual(errors, [ 90 | ['0', 'integer', 'value', 3], 91 | ['1', 'integer', 'value', 1], 92 | ['2', 'integer', 'type', '2'] 93 | ]); 94 | 95 | errors = registry.test('meArray', [3, 2, 1]); 96 | 97 | assert.deepEqual(errors, [ 98 | ['0', 'integer', 'value', 3], 99 | ['2', 'integer', 'value', 1] 100 | ]); 101 | }); 102 | 103 | 104 | test('allowNullValue', function() { 105 | registry.addSchema({ 106 | id: 'meArray', 107 | type: 'array', 108 | min: 2, 109 | allowNullValue: true 110 | }); 111 | 112 | var errors = registry.test('meArray', null); 113 | assert(!errors); 114 | 115 | 116 | registry.addSchema({ 117 | id: 'me2Array', 118 | type: 'array', 119 | min: 2 120 | }); 121 | 122 | errors = registry.test('me2Array', null); 123 | assert.deepEqual(errors, [ 124 | [null, 'array', 'type', null] 125 | ]); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/test-function.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global suite: false, setup: false, test: false, 3 | teardown: false, suiteSetup: false, suiteTeardown: false */ 4 | var assert = require('assert'); 5 | var Registry = require('../'); 6 | 7 | 8 | suite('function', function() { 9 | var registry = new Registry(); 10 | 11 | test('properties', function() { 12 | registry.addSchema({ 13 | id: 'IAmFunction', 14 | type: 'function', 15 | properties: { 16 | mini: { 17 | type: 'boolean' 18 | }, 19 | name: 'you' 20 | } 21 | }); 22 | 23 | function me() { /* empty */ } 24 | me.mini = true; 25 | 26 | var errors = registry.test('IAmFunction', me); 27 | assert.deepEqual(errors, [[ 28 | 'name', 29 | 'string', 30 | 'value', 31 | 'me' 32 | ]]); 33 | }); 34 | 35 | 36 | test('min/max', function() { 37 | registry.addSchema({ 38 | id: 'IAmFunction', 39 | type: 'function', 40 | min: 1, 41 | max: 2 42 | }); 43 | 44 | /* eslint-disable no-unused-vars */ 45 | function me(a, b) { /* empty */ } 46 | /* eslint-enable no-unused-vars */ 47 | 48 | var errors = registry.test('IAmFunction', me); 49 | assert(!errors); 50 | }); 51 | 52 | 53 | test('min', function() { 54 | registry.addSchema({ 55 | id: 'IAmFunction', 56 | type: 'function', 57 | min: 1 58 | }); 59 | 60 | function me() { /* empty */ } 61 | 62 | var errors = registry.test('IAmFunction', me); 63 | assert.deepEqual(errors, [[ 64 | null, 65 | 'function', 66 | 'min', 67 | 0 68 | ]]); 69 | }); 70 | 71 | 72 | test('max', function() { 73 | registry.addSchema({ 74 | id: 'IAmFunction', 75 | type: 'function', 76 | max: 0 77 | }); 78 | 79 | /* eslint-disable no-unused-vars */ 80 | function me(a) { /* empty */ } 81 | /* eslint-enable no-unused-vars */ 82 | 83 | var errors = registry.test('IAmFunction', me); 84 | assert.deepEqual(errors, [[ 85 | null, 86 | 'function', 87 | 'max', 88 | 1 89 | ]]); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/test-mixed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global suite: false, setup: false, test: false, 3 | teardown: false, suiteSetup: false, suiteTeardown: false */ 4 | var assert = require('assert'); 5 | var Registry = require('../'); 6 | 7 | 8 | suite('mixed', function() { 9 | var registry = new Registry(); 10 | 11 | test('enum', function() { 12 | registry.addSchema({ 13 | id: 'mixed', 14 | type: 'mixed', 15 | enum: [ 16 | { 17 | type: 'function', 18 | min: 2, 19 | max: 2 20 | }, 21 | { 22 | id: 'uuid', 23 | type: 'string', 24 | pattern: '[a-f\\d]{8}(-[a-f\\d]{4}){3}-[a-f\\d]{12}' 25 | }, 26 | { 27 | type: 'object', 28 | properties: { 29 | whateva: { 30 | type: 'mixed', 31 | enum: ['$id:uuid', { 32 | type: 'number' 33 | }] 34 | } 35 | } 36 | } 37 | ] 38 | }); 39 | 40 | /* eslint-disable no-unused-vars */ 41 | var errors = registry.test('mixed', function(a, b) { /* empty */ }); 42 | /* eslint-enable no-unused-vars */ 43 | assert(!errors); 44 | 45 | errors = registry.test('mixed', 'c71960cc-d90c-4ac9-91f2-abb0f29543ac'); 46 | assert(!errors); 47 | 48 | errors = registry.test('mixed', 'xxx'); 49 | assert.deepEqual(errors, [ 50 | ['*', 'function', 'type', 'xxx'], 51 | ['*', 'string', 'pattern', 'xxx'], 52 | ['*', 'object', 'type', 'xxx'] 53 | ]); 54 | 55 | errors = registry.test('mixed', { 56 | whateva: 'c71960cc-d90c-4ac9-91f2-abb0f29543ac' 57 | }); 58 | assert(!errors); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/test-number.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global suite: false, setup: false, test: false, 3 | teardown: false, suiteSetup: false, suiteTeardown: false */ 4 | var assert = require('assert'); 5 | var Registry = require('../'); 6 | 7 | 8 | suite('number', function() { 9 | var registry; 10 | 11 | setup(function() { 12 | registry = new Registry(); 13 | }); 14 | 15 | 16 | test('value 0', function() { 17 | registry.addSchema({ 18 | id: 'meNumber', 19 | type: 'object', 20 | properties: { 21 | n: 0 22 | } 23 | }); 24 | 25 | var errors = registry.test('meNumber', {n: 1}); 26 | assert.deepEqual(errors, [[ 27 | 'n', 28 | 'integer', 29 | 'value', 30 | 1 31 | ]]); 32 | }); 33 | 34 | 35 | test('value 1', function() { 36 | registry.addSchema({ 37 | id: 'meNumber', 38 | type: 'object', 39 | properties: { 40 | n: 1 41 | } 42 | }); 43 | 44 | var errors = registry.test('meNumber', {n: 0}); 45 | assert.deepEqual(errors, [[ 46 | 'n', 47 | 'integer', 48 | 'value', 49 | 0 50 | ]]); 51 | }); 52 | 53 | 54 | test('value -1', function() { 55 | registry.addSchema({ 56 | id: 'meNumber', 57 | type: 'object', 58 | properties: { 59 | n: -1 60 | } 61 | }); 62 | 63 | var errors = registry.test('meNumber', {n: 0}); 64 | assert.deepEqual(errors, [[ 65 | 'n', 66 | 'integer', 67 | 'value', 68 | 0 69 | ]]); 70 | }); 71 | 72 | 73 | test('allowNullValue', function() { 74 | registry.addSchema({ 75 | id: 'meNumber', 76 | type: 'object', 77 | properties: { 78 | n: 0 79 | }, 80 | allowNullValue: true 81 | }); 82 | 83 | var errors = registry.test('meNumber', null); 84 | assert(!errors); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/test-object-default-properties.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global suite: false, setup: false, test: false, 3 | teardown: false, suiteSetup: false, suiteTeardown: false */ 4 | var assert = require('assert'); 5 | var Registry = require('../'); 6 | 7 | 8 | suite('object-default-properties', function() { 9 | var registry = new Registry(); 10 | 11 | test('defaults', function() { 12 | registry.addSchema({ 13 | id: 'restful_acns', 14 | type: 'object', 15 | properties: { 16 | visibilityTimeout: { 17 | type: 'number', 18 | min: 0, 19 | max: 43200, 20 | default: 30 21 | }, 22 | maximumMessageSize: { 23 | type: 'number', 24 | min: 1024, 25 | max: 65536, 26 | default: 65536 27 | }, 28 | messageRetentionPeriod: { 29 | type: 'number', 30 | min: 60, 31 | max: 1209600, 32 | default: 345600 33 | }, 34 | delay: { 35 | type: 'number', 36 | min: 0, 37 | max: 900, 38 | default: 0 39 | } 40 | } 41 | }); 42 | 43 | var data = {}; 44 | 45 | var errors = registry.test('restful_acns', data); 46 | assert(!errors); 47 | 48 | assert.strictEqual(30, data.visibilityTimeout); 49 | assert.strictEqual(65536, data.maximumMessageSize); 50 | assert.strictEqual(345600, data.messageRetentionPeriod); 51 | assert.strictEqual(0, data.delay); 52 | }); 53 | 54 | 55 | test('allowNullValue', function() { 56 | registry.addSchema({ 57 | id: 'foobar', 58 | type: 'object', 59 | allowUnknownProperties: true, 60 | min: 2, 61 | allowNullValue: true 62 | }); 63 | 64 | var errors = registry.test('foobar', null); 65 | assert(!errors); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/test-object-property-count.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global suite: false, setup: false, test: false, 3 | teardown: false, suiteSetup: false, suiteTeardown: false */ 4 | var assert = require('assert'); 5 | var Registry = require('../'); 6 | 7 | 8 | suite('object-property-count', function() { 9 | var registry = new Registry(); 10 | 11 | test('minimum count', function() { 12 | var schema = 'minobject'; 13 | 14 | registry.addSchema({ 15 | id: schema, 16 | type: 'object', 17 | allowUnknownProperties: true, 18 | min: 2 19 | }); 20 | 21 | var errors = registry.test(schema, { 22 | test: 1, 23 | test2: 1 24 | }); 25 | assert(!errors); 26 | 27 | errors = registry.test(schema, {test: 1}); 28 | assert.deepEqual(errors, [[ 29 | null, 30 | 'object', 31 | 'min', 32 | 1 33 | ]]); 34 | }); 35 | 36 | 37 | test('maximum count', function() { 38 | var schema = 'maxobject'; 39 | 40 | registry.addSchema({ 41 | id: schema, 42 | type: 'object', 43 | allowUnknownProperties: true, 44 | max: 2 45 | }); 46 | 47 | var errors = registry.test(schema, { 48 | test: 1 49 | }); 50 | assert(!errors); 51 | 52 | errors = registry.test(schema, { 53 | test: 1, 54 | test2: 1 55 | }); 56 | assert(!errors); 57 | 58 | errors = registry.test(schema, { 59 | test: 1, 60 | test2: 1, 61 | test3: 'thats too much' 62 | }); 63 | assert.deepEqual(errors, [[ 64 | null, 65 | 'object', 66 | 'max', 67 | 3 68 | ]]); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/test-object-wildcard-properties.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global suite: false, setup: false, test: false, 3 | teardown: false, suiteSetup: false, suiteTeardown: false */ 4 | var assert = require('assert'); 5 | var Registry = require('../'); 6 | 7 | 8 | suite('object-wildcard-properties', function() { 9 | var registry = new Registry(); 10 | 11 | test('add "wildcard" schema', function() { 12 | registry.addSchema({ 13 | id: 'wildcard', 14 | type: 'object', 15 | properties: { 16 | '*': { 17 | type: 'string' 18 | }, 19 | key: { 20 | type: 'number', 21 | min: 0 22 | }, 23 | notAWildcard: { 24 | type: 'number', 25 | enum: [1, 2, 3] 26 | } 27 | } 28 | }); 29 | }); 30 | 31 | 32 | test('validate "wildcard"', function() { 33 | var errors = registry.test('wildcard', { 34 | someKey: 'OK', 35 | someOtherKey: -1, 36 | key: -34, 37 | notAWildcard: 10 38 | }); 39 | 40 | assert.deepEqual(errors, [ 41 | ['someOtherKey', 'string', 'type', -1], 42 | ['key', 'number', 'min', -34], 43 | ['notAWildcard', 'number', 'value', 10] 44 | ]); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/test-optional.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global suite: false, setup: false, test: false, 3 | teardown: false, suiteSetup: false, suiteTeardown: false */ 4 | var assert = require('assert'); 5 | var Registry = require('../'); 6 | 7 | 8 | suite('optional', function() { 9 | 10 | 11 | test('string', function() { 12 | var registry = new Registry(); 13 | 14 | registry.addSchema({ 15 | id: 'number-string', 16 | type: 'number', 17 | optional: true 18 | }); 19 | 20 | var errors = registry.test('number-string', 'abc'); 21 | assert.deepEqual(errors, [[ 22 | null, 23 | 'number', 24 | 'type', 25 | 'abc' 26 | ]]); 27 | }); 28 | 29 | 30 | test('undefined', function() { 31 | var registry = new Registry(); 32 | 33 | registry.addSchema({ 34 | id: 'string-undefined', 35 | type: 'string', 36 | optional: true 37 | }); 38 | 39 | var errors = registry.test('string-undefined', undefined); 40 | assert(!errors); 41 | }); 42 | 43 | 44 | test('number', function() { 45 | var registry = new Registry(); 46 | 47 | registry.addSchema({ 48 | id: 'number-number', 49 | type: 'number', 50 | optional: true 51 | }); 52 | 53 | var errors = registry.test('number-number', 123); 54 | assert(!errors); 55 | }); 56 | 57 | 58 | test('!optional number', function() { 59 | var registry = new Registry(); 60 | 61 | registry.addSchema({ 62 | id: 'number', 63 | type: 'number' 64 | }); 65 | 66 | var errors = registry.test('number', undefined); 67 | assert.deepEqual(errors, [[ 68 | null, 69 | 'number', 70 | 'type', 71 | undefined 72 | ]]); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/test-primitive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global suite: false, setup: false, test: false, 3 | teardown: false, suiteSetup: false, suiteTeardown: false */ 4 | var assert = require('assert'); 5 | var Registry = require('../'); 6 | 7 | 8 | suite('primitive', function() { 9 | var registry = new Registry(); 10 | 11 | 12 | test('add "primitive" schema', function() { 13 | registry.addSchema({ 14 | id: 'primitive', 15 | type: 'object', 16 | properties: { 17 | flag: true, 18 | text: 'abc', 19 | v0id: null, 20 | uint: 123 21 | } 22 | }); 23 | }); 24 | 25 | 26 | test('validate "primitive"', function() { 27 | var errors = registry.test('primitive', { 28 | v0id: 1, 29 | flag: false, 30 | text: 'abc', 31 | uint: 123 32 | }); 33 | 34 | assert.deepEqual(errors, [ 35 | ['flag', 'boolean', 'value', false], 36 | ['v0id', 'null', 'type', 1] 37 | ]); 38 | }); 39 | 40 | 41 | test('remove "primitive" schema', function() { 42 | assert.deepEqual(registry.getSchemas(), ['primitive']); 43 | registry.removeSchema('primitive'); 44 | assert.deepEqual([], registry.getSchemas()); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/test-recursive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global suite: false, setup: false, test: false, 3 | teardown: false, suiteSetup: false, suiteTeardown: false */ 4 | var assert = require('assert'); 5 | var Registry = require('../'); 6 | 7 | 8 | suite('recursive', function() { 9 | 10 | 11 | test('string', function() { 12 | var registry = new Registry(); 13 | 14 | registry.addSchema({ 15 | id: 'virtual-condition', 16 | type: 'array', 17 | min: 2, 18 | max: 2, 19 | enum: { 20 | id: 'virtual-condition-set', 21 | type: 'mixed', 22 | enum: [ 23 | {type: 'string'}, 24 | {type: 'array', enum: '$id:virtual-condition-set'} 25 | ] 26 | } 27 | }); 28 | 29 | var errors = registry.test('virtual-condition', [ 30 | ['a', ['b', ['c', 'd']]], ['action'] 31 | ]); 32 | assert(!errors); 33 | }); 34 | 35 | 36 | test('ordered (limited)', function() { 37 | var registry = new Registry(); 38 | 39 | registry.addSchema({ 40 | id: 'virtual-condition', 41 | type: 'array', 42 | ordered: true, 43 | min: 2, 44 | max: 2, 45 | enum: [ 46 | { 47 | id: 'virtual-condition-set', 48 | type: 'mixed', 49 | enum: [ 50 | {type: 'string', min: 3}, 51 | {type: 'array', enum: '$id:virtual-condition-set', min: 1} 52 | ] 53 | }, 54 | { 55 | type: 'array', 56 | min: 1, 57 | enum: { 58 | type: 'string', 59 | min: 1 60 | } 61 | } 62 | ] 63 | }); 64 | 65 | var errors = registry.test('virtual-condition', [ 66 | ['abc', ['bcd', ['cde', 'def']]], ['action'] 67 | ]); 68 | assert(!errors); 69 | 70 | errors = registry.test('virtual-condition', [ 71 | ['abc', ['bcd', ['cde', 'def']]], ['action', ['action']] 72 | ]); 73 | assert.deepEqual(errors, [[ 74 | '1.1', 75 | 'string', 76 | 'type', 77 | ['action'] 78 | ]]); 79 | }); 80 | 81 | 82 | test('ordered (periodic)', function() { 83 | var registry = new Registry(); 84 | 85 | registry.addSchema({ 86 | id: 'virtual-condition', 87 | type: 'array', 88 | ordered: true, 89 | enum: [ 90 | { 91 | id: 'virtual-condition-set', 92 | type: 'mixed', 93 | enum: [ 94 | {type: 'string', min: 3}, 95 | {type: 'array', enum: '$id:virtual-condition-set', min: 1} 96 | ] 97 | }, 98 | { 99 | type: 'array', 100 | min: 1, 101 | enum: { 102 | type: 'string', 103 | min: 1 104 | } 105 | } 106 | ] 107 | }); 108 | 109 | var errors = registry.test('virtual-condition', [ 110 | ['abc', ['bcd', ['cde', 'def']]], ['action'] 111 | ]); 112 | assert(!errors); 113 | 114 | errors = registry.test('virtual-condition', [ 115 | ['abc', ['bcd', ['cde', 'def']]], ['action'], ['xxx', ['xxx']] 116 | ]); 117 | assert(!errors); 118 | 119 | errors = registry.test('virtual-condition', [ 120 | ['abc', ['bcd', ['cde', 'def']]], ['action'], ['xxx', ['xxx']], ['xxx', ['fails']] 121 | ]); 122 | 123 | assert.deepEqual(errors, [[ 124 | '3.1', 125 | 'string', 126 | 'type', 127 | ['fails'] 128 | ]]); 129 | }); 130 | 131 | 132 | test('object tree', function() { 133 | var registry = new Registry(); 134 | 135 | registry.addSchema({ 136 | id: 'tree-node', 137 | type: 'object', 138 | properties: { 139 | name: {type: 'string', min: 1}, 140 | children: { 141 | type: 'array', 142 | optional: true, 143 | enum: '$id:tree-node' 144 | } 145 | } 146 | }); 147 | 148 | var errors = registry.test('tree-node', { 149 | name: 'a0', 150 | children: [{ 151 | name: 'b0', 152 | children: [{ 153 | name: 'c0', 154 | children: [{ 155 | name: 'd0' 156 | }, { 157 | name: 'd1' 158 | }] 159 | }] 160 | }] 161 | }); 162 | 163 | assert(!errors); 164 | }); 165 | 166 | 167 | test('object tree (nesting limit)', function() { 168 | var registry = new Registry(); 169 | 170 | registry.addSchema({ 171 | id: 'tree-node', 172 | type: 'object', 173 | depth: 3, 174 | properties: { 175 | name: {type: 'string', min: 1}, 176 | children: { 177 | type: 'array', 178 | optional: true, 179 | enum: '$id:tree-node' 180 | } 181 | } 182 | }); 183 | 184 | var errors = registry.test('tree-node', { 185 | name: 'a0', 186 | children: [{ 187 | name: 'b0', 188 | children: [{ 189 | name: 'c0', 190 | children: [{ 191 | name: 'd0' 192 | }, { 193 | name: 'd1' 194 | }] 195 | }] 196 | }] 197 | }); 198 | 199 | assert.deepEqual(errors, [ 200 | ['children.0.children.0', 'object', 'depth', {name: 'c0', children: [{ 201 | name: 'd0' 202 | }, { 203 | name: 'd1' 204 | }]}] 205 | ]); 206 | }); 207 | 208 | 209 | test('object tree (global nesting limit)', function() { 210 | var registry = new Registry({ 211 | depth: 3 212 | }); 213 | 214 | registry.addSchema({ 215 | id: 'tree-node', 216 | type: 'object', 217 | properties: { 218 | name: {type: 'string', min: 1}, 219 | children: { 220 | type: 'array', 221 | optional: true, 222 | enum: '$id:tree-node' 223 | } 224 | } 225 | }); 226 | 227 | var errors = registry.test('tree-node', { 228 | name: 'a0', 229 | children: [{ 230 | name: 'b0', 231 | children: [{ 232 | name: 'c0', 233 | children: [{ 234 | name: 'd0' 235 | }, { 236 | name: 'd1' 237 | }] 238 | }] 239 | }] 240 | }); 241 | 242 | assert.deepEqual(errors, [ 243 | ['children.0.children.0', 'object', 'depth', {name: 'c0', children: [{ 244 | name: 'd0' 245 | }, { 246 | name: 'd1' 247 | }]}] 248 | ]); 249 | }); 250 | 251 | 252 | test('object tree (nesting limit inheritance)', function() { 253 | var registry = new Registry(); 254 | 255 | registry.addSchema({ 256 | id: 'tree-node', 257 | type: 'object', 258 | depth: 3, 259 | properties: { 260 | name: {type: 'string', min: 1}, 261 | children: { 262 | id: 'children', 263 | type: 'array', 264 | optional: true, 265 | enum: '$id:tree-node' 266 | } 267 | } 268 | }); 269 | 270 | assert.strictEqual(registry.depth, 10); 271 | assert.strictEqual(registry.schemas.children.depth, 3); 272 | }); 273 | }); 274 | -------------------------------------------------------------------------------- /test/test-schema-errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global suite: false, setup: false, test: false, 3 | teardown: false, suiteSetup: false, suiteTeardown: false */ 4 | var assert = require('assert'); 5 | var Registry = require('../'); 6 | 7 | 8 | suite('schema-errors', function() { 9 | var registry = new Registry(); 10 | 11 | 12 | test('Invalid type definition #1', function() { 13 | assert.throws(function() { 14 | registry.addSchema({id: 'xxx', a: 1}); 15 | }, function(err) { 16 | assert.strictEqual('Invalid type definition', err.message); 17 | return true; 18 | }); 19 | }); 20 | 21 | 22 | test('Invalid type definition #2', function() { 23 | assert.throws(function() { 24 | registry.addSchema({ 25 | type: 'garbage', 26 | id: 'xxx', 27 | a: 1 28 | }); 29 | }, function(err) { 30 | assert.strictEqual('Invalid type definition', err.message); 31 | return true; 32 | }); 33 | }); 34 | 35 | 36 | test('Invalid type definition #3', function() { 37 | assert.throws(function() { 38 | registry.addSchema({ 39 | type: 'unknown', 40 | id: 'xxx' 41 | }); 42 | }, function(err) { 43 | assert.strictEqual('Invalid type definition', err.message); 44 | return true; 45 | }); 46 | }); 47 | 48 | 49 | test('Unknown reference schema "unknown"', function() { 50 | assert.throws(function() { 51 | registry.addSchema({ 52 | id: 'xxx', 53 | type: 'object', 54 | properties: { 55 | a: '$id:unknown' 56 | } 57 | }); 58 | }, function(err) { 59 | assert.strictEqual('Unknown reference schema "unknown"', err.message); 60 | return true; 61 | }); 62 | }); 63 | 64 | 65 | test('Invalid properties definition', function() { 66 | assert.throws(function() { 67 | registry.addSchema({ 68 | id: 'xxx', 69 | type: 'object', 70 | properties: [] 71 | }); 72 | }, function(err) { 73 | assert.strictEqual('Invalid properties definition', err.message); 74 | return true; 75 | }); 76 | }); 77 | 78 | 79 | test('Invalid schema definition', function() { 80 | assert.throws(function() { 81 | registry.addSchema({ 82 | id: 'xxx', 83 | type: 'object', 84 | properties: { 85 | xxx: [] 86 | } 87 | }); 88 | }, function(err) { 89 | assert.strictEqual('Invalid schema definition', err.message); 90 | return true; 91 | }); 92 | }); 93 | 94 | 95 | test('Invalid enum element type', function() { 96 | assert.throws(function() { 97 | registry.addSchema({ 98 | id: 'xxx', 99 | type: 'string', 100 | enum: ['abc', 1, 2] 101 | }); 102 | }, function(err) { 103 | assert.strictEqual('Invalid enum element type', err.message); 104 | return true; 105 | }); 106 | }); 107 | 108 | 109 | test('Invalid enum definition', function() { 110 | assert.throws(function() { 111 | registry.addSchema({ 112 | id: 'xxx', 113 | type: 'string', 114 | enum: true 115 | }); 116 | }, function(err) { 117 | assert.strictEqual('Invalid enum definition', err.message); 118 | return true; 119 | }); 120 | }); 121 | 122 | 123 | test('Invalid enum definition # type = mixed', function() { 124 | assert.throws(function() { 125 | registry.addSchema({ 126 | id: 'xxx', 127 | type: 'mixed', 128 | enum: 'xxx' 129 | }); 130 | }, function(err) { 131 | assert.strictEqual('Invalid enum definition', err.message); 132 | return true; 133 | }); 134 | }); 135 | 136 | 137 | test('Unknown schema "unknown"', function() { 138 | assert.throws(function() { 139 | registry.removeSchema('unknown'); 140 | }, function(err) { 141 | assert.strictEqual('Unknown schema "unknown"', err.message); 142 | return true; 143 | }); 144 | }); 145 | 146 | 147 | test('Invalid enum element type #1', function() { 148 | assert.throws(function() { 149 | registry.addSchema({ 150 | id: 'erroneous', 151 | type: 'string', 152 | enum: ['a', 2] 153 | }); 154 | }, function(err) { 155 | assert.strictEqual('Invalid enum element type', err.message); 156 | return true; 157 | }); 158 | }); 159 | 160 | 161 | test('Invalid enum element type #2', function() { 162 | assert.throws(function() { 163 | registry.addSchema({ 164 | id: 'erroneous', 165 | type: 'string', 166 | enum: [2] 167 | }); 168 | }, function(err) { 169 | assert.strictEqual('Invalid enum element type', err.message); 170 | return true; 171 | }); 172 | }); 173 | 174 | 175 | test('Invalid enum definition', function() { 176 | assert.throws(function() { 177 | registry.addSchema({ 178 | id: 'erroneous', 179 | type: 'array', 180 | enum: 1 181 | }); 182 | }, function(err) { 183 | assert.strictEqual('Invalid enum definition', err.message); 184 | return true; 185 | }); 186 | }); 187 | 188 | 189 | test('Invalid schema id', function() { 190 | assert.throws(function() { 191 | registry.addSchema({ 192 | id: 2, 193 | type: 'array' 194 | }); 195 | }, function(err) { 196 | assert.strictEqual('Invalid schema id', err.message); 197 | return true; 198 | }); 199 | }); 200 | 201 | 202 | test('Missing schema id', function() { 203 | assert.throws(function() { 204 | registry.addSchema({ 205 | type: 'array' 206 | }); 207 | }, function(err) { 208 | assert.strictEqual('Missing schema id', err.message); 209 | return true; 210 | }); 211 | }); 212 | }); 213 | -------------------------------------------------------------------------------- /test/test-schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global suite: false, setup: false, test: false, 3 | teardown: false, suiteSetup: false, suiteTeardown: false */ 4 | var assert = require('assert'); 5 | var validate = require('../'); 6 | 7 | 8 | suite('schema', function() { 9 | var registry = validate(); 10 | 11 | 12 | test('add "uuid" schema', function() { 13 | registry.addSchema({ 14 | id: 'uuid', 15 | type: 'string', 16 | pattern: '[a-f\\d]{8}(-[a-f\\d]{4}){3}-[a-f\\d]{12}' 17 | }); 18 | 19 | registry.schemas.uuid.typeOf(new Buffer(1)); 20 | }); 21 | 22 | 23 | test('validate "uuid"', function() { 24 | registry.test('uuid', 'c71960cc-d90c-4ac9-91f2-abb0f29543ac'); 25 | 26 | var errors = registry.test('uuid', 'c71960cc-d90c-4ac991f2-abb0f29543ac'); 27 | assert.deepEqual(errors, [[ 28 | null, 29 | 'string', 30 | 'pattern', 31 | 'c71960cc-d90c-4ac991f2-abb0f29543ac' 32 | ]]); 33 | }); 34 | 35 | 36 | test('remove "uuid" schema', function() { 37 | assert.deepEqual(['uuid'], registry.getSchemas()); 38 | registry.removeSchema('uuid'); 39 | assert.deepEqual([], registry.getSchemas()); 40 | }); 41 | 42 | 43 | test('add "uuid" schema, again', function() { 44 | registry.addSchema({ 45 | id: 'uuid', 46 | type: 'string', 47 | pattern: '[a-f\\d]{8}(-[a-f\\d]{4}){3}-[a-f\\d]{12}' 48 | }); 49 | }); 50 | 51 | 52 | test('add "document" schema', function() { 53 | registry.addSchema({ 54 | id: 'document', 55 | type: 'object', 56 | allowUnknownProperties: true, 57 | properties: { 58 | id: '$id:uuid', 59 | hello: 'world', 60 | things: { 61 | type: 'number', 62 | min: 0, 63 | max: 10 64 | }, 65 | cars: { 66 | type: 'integer' 67 | }, 68 | flag: { 69 | type: 'boolean' 70 | }, 71 | test: { 72 | type: 'string', 73 | min: 3, 74 | max: 255, 75 | optional: true 76 | }, 77 | list: { 78 | type: 'array', 79 | enum: ['a', 'b', 'c'] 80 | }, 81 | list2: { 82 | type: 'array', 83 | enum: { 84 | type: 'number' 85 | } 86 | }, 87 | obj: { 88 | type: 'object' 89 | } 90 | } 91 | }); 92 | 93 | assert.strictEqual( 94 | registry.schemas.document.properties.id, registry.schemas.uuid); 95 | }); 96 | 97 | 98 | test('add "stream" schema', function() { 99 | registry.addSchema({ 100 | id: 'stream', 101 | type: 'array', 102 | min: 1, 103 | max: 100, 104 | enum: '$id:document' 105 | }); 106 | 107 | assert.deepEqual([ 108 | 'uuid', 109 | 'document', 110 | 'stream' 111 | ], registry.getSchemas()); 112 | }); 113 | 114 | 115 | test('validate unknown schema', function() { 116 | assert.throws(function() { 117 | registry.test('unknown', {}); 118 | }, function(err) { 119 | assert.strictEqual('Unknown schema "unknown"', err.message); 120 | return true; 121 | }); 122 | }); 123 | 124 | 125 | test('validate "stream"', function() { 126 | var errors = registry.test('stream', [{ 127 | id: 'c71960cc-d90c-4ac9-91f2-abb0f29543ac', 128 | hello: 'world', 129 | test: 'wtf?!', 130 | things: 30, 131 | cars: 2.5, 132 | flag: 1, 133 | list: ['a', 'b'], 134 | list2: [1, 2, 3] 135 | }, { 136 | id: 'c71960cc-d90c4ac9-91f2-abb0f29543ac', 137 | hello: 'word', 138 | test: 1, 139 | things: 2, 140 | cars: 43, 141 | flag: true, 142 | list: ['d'], 143 | list2: [1, 'c', 3] 144 | }, { 145 | id: 'c71960cc-d90c-4ac9-91f2-abb0f29543ac', 146 | wtf: 1, 147 | list2: [1, 2, 3], 148 | obj: {} 149 | }]); 150 | 151 | assert.deepEqual(errors, [ 152 | ['0.things', 'number', 'max', 30], 153 | ['0.cars', 'integer', 'type', 2.5], 154 | ['0.flag', 'boolean', 'type', 1], 155 | ['0', 'object', 'undefined', 'obj'], 156 | ['1.id', 'string', 'pattern', 'c71960cc-d90c4ac9-91f2-abb0f29543ac'], 157 | ['1.hello', 'string', 'value', 'word'], 158 | ['1.test', 'string', 'type', 1], 159 | ['1.list.0', 'string', 'value', 'd'], 160 | ['1.list2.1', 'number', 'type', 'c'], 161 | ['1', 'object', 'undefined', 'obj'], 162 | ['2', 'object', 'undefined', 'hello'], 163 | ['2', 'object', 'undefined', 'things'], 164 | ['2', 'object', 'undefined', 'cars'], 165 | ['2', 'object', 'undefined', 'flag'], 166 | ['2', 'object', 'undefined', 'list'] 167 | ]); 168 | }); 169 | 170 | 171 | test('validate "stream" w/ breakOnError', function() { 172 | registry.breakOnError = true; 173 | 174 | var errors = registry.test('stream', [{ 175 | id: 'c71960cc-d90c-4ac9-91f2-abb0f29543ac', 176 | hello: 'world', 177 | test: 'wtf?!', 178 | things: 30, 179 | flag: 1, 180 | list: ['a', 'b'] 181 | }, { 182 | id: 'c71960cc-d90c4ac9-91f2-abb0f29543ac', 183 | hello: 'word', 184 | test: 1, 185 | things: 2, 186 | flag: true, 187 | list: ['d'] 188 | }]); 189 | 190 | assert.deepEqual(errors, [ 191 | ['0.things', 'number', 'max', 30], 192 | ['0', 'object', 'undefined', 'cars'], 193 | ['0', 'object', 'undefined', 'list2'], 194 | ['0', 'object', 'undefined', 'obj'] 195 | ]); 196 | 197 | registry.breakOnError = false; 198 | }); 199 | 200 | 201 | test('add "kitkat" schema', function() { 202 | registry.addSchema({ 203 | id: 'kitkat', 204 | type: 'object', 205 | properties: { 206 | key: { 207 | type: 'number', 208 | min: 0 209 | }, 210 | notAWildcard: { 211 | type: 'number', 212 | enum: [1, 2, 3] 213 | } 214 | } 215 | }); 216 | }); 217 | 218 | 219 | test('validate "kitkat"', function() { 220 | var errors = registry.test('kitkat', { 221 | someKey: 'OK', 222 | someOtherKey: -1, 223 | key: -34, 224 | notAWildcard: 10 225 | }); 226 | 227 | assert.deepEqual(errors, [ 228 | [null, 'object', 'unknown', 'someKey'], 229 | [null, 'object', 'unknown', 'someOtherKey'], 230 | ['key', 'number', 'min', -34], 231 | ['notAWildcard', 'number', 'value', 10] 232 | ]); 233 | }); 234 | }); 235 | -------------------------------------------------------------------------------- /test/test-string.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global suite: false, setup: false, test: false, 3 | teardown: false, suiteSetup: false, suiteTeardown: false */ 4 | var assert = require('assert'); 5 | var Registry = require('../'); 6 | 7 | 8 | suite('string', function() { 9 | var registry; 10 | 11 | setup(function() { 12 | registry = new Registry(); 13 | }); 14 | 15 | 16 | test('min/max', function() { 17 | registry.addSchema({ 18 | id: 'meString', 19 | type: 'string', 20 | min: 1, 21 | max: 2 22 | }); 23 | 24 | var errors = registry.test('meString', '1'); 25 | assert(!errors); 26 | }); 27 | 28 | 29 | test('min', function() { 30 | registry.addSchema({ 31 | id: 'meString', 32 | type: 'string', 33 | min: 1 34 | }); 35 | 36 | var errors = registry.test('meString', ''); 37 | assert.deepEqual(errors, [[null, 'string', 'min', 0]]); 38 | }); 39 | 40 | 41 | test('max', function() { 42 | registry.addSchema({ 43 | id: 'meString', 44 | type: 'string', 45 | max: 0 46 | }); 47 | 48 | var errors = registry.test('meString', '1'); 49 | assert.deepEqual(errors, [[null, 'string', 'max', 1]]); 50 | }); 51 | 52 | 53 | test('enum ok', function() { 54 | registry.addSchema({ 55 | id: 'meString', 56 | type: 'string', 57 | enum: ['1', '2'] 58 | }); 59 | 60 | var errors = registry.test('meString', '1'); 61 | assert(!errors); 62 | }); 63 | 64 | 65 | test('enum err', function() { 66 | registry.addSchema({ 67 | id: 'meString', 68 | type: 'string', 69 | enum: ['1', '2'] 70 | }); 71 | 72 | var errors = registry.test('meString', '3'); 73 | assert.deepEqual(errors, [[null, 'string', 'value', '3']]); 74 | }); 75 | 76 | 77 | test('enum (single element)', function() { 78 | registry.addSchema({ 79 | id: 'meString', 80 | type: 'string', 81 | enum: ['1'] 82 | }); 83 | 84 | var errors = registry.test('meString', '3'); 85 | assert.deepEqual(errors, [[null, 'string', 'value', '3']]); 86 | }); 87 | 88 | 89 | test('allowNullValue', function() { 90 | registry.addSchema({ 91 | id: 'meString', 92 | type: 'string', 93 | enum: ['1'], 94 | allowNullValue: true 95 | }); 96 | 97 | var errors = registry.test('meString', null); 98 | assert(!errors); 99 | }); 100 | }); 101 | --------------------------------------------------------------------------------