├── .gitignore ├── .jscsrc ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── EXAMPLE.md ├── LICENSE ├── README.md ├── doc ├── example.js ├── example.json ├── example │ ├── additional.js │ ├── assigned-rule.js │ ├── bail.js │ ├── deep.js │ ├── inline-rule.js │ ├── instanceof.js │ ├── len.js │ ├── match.js │ ├── max.js │ ├── message-clone.js │ ├── message-function.js │ ├── message-override.js │ ├── message.js │ ├── min.js │ ├── multiple-rules.js │ ├── multiple-types.js │ ├── pattern.js │ ├── placeholder.js │ ├── plugin-rule.js │ ├── range.js │ ├── required.js │ ├── source-type.js │ ├── state.js │ ├── type.js │ └── whitespace.js ├── readme.md ├── readme │ ├── api.md │ ├── developer.md │ ├── guide.md │ ├── install.md │ ├── introduction.md │ ├── license.md │ ├── links.md │ ├── messages.md │ ├── transform.md │ └── usage.md ├── transform.js └── usage.js ├── index.js ├── lib ├── iterator.js ├── reason.js ├── rule.js └── schema.js ├── messages.js ├── mkdoc.js ├── package.json ├── plugin ├── all.js ├── array.js ├── boolean.js ├── date.js ├── enum.js ├── float.js ├── function.js ├── integer.js ├── multiple.js ├── null.js ├── number.js ├── object.js ├── regexp.js ├── string.js ├── util.js └── util │ ├── pattern.js │ ├── range.js │ ├── required.js │ └── type.js ├── system.js ├── test ├── build.js ├── fixtures │ ├── component.js │ ├── email.js │ ├── model.js │ ├── plugin.js │ ├── schema │ │ ├── additional.js │ │ ├── array-max.js │ │ ├── array-min.js │ │ ├── array-range.js │ │ ├── array.js │ │ ├── assigned-rule.js │ │ ├── boolean.js │ │ ├── date-no-format.js │ │ ├── date-pattern.js │ │ ├── date-range.js │ │ ├── date.js │ │ ├── deep-array.js │ │ ├── deep-details.js │ │ ├── deep-object.js │ │ ├── deep-required.js │ │ ├── deep.js │ │ ├── enum.js │ │ ├── error-fields.js │ │ ├── float.js │ │ ├── function-length.js │ │ ├── function-max.js │ │ ├── function-min.js │ │ ├── function-range.js │ │ ├── function.js │ │ ├── instanceof-anonymous.js │ │ ├── instanceof-message.js │ │ ├── instanceof.js │ │ ├── integer.js │ │ ├── internal-error.js │ │ ├── length-array.js │ │ ├── length-number.js │ │ ├── length-string.js │ │ ├── match.js │ │ ├── message-function-error.js │ │ ├── message-function.js │ │ ├── message-literal.js │ │ ├── message-object.js │ │ ├── message-string-override.js │ │ ├── message-string.js │ │ ├── multiple-rules-function.js │ │ ├── multiple-rules-source.js │ │ ├── multiple-rules.js │ │ ├── multiple-types-required.js │ │ ├── multiple-types.js │ │ ├── null.js │ │ ├── number-max.js │ │ ├── number-min.js │ │ ├── number-range.js │ │ ├── number.js │ │ ├── object-additional.js │ │ ├── object.js │ │ ├── options-pattern.js │ │ ├── options.js │ │ ├── parallel.js │ │ ├── placeholder.js │ │ ├── plugin.js │ │ ├── raise.js │ │ ├── regexp.js │ │ ├── source-additional.js │ │ ├── state.js │ │ ├── transform.js │ │ ├── typed-array-empty.js │ │ ├── typed-array-mixed.js │ │ ├── typed-array.js │ │ └── vars.js │ ├── value-message.js │ └── vars-plugin.js ├── global.js ├── index.html ├── mocha.opts └── spec │ ├── additional.js │ ├── array-values.js │ ├── array.js │ ├── assigned-rule.js │ ├── boolean.js │ ├── clone.js │ ├── date.js │ ├── deep.js │ ├── enum.js │ ├── error-fields.js │ ├── float.js │ ├── function.js │ ├── instanceof.js │ ├── integer.js │ ├── internal-error.js │ ├── iterator.js │ ├── length.js │ ├── match.js │ ├── message-literal.js │ ├── message-object.js │ ├── messages.js │ ├── multiple-rules.js │ ├── multiple-types.js │ ├── null.js │ ├── number.js │ ├── object-additional.js │ ├── object.js │ ├── options.js │ ├── parallel.js │ ├── placeholder.js │ ├── plugin.js │ ├── raise.js │ ├── regexp.js │ ├── resolve.js │ ├── revalidate.js │ ├── rule.js │ ├── schema.js │ ├── source-additional.js │ ├── source.js │ ├── state.js │ ├── string.js │ ├── transform.js │ ├── undef.js │ └── vars.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /async-validate.js 3 | /test/spec.js 4 | /test/index.js 5 | *.bak 6 | *.log 7 | node_modules 8 | coverage 9 | target 10 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "fileExtensions": [".js", "jscs"], 3 | "requireSemicolons": false, 4 | "requireParenthesesAroundIIFE": true, 5 | "maximumLineLength": 80, 6 | "validateLineBreaks": "LF", 7 | "validateIndentation": 2, 8 | "requireMultipleVarDecl": false, 9 | "disallowTrailingComma": true, 10 | "disallowSpacesInConditionalExpression": false, 11 | "disallowSpaceAfterKeywords": false, 12 | "disallowSpaceBeforeBlockStatements": false, 13 | "disallowSpacesInsideObjectBrackets": null, 14 | "excludeFiles": [ 15 | "test/spec.js", 16 | "node_modules", 17 | "coverage", 18 | "async-validate.js" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/spec.js 3 | coverage 4 | async-validate.js 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "laxcomma" : true, 3 | "laxbreak" : true, 4 | "node" : true, 5 | "curly" : true, 6 | "bitwise" : false, 7 | "undef" : true, 8 | "eqeqeq" : true, 9 | "noarg" : true, 10 | "mocha" : true, 11 | "unused" : true, 12 | "asi" : true 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | script: npm run cover 3 | node_js: 4 | - stable 5 | after_success: 6 | - npm run coveralls 7 | deploy: 8 | provider: npm 9 | email: freeformsystems@gmail.com 10 | api_key: 11 | secure: kOctSa5zH42RbwgIVF8z8MuVoqq283QVWqxAVCoHExHk8y6yM3KHyTHyVR+rxUUzZrcdLk9jxB6A4aVP0+6FQNVFpnp/kqO9BpNX0ROofrjst0xMlaZ6wUFMLlpdiauRhn3FUfyRy9PChz/7yPQ/OnlKSNr5zUKtIvtZXN37NQSK1jC27LmFMIpUSpZ0b0XWhx33xbkBQXvKvdmCv2ISs+mrex1lQJiDZQ9cpRS/7dJMIjool8S+IfOZCNfGwHg4dClgKD6XJc9KDxrOaTmQU+/H8xasC/SCXHDNz9GfsYOe2AIPBvRGvU8bn6AuIvUBWvn0gyNaqsRDfbBXWer+sMgzzhJnrEOU9v8m76U1Z1KT8UfWOBQ1Dcb9UTvoOhA00jJSRUA64t1pUPy/ErC8mCLS2TPxGLGH5X0Cgnz2jC1a36nMUzdlGHUC7ZZH216RxMPPLUZi3QWwDuyxtQeotSiBvFCPtZ97MV9TmoNe8laW9lVDGb6uVwTz1rTndAMhbQu/hMBm0AhvkHO0nPhB/xlC2GGfZR8ivZEYx75jD8XQJk9mMEwrDvVkBl07AH1VoDWOVEX2le4YmHhuxKjqct0SglymPvaa/WRl1XaEY1l3SSVmR7k4WD6XcYySgEsHco+RUWdZcjHTaERCK9NDDuNKyfTDnvEYm7pw/6t4vQU= 12 | on: 13 | tags: true 14 | repo: tmpfs/async-validate 15 | node: 'stable' 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Freeform Systems and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | NO WARRANTY OR LIABILITY 23 | 24 | YOU EXPRESSLY AGREE THAT DOWNLOADING THE SOFTWARE AND ANY USE OF THE 25 | SOFTWARE IS AT YOUR OWN RISK. NO WARRANTY, REPRESENTATION, CONDITION, 26 | UNDERTAKING OR TERM - EXPRESS OR IMPLIED, STATUTORY OR OTHERWISE - 27 | INCLUDING BUT NOT LIMITED TO THE CONDITION, QUALITY, DURABILITY, 28 | PERFORMANCE, ACCURACY, STABILITY, RELIABILITY, NON-INFRINGEMENT, 29 | MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE OR USE OF THE 30 | SOFTWARE IS GIVEN OR ASSUMED BY FREEFORM SYSTEMS LTD. ALL SUCH 31 | WARRANTIES, REPRESENTATIONS, CONDITIONS, UNDERTAKINGS AND TERMS ARE 32 | HEREBY EXCLUDED. FREEFORM SYSTEMS LTD MAKES NO WARRANTY THAT THE 33 | SOFTWARE WILL MEET YOUR REQUIREMENTS, OR THAT IT WILL BE UNINTERRUPTED, 34 | TIMELY, SECURE, OR ERROR FREE; IN NO EVENT SHALL FREEFORM SYSTEMS LTD 35 | BE LIABLE TO ANY PARTY FOR ANY DAMAGES INCLUDING WITHOUT LIMITATION, 36 | ANY DIRECT, INDIRECT, SPECIAL, PUNITIVE, INCIDENTAL OR CONSEQUENTIAL 37 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, DAMAGES FOR LOSS OF BUSINESS 38 | PROFITS, BUSINESS INTERRUPTION, LOSS OF PROGRAMS OR INFORMATION, LOSS 39 | OF PROFITS AND SAVINGS AND THE LIKE), OR ANY OTHER DAMAGES ARISING - 40 | IN ANY WAY, SHAPE OR FORM - OUT OF THE AVAILABILITY, USE, RELIANCE ON, 41 | INABILITY TO UTILIZE OR IMPROPER USE OF THE SOFTWARE EVEN IF FREEFORM 42 | SYSTEMS LTD SHALL HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, 43 | AND REGARDLESS OF THE FORM OF ACTION, WHETHER IN CONTRACT, TORT, OR 44 | OTHERWISE. BECAUSE SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR 45 | LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, THE ABOVE EXCLUSIONS 46 | OF INCIDENTAL AND CONSEQUENTIAL DAMAGES MAY NOT APPLY TO YOU. 47 | -------------------------------------------------------------------------------- /doc/example.js: -------------------------------------------------------------------------------- 1 | // build the examples to markdown 2 | var fs = require('fs') 3 | , path = require('path') 4 | , async = require('async') 5 | , exec = require('child_process').execSync 6 | , dir = path.join(__dirname, 'example') 7 | , contents = fs.readdirSync(dir) 8 | , pkg = require('../package.json'); 9 | 10 | contents = contents.map(function(file) { 11 | return path.join(dir, file); 12 | }); 13 | 14 | console.log('### Examples\n'); 15 | async.eachSeries(contents, function(file, cb) { 16 | var name = path.basename(file, '.js') 17 | , js = '' + fs.readFileSync(file) 18 | , cmd = 'node ' + file 19 | , res = exec(cmd); 20 | 21 | js = js.replace(/\.\.\/\.\./g, pkg.name); 22 | 23 | console.log('#### %s\n', name); 24 | console.log('- [doc/example/%s](%s)\n', name, '/doc/example/' + name + '.js'); 25 | 26 | console.log('```javascript\n%s\n```\n', js); 27 | console.log('```\n%s\n```\n', res); 28 | 29 | cb(); 30 | }, function(err) { 31 | if(err) { 32 | throw err; 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /doc/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Async Validate", 3 | "pedantic": true, 4 | "include": "doc/readme", 5 | "require": "lib", 6 | "links": "links.md", 7 | "toc": "Table of Contents", 8 | "base": "https://github.com/tmpfs/async-validate", 9 | "partial": [ 10 | { 11 | "bin": "node doc/example.js" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /doc/example/additional.js: -------------------------------------------------------------------------------- 1 | // generate errors when additional fields are present 2 | var Schema = require('../..') 3 | , opts = {field: 'root'} 4 | , descriptor = { 5 | type: 'object', 6 | additional: false, 7 | fields: { 8 | address: { 9 | type: 'object', 10 | required: true, 11 | additional: false, 12 | fields: { 13 | street: {type: 'string', required: true}, 14 | city: {type: 'string', required: true}, 15 | zip: { 16 | type: 'string', 17 | required: true, 18 | len: 8, 19 | message: 'Invalid zip' 20 | } 21 | } 22 | } 23 | } 24 | } 25 | , source = { 26 | id: 'unknown-field', 27 | name: 'unknown-field', 28 | address: { 29 | name: 'unknown-field', 30 | street: 'Mock St', 31 | city: 'Mock City', 32 | zip: '12345678' 33 | } 34 | } 35 | , schema; 36 | 37 | require('../../plugin/all'); 38 | 39 | schema = new Schema(descriptor); 40 | schema.validate(source, opts, function(err, res) { 41 | console.dir(res.errors); 42 | }); 43 | -------------------------------------------------------------------------------- /doc/example/assigned-rule.js: -------------------------------------------------------------------------------- 1 | // assign a function to a rule 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | id: { 7 | expected: 'foo', 8 | test: function(cb) { 9 | if(this.value !== this.expected) { 10 | this.raise( 11 | this.reason('unexpected-id'), 12 | 'id expects %s, got %s', 13 | this.expected, 14 | this.value 15 | ) 16 | } 17 | cb(); 18 | } 19 | } 20 | } 21 | } 22 | , source = {id: 'qux'} 23 | , schema; 24 | 25 | require('../../plugin/all'); 26 | 27 | schema = new Schema(descriptor); 28 | schema.validate(source, function(err, res) { 29 | console.dir(res.errors); 30 | }); 31 | -------------------------------------------------------------------------------- /doc/example/bail.js: -------------------------------------------------------------------------------- 1 | // bail on first error encountered 2 | var Schema = require('../..') 3 | , opts = {bail: true} 4 | , descriptor = { 5 | type: 'object', 6 | fields: { 7 | address: { 8 | type: 'object', 9 | fields: { 10 | name: {type: 'string', required: true}, 11 | street: {type: 'string', required: true}, 12 | city: {type: 'string', required: true}, 13 | zip: {type: 'string', required: true} 14 | } 15 | } 16 | } 17 | } 18 | , source = {address: {name: '1024c'}} 19 | , schema; 20 | 21 | require('../../plugin/all'); 22 | 23 | schema = new Schema(descriptor); 24 | schema.validate(source, opts, function(err, res) { 25 | console.dir(res.errors); 26 | }); 27 | -------------------------------------------------------------------------------- /doc/example/deep.js: -------------------------------------------------------------------------------- 1 | // validate properties of a nested object 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | address: { 7 | type: 'object', 8 | fields: { 9 | name: {type: 'string', required: true}, 10 | street: {type: 'string', required: true}, 11 | city: {type: 'string', required: true}, 12 | zip: {type: 'string', required: true} 13 | } 14 | } 15 | } 16 | } 17 | , source = {address: {name: '1024c', street: 'Mock St', city: 'Mock City'}} 18 | , schema; 19 | 20 | require('../../plugin/all'); 21 | 22 | schema = new Schema(descriptor); 23 | schema.validate(source, function(err, res) { 24 | console.dir(res.errors); 25 | }); 26 | -------------------------------------------------------------------------------- /doc/example/inline-rule.js: -------------------------------------------------------------------------------- 1 | // assign a function as a rule 2 | var Schema = require('../..') 3 | , reserved = ['foo'] 4 | , descriptor = { 5 | type: 'object', 6 | fields: { 7 | id: function(cb) { 8 | if(~reserved.indexOf(this.value)) { 9 | this.raise('%s is a reserved id', this.value); 10 | } 11 | cb(); 12 | } 13 | } 14 | } 15 | , source = {id: 'foo'} 16 | , schema; 17 | 18 | require('../../plugin/all'); 19 | 20 | schema = new Schema(descriptor); 21 | schema.validate(source, function(err, res) { 22 | console.dir(res.errors); 23 | }); 24 | -------------------------------------------------------------------------------- /doc/example/instanceof.js: -------------------------------------------------------------------------------- 1 | // validate a field is an instanceof a function 2 | var Schema = require('../..') 3 | , Component = function Component(){} 4 | , descriptor = { 5 | type: 'object', 6 | fields: { 7 | comp: {type: Component, required: true} 8 | } 9 | } 10 | , source = {comp: {}} 11 | , schema; 12 | 13 | require('../../plugin/all'); 14 | 15 | schema = new Schema(descriptor); 16 | schema.validate(source, function(err, res) { 17 | console.dir(res.errors); 18 | }); 19 | -------------------------------------------------------------------------------- /doc/example/len.js: -------------------------------------------------------------------------------- 1 | // validate a field length 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | func: {type: 'function', required: true, len: 1} 7 | } 8 | } 9 | , source = {func: function noop(){}} 10 | , schema; 11 | 12 | require('../../plugin/all'); 13 | 14 | schema = new Schema(descriptor); 15 | schema.validate(source, function(err, res) { 16 | console.dir(res.errors); 17 | }); 18 | -------------------------------------------------------------------------------- /doc/example/match.js: -------------------------------------------------------------------------------- 1 | // validate all fields of an object 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | required: true, 6 | fields: { 7 | all: { 8 | match: /./, 9 | type: 'string' 10 | } 11 | } 12 | } 13 | , source = {address1: 'foo', address2: 'bar', address3: false} 14 | , schema; 15 | 16 | require('../../plugin/all'); 17 | 18 | schema = new Schema(descriptor); 19 | schema.validate(source, function(err, res) { 20 | console.dir(res.errors); 21 | }); 22 | -------------------------------------------------------------------------------- /doc/example/max.js: -------------------------------------------------------------------------------- 1 | // validate a field has a maximum length 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | func: {type: 'function', required: true, max: 1} 7 | } 8 | } 9 | , source = { 10 | func: function noop(foo, bar){ 11 | foo(); 12 | bar(); 13 | } 14 | } 15 | , schema; 16 | 17 | require('../../plugin/all'); 18 | 19 | schema = new Schema(descriptor); 20 | schema.validate(source, function(err, res) { 21 | console.dir(res.errors); 22 | }); 23 | -------------------------------------------------------------------------------- /doc/example/message-clone.js: -------------------------------------------------------------------------------- 1 | // clone default messages 2 | var Schema = require('../..') 3 | , messages = Schema.clone(require('../../messages')) 4 | , descriptor = { 5 | type: 'object', 6 | fields: { 7 | name: { 8 | type: 'string', 9 | required: true 10 | } 11 | } 12 | } 13 | , source = {} 14 | , schema; 15 | 16 | require('../../plugin/all'); 17 | 18 | // change message in place 19 | messages.required = '%s is a required field'; 20 | 21 | // pass messages as constructor option 22 | schema = new Schema(descriptor, {messages: messages}); 23 | schema.validate(source, function(err, res) { 24 | console.dir(res.errors); 25 | }); 26 | -------------------------------------------------------------------------------- /doc/example/message-function.js: -------------------------------------------------------------------------------- 1 | // override error message with function 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | name: { 7 | type: 'string', 8 | required: true, 9 | message: function() { 10 | return this.format( 11 | 'name must be specified (field: %s)', this.field); 12 | } 13 | } 14 | } 15 | } 16 | , source = {} 17 | , schema; 18 | 19 | require('../../plugin/all'); 20 | 21 | schema = new Schema(descriptor); 22 | schema.validate(source, function(err, res) { 23 | console.dir(res.errors); 24 | }); 25 | -------------------------------------------------------------------------------- /doc/example/message-override.js: -------------------------------------------------------------------------------- 1 | // override default error message 2 | var Schema = require('../..') 3 | , messages = require('../../messages') 4 | , descriptor = { 5 | type: 'object', 6 | fields: { 7 | name: { 8 | type: 'string', 9 | required: true 10 | } 11 | } 12 | } 13 | , source = {} 14 | , schema; 15 | 16 | require('../../plugin/all'); 17 | 18 | // change default message in place 19 | messages.required = '%s is a required field'; 20 | 21 | schema = new Schema(descriptor); 22 | schema.validate(source, function(err, res) { 23 | console.dir(res.errors); 24 | }); 25 | -------------------------------------------------------------------------------- /doc/example/message.js: -------------------------------------------------------------------------------- 1 | // override error message 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | name: { 7 | type: 'string', 8 | required: true, 9 | message: 'name must be specified' 10 | } 11 | } 12 | } 13 | , source = {} 14 | , schema; 15 | 16 | require('../../plugin/all'); 17 | 18 | schema = new Schema(descriptor); 19 | schema.validate(source, function(err, res) { 20 | console.dir(res.errors); 21 | }); 22 | -------------------------------------------------------------------------------- /doc/example/min.js: -------------------------------------------------------------------------------- 1 | // validate a field has a minimum length 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | func: {type: 'function', required: true, min: 1} 7 | } 8 | } 9 | , source = {func: function noop(){}} 10 | , schema; 11 | 12 | require('../../plugin/all'); 13 | 14 | schema = new Schema(descriptor); 15 | schema.validate(source, function(err, res) { 16 | console.dir(res.errors); 17 | }); 18 | -------------------------------------------------------------------------------- /doc/example/multiple-rules.js: -------------------------------------------------------------------------------- 1 | // validate a field with multiple rules 2 | var Schema = require('../..') 3 | , data = {bar: 'qux'} 4 | , descriptor = { 5 | type: 'object', 6 | fields: { 7 | id: [ 8 | {type: 'string', required: true}, 9 | function exists(cb) { 10 | if(!data[this.value]) { 11 | this.raise( 12 | this.reason('missing-id'), 13 | 'id %s does not exist', this.value); 14 | } 15 | cb(); 16 | } 17 | ] 18 | } 19 | } 20 | , source = {id: 'foo'} 21 | , schema; 22 | 23 | require('../../plugin/all'); 24 | 25 | schema = new Schema(descriptor); 26 | schema.validate(source, function(err, res) { 27 | console.dir(res.errors); 28 | }); 29 | -------------------------------------------------------------------------------- /doc/example/multiple-types.js: -------------------------------------------------------------------------------- 1 | // validate a field as one of multiple types 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | flag: {type: ['boolean', Boolean], required: true} 7 | } 8 | } 9 | , source = {flag: 'foo'} 10 | , schema; 11 | 12 | require('../../plugin/all'); 13 | 14 | schema = new Schema(descriptor); 15 | schema.validate(source, function(err, res) { 16 | console.dir(res.errors); 17 | }); 18 | -------------------------------------------------------------------------------- /doc/example/pattern.js: -------------------------------------------------------------------------------- 1 | // validate a field as matching a pattern 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | name: {type: 'string', required: true, pattern: /^[a-z0-9]+$/i} 7 | } 8 | } 9 | , source = {name: '-name'} 10 | , schema; 11 | 12 | require('../../plugin/all'); 13 | 14 | schema = new Schema(descriptor); 15 | schema.validate(source, function(err, res) { 16 | console.dir(res.errors); 17 | }); 18 | -------------------------------------------------------------------------------- /doc/example/placeholder.js: -------------------------------------------------------------------------------- 1 | // use a placeholder to set a default value 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | list: { 7 | type: 'array', 8 | values: {type: 'integer'}, 9 | placeholder: function() { 10 | return []; 11 | } 12 | } 13 | } 14 | } 15 | , source = {} 16 | , schema; 17 | 18 | require('../../plugin/all'); 19 | 20 | schema = new Schema(descriptor); 21 | schema.validate(source, function() { 22 | console.dir(source); 23 | }); 24 | -------------------------------------------------------------------------------- /doc/example/plugin-rule.js: -------------------------------------------------------------------------------- 1 | // validate a field with a plugin rule 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | id: {type: 'id', required: true} 7 | } 8 | } 9 | , source = {id: '-foo'} 10 | , schema; 11 | 12 | require('../../plugin/all'); 13 | 14 | // create plugin function 15 | function plugin() { 16 | var pattern = /^[a-z0-9]$/i; 17 | // create static rule function 18 | this.main.id = function id(cb) { 19 | if(!pattern.test(this.value)) { 20 | this.raise(this.reason('id'), 'invalid id %s', this.value); 21 | } 22 | cb(); 23 | } 24 | } 25 | 26 | // use plugin 27 | Schema.plugin([plugin]); 28 | 29 | schema = new Schema(descriptor); 30 | schema.validate(source, function(err, res) { 31 | console.dir(res.errors); 32 | }); 33 | -------------------------------------------------------------------------------- /doc/example/range.js: -------------------------------------------------------------------------------- 1 | // validate a field has a length within a range 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | func: {type: 'function', required: true, min: 1, max: 2} 7 | } 8 | } 9 | , source = { 10 | func: function noop(foo, bar, qux){ 11 | foo(); 12 | bar(); 13 | qux(); 14 | } 15 | } 16 | , schema; 17 | 18 | require('../../plugin/all'); 19 | 20 | schema = new Schema(descriptor); 21 | schema.validate(source, function(err, res) { 22 | console.dir(res.errors); 23 | }); 24 | -------------------------------------------------------------------------------- /doc/example/required.js: -------------------------------------------------------------------------------- 1 | // validate a field as required 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | name: {type: 'string', required: true} 7 | } 8 | } 9 | , source = {} 10 | , schema; 11 | 12 | require('../../plugin/all'); 13 | 14 | schema = new Schema(descriptor); 15 | schema.validate(source, function(err, res) { 16 | console.dir(res.errors); 17 | }); 18 | -------------------------------------------------------------------------------- /doc/example/source-type.js: -------------------------------------------------------------------------------- 1 | // validate the type of the source object 2 | var Schema = require('../..') 3 | , descriptor = {type: 'object'} 4 | , source = 'foo' 5 | , schema; 6 | 7 | require('../../plugin/all'); 8 | 9 | schema = new Schema(descriptor); 10 | schema.validate(source, function(err, res) { 11 | console.dir(res.errors); 12 | }); 13 | -------------------------------------------------------------------------------- /doc/example/state.js: -------------------------------------------------------------------------------- 1 | // pass state information between rule test functions 2 | var Schema = require('../..') 3 | , dns = require('dns') 4 | , state = {} 5 | , opts = {state: state} 6 | , descriptor = { 7 | type: 'object', 8 | fields: { 9 | email: [ 10 | {type: 'string', required: true, pattern: /^.+@.+\..+/}, 11 | function parse(cb) { 12 | var at = this.value.indexOf('@') 13 | , user = this.value.substr(0, at) 14 | , domain = this.value.substr(at + 1); 15 | // assign to validation state 16 | this.state.email = {user: user, domain: domain}; 17 | cb(); 18 | }, 19 | function lookup(cb) { 20 | function resolve(err, addr) { 21 | if(err && err.code === 'ENOTFOUND') { 22 | this.raise( 23 | '%s: could not resolve dns for domain %s', 24 | this.field, 25 | this.state.email.domain); 26 | }else if(err) { 27 | return cb(err); 28 | } 29 | this.state.addr = addr; 30 | cb(); 31 | } 32 | dns.resolve(this.state.email.domain, resolve.bind(this)); 33 | } 34 | ] 35 | } 36 | } 37 | // force dns failure with random domain 38 | , source = {email: 'foo@' + Date.now() + '.com'} 39 | , schema; 40 | 41 | require('../../plugin/all'); 42 | 43 | schema = new Schema(descriptor); 44 | schema.validate(source, opts, function(err, res) { 45 | console.dir(res.errors); 46 | }); 47 | -------------------------------------------------------------------------------- /doc/example/type.js: -------------------------------------------------------------------------------- 1 | // validate a field type 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | flag: {type: 'boolean', required: true} 7 | } 8 | } 9 | , source = {flag: 'foo'} 10 | , schema; 11 | 12 | require('../../plugin/all'); 13 | 14 | schema = new Schema(descriptor); 15 | schema.validate(source, function(err, res) { 16 | console.dir(res.errors); 17 | }); 18 | -------------------------------------------------------------------------------- /doc/example/whitespace.js: -------------------------------------------------------------------------------- 1 | // validate a field as whitespace 2 | var Schema = require('../..') 3 | , descriptor = { 4 | type: 'object', 5 | fields: { 6 | name: {type: 'string', required: true, whitespace: true} 7 | } 8 | } 9 | , source = {name: ' '} 10 | , schema; 11 | 12 | require('../../plugin/all'); 13 | 14 | schema = new Schema(descriptor); 15 | schema.validate(source, function(err, res) { 16 | console.dir(res.errors); 17 | }); 18 | -------------------------------------------------------------------------------- /doc/readme.md: -------------------------------------------------------------------------------- 1 | ## Async Validate 2 | 3 | [![Build Status](https://travis-ci.org/tmpfs/async-validate.svg)](https://travis-ci.org/tmpfs/async-validate) 4 | [![npm version](http://img.shields.io/npm/v/async-validate.svg)](https://npmjs.org/package/async-validate) 5 | [![Coverage Status](https://coveralls.io/repos/tmpfs/async-validate/badge.svg?branch=master&service=github&v=1)](https://coveralls.io/github/tmpfs/async-validate?branch=master) 6 | 7 | Asynchronous validation for [node](http://nodejs.org) and the browser. It has minimal dependencies and uses a plugin architecture so you only need to include the plugins for types you want to validate and can easily create your own validation plugins. 8 | 9 | Examples are in [EXAMPLE](/EXAMPLE.md) and the [example directory](/doc/example). 10 | 11 | *** 12 | 13 | *** 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /doc/readme/api.md: -------------------------------------------------------------------------------- 1 | ### API 2 | 3 | #### Schema 4 | 5 | ```javascript 6 | function Schema(rules, [opts]) 7 | ``` 8 | 9 | Encapsulates the rules associated with a schema and the logic for performing validation. 10 | 11 | * `rules`: The schema rules. 12 | * `opts`: Configuration options. 13 | 14 | Options: 15 | 16 | * `messages`: An alternative messages object for the schema. 17 | 18 | ##### messages 19 | 20 | ```javascript 21 | function messages([messages]) 22 | ``` 23 | 24 | Get or set the messages associated with the schema. 25 | 26 | ##### validate 27 | 28 | ```javascript 29 | function validate(source, [opts], cb) 30 | ``` 31 | 32 | Validates a source object against the schema rules. 33 | 34 | * `source`: The object to validate. 35 | * `opts`: Map of processing options for the validation. 36 | * `cb`: Callback function to invoke when validation completes. 37 | 38 | Options: 39 | 40 | * `first`: Invoke callback when the first validation rule generates an error. 41 | * `single`: Only ever return a single error. 42 | * `bail`: Shorthand for `single` and `first`. 43 | * `messages`: Overrides the schema messages. 44 | * `parallel`: A boolean indicating that the validation should be executed in parallel. 45 | * `field`: Field name for the source object, default is `source` when not specified. 46 | * `parent`: Parent object for the `source` value. 47 | * `state`: Object to be used as the initial user data state. 48 | * `vars`: Object map of variables to assign to each rule. 49 | * `literal`: If `true` do not use parameter replacement for messages, pass through literally. 50 | 51 | ##### Schema.plugin 52 | 53 | ```javascript 54 | function plugin(plugins) 55 | ``` 56 | 57 | Static plugin loader method; accepts an array of plugin functions. 58 | 59 | ##### Schema.clone 60 | 61 | ```javascript 62 | function clone(source, [target]) 63 | ``` 64 | 65 | Static clone; deep copies simple objects and arrays, `RegExp` instances are passed by reference. 66 | 67 | #### Rule 68 | 69 | ```javascript 70 | function Rule(opts) 71 | ``` 72 | 73 | Encapsulates the data associated with a validation rule and the value to be validated. Rule functions are invoked in the scope of a `Rule` instance which exposes the following public fields: 74 | 75 | * `rule`: The validation rule in the schema descriptor. 76 | * `value`: The value of the property being validated. 77 | * `field`: The name of the property being validated. 78 | * `parent`: The parent object that declares the property. 79 | * `source`: The source object passed to `validate()`. 80 | * `messages`: Reference to the schema messages. 81 | * `errors`: Array of errors for the field validation. 82 | * `state`: User data for validation state. 83 | * `reasons`: Map of default error reasons. 84 | 85 | ##### isRoot 86 | 87 | ```javascript 88 | function isRoot() 89 | ``` 90 | 91 | Determine if validation is being performed against the root source object. 92 | 93 | ##### reason 94 | 95 | ```javascript 96 | function reason(id, [opts]) 97 | ``` 98 | 99 | Create a reason for a validation error, returns a `Reason` instance suitable for passing as the first argument to [raise](#raise). 100 | 101 | ##### raise 102 | 103 | ```javascript 104 | function raise([reason], message, ...) 105 | ``` 106 | 107 | Adds an error message to the list of errors encountered during validation of a value. 108 | 109 | The first argument may optionally be a `Reason` instance returned by `reason()` allowing a user to associate an identifier with the validation error and optional meta data. An error raised with a `Reason` has a `reason` field referencing the supplied reason. 110 | 111 | When replacement parameters are supplied the behaviour is identical to `util.format`. 112 | 113 | ##### format 114 | 115 | ```javascript 116 | function format(message, ...) 117 | ``` 118 | 119 | Format a message with replacement parameters like `util.format`. 120 | 121 | Useful when a rule declares `message` as a function and wishes to construct the error message with parameters. 122 | 123 | ##### validates 124 | 125 | ```javascript 126 | function validates() 127 | ``` 128 | 129 | Returns a `boolean` derived from the rule `required` property and other factors to determine if the value should be subject to validation. 130 | 131 | ##### diff 132 | 133 | ```javascript 134 | function diff(expected, received) 135 | ``` 136 | 137 | Compare two arrays, return `false` if they are equal otherwise return an array that is the difference between the supplied arrays. 138 | 139 | #### Error 140 | 141 | The errors created by [raise](#raise) are assigned the following public fields: 142 | 143 | * `key`: Unique key for the error, eg: `address.name`. 144 | * `field`: The name of the property that failed validation. 145 | * `value`: The value of the property. 146 | * `parent`: The parent object that declares the property. 147 | * `reason`: A [Reason](#reason) for the error when available. 148 | 149 | #### Reason 150 | 151 | ```javascript 152 | function Reason(id, [opts]) 153 | ``` 154 | 155 | Represents the reason for a validation error, may be created using `reason()`. 156 | 157 | You must supply a reason `id`; if `opts` are passed they are assigned as properties of the reason instance. When `toString()` is called on a `Reason` instance the `id` is returned. 158 | 159 | -------------------------------------------------------------------------------- /doc/readme/developer.md: -------------------------------------------------------------------------------- 1 | ## Developer 2 | 3 | Clone the repository, install project and global dependencies ([mdp][], [jshint][] and [jscs][]): 4 | 5 | ``` 6 | npm i && npm i -g mdp jshint jscs 7 | ``` 8 | 9 | ### Test 10 | 11 | Run the test specifications: 12 | 13 | ``` 14 | npm test 15 | ``` 16 | 17 | ### Spec 18 | 19 | Compile test specifications for the browser (open `test/index.html`): 20 | 21 | ``` 22 | npm run spec 23 | ``` 24 | 25 | ### Cover 26 | 27 | Generate code coverage: 28 | 29 | ``` 30 | npm run cover 31 | ``` 32 | 33 | ### Lint 34 | 35 | Run the source tree through [jshint][] and [jscs][]: 36 | 37 | ``` 38 | npm run lint 39 | ``` 40 | 41 | ### Browser 42 | 43 | Create a standalone [browserify][] build: 44 | 45 | ``` 46 | npm run browser 47 | ``` 48 | 49 | ### Clean 50 | 51 | Remove generated files: 52 | 53 | ``` 54 | npm run clean 55 | ``` 56 | 57 | ### Docs 58 | 59 | To generate all documentation: 60 | 61 | ``` 62 | npm run docs 63 | ``` 64 | 65 | ### Example 66 | 67 | Generate [EXAMPLE](/EXAMPLE.md) (requires [mdp][]): 68 | 69 | ``` 70 | npm run example 71 | ``` 72 | 73 | ### Readme 74 | 75 | Generate the readme file (requires [mdp][]): 76 | 77 | ``` 78 | npm run readme 79 | ``` 80 | -------------------------------------------------------------------------------- /doc/readme/guide.md: -------------------------------------------------------------------------------- 1 | ## Guide 2 | 3 | ### Descriptor 4 | 5 | A descriptor is a collection of validation rules as a map of fields to rules, rules may be declared as an `object`, `array` or `function`. 6 | 7 | #### Object Definition 8 | 9 | ```javascript 10 | var descriptor = { 11 | type: 'object', 12 | fields: { 13 | name: {type: 'string', required: true} 14 | } 15 | } 16 | ``` 17 | 18 | #### Array Definition 19 | 20 | You may declare an `array` to use multiple validation rules per field, see [multiple rules](#multiple-rules). 21 | 22 | #### Function Definition 23 | 24 | Use an inline function definition for application specific rules, see [inline rule](#inline-rule). 25 | 26 | #### Composite Definition 27 | 28 | To share common fields across different schemas move them to a module and require them. 29 | 30 | Module to represent an `address` field: 31 | 32 | ```javascript 33 | module.exports = { 34 | type: 'object', 35 | fields: { 36 | name: {type: 'string', required: true}, 37 | street: {type: 'string', required: true}, 38 | city: {type: 'string', required: true}, 39 | zip: {type: 'string', required: true} 40 | } 41 | } 42 | ``` 43 | 44 | Muliple objects containing an `address` field that need the same validation rules: 45 | 46 | ```javascript 47 | var address = require('./address') 48 | , user = { 49 | type: 'object', 50 | fields: { 51 | address: address 52 | } 53 | } 54 | , invoice = { 55 | type: 'object', 56 | fields: { 57 | bill: address 58 | } 59 | } 60 | ``` 61 | 62 | ### Rules 63 | 64 | ```javascript 65 | function rule(cb) 66 | ``` 67 | 68 | Rules are functions that perform validation of a value, they are invoked in the scope of a rule instance ([file](/lib/rule.js), [api docs](#rule)). 69 | 70 | A rule function can access all relevant properties and methods using `this` and should [raise](#raise) an error if `this.value` fails a validation test, see [errors](#errors). Rule functions may raise multiple errors for different validation failures. 71 | 72 | The [plugin rule](#plugin-rule) method of declaring rule functions is preferred as it is the most modular. 73 | 74 | #### Inline Rule 75 | 76 | The rule function is assigned directly to the field: 77 | 78 | ```javascript 79 | var descriptor = { 80 | type: 'object', 81 | fields: { 82 | id: function(cb) { 83 | // if this.value has error condition call this.raise() 84 | cb(); 85 | } 86 | } 87 | } 88 | ``` 89 | 90 | #### Assigned Rule 91 | 92 | Assigned to the `test` field so that you may pass data from the rule to the function: 93 | 94 | ```javascript 95 | var descriptor = { 96 | type: 'object', 97 | fields: { 98 | id: { 99 | foo: 'bar', 100 | test: function(cb) { 101 | console.log(this.foo); 102 | // if this.value has error condition call this.raise() 103 | cb(); 104 | } 105 | } 106 | } 107 | } 108 | ``` 109 | 110 | #### Plugin Rule 111 | 112 | Plugin that assigns the rule function as a static method. 113 | 114 | Create a plugin module: 115 | 116 | ```javascript 117 | module.exports = function() { 118 | // declare static rule function with name `id` 119 | this.main.id = function id(cb) { 120 | // if this.value has error condition call this.raise() 121 | cb(); 122 | } 123 | } 124 | ``` 125 | 126 | Load and use the plugin: 127 | 128 | ```javascript 129 | var Schema = require('async-validate'); 130 | Schema.plugin([require('./rule')]); 131 | var descriptor = { 132 | type: 'object', 133 | fields: { 134 | id: {type: 'id'} 135 | } 136 | } 137 | ``` 138 | 139 | The static `id` method will then be invoked for every rule of type `id`, this is the most portable style as it enables easily moving validation rules into modules and packages that may be shared. 140 | 141 | #### Multiple Rules 142 | 143 | It is often useful to test against multiple validation rules for a single field, to do so make the rule an array of objects, for example: 144 | 145 | ```javascript 146 | var descriptor = { 147 | type: 'object', 148 | fields: { 149 | email: [ 150 | {type: "string", required: true}, 151 | function(cb) { 152 | // test if email address (this.value) already exists 153 | // in a database and call this.raise() if it does 154 | cb(); 155 | } 156 | ] 157 | } 158 | } 159 | ``` 160 | 161 | #### Deep Rules 162 | 163 | If you need to validate deep object properties you may do so for validation rules that are of the `object` or `array` type by assigning nested rules to a `fields` property of the rule. 164 | 165 | ```javascript 166 | var descriptor = { 167 | type: 'object', 168 | fields: { 169 | name: {type: "string", required: true}, 170 | address: { 171 | type: "object", 172 | required: true, 173 | fields: { 174 | street: {type: "string", required: true}, 175 | city: {type: "string", required: true}, 176 | zip: {type: "string", required: true, len: 8, message: "invalid zip"} 177 | } 178 | } 179 | } 180 | } 181 | var schema = new Schema(descriptor); 182 | schema.validate({address: {}}, function(err, res) { 183 | // res.errors contains errors for name, street, city, zip 184 | }); 185 | ``` 186 | 187 | Note that if you do not specify the `required` property on the parent rule it is perfectly valid for the field not to be declared on the source object and the deep validation rules will not be executed as there is nothing to validate against. 188 | 189 | The parent rule is also validated so if you have a set of rules such as: 190 | 191 | ```javascript 192 | var descriptor = { 193 | type: 'object', 194 | fields: { 195 | roles: { 196 | type: "array", 197 | required: true, 198 | len: 3, 199 | fields: { 200 | 0: {type: "string", required: true}, 201 | 1: {type: "string", required: true}, 202 | 2: {type: "string", required: true} 203 | } 204 | } 205 | } 206 | } 207 | ``` 208 | 209 | And supply a source object of `{roles: ["admin", "user"]}` then two errors will be created. One for the array length mismatch and one for the missing required array entry at index 2. 210 | 211 | #### Properties 212 | 213 | This section describes the recognised rule properties and their behaviour, if you are using an [assigned rule](#assigned-rule) or [plugin rule](#plugin-rule) you can define properties on the rule object and they are available to the rule function via `this`. 214 | 215 | See the [system schema](/system.js). 216 | 217 | ##### Type Identifier 218 | 219 | * `type `: Type identifier, constructor function or list of types. 220 | 221 | The `type` property indicates the type of rule to use, a type corresponds to a plugin function and the plugin should have been loaded. 222 | 223 | A type identifier is required if the rule is not an inline or assigned rule. 224 | 225 | Recognised type values are: 226 | 227 | * `array`: Must be an array as determined by `Array.isArray`. 228 | * `boolean`: Must be of type `boolean`. 229 | * `date`: Value must be valid as determined by `moment().isValid()`. 230 | * `enum`: Value must exist in the `list`. 231 | * `float`: Must be of type `number` and a floating point number. 232 | * `function`: Must be of type `function`. 233 | * `integer`: Must be of type `number` and an integer. 234 | * `null`: Must strictly equal `null`. 235 | * `number`: Must be of type `number`. 236 | * `object`: Must be of type `object` and not `Array.isArray`. 237 | * `regexp`: Must be an instance of `RegExp` or a valid string regexp. 238 | * `string`: Must be of type `string`. 239 | 240 | When the `object` plugin has been loaded the `type` field may be a function in which case the value must be an `instanceof` the function assigned to `type`. 241 | 242 | To allow a field to be of multiple types you may declare an array of valid type identifiers, for example: 243 | 244 | ```javascript 245 | {type: ['string', String, Number], required: true} 246 | ``` 247 | 248 | ##### Enumerable 249 | 250 | * `list `: The list of enumerable values. 251 | 252 | To validate a value from a list of possible values use the `enum` type with a `list` property containing the valid values for the field, for example: 253 | 254 | ```javascript 255 | var descriptor = { 256 | type: 'object', 257 | fields: { 258 | role: {type: "enum", list: ['admin', 'user', 'guest']} 259 | } 260 | } 261 | ``` 262 | 263 | ##### Date Format 264 | 265 | * `format `: Date format string. 266 | * `local `: Use local time rather than UTC. 267 | 268 | Validating dates can be complex but using [moment](http://momentjs.com/) date validation is substantially easier. 269 | 270 | If no `format` is specified for a rule that is a `date` type then it is assumed the date is ISO 8601. If a format is specified then the date is validated according to the specified format. 271 | 272 | It is recommended you read the [moment documentation](http://momentjs.com/docs/#/parsing/is-valid/) on the `isValid` method to understand what validation is performed. 273 | 274 | The important part is: 275 | 276 | > Note: It is not intended to be used to validate that the input string matches the format string. Because the strictness of format matching can vary depending on the application and business requirements, this sort of validation is not included in Moment.js. 277 | 278 | This limitation may be overcome by combining a `pattern` in a date rule, for example: 279 | 280 | ```javascript 281 | var descriptor = { 282 | type: 'object', 283 | fields: { 284 | active: { 285 | type: "date", 286 | format: "YYYY-MM-DD", 287 | pattern: /^([\d]{4})-([\d]{2})-([\d]{2})$/ 288 | } 289 | } 290 | } 291 | ``` 292 | 293 | ##### Message 294 | 295 | * `message `: Custom error message. 296 | 297 | The `message` property defines the error message when validation fails, it overrides any default message. The property may be a `string` or `function`, see [messages](#messages). 298 | 299 | ##### Required 300 | 301 | * `required `: Field is required flag. 302 | 303 | The `required` property indicates that the field must exist on the source object being validated. 304 | 305 | ##### Additional 306 | 307 | * `additional `: Determines if additional properties are allowed. 308 | 309 | When a rule is of the `object` type and `additional` is set to `false` an error is raised if the source object contains any properties not in the schema. 310 | 311 | ##### Fields 312 | 313 | * `fields `: Map containing rules for object properties. 314 | 315 | Rules of the `object` and `array` type may declare a `fields` object which creates a nested schema, see [deep rules](#deep-rules). 316 | 317 | 318 | ##### Pattern 319 | 320 | * `pattern `: Pattern match regular expression. 321 | 322 | The `pattern` property is a regular expression that the value must match to pass validation. 323 | 324 | ##### Placeholder 325 | 326 | * `placeholder `: Placeholder function. 327 | 328 | A `function` that may return a default value for a field, it is invoked when the field value is `undefined` and the return value is assigned to the property. 329 | 330 | ##### Range 331 | 332 | * `min `: Minimum length value. 333 | * `max `: Maximum length value. 334 | 335 | A range is defined using the `min` and `max` properties. For `string`, `function` and `array` types comparison is performed against the `length`, for `number` types the number must not be less than `min` nor greater than `max`. 336 | 337 | ##### Length 338 | 339 | * `len `: Length constraint. 340 | 341 | To validate an exact length of a field specify the `len` property. For `string`, `function` and `array` types comparison is performed on the `length` property, for the `number` type this property indicates an exact match for the `number`, ie, it may only be strictly equal to `len`. 342 | 343 | If the `len` property is combined with the `min` and `max` range properties, `len` takes precedence. 344 | 345 | ##### Values 346 | 347 | * `values `: Array of rules for array types. 348 | 349 | Used with the `array` type as a shorthand for validating array values, may be an `object` or `array` containing validation rules. 350 | 351 | When `values` is an object it is applied to all array elements in the source array otherwise each `values` entry is compared against each source array entry which allows mixed types to be used in arrays. 352 | 353 | Note that `values` is expanded to `fields`, see [deep rules](#deep-rules). 354 | 355 | ##### Match 356 | 357 | * `match `: Expands a rule to multiple properties. 358 | 359 | The `match` property may be used to apply a rule to multiple properties of the same object, the rule is cloned for each property name that matches the regular expression and applied to the matched property. 360 | 361 | In this scenario specifying `required` on the match rule would be a non-operation. 362 | 363 | This is useful when you have a sequence of properties that share the same rules: 364 | 365 | ```javascript 366 | {match: /^address[1-3]$/, type: 'string'} 367 | ``` 368 | 369 | ##### Resolve 370 | 371 | * `resolve `: Rule location function. 372 | 373 | A function that may be declared to conditionally determine the rule to use for a given object, if is invoked *synchronously* in the scope of the object being validated. It should inspect the object and return a rule to use for that particular object. 374 | 375 | This is typically used to allow rules to be conditional on a property of an object, for example an object may have a `type` field that determines the type or class of object and validation needs to change for the different types. 376 | 377 | ##### Test 378 | 379 | * `test `: Rule function. 380 | 381 | The function to use for rule validation. 382 | 383 | ##### Whitespace 384 | 385 | * `whitespace `: Determines if whitespace input should be an error. 386 | 387 | It is typical to treat required fields that only contain whitespace as errors. To add an additional test for a string that consists solely of whitespace add a `whitespace` property to a rule with a value of `true`. The rule must be a `string` type. 388 | 389 | You may wish to sanitize user input instead of testing for whitespace, see [transform](#transform) for an example that would allow you to strip whitespace. 390 | 391 | ### Errors 392 | 393 | To raise an error in a validation rule call [raise](#raise), the signature for raise is equivalent to `util.format` except that it may also accept a [Reason](#reason) as the first argument. 394 | 395 | ```javascript 396 | function id(cb) { 397 | if(!/^[a-z0-9-]+$/i.test(this.value)) { 398 | this.raise('%s is not a valid id', this.field); 399 | } 400 | cb(); 401 | } 402 | ``` 403 | 404 | Decorate the error with a reason: 405 | 406 | ```javascript 407 | function id(cb) { 408 | if(!/^[a-z0-9-]+$/i.test(this.value)) { 409 | this.raise( 410 | this.reason('id', {level: 'warn'}), 411 | '%s is not a valid id', 412 | this.field); 413 | } 414 | cb(); 415 | } 416 | ``` 417 | 418 | Adding a reason allows associating an identifier with an error and optional meta data about the error which can be useful if you need to associate a *severity* with errors to distinguish between error types. 419 | 420 | To signal that an internal processing error has occured pass an Error to the callback, for example: 421 | 422 | ```javascript 423 | function id(cb) { 424 | this.model.findById(this.value, function(err, id) { 425 | if(err) { 426 | return cb(err); 427 | } 428 | // validate id for error conditions 429 | cb(); 430 | }); 431 | } 432 | ``` 433 | 434 | ### Plugins 435 | 436 | Plugins are modules defining functions that allow users to only load functionality specific to the rule types being used which allows builds for the browser to be as lean as possible. 437 | 438 | See [zephyr][] for plugin system documentation. 439 | 440 | #### Loading Plugins 441 | 442 | To load all plugins: 443 | 444 | ```javascript 445 | require('async-validate/plugin/all'); 446 | ``` 447 | 448 | It is preferable to only use plugins for the types you are using: 449 | 450 | ```javascript 451 | var Schema = require('async-validate'); 452 | Schema.plugin([ 453 | require('async-validate/plugin/util'), 454 | require('async-validate/plugin/array'), 455 | require('async-validate/plugin/boolean'), 456 | require('async-validate/plugin/number'), 457 | require('async-validate/plugin/string') 458 | ]) 459 | ``` 460 | 461 | #### Creating Plugins 462 | 463 | Static plugins are mapped to [type identifiers](#type-identifiers) and instance plugins may be used to extend [Rule](#rule) which is useful for sharing functionality across rule plugins, see the [util plugins](/plugin/util). 464 | 465 | See [plugin rule](#plugin-rule) for an example and [plugin](/plugin) contains the plugins that ship with this package. 466 | 467 | The important point to remember is that for helper methods assign to `this` and for static rule functions (located by `type`) assign to `this.main` in the plugin. 468 | 469 | Helper method: 470 | 471 | ```javascript 472 | module.exports = function() { 473 | // create a helper method on the prototype 474 | // of the class used for static function scope 475 | this.helper = function(value) { 476 | return value; 477 | } 478 | } 479 | ``` 480 | 481 | Static method: 482 | 483 | ```javascript 484 | module.exports = function() { 485 | this.main.id = function id(cb) { 486 | // use helper method 487 | var val = this.helper(this.value); 488 | // implement validation for `id` type 489 | cb(); 490 | } 491 | } 492 | ``` 493 | 494 | #### Helper Plugins 495 | 496 | The following helper plugins ship with this package, you can use them all with: 497 | 498 | ```javascript 499 | Schema.plugin([require('async-validate/plugin/util')]); 500 | ``` 501 | 502 | ##### pattern 503 | 504 | ```javascript 505 | function pattern() 506 | ``` 507 | 508 | Validate using a regexp pattern, typically invoked from a rule function, raises an error if a value fails to match a rule regexp pattern. 509 | 510 | ##### range 511 | 512 | ```javascript 513 | function range() 514 | ``` 515 | 516 | Validates that a value falls within a given range or is of a specific length, typically invoked from a rule function, raises an error if a value is out of bounds. 517 | 518 | ##### required 519 | 520 | ```javascript 521 | function required() 522 | ``` 523 | 524 | Validate a required field, typically invoked from a rule function, raises an error if a required field is not present. 525 | 526 | ##### type 527 | 528 | ```javascript 529 | function type() 530 | ``` 531 | 532 | Validate a value is one of the expected type(s), typically invoked from a rule function, raises an error if the value is not one of the declared types. 533 | 534 | ### Validation 535 | 536 | This section describes using the processing options available when calling [validate](#validate). 537 | 538 | #### Bail 539 | 540 | To callback early on the first value that generates a validation error and only report a single error use the `bail` option. This is useful when a user interface only needs to show the first error condition or if continuing processing would add unnecessary overhead. 541 | 542 | Remember that a rule can generate multiple validation errors so if you need more fine grained control you can use the `single` and `first` options. 543 | 544 | #### Variables 545 | 546 | Sometimes it is useful to pass existing data into all rule functions as transient data so that your rule functions may reference existing code for performing async operations. A common use case would be using a model class to query a database and then validate on the returned data. 547 | 548 | To do this you may use the `vars` processing option when calling [validate](#validate). 549 | 550 | The value should be an Object; each property of the `vars` object is passed into the [Rule](#rule) scope so that they are available via `this`. 551 | 552 | Be aware that if you use a built in field (see [Rule](#rule)) it will be overwritten. 553 | 554 | See the [vars test](/test/spec/vars.js) and [model fixture](/test/fixtures/model.js) for an example. 555 | 556 | #### State 557 | 558 | To pass state information use `this.state` in test functions, set the `state` option to specify an alternative object to use for the initial state. When no state is given the empty object is used. 559 | 560 | See the [state example](/doc/example/state.js). 561 | -------------------------------------------------------------------------------- /doc/readme/install.md: -------------------------------------------------------------------------------- 1 | ## Install 2 | 3 | ``` 4 | npm i async-validate 5 | ``` 6 | -------------------------------------------------------------------------------- /doc/readme/introduction.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/tmpfs/async-validate.svg)](https://travis-ci.org/tmpfs/async-validate) 2 | [![npm version](http://img.shields.io/npm/v/async-validate.svg)](https://npmjs.org/package/async-validate) 3 | [![Coverage Status](https://coveralls.io/repos/tmpfs/async-validate/badge.svg?branch=master&service=github&v=1)](https://coveralls.io/github/tmpfs/async-validate?branch=master) 4 | 5 | Asynchronous validation for [node](http://nodejs.org) and the browser. 6 | 7 | Examples are in [EXAMPLE](/EXAMPLE.md) and the [example directory](/doc/example). 8 | -------------------------------------------------------------------------------- /doc/readme/license.md: -------------------------------------------------------------------------------- 1 | ## License 2 | 3 | Everything is [MIT](http://en.wikipedia.org/wiki/MIT_License). Read the [license](/LICENSE) if you feel inclined. 4 | -------------------------------------------------------------------------------- /doc/readme/links.md: -------------------------------------------------------------------------------- 1 | [node]: http://nodejs.org 2 | [npm]: http://www.npmjs.org 3 | [mdp]: https://github.com/tmpfs/mdp 4 | [browserify]: http://browserify.org/ 5 | [jshint]: http://jshint.com 6 | [jscs]: http://jscs.info 7 | [zephyr]: https://github.com/tmpfs/zephyr 8 | -------------------------------------------------------------------------------- /doc/readme/messages.md: -------------------------------------------------------------------------------- 1 | ### Messages 2 | 3 | Depending upon your application requirements, you may need i18n support or you may prefer different validation error messages. 4 | 5 | The easiest way to achieve this is to assign a `message` to a rule: 6 | 7 | ```javascript 8 | {name:{type: "string", required: true, message: "Name is required"}} 9 | ``` 10 | 11 | You may also use a function for the rule message, it is invoked in the scope of the [Rule](#rule) and passed the original message and replacement parameters: 12 | 13 | ```javascript 14 | var descriptor = { 15 | type: 'object', 16 | fields: { 17 | name: { 18 | type: "string", 19 | required: true, 20 | message: function(message, parameters) { 21 | return this.field + ' is required'; 22 | } 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | If you just want to change the default messages: 29 | 30 | ```javascript 31 | var Schema = require('async-validate') 32 | , messages = require('async-validate/messages') 33 | , descriptor = { 34 | type: 'object', 35 | fields: { 36 | name:{type: "string", required: true}} 37 | } 38 | } 39 | , schema; 40 | messages.required = "%s is a required field"; 41 | schema = new Schema(descriptor, {messages: messages}); 42 | ``` 43 | 44 | Potentially you may require the same schema validation rules for different languages, in which case duplicating the schema rules for each language does not make sense. 45 | 46 | In this scenario you could just require your own messages file for the language and assign it to the schema: 47 | 48 | ```javascript 49 | var Schema = require('async-validate') 50 | , messages = require('messages-es') 51 | , descriptor = { 52 | type: 'object', 53 | fields: { 54 | name:{type: "string", required: true}} 55 | } 56 | } 57 | , schema = new Schema(descriptor, {messages: messages}); 58 | ``` 59 | 60 | If you are defining your own rule functions it is better practice to assign the message strings to a messages object and then access the messages via the `this.messages` property within the function. 61 | -------------------------------------------------------------------------------- /doc/readme/transform.md: -------------------------------------------------------------------------------- 1 | ### Transform 2 | 3 | Sometimes it is necessary to transform a value before validation, possibly to coerce the value or to sanitize it in some way. To do this add a `transform` function to the validation rule. The property is transformed prior to validation and re-assigned to the source object to mutate the value of the property in place. 4 | 5 | Without the `transform` function validation would fail due to the pattern not matching as the input contains leading and trailing whitespace, but by adding the transform function validation passes and the field value is sanitized at the same time. 6 | 7 | -------------------------------------------------------------------------------- /doc/readme/usage.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | Define validation rules, assign them to a schema using the necessary plugins and call validate: 4 | 5 | -------------------------------------------------------------------------------- /doc/transform.js: -------------------------------------------------------------------------------- 1 | var Schema = require('..') 2 | , descriptor = { 3 | type: 'object', 4 | fields: { 5 | name: { 6 | type: "string", 7 | required: true, pattern: /^[a-z]+$/, 8 | transform: function(value) { 9 | return value.trim(); 10 | } 11 | } 12 | } 13 | } 14 | , schema = new Schema(descriptor) 15 | , source = {name: " user "}; 16 | 17 | Schema.plugin([ 18 | require('../plugin/object'), 19 | require('../plugin/string'), 20 | require('../plugin/util') 21 | ]); 22 | 23 | schema.validate(source, function() { 24 | console.dir(source.name); 25 | }); 26 | -------------------------------------------------------------------------------- /doc/usage.js: -------------------------------------------------------------------------------- 1 | var Schema = require('async-validate') 2 | , descriptor = { 3 | type: 'object', 4 | fields: { 5 | name: {type: "string", required: true} 6 | } 7 | } 8 | , schema = new Schema(descriptor) 9 | , source = {}; 10 | 11 | Schema.plugin([ 12 | require('async-validate/plugin/object'), 13 | require('async-validate/plugin/string'), 14 | require('async-validate/plugin/util') 15 | ]); 16 | 17 | schema.validate(source, function(err, res) { 18 | if(err) { 19 | throw err; 20 | }else if(res) { 21 | // validation failed, res.errors is an array of all errors 22 | // res.fields is a map keyed by field unique id (eg: `address.name`) 23 | // assigned an array of errors per field 24 | return console.dir(res.errors) 25 | } 26 | // validation passed 27 | }); 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/schema'); 2 | -------------------------------------------------------------------------------- /lib/iterator.js: -------------------------------------------------------------------------------- 1 | function mapSeries(list, cb, complete) { 2 | var item = list.shift() 3 | , out = []; 4 | function run(item) { 5 | cb(item, function(err, result) { 6 | if(err) { 7 | return complete(err, out); 8 | } 9 | out.push(result); 10 | item = list.shift(); 11 | if(item) { 12 | return run(item); 13 | } 14 | complete(null, out); 15 | }); 16 | } 17 | if(item) { 18 | return run(item); 19 | } 20 | complete(null, out); 21 | } 22 | 23 | function map(list, cb, complete) { 24 | var out = [] 25 | , e; 26 | function run(item, index) { 27 | cb(item, function(err, result) { 28 | // do not call complete again on error 29 | // condition 30 | if(e) { 31 | return; 32 | }else if(err) { 33 | e = err; 34 | return complete(err, out); 35 | } 36 | out[index] = result; 37 | if(out.length === list.length) { 38 | return complete(null, out); 39 | } 40 | }); 41 | } 42 | list.forEach(function(item, index) { 43 | run(item, index); 44 | }); 45 | } 46 | 47 | module.exports = { 48 | map: map, 49 | mapSeries: mapSeries 50 | } 51 | -------------------------------------------------------------------------------- /lib/reason.js: -------------------------------------------------------------------------------- 1 | function Reason(id, meta) { 2 | for(var k in meta) { 3 | this[k] = meta[k]; 4 | } 5 | this.id = id; 6 | } 7 | 8 | Reason.prototype.toString = function() { 9 | return this.id; 10 | } 11 | 12 | var reasons = { 13 | type: new Reason('type'), 14 | required: new Reason('required'), 15 | pattern: new Reason('pattern'), 16 | length: new Reason('length'), 17 | instance: new Reason('instanceof'), 18 | additional: new Reason('additional'), 19 | enumerable: new Reason('enum'), 20 | date: new Reason('date'), 21 | whitespace: new Reason('whitespace'), 22 | min: new Reason('min'), 23 | max: new Reason('max') 24 | } 25 | 26 | Reason.reasons = reasons; 27 | 28 | module.exports = Reason; 29 | -------------------------------------------------------------------------------- /lib/rule.js: -------------------------------------------------------------------------------- 1 | var plugin = require('zephyr') 2 | , format = require('format-util') 3 | , Reason = require('./reason'); 4 | 5 | /** 6 | * Represents a rule associated with a value to validate. 7 | */ 8 | function Rule(opts) { 9 | if(!(this instanceof Rule)) { 10 | return new Rule(opts); 11 | } 12 | 13 | for(var k in opts) { 14 | this[k] = opts[k]; 15 | } 16 | 17 | // reason constants 18 | this.reasons = Reason.reasons; 19 | } 20 | 21 | /** 22 | * @private 23 | * 24 | * Helper for creating validation errors. 25 | * 26 | * If a rule has a message property it takes precedence. 27 | * 28 | * @param reason A reason for the validation error. 29 | * @param message A custom messaage for the error. 30 | * @param ... Replacement parameters passed to format. 31 | */ 32 | function error(message) { 33 | var msg 34 | , err 35 | , res 36 | , args = Array.prototype.slice.call(arguments, 1) 37 | , reason 38 | , tmp; 39 | 40 | // allow reason as first argument 41 | if(message instanceof Reason) { 42 | reason = message; 43 | message = arguments[1]; 44 | args = args.slice(1); 45 | } 46 | 47 | if(typeof(this.rule.message) === 'function') { 48 | // allow calls to error() so that message functions 49 | // may call `this.error()` and not create a stack 50 | // overflow, if the function returns an error it is used 51 | tmp = this.rule.message; 52 | this.rule.message = null; 53 | res = tmp.call(this, message, args); 54 | this.rule.message = tmp; 55 | if(res instanceof Error) { 56 | err = res; 57 | }else{ 58 | msg = '' + res; 59 | } 60 | }else{ 61 | msg = typeof(this.rule.message) === 'string' 62 | ? this.rule.message 63 | : message 64 | || format(require('../messages').default, this.field); 65 | } 66 | 67 | if(typeof this.rule.message === 'object' 68 | && reason 69 | && this.rule.message[reason]) { 70 | msg = this.rule.message[reason]; 71 | } 72 | 73 | if(typeof(msg) === 'string' 74 | && arguments.length > 1 && !this.rule.message && !this.literal) { 75 | msg = format.apply(null, [msg].concat(args)); 76 | } 77 | 78 | err = err || new Error(msg); 79 | err.field = this.field; 80 | err.value = this.value; 81 | err.parent = this.parent; 82 | err.names = this.names; 83 | err.key = this.key || err.field; 84 | 85 | //console.dir('raising error with names: ' + this.names); 86 | 87 | if(!this.key && this.names && this.names.length) { 88 | err.key = this.names.join('.'); 89 | } 90 | 91 | //console.dir(err.key); 92 | 93 | if(reason) { 94 | err.reason = reason; 95 | } 96 | return err; 97 | } 98 | 99 | /** 100 | * Get a new error reason. 101 | */ 102 | function reason(id, opts) { 103 | return new Reason(id, opts); 104 | } 105 | 106 | /** 107 | * Create an error and adds it to the list of errors to be reported. 108 | */ 109 | function raise(message) { 110 | var parameters = []; 111 | if(arguments.length > 1) { 112 | parameters = Array.prototype.slice.call(arguments, 1); 113 | } 114 | var err = this.error.apply(this, [message].concat(parameters)); 115 | this.errors.push(err); 116 | return err; 117 | } 118 | 119 | /** 120 | * Determine if validation is required for a field. 121 | */ 122 | function validates() { 123 | if(this.isRoot()) { 124 | return true; 125 | }else if(this.value === undefined && !this.rule.required) { 126 | return false; 127 | } 128 | return this.rule.required 129 | || (!this.rule.required && this.source.hasOwnProperty(this.field)); 130 | } 131 | 132 | /** 133 | * Determine is additional fields are present. 134 | */ 135 | function diff(expected, received) { 136 | var i 137 | , results = received.slice(0); 138 | for(i = 0;i < results.length;i++) { 139 | if(~expected.indexOf(results[i])) { 140 | results.splice(i, 1); 141 | i--; 142 | } 143 | } 144 | // no additional fields found 145 | if(results.length === 0) { 146 | return false; 147 | } 148 | // return diff array 149 | return results; 150 | } 151 | 152 | /** 153 | * Determine if we are validating the root source object. 154 | */ 155 | function isRoot() { 156 | return this.source === this.value; 157 | } 158 | 159 | Rule.prototype.reason = reason; 160 | Rule.prototype.error = error; 161 | Rule.prototype.raise = raise; 162 | Rule.prototype.format = format; 163 | Rule.prototype.isRoot = isRoot; 164 | Rule.prototype.validates = validates; 165 | Rule.prototype.diff = diff; 166 | 167 | module.exports = plugin({type: Rule, proto: Rule.prototype}); 168 | -------------------------------------------------------------------------------- /lib/schema.js: -------------------------------------------------------------------------------- 1 | var iterator = require('./iterator') 2 | , format = require('format-util') 3 | , Rule = require('./rule'); 4 | 5 | /** 6 | * @private 7 | * 8 | * Validate the type field. 9 | */ 10 | function validateRule(rule) { 11 | var i; 12 | 13 | if(typeof(rule) === 'function' || typeof(rule.test) === 'function') { 14 | return true; 15 | } 16 | 17 | function invalid() { 18 | throw new Error( 19 | 'type property must be string or function: ' + rule.field); 20 | } 21 | 22 | function isValid(type) { 23 | return type 24 | && (typeof(type) === 'string' || typeof(type) === 'function'); 25 | } 26 | 27 | if(Array.isArray(rule.type)) { 28 | for(i = 0;i < rule.type.length;i++) { 29 | if(!isValid(rule.type[i])) { 30 | invalid(); 31 | } 32 | } 33 | }else if(!isValid(rule.type)) { 34 | invalid(); 35 | } 36 | } 37 | 38 | /** 39 | * Encapsulates a validation schema. 40 | * 41 | * @param rules Validation rules for this schema. 42 | * @param opts Options for the schema. 43 | */ 44 | function Schema(rules, opts) { 45 | opts = opts || {}; 46 | 47 | if(rules === undefined) { 48 | throw new Error('Cannot configure a schema with no rules'); 49 | } 50 | 51 | if(!rules || typeof rules !== 'object' && typeof rules !== 'function') { 52 | throw new Error('Rules must be an object') 53 | } 54 | 55 | this.messages(opts.messages || require('../messages')); 56 | this.rules = rules; 57 | } 58 | 59 | /** 60 | * Get or set the messages used for this schema. 61 | * 62 | * @param msg The validation messages. 63 | * 64 | * @return The validation messages. 65 | */ 66 | function messages(msg) { 67 | if(msg!== undefined) { 68 | this._messages = msg; 69 | } 70 | return this._messages; 71 | } 72 | 73 | /** 74 | * Validate an object against this schema. 75 | * 76 | * @param source The object to validate. 77 | * @param opts Validation options. 78 | * @param cb Callback to invoke when validation is complete. 79 | */ 80 | function validate(source, opts, cb) { 81 | if(typeof opts === 'function') { 82 | cb = opts; 83 | opts = null; 84 | } 85 | opts = opts || {}; 86 | 87 | if(source === undefined && !opts._deep) { 88 | throw new Error('Cannot validate with no source.'); 89 | }else if(typeof cb !== 'function') { 90 | throw new Error('Cannot validate with no callback.'); 91 | } 92 | 93 | if(opts.bail) { 94 | opts.first = opts.single = true; 95 | } 96 | 97 | opts.messages = opts.messages || this.messages(); 98 | 99 | var series = [] 100 | , k 101 | , z 102 | , matcher 103 | , matchtmp 104 | , fields = this.rules.fields 105 | , state = opts.state || {} 106 | // iterator function series/parallel 107 | , func = opts.parallel ? iterator.map : iterator.mapSeries 108 | // configure messages to use defaults where necessary 109 | , messages = opts.messages; 110 | 111 | for(k in fields) { 112 | if(typeof fields[k] === 'object' 113 | && (fields[k].match instanceof RegExp)) { 114 | matcher = fields[k]; 115 | delete fields[k]; 116 | for(z in source) { 117 | if(matcher.match.test(z)) { 118 | matchtmp = Schema.clone(matcher); 119 | delete matchtmp.match; 120 | fields[z] = getRule(matchtmp, z, source[z]); 121 | } 122 | } 123 | } 124 | } 125 | 126 | this.rules = opts._deep ? this.rules : clone(this.rules); 127 | 128 | series = Array.isArray(this.rules) ? this.rules : [this.rules]; 129 | 130 | function getRule(rule, field, value) { 131 | var assign; 132 | 133 | value = value || source 134 | rule.field = field || opts.field || 'source'; 135 | 136 | if(typeof rule.resolve === 'function') { 137 | rule = rule.resolve.call(value); 138 | } 139 | 140 | // default value placeholder 141 | if(value === undefined 142 | && typeof rule.placeholder === 'function') { 143 | value = assign = rule.placeholder(); 144 | } 145 | 146 | // handle transformation 147 | if(typeof(rule.transform) === 'function') { 148 | value = assign = rule.transform(value); 149 | } 150 | 151 | if(assign && opts.parent && rule.field) { 152 | opts.parent[rule.field] = assign; 153 | } 154 | 155 | // handle instanceof tests for object type 156 | if(typeof rule.type === 'function') { 157 | rule.Type = rule.type; 158 | rule.type = 'object'; 159 | } 160 | 161 | validateRule(rule); 162 | 163 | if(typeof(rule.test) !== 'function' 164 | && Array.isArray(rule.type)) { 165 | rule.test = Rule.types; 166 | } 167 | 168 | if(typeof rule === 'function') { 169 | rule.test = rule; 170 | }else if(typeof rule.test !== 'function') { 171 | // scope plugin functions are static methods 172 | rule.test = Rule[rule.type]; 173 | } 174 | 175 | rule.names = opts._names || []; 176 | 177 | if(rule.field && opts._deep) { 178 | rule.names = rule.names.concat(rule.field); 179 | } 180 | 181 | rule.parent = opts.parent || source; 182 | rule.value = value; 183 | rule.source = opts._source || source; 184 | 185 | if(typeof rule.test !== 'function') { 186 | throw new Error(format('Unknown rule type %s', rule.type)); 187 | } 188 | 189 | return rule; 190 | } 191 | 192 | series = series.map(function(rule) { 193 | return getRule(rule); 194 | }); 195 | 196 | // iterate list data 197 | func(series, function(rule, callback) { 198 | 199 | var vars = {} 200 | , k 201 | , scope 202 | , isDeep; 203 | 204 | // assign rule fields first 205 | for(k in rule) { 206 | // do not overwrite existing fields, eg: helper functions 207 | if(Rule.Type.prototype[k] === undefined) { 208 | vars[k] = rule[k]; 209 | } 210 | } 211 | 212 | // next transient variables 213 | if(opts.vars) { 214 | for(k in opts.vars) { 215 | vars[k] = opts.vars[k]; 216 | } 217 | } 218 | 219 | // final built in properties 220 | vars.rule = rule; 221 | vars.field = rule.field; 222 | vars.value = rule.value; 223 | vars.source = rule.source; 224 | vars.names = rule.names; 225 | vars.errors = []; 226 | vars.state = state; 227 | vars.messages = messages; 228 | vars.literal = opts.literal; 229 | 230 | scope = Rule(vars); 231 | 232 | isDeep = (rule.type === 'object' || rule.type === 'array') 233 | && typeof(rule.fields) === 'object'; 234 | isDeep = isDeep && (rule.required || (!rule.required && rule.value)); 235 | 236 | function onValidate(err) { 237 | 238 | if(err) { 239 | return callback(err, scope.errors); 240 | } 241 | 242 | // not deep so continue on to next in series 243 | if(!isDeep) { 244 | return callback(err, scope.errors); 245 | 246 | // generate temp schema for nested rules 247 | }else{ 248 | 249 | var keys = opts.keys || Object.keys(rule.fields); 250 | 251 | func(keys, function iterator(key, cb) { 252 | 253 | // nested options for property iteration 254 | var options = clone(opts) 255 | , descriptor = rule.fields[key] 256 | , value = rule.value[key] 257 | , schema 258 | , i 259 | , len 260 | , tmp; 261 | 262 | // state object is by pointer 263 | options.state = state; 264 | 265 | if(descriptor.type === 'array' 266 | && typeof descriptor.values === 'object') { 267 | 268 | // wrap objects as arrays 269 | len = Array.isArray(descriptor.values) 270 | ? descriptor.values.length : Array.isArray(value) 271 | ? value.length : 0; 272 | 273 | if(len) { 274 | descriptor.fields = {}; 275 | } 276 | 277 | // object declaration applies to all array values 278 | if(!Array.isArray(descriptor.values)) { 279 | for(i = 0;i < len;i++) { 280 | descriptor.fields[i] = descriptor.values; 281 | } 282 | }else{ 283 | for(i = 0;i < len;i++) { 284 | descriptor.fields[i] = descriptor.values[i]; 285 | } 286 | } 287 | } 288 | 289 | // if rule is required but the target object 290 | // does not exist fail at the rule level and don't 291 | // go deeper 292 | if(descriptor.required && value === undefined) { 293 | tmp = Rule({ 294 | field: key, 295 | rule: descriptor, 296 | names: rule.names, 297 | key: rule.names.concat(key).join('.'), 298 | errors: scope.errors, 299 | literal: opts.literal 300 | }); 301 | tmp.raise(tmp.reasons.required, messages.required, key); 302 | return cb(); 303 | } 304 | 305 | schema = new Schema(descriptor, {messages: options.messages}); 306 | 307 | options.field = key; 308 | options.parent = rule.value; 309 | // important to maintain original source for isRoot() 310 | options._source = source; 311 | options._names = rule.names; 312 | options._deep = true; 313 | 314 | schema.validate(value, options, function(err, res) { 315 | if(res && res.errors.length) { 316 | scope.errors = scope.errors.concat(res.errors); 317 | } 318 | cb(err, null); 319 | }); 320 | }, function(err) { 321 | // bail on first error 322 | if(opts.first && scope.errors && scope.errors.length) { 323 | return complete(err, scope.errors, opts, cb); 324 | } 325 | callback(err, scope.errors); 326 | }) 327 | } 328 | } 329 | 330 | // invoke rule validation function 331 | rule.test.call(scope, onValidate); 332 | 333 | }, function(err, results) { 334 | complete(err, results, opts, cb); 335 | }); 336 | } 337 | 338 | /** 339 | * @private 340 | * 341 | * Collates the errors arrays and maps field names to errors 342 | * specific to the field. 343 | * 344 | * Invokes callback when done. 345 | */ 346 | function complete(err, results, opts, callback) { 347 | var i 348 | , field 349 | , errors = [] 350 | , fields = {}; 351 | 352 | for(i = 0;i < results.length;i++) { 353 | errors = errors.concat(results[i]); 354 | } 355 | 356 | if(errors.length) { 357 | if(opts.single) { 358 | errors = errors.slice(0,1); 359 | } 360 | for(i = 0;i < errors.length;i++) { 361 | field = errors[i].key; 362 | fields[field] = fields[field] || []; 363 | fields[field].push(errors[i]); 364 | } 365 | } 366 | 367 | callback(err, !errors.length ? null : {errors: errors, fields: fields}); 368 | } 369 | 370 | 371 | /** 372 | * Clone helper function. 373 | */ 374 | function clone(source, target) { 375 | var k 376 | , v; 377 | 378 | function isComplex(obj) { 379 | return Array.isArray(obj) 380 | || (obj && typeof obj === 'object') && !(obj instanceof RegExp); 381 | } 382 | 383 | // simple source object 384 | if(!isComplex(source)) { 385 | return source; 386 | } 387 | 388 | target = target || (Array.isArray(source) ? [] : {}); 389 | for(k in source) { 390 | v = source[k]; 391 | if(isComplex(v)) { 392 | target[k] = Array.isArray(v) ? [] : {}; 393 | clone(v, target[k]); 394 | }else{ 395 | target[k] = v; 396 | } 397 | } 398 | return target; 399 | } 400 | 401 | Schema.prototype.messages = messages; 402 | Schema.prototype.validate = validate; 403 | 404 | // static 405 | Schema.clone = clone; 406 | Schema.plugin = Rule.plugin; 407 | 408 | module.exports = Schema; 409 | -------------------------------------------------------------------------------- /messages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default validation error messages. 3 | */ 4 | var messages = { 5 | default: 'error on field %s', 6 | required: '%s is required', 7 | enum: '%s must be one of %s', 8 | whitespace: '%s cannot be empty', 9 | additional: 'extraneous fields (%s) found in %s', 10 | date: { 11 | format: '%s date %s is invalid for format %s', 12 | invalid: '%s date %s is invalid' 13 | }, 14 | types: { 15 | string: '%s is not a %s', 16 | null: '%s is not %s', 17 | function: '%s is not a %s', 18 | instance: '%s is not an instance of %s', 19 | array: '%s is not an %s', 20 | object: '%s is not an %s', 21 | number: '%s is not a %s', 22 | boolean: '%s is not a %s', 23 | integer: '%s is not an %s', 24 | float: '%s is not a %s', 25 | regexp: '%s is not a valid %s', 26 | multiple: '%s is not one of the allowed types %s' 27 | }, 28 | function: { 29 | len: '%s must have exactly %s arguments', 30 | min: '%s must have at least %s arguments', 31 | max: '%s cannot have more than %s arguments', 32 | range: '%s must have arguments length between %s and %s' 33 | }, 34 | string: { 35 | len: '%s must be exactly %s characters', 36 | min: '%s must be at least %s characters', 37 | max: '%s cannot be longer than %s characters', 38 | range: '%s must be between %s and %s characters' 39 | }, 40 | number: { 41 | len: '%s must equal %s', 42 | min: '%s cannot be less than %s', 43 | max: '%s cannot be greater than %s', 44 | range: '%s must be between %s and %s' 45 | }, 46 | array: { 47 | len: '%s must be exactly %s in length', 48 | min: '%s cannot be less than %s in length', 49 | max: '%s cannot be greater than %s in length', 50 | range: '%s must be between %s and %s in length' 51 | }, 52 | pattern: { 53 | mismatch: '%s value %s does not match pattern %s' 54 | } 55 | }; 56 | 57 | module.exports = messages; 58 | -------------------------------------------------------------------------------- /mkdoc.js: -------------------------------------------------------------------------------- 1 | var mk = require('mktask'); 2 | 3 | // @task readme build the readme file. 4 | function readme(cb) { 5 | mk.doc('doc/readme.md') 6 | .pipe(mk.pi()) 7 | .pipe(mk.ref()) 8 | .pipe(mk.abs()) 9 | .pipe(mk.msg()) 10 | .pipe(mk.toc({depth: 2, max: 3})) 11 | .pipe(mk.out()) 12 | .pipe(mk.dest('README.md')) 13 | .on('finish', cb); 14 | } 15 | 16 | mk.task(readme); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-validate", 3 | "description": "Asynchronous validation for node and the browser", 4 | "version": "1.0.3", 5 | "author": "muji ", 6 | "license": "MIT", 7 | "browser": "./lib/schema.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/tmpfs/async-validate.git" 11 | }, 12 | "keywords": [ 13 | "validation", 14 | "validate", 15 | "valid", 16 | "object", 17 | "type" 18 | ], 19 | "dependencies": { 20 | "format-util": "^1.0.3", 21 | "zephyr": "^1.3.6" 22 | }, 23 | "devDependencies": { 24 | "browserify": "^16.2.3", 25 | "chai": "^4.2.0", 26 | "coveralls": "^3.0.2", 27 | "istanbul": "~0.4.3", 28 | "mocha": "^5.2.0", 29 | "moment": "^2.22.2" 30 | }, 31 | "scripts": { 32 | "lint": "jshint . && jscs .", 33 | "docs": "npm run example && npm run readme", 34 | "readme": "mk readme", 35 | "example": "mdp --force -v -i doc/example.json -o EXAMPLE.md", 36 | "browser": "browserify -o async-validate.js -e ./lib/schema.js && du -bh async-validate.js", 37 | "clean": "rm -rf coverage ./async-validate.js ./test/spec.js", 38 | "spec": "node test/build.js > test/index.js && browserify -o test/spec.js -e test/index.js", 39 | "test": "NODE_ENV=test mocha test/global ${SPEC:-test/spec}", 40 | "cover": "NODE_ENV=test istanbul cover _mocha -- test/global ${SPEC:-test/spec}", 41 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /plugin/all.js: -------------------------------------------------------------------------------- 1 | var rule = require('../lib/rule'); 2 | rule.plugin([ 3 | require('./array'), 4 | require('./boolean'), 5 | require('./date'), 6 | require('./enum'), 7 | require('./float'), 8 | require('./integer'), 9 | require('./function'), 10 | require('./null'), 11 | require('./number'), 12 | require('./object'), 13 | require('./regexp'), 14 | require('./string'), 15 | require('./util') 16 | ]); 17 | -------------------------------------------------------------------------------- /plugin/array.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Validates an array. 5 | * 6 | * @param cb The callback function. 7 | */ 8 | this.main.array = function array(cb) { 9 | if(this.validates()) { 10 | this.required(); 11 | this.type(); 12 | this.range(); 13 | } 14 | cb(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /plugin/boolean.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Validates a boolean. 5 | * 6 | * @param cb The callback function. 7 | */ 8 | this.main.boolean = function bool(cb) { 9 | if(this.validates()) { 10 | this.required(); 11 | this.type(); 12 | } 13 | cb(); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /plugin/date.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rule for validating a date against a format. 3 | */ 4 | function validator() { 5 | var moment = require('moment'); 6 | var mmt = this.rule.local ? moment : moment.utc; 7 | var dt = !this.rule.format 8 | ? mmt(new Date(this.value)) : mmt(this.value, this.rule.format); 9 | if(!dt.isValid()) { 10 | if(this.rule.format) { 11 | this.raise( 12 | this.messages.date.format, 13 | this.field, this.value, this.rule.format); 14 | }else{ 15 | this.raise( 16 | this.messages.date.invalid, this.field, this.value); 17 | } 18 | } 19 | } 20 | 21 | module.exports = function() { 22 | 23 | /** 24 | * Validates a date against the format property. 25 | * 26 | * @param cb The callback function. 27 | */ 28 | this.main.date = function date(cb) { 29 | var validate = this.isRoot() || this.rule.required 30 | || (!this.rule.required && this.source.hasOwnProperty(this.field) 31 | && this.source[this.field]); 32 | 33 | if(validate) { 34 | this.required(); 35 | this.pattern(); 36 | validator.call(this); 37 | } 38 | 39 | cb(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /plugin/enum.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Validates an enumerable list. 5 | * 6 | * @param cb The callback function. 7 | */ 8 | this.main.enum = function enumerable(cb) { 9 | if(this.validates()) { 10 | this.required(); 11 | var list = this.rule.list; 12 | if(!~list.indexOf(this.value)) { 13 | this.raise( 14 | this.reasons.enumerable, 15 | this.messages.enum, 16 | this.field, list.join(', ')); 17 | } 18 | } 19 | cb(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /plugin/float.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Validates a number is a floating point number. 5 | * 6 | * @param cb The callback function. 7 | */ 8 | this.main.float = function fraction(cb) { 9 | if(this.validates()) { 10 | this.required(); 11 | this.type(); 12 | this.range(); 13 | } 14 | cb(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /plugin/function.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Validates a function. 5 | * 6 | * @param cb The callback function. 7 | */ 8 | this.main.function = function method(cb) { 9 | if(this.validates()) { 10 | this.required(); 11 | this.type(); 12 | this.range(); 13 | } 14 | cb(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /plugin/integer.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Validates a number is an integer. 5 | * 6 | * @param cb The callback function. 7 | */ 8 | this.main.integer = function integer(cb) { 9 | if(this.validates()) { 10 | this.required(); 11 | this.type(); 12 | this.range(); 13 | } 14 | cb(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /plugin/multiple.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmpfs/async-validate/20e0c621c6a48cbfbd1c0c5782972869ab272b58/plugin/multiple.js -------------------------------------------------------------------------------- /plugin/null.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Validates a value is null. 5 | * 6 | * @param cb The callback function. 7 | */ 8 | this.main.null = function validate(cb) { 9 | if(this.validates()) { 10 | this.required(); 11 | this.type(); 12 | } 13 | cb(); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /plugin/number.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Validates a number. 5 | * 6 | * @param cb The callback function. 7 | */ 8 | this.main.number = function number(cb) { 9 | if(this.validates()) { 10 | this.required(); 11 | this.type(); 12 | this.range(); 13 | } 14 | cb(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /plugin/object.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Validates an object. 5 | * 6 | * @param cb The callback function. 7 | */ 8 | this.main.object = function object(cb) { 9 | var expected 10 | , additional; 11 | if(this.validates()) { 12 | this.required(); 13 | this.type(); 14 | 15 | // nested deep properties 16 | if(this.rule.additional === false) { 17 | // NOTE: Object.keys() will throw if you declare `additional` 18 | // NOTE: for the `object` type but do not declare nested `fields` object 19 | expected = Object.keys(this.rule.fields); 20 | 21 | additional = this.diff( 22 | expected, Object.keys(this.value)); 23 | 24 | if(additional) { 25 | this.raise( 26 | this.reasons.additional, 27 | this.messages.additional, additional.join(', '), this.field); 28 | } 29 | } 30 | } 31 | cb(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /plugin/regexp.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Validates the regular expression type. 5 | * 6 | * @param cb The callback function. 7 | */ 8 | this.main.regexp = function regexp(cb) { 9 | if(this.validates()) { 10 | this.required(); 11 | this.type(); 12 | } 13 | cb(); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /plugin/string.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Performs validation for string types. 5 | * 6 | * @param cb The callback function. 7 | */ 8 | this.main.string = function string(cb) { 9 | if(this.validates() || this.rule.pattern) { 10 | this.required(); 11 | 12 | // if value is required and value is undefined 13 | // no need to add other error messages 14 | if(this.rule.required && this.value === undefined) { 15 | return cb(); 16 | } 17 | 18 | this.type(); 19 | this.range(); 20 | this.pattern(); 21 | 22 | if(this.rule.whitespace) { 23 | if(/^\s+$/.test(this.value) || this.value === '') { 24 | this.raise( 25 | this.reasons.whitespace, 26 | this.messages.whitespace, this.field); 27 | } 28 | } 29 | } 30 | cb(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /plugin/util.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.plugin( 3 | [ 4 | require('./util/pattern'), 5 | require('./util/range'), 6 | require('./util/required'), 7 | require('./util/type') 8 | ] 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /plugin/util/pattern.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Rule for validating a regular expression pattern. 5 | */ 6 | this.pattern = function pattern() { 7 | if((this.rule.pattern instanceof RegExp) 8 | && (!this.rule.pattern.test(this.value))) { 9 | this.raise( 10 | this.reasons.pattern, 11 | this.messages.pattern.mismatch, 12 | this.field, this.value, this.rule.pattern); 13 | } 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /plugin/util/range.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Rule for validating minimum and maximum allowed values. 5 | */ 6 | this.range = function range() { 7 | var rule = this.rule 8 | , value = this.value 9 | , val = value 10 | , key = null 11 | , arr = Array.isArray(value) 12 | , len = typeof rule.len === 'number' 13 | , min = typeof rule.min === 'number' 14 | , max = typeof rule.max === 'number' 15 | , num = typeof(value) === 'number' 16 | , str = typeof(value) === 'string' 17 | , fun = typeof(value) === 'function'; 18 | 19 | if(num || str || fun) { 20 | key = typeof(value); 21 | }else if(arr) { 22 | key = 'array'; 23 | } 24 | 25 | // if the value is not a supported range type ignore validation 26 | if(!key) { 27 | return false; 28 | } 29 | 30 | // use `length` property of `value` 31 | if(str || arr || fun) { 32 | val = value.length; 33 | } 34 | 35 | // length equality test 36 | if(len && (val !== rule.len)) { 37 | this.raise( 38 | this.reasons.length, 39 | this.messages[key].len, this.field, rule.len); 40 | // minimum value 41 | }else if(min && !max && val < rule.min ) { 42 | this.raise( 43 | this.reasons.min, 44 | this.messages[key].min, this.field, rule.min); 45 | // maximum value 46 | }else if( max && !min && val > rule.max ) { 47 | this.raise( 48 | this.reasons.max, 49 | this.messages[key].max, this.field, rule.max); 50 | // range test 51 | }else if(min && max && (val < rule.min || val > rule.max) ) { 52 | this.raise( 53 | val < rule.min ? this.reasons.min : this.reasons.max, 54 | this.messages[key].range, this.field, rule.min, rule.max); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /plugin/util/required.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Rule for validating required fields. 5 | */ 6 | this.required = function required() { 7 | if(!this.isRoot() 8 | && this.rule.required 9 | && (!this.source.hasOwnProperty(this.field) 10 | || this.value === undefined)) { 11 | this.raise( 12 | this.reasons.required, 13 | this.messages.required, this.field); 14 | } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /plugin/util/type.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | /** 4 | * Rule for validating a value type. 5 | */ 6 | this.type = function type() { 7 | var id = this.rule.type 8 | , Type = this.rule.Type 9 | , invalid = false; 10 | 11 | // instanceof comparison on `type` as function 12 | if(typeof(Type) === 'function') { 13 | if(!(this.value instanceof Type)) { 14 | this.raise( 15 | this.reasons.instance, 16 | this.messages.types.instance, 17 | this.field, Type.name || 'function (anonymous)'); 18 | } 19 | }else if(id === 'null') { 20 | invalid = this.value !== null; 21 | }else if(id === 'regexp') { 22 | if(!(this.value instanceof RegExp)) { 23 | try { 24 | new RegExp(this.value); 25 | }catch(e) { 26 | invalid = true; 27 | } 28 | } 29 | }else if(id === 'string') { 30 | invalid = typeof this.value !== 'string' && this.validates(); 31 | }else if(id === 'object') { 32 | invalid = (typeof(this.value) !== 'object' 33 | || Array.isArray(this.value)); 34 | }else if(id === 'array') { 35 | invalid = !Array.isArray(this.value); 36 | }else if(id === 'float') { 37 | invalid = typeof(this.value) !== 'number' 38 | || Number(this.value) === parseInt(this.value); 39 | }else if(id === 'integer') { 40 | invalid = typeof(this.value) !== 'number' 41 | || Number(this.value) !== parseInt(this.value); 42 | // straight typeof test 43 | }else{ 44 | invalid = typeof(this.value) !== id; 45 | } 46 | 47 | if(invalid) { 48 | this.raise( 49 | this.reasons.type, 50 | this.messages.types[this.rule.type], 51 | this.field, this.rule.type); 52 | } 53 | } 54 | 55 | /** 56 | * Validate an array of types using logical or. 57 | * 58 | * @param cb Callback function. 59 | */ 60 | function types(cb) { 61 | var list = this.rule.type.slice(0) 62 | , i 63 | , type 64 | , length = this.errors.length 65 | , invalid; 66 | 67 | if(this.validates()) { 68 | this.required(); 69 | 70 | for(i = 0;i < list.length;i++) { 71 | type = list[i]; 72 | delete this.rule.Type; 73 | if(typeof type === 'function') { 74 | this.rule.Type = type; 75 | } 76 | this.rule.type = type; 77 | this.type(); 78 | } 79 | 80 | invalid = (this.errors.length - length) === list.length; 81 | // remove raised errors 82 | this.errors = this.errors.slice(0, length); 83 | 84 | // all of the type tests failed 85 | if(invalid) { 86 | this.raise( 87 | this.reasons.type, 88 | this.messages.types.multiple, 89 | this.field, 90 | list.map(function(type) { 91 | if(typeof(type) === 'function') { 92 | return type.name || 'function (anonymous)'; 93 | } 94 | return type; 95 | }).join(', ') 96 | ) 97 | } 98 | } 99 | cb(); 100 | } 101 | 102 | this.main.types = types; 103 | } 104 | -------------------------------------------------------------------------------- /system.js: -------------------------------------------------------------------------------- 1 | // schema for a schema definition 2 | var schema = { 3 | type: 'object', 4 | fields: { 5 | additional: {type: 'boolean'}, 6 | fields: {type: 'object'}, 7 | format: {type: 'string'}, 8 | len: {type: 'integer'}, 9 | list: {type: 'array'}, 10 | local: {type: 'boolean'}, 11 | min: {type: 'integer'}, 12 | max: {type: 'integer'}, 13 | match: {type: 'regexp'}, 14 | pattern: {type: 'regexp'}, 15 | placeholder: {type: 'function'}, 16 | required: {type: 'boolean'}, 17 | test: {type: 'function'}, 18 | type: { 19 | type: ['string', 'function', 'array'], 20 | required: true 21 | }, 22 | values: {type: ['object', 'array']}, 23 | whitespace: {type: 'boolean'} 24 | } 25 | } 26 | 27 | module.exports = schema; 28 | -------------------------------------------------------------------------------- /test/build.js: -------------------------------------------------------------------------------- 1 | // build the browser index file 2 | // unfortunately require-dir was 3 | // not working with browserify 4 | var path = require('path') 5 | , fs = require('fs') 6 | , files = ['global'] 7 | // relative to project as cwd 8 | , contents = fs.readdirSync('test/spec') 9 | 10 | contents = contents.map(function(file) { 11 | return 'spec/' + path.basename(file, '.js'); 12 | }); 13 | 14 | files = files.concat(contents); 15 | 16 | files.forEach(function(file) { 17 | console.log('require(\'./%s\')', file); 18 | }); 19 | 20 | //console.dir(contents) 21 | -------------------------------------------------------------------------------- /test/fixtures/component.js: -------------------------------------------------------------------------------- 1 | // mock class for instanceof tests 2 | function Component(){} 3 | 4 | module.exports = Component; 5 | -------------------------------------------------------------------------------- /test/fixtures/email.js: -------------------------------------------------------------------------------- 1 | module.exports = /^.+@.+\..+/; 2 | -------------------------------------------------------------------------------- /test/fixtures/model.js: -------------------------------------------------------------------------------- 1 | var data = { 2 | foo: {id: 'foo'}, 3 | bar: {id: 'bar'} 4 | } 5 | 6 | // mock model class (vars) 7 | function Model() {} 8 | 9 | function findUserById(id, cb) { 10 | // normally find in a database 11 | return cb(null, data[id]); 12 | } 13 | 14 | Model.prototype.findUserById = findUserById; 15 | 16 | module.exports = Model; 17 | -------------------------------------------------------------------------------- /test/fixtures/plugin.js: -------------------------------------------------------------------------------- 1 | function id(cb) { 2 | var re = /^[^-][a-zA-Z0-9-]+$/; 3 | if(!re.test(this.value)) { 4 | this.raise('%s is not a valid identifier', this.field); 5 | } 6 | cb(); 7 | } 8 | 9 | module.exports = function() { 10 | // add static `id` type method 11 | this.main.id = id; 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/schema/additional.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | additional: false, 4 | fields: { 5 | address: { 6 | type: 'object', 7 | required: true, 8 | additional: false, 9 | fields: { 10 | street: {type: 'string', required: true}, 11 | city: {type: 'string', required: true}, 12 | zip: {type: 'string', required: true, len: 8, message: 'Invalid zip'} 13 | } 14 | } 15 | } 16 | } 17 | 18 | module.exports = schema; 19 | -------------------------------------------------------------------------------- /test/fixtures/schema/array-max.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | list: {type: 'array', max: 2} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/array-min.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | list: {type: 'array', min: 2} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/array-range.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | list: {type: 'array', min: 1, max: 2} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/array.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | list: {type: 'array'} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/assigned-rule.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , schema = { 3 | type: 'object', 4 | fields: { 5 | id: { 6 | foo: 'bar', 7 | test: function(cb) { 8 | expect(this.foo).to.eql('bar'); 9 | cb(); 10 | } 11 | } 12 | } 13 | } 14 | 15 | module.exports = schema; 16 | -------------------------------------------------------------------------------- /test/fixtures/schema/boolean.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | flag: {type: 'boolean'} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/date-no-format.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | active: {type: 'date'} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/date-pattern.js: -------------------------------------------------------------------------------- 1 | var ptn = /^([\d]{4})-([\d]{2})-([\d]{2})$/ 2 | , schema = { 3 | type: 'object', 4 | fields: { 5 | active: { 6 | type: 'date', 7 | format: 'YYYY-MM-DD', 8 | pattern: ptn 9 | } 10 | } 11 | } 12 | 13 | module.exports = schema; 14 | -------------------------------------------------------------------------------- /test/fixtures/schema/date-range.js: -------------------------------------------------------------------------------- 1 | var date = require('./date-pattern').fields.active 2 | , schema = { 3 | type: 'object', 4 | fields: { 5 | start: date, 6 | end: date 7 | } 8 | } 9 | 10 | module.exports = schema; 11 | -------------------------------------------------------------------------------- /test/fixtures/schema/date.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | active: {type: 'date', format: 'YYYY-MM-DD'} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/deep-array.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | roles: { 5 | type: 'array', required: true, len: 3, 6 | fields: { 7 | 0: {type: 'string', required: true}, 8 | 1: {type: 'string', required: true}, 9 | 2: {type: 'string', required: true} 10 | } 11 | } 12 | } 13 | } 14 | 15 | module.exports = schema; 16 | -------------------------------------------------------------------------------- /test/fixtures/schema/deep-details.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | name: {type: 'string', required: true}, 5 | address: { 6 | type: 'object', 7 | required: true, 8 | fields: { 9 | street: {type: 'string', required: true}, 10 | city: {type: 'string', required: true}, 11 | zip: {type: 'string', required: true, len: 8, message: 'invalid zip'} 12 | } 13 | } 14 | } 15 | } 16 | 17 | module.exports = schema; 18 | -------------------------------------------------------------------------------- /test/fixtures/schema/deep-object.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | address: { 5 | type: 'object', 6 | required: true, 7 | fields: { 8 | house: { 9 | type: 'object', 10 | required: true, 11 | fields: { 12 | name: {type: 'string', required: true}, 13 | number: {type: 'string', required: true} 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | 21 | module.exports = schema; 22 | -------------------------------------------------------------------------------- /test/fixtures/schema/deep-required.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | address: { 5 | type: 'object', 6 | required: true, 7 | fields: { 8 | street: {type: 'string', required: true}, 9 | city: {type: 'string', required: true}, 10 | zip: {type: 'string', required: true, len: 8, message: 'Invalid zip'} 11 | } 12 | } 13 | } 14 | } 15 | 16 | module.exports = schema; 17 | -------------------------------------------------------------------------------- /test/fixtures/schema/deep.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | address: { 5 | type: 'object', 6 | fields: { 7 | street: {type: 'string', required: true}, 8 | city: {type: 'string', required: true}, 9 | zip: {type: 'string', required: true, len: 8, message: 'Invalid zip'} 10 | } 11 | } 12 | } 13 | } 14 | 15 | module.exports = schema; 16 | -------------------------------------------------------------------------------- /test/fixtures/schema/enum.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | role: {type: 'enum', list: ['admin', 'user', 'guest']} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/error-fields.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | name: { 5 | type: 'string', 6 | required: true 7 | }, 8 | address: { 9 | type: 'object', 10 | fields: { 11 | name: { 12 | type: 'string', 13 | required: true 14 | } 15 | } 16 | } 17 | } 18 | } 19 | 20 | module.exports = schema; 21 | -------------------------------------------------------------------------------- /test/fixtures/schema/float.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | ratio: {type: 'float'} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/function-length.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | mock: {type: 'function', len: 1} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/function-max.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | mock: {type: 'function', max: 0} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/function-min.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | mock: {type: 'function', min: 1} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/schema/function-range.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | mock: {type: 'function', min: 0, max: 1} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/function.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | mock: {type: 'function'} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/instanceof-anonymous.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | instance: { 5 | type: function(){} 6 | } 7 | } 8 | } 9 | 10 | module.exports = schema; 11 | -------------------------------------------------------------------------------- /test/fixtures/schema/instanceof-message.js: -------------------------------------------------------------------------------- 1 | var Component = require('../component') 2 | , schema = { 3 | type: 'object', 4 | fields: { 5 | instance: { 6 | type: Component, 7 | message: function(message, parameters) { 8 | message = '%s is not a %s'; 9 | parameters[1] = this.rule.Type.name; 10 | return this.format.apply(this, [message].concat(parameters)); 11 | } 12 | } 13 | } 14 | } 15 | 16 | module.exports = schema; 17 | -------------------------------------------------------------------------------- /test/fixtures/schema/instanceof.js: -------------------------------------------------------------------------------- 1 | var Component = require('../component') 2 | , schema = { 3 | type: 'object', 4 | fields: { 5 | instance: { 6 | type: Component 7 | } 8 | } 9 | } 10 | 11 | module.exports = schema; 12 | -------------------------------------------------------------------------------- /test/fixtures/schema/integer.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | port: {type: 'integer'} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/internal-error.js: -------------------------------------------------------------------------------- 1 | var schema= { 2 | type: 'object', 3 | fields: { 4 | mock: function(cb) { 5 | cb(new Error('query error')); 6 | }, 7 | next: function() { 8 | throw new Error('rule validation function invoked unexpectedly'); 9 | } 10 | } 11 | } 12 | 13 | module.exports = schema; 14 | -------------------------------------------------------------------------------- /test/fixtures/schema/length-array.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | roles: {type: "array", len: 2} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/length-number.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | port: {type: "number", len: 80} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/length-string.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | name: {type: "string", len: 8} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/match.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | required: true, 4 | fields: { 5 | all: { 6 | match: /^address[1-3]$/, 7 | type: 'string' 8 | } 9 | } 10 | } 11 | 12 | module.exports = schema; 13 | -------------------------------------------------------------------------------- /test/fixtures/schema/message-function-error.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , schema = { 3 | type: 'object', 4 | fields: { 5 | name: { 6 | type: 'string', 7 | required: true, 8 | message: function(message, parameters) { 9 | expect(message).to.be.a('string'); 10 | expect(parameters).to.be.an('array'); 11 | return new Error('Name is required'); 12 | } 13 | } 14 | } 15 | } 16 | 17 | module.exports = schema; 18 | -------------------------------------------------------------------------------- /test/fixtures/schema/message-function.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , schema = { 3 | type: 'object', 4 | fields: { 5 | name: { 6 | type: 'string', 7 | required: true, 8 | message: function(message, parameters) { 9 | expect(message).to.be.a('string'); 10 | expect(parameters).to.be.an('array'); 11 | return 'Name is required'; 12 | } 13 | } 14 | } 15 | } 16 | 17 | module.exports = schema; 18 | -------------------------------------------------------------------------------- /test/fixtures/schema/message-literal.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var schema = { 3 | type: 'object', 4 | fields: { 5 | name: {type: 'string', required: true}, 6 | job: { 7 | type: 'string', 8 | required: true, 9 | message: function(msg, params) { 10 | return util.format.apply(util, [msg].concat(params)); 11 | } 12 | } 13 | } 14 | } 15 | 16 | module.exports = schema; 17 | -------------------------------------------------------------------------------- /test/fixtures/schema/message-object.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | num: { 5 | type: 'number', min: 0, max: 10, 6 | message: { 7 | min: 'Number may not be below zero', 8 | max: 'Number may not be above ten' 9 | } 10 | } 11 | } 12 | } 13 | 14 | module.exports = schema; 15 | -------------------------------------------------------------------------------- /test/fixtures/schema/message-string-override.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | name: {type: 'string', required: true} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/message-string.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | name: {type: 'string', required: true, message: 'Name is required'} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/multiple-rules-function.js: -------------------------------------------------------------------------------- 1 | var email = require('../email') 2 | , schema = { 3 | type: 'object', 4 | fields: { 5 | email: [ 6 | {type: 'string', pattern: email, required: true}, 7 | function(cb) { 8 | var email = 'user@example.com'; 9 | if(this.value === email) { 10 | this.raise('Email address %s already exists', email); 11 | } 12 | cb(); 13 | } 14 | ] 15 | } 16 | } 17 | 18 | module.exports = schema; 19 | -------------------------------------------------------------------------------- /test/fixtures/schema/multiple-rules-source.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | var email = require('../email') 4 | , schema = { 5 | type: 'object', 6 | fields: { 7 | email: [ 8 | {type: 'string', required: true}, 9 | {type: 'string', pattern: email} 10 | ], 11 | token: function(cb) { 12 | expect(this.source.email).to.be.a('string'); 13 | cb(); 14 | } 15 | } 16 | } 17 | 18 | module.exports = schema; 19 | -------------------------------------------------------------------------------- /test/fixtures/schema/multiple-rules.js: -------------------------------------------------------------------------------- 1 | var email = require('../email') 2 | , schema = { 3 | type: 'object', 4 | fields: { 5 | email: [ 6 | {type: 'string', required: true}, 7 | {type: 'string', pattern: email} 8 | ] 9 | } 10 | } 11 | 12 | module.exports = schema; 13 | -------------------------------------------------------------------------------- /test/fixtures/schema/multiple-types-required.js: -------------------------------------------------------------------------------- 1 | var Component = require('../component') 2 | , schema = { 3 | type: 'object', 4 | fields: { 5 | prop: { 6 | type: [Boolean, 'string', Component, function(){}], 7 | required: true 8 | } 9 | } 10 | } 11 | 12 | module.exports = schema; 13 | -------------------------------------------------------------------------------- /test/fixtures/schema/multiple-types.js: -------------------------------------------------------------------------------- 1 | var Component = require('../component') 2 | , schema = { 3 | type: 'object', 4 | fields: { 5 | prop: { 6 | type: [Boolean, 'string', Component, function(){}] 7 | } 8 | } 9 | } 10 | 11 | module.exports = schema; 12 | -------------------------------------------------------------------------------- /test/fixtures/schema/null.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | value: { 5 | type: 'null' 6 | } 7 | } 8 | } 9 | 10 | module.exports = schema; 11 | -------------------------------------------------------------------------------- /test/fixtures/schema/number-max.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | port: {type: 'number', max: 80} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/number-min.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | port: {type: 'number', min: 8080} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/number-range.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | port: {type: 'number', min: 80, max: 1024} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/number.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | port: {type: 'number'} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/object-additional.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | address: { 5 | type: 'object', 6 | required: true, 7 | additional: false, 8 | fields: { 9 | street: {type: 'string', required: true}, 10 | city: {type: 'string', required: true}, 11 | zip: {type: 'string', required: true, len: 8, message: 'Invalid zip'} 12 | } 13 | } 14 | } 15 | } 16 | 17 | module.exports = schema; 18 | -------------------------------------------------------------------------------- /test/fixtures/schema/object.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | address: {type: "object", required: true} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/options-pattern.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | name: {type: 'string', required: true, min: 10, pattern: /^[^-].*$/} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/options.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | firstname: {type: 'string', required: true}, 5 | surname: {type: 'string', required: true} 6 | } 7 | } 8 | 9 | module.exports = schema; 10 | -------------------------------------------------------------------------------- /test/fixtures/schema/parallel.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object' 3 | } 4 | 5 | module.exports = schema; 6 | -------------------------------------------------------------------------------- /test/fixtures/schema/placeholder.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | list: { 5 | type: 'array', 6 | values: {type: 'integer'}, 7 | placeholder: function() { 8 | return []; 9 | } 10 | } 11 | } 12 | } 13 | 14 | module.exports = schema; 15 | -------------------------------------------------------------------------------- /test/fixtures/schema/plugin.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | id: {type: 'id'} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/raise.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | name: function(cb) { 5 | this.raise( 6 | this.reasons.required, 7 | '%s is a required field', this.field); 8 | cb(); 9 | } 10 | } 11 | } 12 | 13 | module.exports = schema; 14 | -------------------------------------------------------------------------------- /test/fixtures/schema/regexp.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | re: {type: "regexp"} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/schema/source-additional.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | additional: false, 4 | // trigger code path whereby transform cannot assign inline 5 | // on root object as there is no parent object to assign to 6 | transform: function(value) { 7 | return value; 8 | }, 9 | fields: { 10 | address: { 11 | type: "object", 12 | required: true 13 | } 14 | } 15 | } 16 | 17 | module.exports = schema; 18 | -------------------------------------------------------------------------------- /test/fixtures/schema/state.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , schema = { 3 | type: 'object', 4 | fields: { 5 | email: [ 6 | {type: 'string', required: true, pattern: /^.+@.+\..+/}, 7 | function parse(cb) { 8 | var at = this.value.indexOf('@') 9 | , user = this.value.substr(0, at) 10 | , domain = this.value.substr(at + 1); 11 | // assign to validation state 12 | this.state.email = {user: user, domain: domain}; 13 | cb(); 14 | }, 15 | function assert(cb) { 16 | expect(this.state.email).to.be.an('object'); 17 | expect(this.state.email.user).to.eql('user'); 18 | expect(this.state.email.domain).to.eql('example.com'); 19 | cb(); 20 | } 21 | ] 22 | } 23 | } 24 | 25 | module.exports = schema; 26 | -------------------------------------------------------------------------------- /test/fixtures/schema/transform.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | name: { 5 | type: 'string', 6 | required: true, pattern: /^[a-z]+$/, 7 | transform: function(value) { 8 | return value.trim(); 9 | } 10 | } 11 | } 12 | } 13 | 14 | module.exports = schema; 15 | -------------------------------------------------------------------------------- /test/fixtures/schema/typed-array-empty.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | list: { 5 | type: 'array', 6 | values: [] 7 | } 8 | } 9 | } 10 | 11 | module.exports = schema; 12 | -------------------------------------------------------------------------------- /test/fixtures/schema/typed-array-mixed.js: -------------------------------------------------------------------------------- 1 | var value = require('../value-message') 2 | , schema = { 3 | type: 'object', 4 | fields: { 5 | list: { 6 | type: 'array', 7 | values: [ 8 | {type: 'integer', message: value}, 9 | {type: 'string', message: value}, 10 | {type: 'float', message: value} 11 | ] 12 | } 13 | } 14 | } 15 | 16 | module.exports = schema; 17 | -------------------------------------------------------------------------------- /test/fixtures/schema/typed-array.js: -------------------------------------------------------------------------------- 1 | var value = require('../value-message') 2 | , schema = { 3 | type: 'object', 4 | fields: { 5 | list: { 6 | type: 'array', 7 | values: { 8 | type: 'integer', 9 | message: value 10 | } 11 | } 12 | } 13 | } 14 | 15 | module.exports = schema; 16 | -------------------------------------------------------------------------------- /test/fixtures/schema/vars.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | type: 'object', 3 | fields: { 4 | id: {type: 'id'} 5 | } 6 | } 7 | 8 | module.exports = schema; 9 | -------------------------------------------------------------------------------- /test/fixtures/value-message.js: -------------------------------------------------------------------------------- 1 | // an error message with the array index is not very useful 2 | // in this instance use the value instead 3 | function value(message, parameters) { 4 | parameters[0] = this.value; 5 | return this.format.apply(this, [message].concat(parameters)); 6 | } 7 | 8 | module.exports = value; 9 | -------------------------------------------------------------------------------- /test/fixtures/vars-plugin.js: -------------------------------------------------------------------------------- 1 | function id(cb) { 2 | var rule = this 3 | , val = this.value; 4 | 5 | this.model.findUserById(val, function(err, user) { 6 | if(err) { 7 | return cb(err); 8 | } 9 | if(!user) { 10 | rule.raise('user not found for id %s', val); 11 | } 12 | cb(); 13 | }) 14 | } 15 | 16 | module.exports = function() { 17 | // add static `id` type method 18 | this.main.id = id; 19 | } 20 | -------------------------------------------------------------------------------- /test/global.js: -------------------------------------------------------------------------------- 1 | // load all plugins for tests 2 | require('../plugin/all'); 3 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Async Validate: Test Specifications 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 19 | 20 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | -u bdd 2 | --recursive 3 | --bail 4 | --check-leaks 5 | --reporter list 6 | -A 7 | -------------------------------------------------------------------------------- /test/spec/additional.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/additional'); 4 | 5 | describe('async-validate:', function() { 6 | 7 | it('should error on invalid object (additional properties)', function(done) { 8 | var opts = { 9 | // set root source object field name 10 | field: 'root' 11 | } 12 | var source = { 13 | name: 'Oops', 14 | address: { 15 | name: 'Oops', 16 | street: 'Mock St', 17 | city: 'Mock City', 18 | zip: '12345678' 19 | } 20 | } 21 | 22 | var schema = new Schema(descriptor); 23 | schema.validate(source, opts, function(err, res) { 24 | //console.dir(res); 25 | expect(res.errors.length).to.eql(2); 26 | expect(res.errors[0].message).to.eql( 27 | 'extraneous fields (name) found in root'); 28 | expect(res.errors[1].message).to.eql( 29 | 'extraneous fields (name) found in address'); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should validate with no additional properties', function(done) { 35 | var source = { 36 | address: { 37 | street: 'Mock St', 38 | city: 'Mock City', 39 | zip: '12345678' 40 | } 41 | } 42 | var schema = new Schema(descriptor); 43 | schema.validate(source, function(err, res) { 44 | expect(err).to.eql(null); 45 | expect(res).to.eql(null); 46 | done(); 47 | }); 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /test/spec/array-values.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/typed-array') 4 | , mixed = require('../fixtures/schema/typed-array-mixed') 5 | , empty = require('../fixtures/schema/typed-array-empty'); 6 | 7 | describe('async-validate:', function() { 8 | 9 | it('should error on invalid array value type', function(done) { 10 | var schema = new Schema(descriptor); 11 | schema.validate({list: [1,2,3,'foo']}, function(err, res) { 12 | expect(res.errors.length).to.eql(1); 13 | expect(res.errors[0].message).to.eql('foo is not an integer'); 14 | done(); 15 | }); 16 | }); 17 | 18 | it('should error on invalid array value types', function(done) { 19 | var schema = new Schema(mixed); 20 | schema.validate({list: [16,'foo', 12]}, function(err, res) { 21 | expect(res.errors.length).to.eql(1); 22 | expect(res.errors[0].message).to.eql('12 is not a float'); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('should validate array values', function(done) { 28 | var schema = new Schema(descriptor); 29 | schema.validate({list: [1,2,3]}, function(err, res) { 30 | expect(err).to.eql(null); 31 | expect(res).to.eql(null); 32 | done(); 33 | }); 34 | }); 35 | 36 | it('should validate array values with empty array', function(done) { 37 | var schema = new Schema(descriptor); 38 | schema.validate({list: []}, function(err, res) { 39 | expect(err).to.eql(null); 40 | expect(res).to.eql(null); 41 | done(); 42 | }); 43 | }); 44 | 45 | it('should validate array values with no field', function(done) { 46 | var schema = new Schema(descriptor); 47 | schema.validate({}, function(err, res) { 48 | expect(err).to.eql(null); 49 | expect(res).to.eql(null); 50 | done(); 51 | }); 52 | }); 53 | 54 | it('should ignore validation with empty array as values', function(done) { 55 | var schema = new Schema(empty); 56 | schema.validate({list: [1,2,3, 'foo']}, function(err, res) { 57 | expect(err).to.eql(null); 58 | expect(res).to.eql(null); 59 | done(); 60 | }); 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /test/spec/array.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/array') 4 | , min = require('../fixtures/schema/array-min') 5 | , max = require('../fixtures/schema/array-max') 6 | , range = require('../fixtures/schema/array-range') 7 | 8 | describe('async-validate:', function() { 9 | 10 | it('should error on non-array type', function(done) { 11 | var schema = new Schema(descriptor); 12 | schema.validate({list: false}, function(err, res) { 13 | expect(res.errors.length).to.eql(1); 14 | expect(res.errors[0].message).to.eql( 15 | 'list is not an array'); 16 | done(); 17 | }); 18 | }); 19 | 20 | it('should error on array length minimum', function(done) { 21 | var schema = new Schema(min); 22 | schema.validate({list: [1]}, function(err, res) { 23 | expect(res.errors.length).to.eql(1); 24 | expect(res.errors[0].message).to.eql( 25 | 'list cannot be less than 2 in length'); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('should error on array length maximum', function(done) { 31 | var schema = new Schema(max); 32 | schema.validate({list: [1,2,3]}, function(err, res) { 33 | expect(res.errors.length).to.eql(1); 34 | expect(res.errors[0].message).to.eql( 35 | 'list cannot be greater than 2 in length'); 36 | done(); 37 | }); 38 | }); 39 | 40 | it('should error on array length range', function(done) { 41 | var schema = new Schema(range); 42 | schema.validate({list: [1,2,3]}, function(err, res) { 43 | expect(res.errors.length).to.eql(1); 44 | expect(res.errors[0].message).to.eql( 45 | 'list must be between 1 and 2 in length'); 46 | done(); 47 | }); 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /test/spec/assigned-rule.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../../index') 2 | , descriptor = require('../fixtures/schema/assigned-rule'); 3 | 4 | describe('async-validate:', function() { 5 | 6 | it('should access rule property from test function', function(done) { 7 | var schema = new Schema(descriptor); 8 | schema.validate({id: 'mock'}, function() { 9 | done(); 10 | }); 11 | }); 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /test/spec/boolean.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/boolean'); 4 | 5 | describe('async-validate:', function() { 6 | 7 | it('should error on non-boolean type', function(done) { 8 | var schema = new Schema(descriptor); 9 | schema.validate({flag: 'false'}, function(err, res) { 10 | expect(res.errors.length).to.eql(1); 11 | expect(res.errors[0].message).to.eql('flag is not a boolean'); 12 | done(); 13 | }); 14 | }); 15 | 16 | it('should validate boolean pass', function(done) { 17 | var schema = new Schema(descriptor); 18 | schema.validate({flag: true}, function(err, res) { 19 | expect(err).to.eql(null); 20 | expect(res).to.eql(null); 21 | done(); 22 | }); 23 | }); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /test/spec/clone.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index'); 3 | 4 | describe('async-validate:', function() { 5 | 6 | it('should return simple object on clone', function(done) { 7 | var value = 'mock'; 8 | expect(Schema.clone(value)).to.eql(value); 9 | done(); 10 | }); 11 | 12 | it('should clone array', function(done) { 13 | var value = [1,2,['foo']]; 14 | expect(Schema.clone(value)).to.eql([1,2,['foo']]); 15 | done(); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /test/spec/date.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/date') 4 | , noformat = require('../fixtures/schema/date-no-format') 5 | , pattern = require('../fixtures/schema/date-pattern') 6 | , range = require('../fixtures/schema/date-range'); 7 | 8 | describe('async-validate:', function() { 9 | 10 | it('should error on invalid date value using a format', function(done) { 11 | var schema = new Schema(descriptor); 12 | schema.validate({active: '2013-06-50'}, function(err, res) { 13 | expect(res.errors.length).to.eql(1); 14 | expect(res.errors[0].message).to.eql( 15 | 'active date 2013-06-50 is invalid for format YYYY-MM-DD'); 16 | done(); 17 | }); 18 | }); 19 | 20 | it('should error on invalid date value no format (ISO 8601)', 21 | function(done) { 22 | var schema = new Schema(noformat); 23 | schema.validate({active: '2011-10-10T10:20:90'}, function(err, res) { 24 | expect(res.errors.length).to.eql(1); 25 | expect(res.errors[0].message).to.eql( 26 | 'active date 2011-10-10T10:20:90 is invalid'); 27 | done(); 28 | }); 29 | } 30 | ); 31 | 32 | it('should error on invalid date value no format (bad input)', 33 | function(done) { 34 | var schema = new Schema(noformat); 35 | schema.validate({active: 'not a date'}, function(err, res) { 36 | expect(res.errors.length).to.eql(1); 37 | expect(res.errors[0].message).to.eql( 38 | 'active date not a date is invalid'); 39 | done(); 40 | }); 41 | } 42 | ); 43 | 44 | it('should error on invalid date value using a format and pattern', 45 | function(done) { 46 | var schema = new Schema(pattern); 47 | schema.validate({active: '13-06-24'}, function(err, res) { 48 | expect(res.errors.length).to.eql(1); 49 | expect(res.errors[0].message).to.eql( 50 | 'active value 13-06-24 does not match pattern ' 51 | + pattern.fields.active.pattern); 52 | done(); 53 | }); 54 | } 55 | ); 56 | 57 | it('should validate date value using a format', function(done) { 58 | var schema = new Schema(descriptor); 59 | schema.validate({active: '2013-06-24'}, function(err, res) { 60 | expect(err).to.eql(null); 61 | expect(res).to.eql(null); 62 | done(); 63 | }); 64 | }); 65 | 66 | it('should validate date value using a format and local', function(done) { 67 | var descriptor = { 68 | type: 'object', 69 | fields: { 70 | active: {type: 'date', format: 'YYYY-MM-DD', local: true} 71 | } 72 | } 73 | var schema = new Schema(descriptor); 74 | schema.validate({active: '2013-06-24'}, function(err, res) { 75 | expect(err).to.eql(null); 76 | expect(res).to.eql(null); 77 | done(); 78 | }); 79 | }); 80 | 81 | it('should validate date value no format (ISO 8601)', function(done) { 82 | var schema = new Schema(descriptor); 83 | schema.validate({active: '2011-10-10T10:20:30'}, function(err, res) { 84 | expect(err).to.eql(null); 85 | expect(res).to.eql(null); 86 | done(); 87 | }); 88 | }); 89 | 90 | it('should validate optional date range reference', function(done) { 91 | var schema = new Schema(range); 92 | schema.validate({start: '', end: '2013-06-24'}, function(err, res) { 93 | expect(err).to.eql(null); 94 | expect(res).to.eql(null); 95 | done(); 96 | }); 97 | }); 98 | 99 | }); 100 | -------------------------------------------------------------------------------- /test/spec/deep.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/deep') 4 | , required = require('../fixtures/schema/deep-required') 5 | , details = require('../fixtures/schema/deep-details') 6 | , arrayFields = require('../fixtures/schema/deep-array') 7 | , deepObject = require('../fixtures/schema/deep-object'); 8 | 9 | describe('async-validate:', function() { 10 | 11 | it('should error on invalid deep rule (required/no matching property)', 12 | function(done) { 13 | var schema = new Schema(required); 14 | schema.validate({}, function(err, res) { 15 | expect(res.errors.length).to.eql(1); 16 | expect(res.errors[0].message).to.eql('address is required'); 17 | done(); 18 | }); 19 | } 20 | ); 21 | 22 | it('should error on invalid deep rule (required and failure on deep rule)', 23 | function(done) { 24 | var schema = new Schema(details); 25 | schema.validate({ address: {} }, function(err, res) { 26 | expect(res.errors.length).to.eql(4); 27 | expect(res.errors[0].message).to.eql('name is required'); 28 | expect(res.errors[1].message).to.eql('street is required'); 29 | expect(res.errors[2].message).to.eql('city is required'); 30 | expect(res.errors[3].message).to.eql('invalid zip'); 31 | done(); 32 | }); 33 | } 34 | ); 35 | 36 | it('should error on deep rule (array type length mismatch)', function(done) { 37 | var schema = new Schema(arrayFields); 38 | schema.validate({ roles: ['admin', 'user'] }, function(err, res) { 39 | expect(res.errors.length).to.eql(2); 40 | expect(res.errors[0].message).to.eql('roles must be exactly 3 in length'); 41 | expect(res.errors[1].message).to.eql('2 is required'); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('should error on invalid multiple deep rule', function(done) { 47 | var schema = new Schema(deepObject); 48 | schema.validate({ address: {house: {}} }, function(err, res) { 49 | expect(res.errors.length).to.eql(2); 50 | expect(res.errors[0].message).to.eql('name is required'); 51 | expect(res.errors[1].message).to.eql('number is required'); 52 | done(); 53 | }); 54 | }); 55 | 56 | it('should validate deep rule (not required/no matching property)', 57 | function(done) { 58 | var schema = new Schema(descriptor); 59 | schema.validate({}, function(err, res) { 60 | expect(err).to.eql(null); 61 | expect(res).to.eql(null); 62 | done(); 63 | }); 64 | } 65 | ); 66 | 67 | it('should validate deep rule (array type)', function(done) { 68 | var schema = new Schema(arrayFields) 69 | , source = {roles: ['admin', 'user', 'guest']}; 70 | schema.validate(source, function(err, res) { 71 | expect(err).to.eql(null); 72 | expect(res).to.eql(null); 73 | done(); 74 | }); 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /test/spec/enum.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/enum'); 4 | 5 | describe('async-validate:', function() { 6 | 7 | it('should error on invalid enum value', function(done) { 8 | var schema = new Schema(descriptor); 9 | schema.validate({role: 'manager'}, function(err, res) { 10 | expect(res.errors.length).to.eql(1); 11 | expect(res.errors[0].message).to.eql( 12 | 'role must be one of admin, user, guest'); 13 | done(); 14 | }); 15 | }); 16 | 17 | it('should validate enum value', function(done) { 18 | var schema = new Schema(descriptor); 19 | schema.validate({role: 'user'}, function(err, res) { 20 | expect(err).to.eql(null); 21 | expect(res).to.eql(null); 22 | done(); 23 | }); 24 | }); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /test/spec/error-fields.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/error-fields'); 4 | 5 | describe('async-validate:', function() { 6 | 7 | it('should use unique error field names', function(done) { 8 | var schema = new Schema(descriptor) 9 | , source = {address:{}}; 10 | schema.validate(source, function(err, res) { 11 | expect(res.fields.name.length).to.eql(1); 12 | expect(res.fields['address.name'].length).to.eql(1); 13 | done(); 14 | }); 15 | }); 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /test/spec/float.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/float'); 4 | 5 | describe('async-validate:', function() { 6 | 7 | it('should error when a number is not a float', function(done) { 8 | var schema = new Schema(descriptor); 9 | schema.validate({ratio: 1618}, function(err, res) { 10 | expect(res.errors.length).to.eql(1); 11 | expect(res.errors[0].message).to.eql('ratio is not a float'); 12 | done(); 13 | }); 14 | }); 15 | 16 | it('should validate a number is a float', function(done) { 17 | var schema = new Schema(descriptor); 18 | schema.validate({ratio: 1.667}, function(err, res) { 19 | expect(err).to.eql(null); 20 | expect(res).to.eql(null); 21 | done(); 22 | }); 23 | }); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /test/spec/function.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/function') 4 | , length = require('../fixtures/schema/function-length') 5 | , min = require('../fixtures/schema/function-min') 6 | , max = require('../fixtures/schema/function-max') 7 | , range = require('../fixtures/schema/function-range'); 8 | 9 | describe('async-validate:', function() { 10 | 11 | it('should error on value that is not a function', function(done) { 12 | var schema = new Schema(descriptor); 13 | schema.validate({mock: 80}, function(err, res) { 14 | expect(res.errors.length).to.eql(1); 15 | expect(res.errors[0].message).to.eql('mock is not a function'); 16 | done(); 17 | }); 18 | }); 19 | 20 | it('should error on invalid arity (len: 1)', function(done) { 21 | var schema = new Schema(length); 22 | schema.validate({mock: function(){}}, function(err, res) { 23 | expect(res.errors.length).to.eql(1); 24 | expect(res.errors[0].message).to.eql( 25 | 'mock must have exactly 1 arguments'); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('should error on invalid arity (min: 1)', function(done) { 31 | var schema = new Schema(min); 32 | schema.validate({mock: function(){}}, function(err, res) { 33 | expect(res.errors.length).to.eql(1); 34 | expect(res.errors[0].message).to.eql( 35 | 'mock must have at least 1 arguments'); 36 | done(); 37 | }); 38 | }); 39 | 40 | it('should error on invalid arity (max: 0)', function(done) { 41 | var schema = new Schema(max); 42 | schema.validate({mock: function(foo){foo();}}, function(err, res) { 43 | expect(res.errors.length).to.eql(1); 44 | expect(res.errors[0].message).to.eql( 45 | 'mock cannot have more than 0 arguments'); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should error on invalid arity (min: 0, max: 1)', function(done) { 51 | var schema = new Schema(range) 52 | , source = {mock: function(foo, bar){foo();bar()}}; 53 | 54 | schema.validate(source, function(err, res) { 55 | expect(res.errors.length).to.eql(1); 56 | expect(res.errors[0].message).to.eql( 57 | 'mock must have arguments length between 0 and 1'); 58 | done(); 59 | }); 60 | }); 61 | 62 | it('should validate function type', function(done) { 63 | var schema = new Schema(descriptor); 64 | schema.validate({mock: function(){}}, function(err, res) { 65 | expect(err).to.eql(null); 66 | expect(res).to.eql(null); 67 | done(); 68 | }); 69 | }); 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /test/spec/instanceof.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , Component = require('../fixtures/component') 4 | , descriptor = require('../fixtures/schema/instanceof') 5 | , anonymous = require('../fixtures/schema/instanceof-anonymous') 6 | , message = require('../fixtures/schema/instanceof-message'); 7 | 8 | describe("async-validate:", function() { 9 | 10 | it("should error on type as class (instanceof)", function(done) { 11 | var schema = new Schema(descriptor); 12 | schema.validate({instance: []}, function(err, res) { 13 | expect(res.errors.length).to.eql(1); 14 | expect(res.errors[0].message).to.eql( 15 | 'instance is not an instance of Component'); 16 | done(); 17 | }); 18 | }); 19 | 20 | it("should error on type as class (instanceof) anonymous", function(done) { 21 | var schema = new Schema(anonymous); 22 | schema.validate({instance: []}, function(err, res) { 23 | expect(res.errors.length).to.eql(1); 24 | expect( 25 | /instance is not an instance of/.test(res.errors[0].message)) 26 | .to.eql(true) 27 | done(); 28 | }); 29 | }); 30 | 31 | it("should error on type as class (instanceof) w/ message", function(done) { 32 | var schema = new Schema(message); 33 | schema.validate({instance: []}, function(err, res) { 34 | expect(res.errors.length).to.eql(1); 35 | expect(res.errors[0].message).to.eql('instance is not a Component'); 36 | done(); 37 | }); 38 | }); 39 | 40 | it("should validate on type as class (instanceof)", function(done) { 41 | var schema = new Schema(message); 42 | schema.validate({instance: new Component()}, function(err, res) { 43 | expect(err).to.eql(null); 44 | expect(res).to.eql(null); 45 | done(); 46 | }); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /test/spec/integer.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/integer'); 4 | 5 | describe('async-validate:', function() { 6 | 7 | it('should error on number not an integer', function(done) { 8 | var schema = new Schema(descriptor); 9 | schema.validate({port: 1.618}, function(err, res) { 10 | expect(res.errors.length).to.eql(1); 11 | expect(res.errors[0].message).to.eql('port is not an integer'); 12 | done(); 13 | }); 14 | }); 15 | 16 | it('should validate integer type', function(done) { 17 | var schema = new Schema(descriptor); 18 | schema.validate({port: 2048}, function(err, res) { 19 | expect(err).to.eql(null); 20 | expect(res).to.eql(null); 21 | done(); 22 | }); 23 | }); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /test/spec/internal-error.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/internal-error'); 4 | 5 | describe('async-validate:', function() { 6 | 7 | it('should callback with error', function(done) { 8 | var schema = new Schema(descriptor); 9 | schema.validate({}, function(err) { 10 | function fn() { 11 | throw err; 12 | } 13 | expect(fn).throws(Error); 14 | expect(fn).throws(/query error/); 15 | done(); 16 | }); 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /test/spec/iterator.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , iterator = require('../../lib/iterator'); 3 | 4 | describe("async-validate:", function() { 5 | 6 | it("should call complete on empty iterator list", function(done) { 7 | iterator.mapSeries([], function noop(){}, done); 8 | }); 9 | 10 | it("should map multiple items", function(done) { 11 | iterator.map([1,2,3], 12 | function multiply(num, cb) { 13 | cb(null, num * 10); 14 | }, 15 | function complete(err, results) { 16 | expect(err).to.eql(null); 17 | expect(results).to.eql([10,20,30]); 18 | done(); 19 | }); 20 | }); 21 | 22 | it("should callback with error (mapSeries)", function(done) { 23 | iterator.mapSeries([1,2,3], 24 | function multiply(num, cb) { 25 | cb(new Error('mock error')); 26 | }, 27 | function complete(err) { 28 | function fn() { 29 | throw err; 30 | } 31 | expect(fn).throws(Error); 32 | done(); 33 | }); 34 | }); 35 | 36 | it("should callback with error (map)", function(done) { 37 | iterator.map([1,2,3], 38 | function multiply(num, cb) { 39 | cb(new Error('mock error')); 40 | }, 41 | function complete(err) { 42 | function fn() { 43 | throw err; 44 | } 45 | expect(fn).throws(Error); 46 | done(); 47 | }); 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /test/spec/length.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/length-string') 4 | , number = require('../fixtures/schema/length-number') 5 | , array = require('../fixtures/schema/length-array'); 6 | 7 | describe("async-validate:", function() { 8 | 9 | it("should error on invalid string length", function(done) { 10 | var schema = new Schema(descriptor); 11 | schema.validate({name: "user"}, function(err, res) { 12 | expect(res.errors.length).to.eql(1); 13 | expect(res.errors[0].message).to.eql( 14 | 'name must be exactly 8 characters'); 15 | done(); 16 | }); 17 | }); 18 | 19 | it("should error on invalid number length", function(done) { 20 | var schema = new Schema(number); 21 | schema.validate({port: 8080}, function(err, res) { 22 | expect(res.errors.length).to.eql(1); 23 | expect(res.errors[0].message).to.eql('port must equal 80'); 24 | done(); 25 | }); 26 | }); 27 | 28 | it("should error on invalid array length", function(done) { 29 | var schema = new Schema(array); 30 | schema.validate({roles: ["user"]}, function(err, res) { 31 | expect(res.errors.length).to.eql(1); 32 | expect(res.errors[0].message).to.eql( 33 | 'roles must be exactly 2 in length'); 34 | done(); 35 | }); 36 | }); 37 | 38 | it("should validate string length", function(done) { 39 | var schema = new Schema(descriptor); 40 | schema.validate({name: "username"}, function(err, res) { 41 | expect(err).to.eql(null); 42 | expect(res).to.eql(null); 43 | done(); 44 | }); 45 | }); 46 | 47 | it("should validate number length", function(done) { 48 | var schema = new Schema(number); 49 | schema.validate({port: 80}, function(err, res) { 50 | expect(err).to.eql(null); 51 | expect(res).to.eql(null); 52 | done(); 53 | }); 54 | }); 55 | 56 | it("should validate array length", function(done) { 57 | var schema = new Schema(array); 58 | schema.validate({roles: ["user", "admin"]}, function(err, res) { 59 | expect(err).to.eql(null); 60 | expect(res).to.eql(null); 61 | done(); 62 | }); 63 | }); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /test/spec/match.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/match') 4 | , source = { 5 | nonmatch: 'foo', 6 | address1: 'foo', 7 | address2: 'bar', 8 | address3: false 9 | }; 10 | 11 | describe('async-validate:', function() { 12 | 13 | it('should error on invalid expanded property (match)', function(done) { 14 | var schema = new Schema(descriptor); 15 | schema.validate(source, function(err, res) { 16 | expect(res.errors.length).to.eql(1); 17 | expect(res.errors[0].message).to.eql('address3 is not a string'); 18 | done(); 19 | }); 20 | }); 21 | 22 | it('should validate on expanded properties', function(done) { 23 | var schema = new Schema(descriptor); 24 | source.address3 = 'qux'; 25 | schema.validate(source, function(err, res) { 26 | expect(err).to.eql(null); 27 | expect(res).to.eql(null); 28 | done(); 29 | }); 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /test/spec/message-literal.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , msg = require('../../messages') 4 | , literal = require('../fixtures/schema/message-literal'); 5 | 6 | describe('async-validate:', function() { 7 | 8 | it('should validate using literal option', function(done) { 9 | var schema = new Schema(literal); 10 | 11 | // clone of the default messages 12 | var clone = Schema.clone(msg); 13 | 14 | // change a message 15 | clone.required = 'REQUIRED !'; 16 | 17 | // assign updated messages to the Schema 18 | schema.messages(clone); 19 | 20 | schema.validate({}, {literal: true}, function(err, res) { 21 | expect(res.errors.length).to.eql(2); 22 | expect(res.errors[0].message).to.eql('REQUIRED !'); 23 | expect(res.errors[1].message).to.eql('REQUIRED ! job'); 24 | done(); 25 | }); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /test/spec/message-object.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/message-object'); 4 | 5 | describe('async-validate:', function() { 6 | 7 | it('should validate using a custom error message as min', function(done) { 8 | var schema = new Schema(descriptor); 9 | schema.validate({num: -1}, function(err, res) { 10 | expect(res.errors[0].message).to.eql('Number may not be below zero'); 11 | done(); 12 | }); 13 | }); 14 | 15 | it('should validate using a custom error message as max', function(done) { 16 | var schema = new Schema(descriptor); 17 | schema.validate({num: 11}, function(err, res) { 18 | expect(res.errors[0].message).to.eql('Number may not be above ten'); 19 | done(); 20 | }); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/spec/messages.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , msg = require('../../messages') 4 | , descriptor = require('../fixtures/schema/message-string') 5 | , func = require('../fixtures/schema/message-function') 6 | , funcerror = require('../fixtures/schema/message-function-error') 7 | , override = require('../fixtures/schema/message-string-override'); 8 | 9 | describe('async-validate:', function() { 10 | 11 | // clone of the default messages 12 | var clone = Schema.clone(msg); 13 | 14 | // change a message 15 | clone.required = '%s is a required field'; 16 | 17 | it('should validate using a custom error message', function(done) { 18 | var schema = new Schema(descriptor); 19 | schema.validate({}, function(err, res) { 20 | expect(res.errors.length).to.eql(1); 21 | expect(res.errors[0].message).to.eql('Name is required'); 22 | done(); 23 | }); 24 | }); 25 | 26 | it('should validate using a custom error message function', function(done) { 27 | var schema = new Schema(func); 28 | schema.validate({}, function(err, res) { 29 | expect(res.errors.length).to.eql(1); 30 | expect(res.errors[0].message).to.eql('Name is required'); 31 | done(); 32 | }); 33 | }); 34 | 35 | it('should validate using an message function (returns Error)', 36 | function(done) { 37 | var schema = new Schema(funcerror); 38 | schema.validate({}, function(err, res) { 39 | expect(res.errors.length).to.eql(1); 40 | expect(res.errors[0].message).to.eql('Name is required'); 41 | done(); 42 | }); 43 | } 44 | ); 45 | 46 | it('should validate using custom messages', function(done) { 47 | var schema = new Schema(override); 48 | 49 | // assign updated messages to the Schema 50 | schema.messages(clone); 51 | 52 | schema.validate({}, function(err, res) { 53 | expect(res.errors.length).to.eql(1); 54 | expect(res.errors[0].message).to.eql('name is a required field'); 55 | done(); 56 | }); 57 | }); 58 | 59 | it('should validate using custom messages as option', function(done) { 60 | // assign updated messages to the Schema 61 | var schema = new Schema(override, {messages: clone}); 62 | 63 | schema.validate({}, function(err, res) { 64 | expect(res.errors.length).to.eql(1); 65 | expect(res.errors[0].message).to.eql('name is a required field'); 66 | done(); 67 | }); 68 | }); 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /test/spec/multiple-rules.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , email = require('../fixtures/email') 4 | , descriptor = require('../fixtures/schema/multiple-rules') 5 | , source = require('../fixtures/schema/multiple-rules-source') 6 | , multi = require('../fixtures/schema/multiple-rules-function'); 7 | 8 | describe('async-validate:', function() { 9 | 10 | it('should error on multiple validation rules for a field', 11 | function(done) { 12 | var schema = new Schema(descriptor); 13 | schema.validate({address: 'joe@example'}, function(err, res) { 14 | expect(res.errors.length).to.eql(2); 15 | expect(res.errors[0].message).to.eql('email is required'); 16 | expect(res.errors[1].message).to.eql( 17 | 'email value undefined does not match pattern ' + email); 18 | done(); 19 | }); 20 | } 21 | ); 22 | 23 | it('should error on multiple validation rules for a field single failure', 24 | function(done) { 25 | var schema = new Schema(source); 26 | schema.validate({email: 'user@example'}, function(err, res) { 27 | expect(res.errors.length).to.eql(1); 28 | expect(res.errors[0].message).to.eql( 29 | 'email value user@example does not match pattern ' + email); 30 | done(); 31 | }); 32 | } 33 | ); 34 | 35 | it('should error on multiple validation rules with a validation function', 36 | function(done) { 37 | var schema = new Schema(multi) 38 | , source = {email: 'user@example.com'}; 39 | 40 | schema.validate(source, function(err, res) { 41 | expect(res.errors.length).to.eql(1); 42 | expect(res.errors[0].message).to.eql( 43 | 'Email address user@example.com already exists'); 44 | done(); 45 | }); 46 | } 47 | ); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /test/spec/multiple-types.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/multiple-types') 4 | , required = require('../fixtures/schema/multiple-types-required'); 5 | 6 | describe('async-validate:', function() { 7 | 8 | it('should error on invalid type with multiple types array', function(done) { 9 | var schema = new Schema(descriptor); 10 | schema.validate({prop: []}, function(err, res) { 11 | expect(res.errors.length).to.eql(1); 12 | expect(res.errors[0].message).to.eql( 13 | 'prop is not one of the allowed types Boolean, ' 14 | + 'string, Component, function (anonymous)'); 15 | done(); 16 | }); 17 | }); 18 | 19 | it('should validate with multiple types array', function(done) { 20 | var schema = new Schema(descriptor); 21 | schema.validate({prop: 'foo'}, function(err, res) { 22 | expect(err).to.eql(null); 23 | expect(res).to.eql(null); 24 | done(); 25 | }); 26 | }); 27 | 28 | it('should error on required with multiple types array', 29 | function(done) { 30 | var schema = new Schema(required); 31 | schema.validate({}, function(err, res) { 32 | expect(res.errors.length).to.eql(1); 33 | expect(res.errors[0].message).to.eql('prop is required'); 34 | done(); 35 | }); 36 | } 37 | ); 38 | 39 | it('should bypass validation with optional field', 40 | function(done) { 41 | var schema = new Schema(descriptor); 42 | schema.validate({prop: undefined}, function(err, res) { 43 | expect(err).to.eql(null); 44 | expect(res).to.eql(null); 45 | done(); 46 | }); 47 | } 48 | ); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /test/spec/null.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/null'); 4 | 5 | describe("async-validate:", function() { 6 | it("should error on non-null value", function(done) { 7 | var schema = new Schema(descriptor); 8 | schema.validate({value: true}, function(err, res) { 9 | expect(res.errors.length).to.eql(1); 10 | expect(res.errors[0].message).to.eql('value is not null'); 11 | done(); 12 | }); 13 | }); 14 | 15 | it("should validate on type null", function(done) { 16 | var schema = new Schema(descriptor); 17 | schema.validate({value: null}, function(err, res) { 18 | expect(err).to.eql(null); 19 | expect(res).to.eql(null); 20 | done(); 21 | }); 22 | }); 23 | 24 | it("should validate on type null with no value", function(done) { 25 | var schema = new Schema(descriptor); 26 | schema.validate({}, function(err, res) { 27 | expect(err).to.eql(null); 28 | expect(res).to.eql(null); 29 | done(); 30 | }); 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /test/spec/number.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/number') 4 | , min = require('../fixtures/schema/number-min') 5 | , max = require('../fixtures/schema/number-max') 6 | , range = require('../fixtures/schema/number-range'); 7 | 8 | describe('async-validate:', function() { 9 | 10 | it('should error on not a number', function(done) { 11 | var schema = new Schema(descriptor); 12 | schema.validate({port: '80'}, function(err, res) { 13 | expect(res.errors.length).to.eql(1); 14 | expect(res.errors[0].message).to.eql( 15 | 'port is not a number'); 16 | done(); 17 | }); 18 | }); 19 | 20 | it('should error on number greater than a minimum value', function(done) { 21 | var schema = new Schema(min); 22 | schema.validate({port: 80}, function(err, res) { 23 | expect(res.errors.length).to.eql(1); 24 | expect(res.errors[0].message).to.eql( 25 | 'port cannot be less than 8080'); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('should error on number greater than a maximum value', 31 | function(done) { 32 | var schema = new Schema(max); 33 | schema.validate({port: 8080}, function(err, res) { 34 | expect(res.errors.length).to.eql(1); 35 | expect(res.errors[0].message).to.eql( 36 | 'port cannot be greater than 80'); 37 | done(); 38 | }); 39 | } 40 | ); 41 | 42 | it('should error on number out of range', 43 | function(done) { 44 | var schema = new Schema(range); 45 | schema.validate({port: 8080}, function(err, res) { 46 | expect(res.errors.length).to.eql(1); 47 | expect(res.errors[0].message).to.eql( 48 | 'port must be between 80 and 1024'); 49 | done(); 50 | }); 51 | } 52 | ); 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /test/spec/object-additional.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/object-additional'); 4 | 5 | describe('async-validate:', function() { 6 | 7 | it('should error on invalid object (additional properties)', function(done) { 8 | var source = { 9 | address: { 10 | name: 'Oops', 11 | street: 'Mock St', 12 | city: 'Mock City', 13 | zip: '12345678' 14 | } 15 | } 16 | var schema = new Schema(descriptor); 17 | schema.validate(source, function(err, res) { 18 | var expected = 'extraneous fields (name) found in address'; 19 | expect(res.errors.length).to.eql(1); 20 | expect(res.errors[0].message).to.eql(expected); 21 | done(); 22 | }); 23 | }); 24 | 25 | it('should validate with no additional properties', function(done) { 26 | var source = { 27 | address: { 28 | street: 'Mock St', 29 | city: 'Mock City', 30 | zip: '12345678' 31 | } 32 | } 33 | var schema = new Schema(descriptor); 34 | schema.validate(source, function(err, res) { 35 | expect(err).to.eql(null); 36 | expect(res).to.eql(null); 37 | done(); 38 | }); 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /test/spec/object.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/object'); 4 | 5 | describe("async-validate:", function() { 6 | 7 | it("should error on invalid object (array specified)", function(done) { 8 | var schema = new Schema(descriptor); 9 | schema.validate({address: []}, function(err, res) { 10 | expect(res.errors.length).to.eql(1); 11 | expect(res.errors[0].message).to.eql('address is not an object'); 12 | done(); 13 | }); 14 | }); 15 | 16 | it("should error on invalid object (required but not specified)", 17 | function(done) { 18 | var schema = new Schema(descriptor); 19 | schema.validate({}, function(err, res) { 20 | expect(res.errors.length).to.eql(1); 21 | expect(res.errors[0].message).to.eql('address is required'); 22 | done(); 23 | }); 24 | } 25 | ); 26 | 27 | it("should validate object (empty object)", function(done) { 28 | var schema = new Schema(descriptor); 29 | schema.validate({address: {}}, function(err, res) { 30 | expect(err).to.eql(null); 31 | expect(res).to.eql(null); 32 | done(); 33 | }); 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /test/spec/options.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/options') 4 | , pattern = require('../fixtures/schema/options-pattern'); 5 | 6 | describe('async-validate:', function() { 7 | 8 | it('should error with multiple errors', function(done) { 9 | var schema = new Schema(descriptor); 10 | schema.validate({}, {first: false}, function(err, res) { 11 | expect(res.errors.length).to.eql(2); 12 | done(); 13 | }); 14 | }); 15 | 16 | it('should error with keys option', function(done) { 17 | var schema = new Schema(descriptor); 18 | schema.validate({}, {keys: ['firstname']}, function(err, res) { 19 | expect(res.errors.length).to.eql(1); 20 | done(); 21 | }); 22 | }); 23 | 24 | it('should error on first error', function(done) { 25 | var schema = new Schema(descriptor); 26 | schema.validate({}, {first: true}, function(err, res) { 27 | expect(res.errors.length).to.eql(2); 28 | done(); 29 | }); 30 | }); 31 | 32 | it('should error with single option', function(done) { 33 | var schema = new Schema(pattern) 34 | , source = {name: '-name'} 35 | , opts = {first: true, single: true}; 36 | 37 | schema.validate(source, opts, function(err, res) { 38 | expect(res.errors.length).to.eql(1); 39 | expect(res.fields.name.length).to.eql(1); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('should error on first error (bail)', function(done) { 45 | var schema = new Schema(descriptor); 46 | schema.validate({}, {bail: true}, function(err, res) { 47 | expect(res.errors.length).to.eql(1); 48 | done(); 49 | }); 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /test/spec/parallel.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/parallel'); 4 | 5 | describe('async-validate:', function() { 6 | 7 | it('should use default series iteration', function(done) { 8 | var schema = new Schema(descriptor); 9 | schema.validate({}, 10 | function(err, res) { 11 | expect(err).to.eql(null); 12 | expect(res).to.eql(null); 13 | done(); 14 | } 15 | ); 16 | }); 17 | 18 | it('should use parallel iteration', function(done) { 19 | var schema = new Schema(descriptor) 20 | schema.validate({}, {parallel: true}, 21 | function(err, res) { 22 | expect(err).to.eql(null); 23 | expect(res).to.eql(null); 24 | done(); 25 | } 26 | ); 27 | }); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /test/spec/placeholder.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/placeholder'); 4 | 5 | describe("async-validate:", function() { 6 | 7 | it("should set default value for field (placeholder)", function(done) { 8 | var schema= new Schema(descriptor) 9 | , source = {}; 10 | schema.validate(source, function() { 11 | expect(source.list).to.eql([]); 12 | done(); 13 | }); 14 | }); 15 | 16 | }); 17 | -------------------------------------------------------------------------------- /test/spec/plugin.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/plugin'); 4 | 5 | describe('async-validate:', function() { 6 | 7 | before(function(done) { 8 | // load plugin definition 9 | Schema.plugin([require('../fixtures/plugin')]); 10 | done(); 11 | }); 12 | 13 | it('should error using custom plugin', function(done) { 14 | var schema = new Schema(descriptor); 15 | schema.validate({id: '-hyphen'}, function(err, res) { 16 | expect(res.errors.length).to.eql(1); 17 | expect(res.errors[0].message).to.eql('id is not a valid identifier'); 18 | done(); 19 | }); 20 | }); 21 | 22 | it('should validate custom plugin', function(done) { 23 | var schema = new Schema(descriptor); 24 | schema.validate({id: 'my-valid-id'}, function(err, res) { 25 | expect(err).to.eql(null); 26 | expect(res).to.eql(null); 27 | done(); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/spec/raise.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/raise'); 4 | 5 | describe('async-validate:', function() { 6 | 7 | it('should use raise() helper method', function(done) { 8 | var schema = new Schema(descriptor) 9 | , source = {}; 10 | schema.validate(source, function(err, res) { 11 | expect(res.errors.length).to.eql(1); 12 | expect(res.errors[0].message).to.eql('name is a required field'); 13 | 14 | expect(res.errors[0].field).to.be.a('string'); 15 | expect(res.errors[0].value).to.eql(undefined); 16 | expect(res.errors[0].parent).to.be.an('object'); 17 | expect(res.errors[0].parent).to.equal(source); 18 | expect(res.errors[0].reason).to.be.an('object'); 19 | done(); 20 | }); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/spec/regexp.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/regexp'); 4 | 5 | describe("async-validate:", function() { 6 | 7 | // NOTE: positive lookbehind is supported in recent versions of node 8 | //it("should error on regexp string (positive lookbehind unsupported)", 9 | //function(done) { 10 | //var schema = new Schema(descriptor) 11 | //, source = {re: "(?<=(category=))[a-z-]+"}; 12 | //schema.validate(source, function(err, res) { 13 | //expect(res.errors.length).to.eql(1); 14 | //expect(res.errors[0].message).to.eql('re is not a valid regexp'); 15 | //done(); 16 | //}); 17 | //} 18 | //); 19 | 20 | it("should validate valid regexp string", function(done) { 21 | var schema = new Schema(descriptor); 22 | schema.validate({re: "^[a-z]+$"}, function(err, res) { 23 | expect(err).to.eql(null); 24 | expect(res).to.eql(null); 25 | done(); 26 | }); 27 | }); 28 | 29 | it("should validate native regexp instance", function(done) { 30 | var schema = new Schema(descriptor); 31 | schema.validate({re: /^[a-z]+$/}, function(err, res) { 32 | expect(err).to.eql(null); 33 | expect(res).to.eql(null); 34 | done(); 35 | }); 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /test/spec/resolve.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index'); 3 | 4 | describe("async-validate:", function() { 5 | 6 | var Person = { 7 | type: 'object', 8 | fields: { 9 | name: { 10 | type: 'string', 11 | required: true 12 | } 13 | } 14 | }; 15 | 16 | var Group = { 17 | type: 'object', 18 | fields: { 19 | name: { 20 | type: 'string', 21 | required: true 22 | }, 23 | brand: { 24 | type: 'string' 25 | } 26 | } 27 | }; 28 | 29 | function resolve() { 30 | switch(this.type) { 31 | case 'Person': 32 | return Person; 33 | case 'Group': 34 | return Group; 35 | } 36 | throw new Error('Unknown performer type'); 37 | } 38 | 39 | var descriptor = { 40 | type: 'object', 41 | fields: { 42 | performer: { 43 | type: 'object', 44 | resolve: resolve 45 | } 46 | } 47 | } 48 | 49 | var mixed = { 50 | type: 'object', 51 | fields: { 52 | performer: { 53 | type: 'array', 54 | values: { 55 | type: 'object', 56 | resolve: resolve 57 | } 58 | } 59 | } 60 | } 61 | 62 | it("should use resolve function error on required (Person)", function(done) { 63 | var schema = new Schema(descriptor) 64 | , source = {performer: {type: 'Person'}}; 65 | schema.validate(source, function(err, res){ 66 | expect(res.errors.length).to.eql(1); 67 | expect(res.fields.name.length).to.eql(1); 68 | expect(res.errors[0].message).to.eql('name is required'); 69 | done(); 70 | }); 71 | }); 72 | 73 | it("should use resolve function (Person type)", function(done) { 74 | var schema = new Schema(descriptor) 75 | , source = {performer: {type: 'Person', name: 'joe'}}; 76 | schema.validate(source, function(err, res){ 77 | expect(err).to.eql(null); 78 | expect(res).to.eql(null); 79 | done(); 80 | }); 81 | }); 82 | 83 | it("should use resolve function error on required (Group)", function(done) { 84 | var schema = new Schema(descriptor) 85 | , source = {performer: {type: 'Group'}}; 86 | schema.validate(source, function(err, res){ 87 | expect(res.errors.length).to.eql(1); 88 | expect(res.fields.name.length).to.eql(1); 89 | expect(res.errors[0].message).to.eql('name is required'); 90 | done(); 91 | }); 92 | }); 93 | 94 | it("should use resolve function (Group type)", function(done) { 95 | var schema = new Schema(descriptor) 96 | , source = {performer: {type: 'Group', name: 'john'}}; 97 | schema.validate(source, function(err, res){ 98 | expect(err).to.eql(null); 99 | expect(res).to.eql(null); 100 | done(); 101 | }); 102 | }); 103 | 104 | it("should throw error on unknown type", function(done) { 105 | var schema = new Schema(descriptor) 106 | , source = {performer: {type: 'Unknown', name: 'joe'}}; 107 | function fn() { 108 | schema.validate(source, function(/*err, res*/){}); 109 | } 110 | expect(fn).throws(/Unknown performer type/i); 111 | done(); 112 | }); 113 | 114 | it("should use resolve function (mixed types)", function(done) { 115 | var schema = new Schema(mixed) 116 | , source = { 117 | performer: [ 118 | {type: 'Person', name: 'joe'}, 119 | {type: 'Group', name: 'john'} 120 | ] 121 | }; 122 | schema.validate(source, function(err, res){ 123 | expect(err).to.eql(null); 124 | expect(res).to.eql(null); 125 | done(); 126 | }); 127 | }); 128 | 129 | }); 130 | -------------------------------------------------------------------------------- /test/spec/revalidate.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index'); 3 | 4 | describe('async-validate:', function() { 5 | 6 | var descriptor = { 7 | type: 'object', 8 | fields: { 9 | name: {type: 'string', required: true} 10 | } 11 | } 12 | 13 | it('should validate after failure', 14 | function(done) { 15 | var schema = new Schema(descriptor); 16 | schema.validate({}, function(err, res) { 17 | expect(res.errors.length).to.eql(1); 18 | expect(res.errors[0].message).to.eql('name is required'); 19 | schema.validate({name: 'user'}, function(err, res) { 20 | expect(err).to.eql(null); 21 | expect(res).to.eql(null); 22 | done(); 23 | }); 24 | }); 25 | } 26 | ); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /test/spec/rule.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Validator = require('../../lib/rule'); 3 | 4 | describe("async-validate:", function() { 5 | 6 | it("should create class without constructor", function(done) { 7 | var v = Validator.Type({}); 8 | expect(v instanceof Validator.Type).to.eql(true); 9 | done(); 10 | }); 11 | 12 | it("should use default message", function(done) { 13 | var v = Validator.Type({field: 'mock', rule: {}, errors: []}); 14 | // trigger default message code path 15 | v.error(); 16 | 17 | // trigger raise with no paramters 18 | var err = v.raise('mock message'); 19 | expect(err.message).to.eql('mock message'); 20 | done(); 21 | }); 22 | 23 | it("should get reason instance", function(done) { 24 | var v = Validator.Type({}); 25 | expect(v.reason('mock-reason', {foo: 'bar'})).to.be.an('object'); 26 | done(); 27 | }); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /test/spec/schema.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index'); 3 | 4 | describe("async-validate:", function() { 5 | 6 | var descriptor = { 7 | type: 'object', 8 | fields: { 9 | name: { 10 | type: 'string' 11 | } 12 | } 13 | } 14 | 15 | var unknown = { 16 | type: 'object', 17 | fields: { 18 | name: { 19 | type: 'unknown-type' 20 | } 21 | } 22 | } 23 | 24 | it("should define with fields", function(done) { 25 | var schema = new Schema(descriptor); 26 | expect(schema.rules).to.be.an('object'); 27 | expect(schema.rules.fields).to.be.an('object'); 28 | done(); 29 | }); 30 | 31 | it("should error with no rules", function(done) { 32 | function fn() { 33 | new Schema(); 34 | } 35 | expect(fn).throws(Error); 36 | expect(fn).throws(/cannot configure/i); 37 | expect(fn).throws(/with no rules/i); 38 | done(); 39 | }); 40 | 41 | it("should error with rules as null", function(done) { 42 | function fn() { 43 | new Schema(null); 44 | } 45 | expect(fn).throws(Error); 46 | expect(fn).throws(/rules must be an object/i); 47 | done(); 48 | }); 49 | 50 | it("should error on validate with no source", function(done) { 51 | var schema = new Schema(descriptor); 52 | function fn() { 53 | schema.validate(); 54 | } 55 | expect(fn).throws(Error); 56 | expect(fn).throws(/cannot validate with no source/i); 57 | done(); 58 | }); 59 | 60 | it("should error on validate with no callback", function(done) { 61 | var schema = new Schema(descriptor); 62 | function fn() { 63 | schema.validate({}); 64 | } 65 | expect(fn).throws(Error); 66 | expect(fn).throws(/cannot validate with no callback/i); 67 | done(); 68 | }); 69 | 70 | it("should error on validate with unknown type", function(done) { 71 | var schema = new Schema(unknown); 72 | 73 | function fn() { 74 | schema.validate({}, function noop(){}); 75 | } 76 | expect(fn).throws(Error); 77 | expect(fn).throws(/unknown rule type/i); 78 | done(); 79 | }); 80 | 81 | it("should error with no rule type", function(done) { 82 | function fn() { 83 | var schema = new Schema({}); 84 | schema.validate({}, function noop(){}); 85 | } 86 | expect(fn).throws(Error); 87 | expect(fn).throws(/type property must be string or function/i); 88 | done(); 89 | }); 90 | 91 | it("should error with no rule type (multiple types)", function(done) { 92 | var descriptor = {type: [{}]}; 93 | function fn() { 94 | var schema = new Schema(descriptor); 95 | schema.validate({}, function noop(){}); 96 | } 97 | expect(fn).throws(Error); 98 | expect(fn).throws(/type property must be string or function/i); 99 | done(); 100 | }); 101 | 102 | }); 103 | -------------------------------------------------------------------------------- /test/spec/source-additional.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/source-additional'); 4 | 5 | describe("async-validate:", function() { 6 | 7 | it("should error on invalid source object (additional properties)", 8 | function(done) { 9 | var source = { 10 | name: 'Oops', 11 | address: {} 12 | } 13 | var schema = new Schema(descriptor); 14 | schema.validate(source, function(err, res) { 15 | // NOTE: `source` is the default field name for root object 16 | var expected = 'extraneous fields (name) found in source'; 17 | expect(res.errors.length).to.eql(1); 18 | expect(res.errors[0].message).to.eql(expected); 19 | done(); 20 | }); 21 | } 22 | ); 23 | 24 | it("should validate with no additional properties", function(done) { 25 | var source = { 26 | address: {} 27 | } 28 | var schema = new Schema(descriptor); 29 | schema.validate(source, function(err, res) { 30 | expect(err).to.eql(null); 31 | expect(res).to.eql(null); 32 | done(); 33 | }); 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /test/spec/source.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , arr = {type: 'array'} 4 | , bool = {type: 'boolean'} 5 | , float = {type: 'float'} 6 | , integer = {type: 'integer'} 7 | , number = {type: 'number'} 8 | , object = {type: 'object'} 9 | , date = {type: 'date'} 10 | , regexp = {type: 'regexp'} 11 | , nil = {type: 'null'} 12 | , func = {type: 'function'} 13 | , string = {type: 'string'} 14 | , pattern = {type: 'string', pattern: /^foo$/} 15 | , dateFormat = {type: 'date', format: 'YYYY-MM-DD'} 16 | , enumerable = {type: 'enum', list: ['foo', 'bar']} 17 | 18 | describe("async-validate:", function() { 19 | 20 | it("should error on source as array type", function(done) { 21 | var schema = new Schema(arr) 22 | , source = 'foo'; 23 | 24 | schema.validate(source, function(err, res) { 25 | expect(err).to.eql(null); 26 | expect(res.errors.length).to.eql(1); 27 | expect(res.errors[0].message).to.eql( 28 | 'source is not an array'); 29 | done(); 30 | }); 31 | }); 32 | 33 | it("should validate on source as array type", function(done) { 34 | var schema = new Schema(arr) 35 | , source = []; 36 | 37 | schema.validate(source, function(err, res) { 38 | expect(err).to.eql(null); 39 | expect(res).to.eql(null); 40 | done(); 41 | }); 42 | }); 43 | 44 | it("should error on source as boolean type", function(done) { 45 | var schema = new Schema(bool) 46 | , source = {}; 47 | 48 | schema.validate(source, function(err, res) { 49 | expect(err).to.eql(null); 50 | expect(res.errors.length).to.eql(1); 51 | expect(res.errors[0].message).to.eql( 52 | 'source is not a boolean'); 53 | done(); 54 | }); 55 | }); 56 | 57 | it("should validate on source as boolean type", function(done) { 58 | var schema = new Schema(bool) 59 | , source = true; 60 | 61 | schema.validate(source, function(err, res) { 62 | expect(err).to.eql(null); 63 | expect(res).to.eql(null); 64 | done(); 65 | }); 66 | }); 67 | 68 | it("should error on source as date type", function(done) { 69 | var schema = new Schema(dateFormat) 70 | , source = '2013-06-50'; 71 | 72 | schema.validate(source, function(err, res) { 73 | expect(err).to.eql(null); 74 | expect(res.errors.length).to.eql(1); 75 | expect(res.errors[0].message).to.eql( 76 | 'source date 2013-06-50 is invalid for format YYYY-MM-DD'); 77 | done(); 78 | }); 79 | }); 80 | 81 | it("should validate on source as date type", function(done) { 82 | var schema = new Schema(date) 83 | , source = true; 84 | 85 | schema.validate(source, function(err, res) { 86 | expect(err).to.eql(null); 87 | expect(res).to.eql(null); 88 | done(); 89 | }); 90 | }); 91 | 92 | 93 | it("should error on source as enum type", function(done) { 94 | var schema = new Schema(enumerable) 95 | , source = 'qux'; 96 | 97 | schema.validate(source, function(err, res) { 98 | expect(err).to.eql(null); 99 | expect(res.errors.length).to.eql(1); 100 | expect(res.errors[0].message).to.eql( 101 | 'source must be one of foo, bar'); 102 | done(); 103 | }); 104 | }); 105 | 106 | it("should validate on source as enum type", function(done) { 107 | var schema = new Schema(enumerable) 108 | , source = 'foo'; 109 | 110 | schema.validate(source, function(err, res) { 111 | expect(err).to.eql(null); 112 | expect(res).to.eql(null); 113 | done(); 114 | }); 115 | }); 116 | 117 | it("should error on source as float type", function(done) { 118 | var schema = new Schema(float) 119 | , source = 'foo'; 120 | 121 | schema.validate(source, function(err, res) { 122 | expect(err).to.eql(null); 123 | expect(res.errors.length).to.eql(1); 124 | expect(res.errors[0].message).to.eql( 125 | 'source is not a float'); 126 | done(); 127 | }); 128 | }); 129 | 130 | it("should validate on source as float type", function(done) { 131 | var schema = new Schema(float) 132 | , source = 1.667; 133 | 134 | schema.validate(source, function(err, res) { 135 | expect(err).to.eql(null); 136 | expect(res).to.eql(null); 137 | done(); 138 | }); 139 | }); 140 | 141 | it("should error on source as integer type", function(done) { 142 | var schema = new Schema(integer) 143 | , source = 'foo'; 144 | 145 | schema.validate(source, function(err, res) { 146 | expect(err).to.eql(null); 147 | expect(res.errors.length).to.eql(1); 148 | expect(res.errors[0].message).to.eql( 149 | 'source is not an integer'); 150 | done(); 151 | }); 152 | }); 153 | 154 | it("should validate on source as integer type", function(done) { 155 | var schema = new Schema(integer) 156 | , source = 1024; 157 | 158 | schema.validate(source, function(err, res) { 159 | expect(err).to.eql(null); 160 | expect(res).to.eql(null); 161 | done(); 162 | }); 163 | }); 164 | 165 | it("should error on source as function type", function(done) { 166 | var schema = new Schema(func) 167 | , source = 'foo'; 168 | 169 | schema.validate(source, function(err, res) { 170 | expect(err).to.eql(null); 171 | expect(res.errors.length).to.eql(1); 172 | expect(res.errors[0].message).to.eql( 173 | 'source is not a function'); 174 | done(); 175 | }); 176 | }); 177 | 178 | it("should validate on source as function type", function(done) { 179 | var schema = new Schema(func) 180 | , source = function noop(){}; 181 | 182 | schema.validate(source, function(err, res) { 183 | expect(err).to.eql(null); 184 | expect(res).to.eql(null); 185 | done(); 186 | }); 187 | }); 188 | 189 | it("should error on source as null type", function(done) { 190 | var schema = new Schema(nil) 191 | , source = 'foo'; 192 | 193 | schema.validate(source, function(err, res) { 194 | expect(err).to.eql(null); 195 | expect(res.errors.length).to.eql(1); 196 | expect(res.errors[0].message).to.eql( 197 | 'source is not null'); 198 | done(); 199 | }); 200 | }); 201 | 202 | it("should validate on source as null type", function(done) { 203 | var schema = new Schema(nil) 204 | , source = null; 205 | 206 | schema.validate(source, function(err, res) { 207 | expect(err).to.eql(null); 208 | expect(res).to.eql(null); 209 | done(); 210 | }); 211 | }); 212 | 213 | it("should error on source as number type", function(done) { 214 | var schema = new Schema(number) 215 | , source = 'foo'; 216 | 217 | schema.validate(source, function(err, res) { 218 | expect(err).to.eql(null); 219 | expect(res.errors.length).to.eql(1); 220 | expect(res.errors[0].message).to.eql( 221 | 'source is not a number'); 222 | done(); 223 | }); 224 | }); 225 | 226 | it("should validate on source as number type", function(done) { 227 | var schema = new Schema(number) 228 | , source = 3; 229 | 230 | schema.validate(source, function(err, res) { 231 | expect(err).to.eql(null); 232 | expect(res).to.eql(null); 233 | done(); 234 | }); 235 | }); 236 | 237 | it("should error on source as object type", function(done) { 238 | var schema = new Schema(object) 239 | , source = 'foo'; 240 | 241 | schema.validate(source, function(err, res) { 242 | expect(err).to.eql(null); 243 | expect(res.errors.length).to.eql(1); 244 | expect(res.errors[0].message).to.eql( 245 | 'source is not an object'); 246 | done(); 247 | }); 248 | }); 249 | 250 | it("should validate on source as object type", function(done) { 251 | var schema = new Schema(object) 252 | , source = {}; 253 | 254 | schema.validate(source, function(err, res) { 255 | expect(err).to.eql(null); 256 | expect(res).to.eql(null); 257 | done(); 258 | }); 259 | }); 260 | 261 | it("should error on source as pattern type", function(done) { 262 | var schema = new Schema(pattern) 263 | , source = 'bar'; 264 | 265 | schema.validate(source, function(err, res) { 266 | expect(err).to.eql(null); 267 | expect(res.errors.length).to.eql(1); 268 | expect(res.errors[0].message).to.eql( 269 | 'source value bar does not match pattern /^foo$/'); 270 | done(); 271 | }); 272 | }); 273 | 274 | it("should validate on source as pattern type", function(done) { 275 | var schema = new Schema(pattern) 276 | , source = 'foo'; 277 | 278 | schema.validate(source, function(err, res) { 279 | expect(err).to.eql(null); 280 | expect(res).to.eql(null); 281 | done(); 282 | }); 283 | }); 284 | 285 | it("should error on source as regexp type", function(done) { 286 | var schema = new Schema(regexp) 287 | , source = '+'; 288 | 289 | schema.validate(source, function(err, res) { 290 | expect(err).to.eql(null); 291 | expect(res.errors.length).to.eql(1); 292 | expect(res.errors[0].message).to.eql( 293 | 'source is not a valid regexp'); 294 | done(); 295 | }); 296 | }); 297 | 298 | it("should validate on source as regexp type", function(done) { 299 | var schema = new Schema(regexp) 300 | , source = /^foo$/; 301 | 302 | schema.validate(source, function(err, res) { 303 | expect(err).to.eql(null); 304 | expect(res).to.eql(null); 305 | done(); 306 | }); 307 | }); 308 | 309 | it("should error on source as string type", function(done) { 310 | var schema = new Schema(string) 311 | , source = {}; 312 | 313 | schema.validate(source, function(err, res) { 314 | expect(err).to.eql(null); 315 | expect(res.errors.length).to.eql(1); 316 | expect(res.errors[0].message).to.eql( 317 | 'source is not a string'); 318 | done(); 319 | }); 320 | }); 321 | 322 | it("should validate on source as string type", function(done) { 323 | var schema = new Schema(string) 324 | , source = 'foo'; 325 | 326 | schema.validate(source, function(err, res) { 327 | expect(err).to.eql(null); 328 | expect(res).to.eql(null); 329 | done(); 330 | }); 331 | }); 332 | 333 | }); 334 | -------------------------------------------------------------------------------- /test/spec/state.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/state'); 4 | 5 | describe('async-validate:', function() { 6 | 7 | var state = {} 8 | , opts = {state: state} 9 | , source = {email: 'user@example.com'}; 10 | 11 | it('should pass state information between rules', function(done) { 12 | var schema = new Schema(descriptor); 13 | schema.validate(source, opts, function() { 14 | expect(state.email).to.be.an('object'); 15 | expect(state.email.user).to.eql('user'); 16 | expect(state.email.domain).to.eql('example.com'); 17 | done(); 18 | }); 19 | }); 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /test/spec/string.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index'); 3 | 4 | describe('async-validate:', function() { 5 | 6 | it('should error on required string field', function(done) { 7 | var descriptor = { 8 | type: 'object', 9 | fields: { 10 | name: {type: 'string', required: true} 11 | } 12 | } 13 | var schema = new Schema(descriptor); 14 | schema.validate({noname: 'field'}, function(err, res) { 15 | expect(res.errors.length).to.eql(1); 16 | expect(res.fields.name.length).to.eql(1); 17 | expect(res.errors[0].message).to.eql('name is required'); 18 | done(); 19 | }); 20 | }); 21 | 22 | it('should error on non-string type', function(done) { 23 | var descriptor = { 24 | type: 'object', 25 | fields: { 26 | name: {type: 'string'} 27 | } 28 | } 29 | var schema = new Schema(descriptor); 30 | schema.validate({name: 10}, function(err, res) { 31 | expect(res.errors.length).to.eql(1); 32 | expect(res.errors[0].message).to.eql('name is not a string'); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should error on required string field with minimum length', 38 | function(done) { 39 | var descriptor = { 40 | type: 'object', 41 | fields: { 42 | name: {type: 'string', required: true, min: 8} 43 | } 44 | } 45 | var schema = new Schema(descriptor); 46 | schema.validate({name: 'field'}, function(err, res) { 47 | expect(res.errors.length).to.eql(1); 48 | expect(res.errors[0].message).to.eql( 49 | 'name must be at least 8 characters'); 50 | done(); 51 | }); 52 | } 53 | ); 54 | 55 | it('should error on required string field with maximum length', 56 | function(done) { 57 | var descriptor = { 58 | type: 'object', 59 | fields: { 60 | name: {type: 'string', required: true, max: 2} 61 | } 62 | } 63 | var schema = new Schema(descriptor); 64 | schema.validate({name: 'field'}, function(err, res) { 65 | expect(res.errors.length).to.eql(1); 66 | expect(res.errors[0].message).to.eql( 67 | 'name cannot be longer than 2 characters'); 68 | done(); 69 | }); 70 | } 71 | ); 72 | 73 | it('should error on required string field is less than a length range', 74 | function(done) { 75 | var descriptor = { 76 | type: 'object', 77 | fields: { 78 | name: {type: 'string', required: true, min: 6, max: 8} 79 | } 80 | } 81 | var schema = new Schema(descriptor); 82 | schema.validate({name: 'field'}, function(err, res) { 83 | expect(res.errors.length).to.eql(1); 84 | expect(res.errors[0].message).to.eql( 85 | 'name must be between 6 and 8 characters'); 86 | done(); 87 | }); 88 | } 89 | ); 90 | 91 | it('should error on required string field is greater than a length range', 92 | function(done) { 93 | var descriptor = { 94 | type: 'object', 95 | fields: { 96 | name: {type: 'string', required: true, min: 2, max: 4} 97 | } 98 | } 99 | var schema = new Schema(descriptor); 100 | schema.validate({name: 'field'}, function(err, res) { 101 | expect(res.errors.length).to.eql(1); 102 | expect(res.errors[0].message).to.eql( 103 | 'name must be between 2 and 4 characters'); 104 | done(); 105 | }); 106 | } 107 | ); 108 | it('should error on regular expression pattern mismatch', 109 | function(done) { 110 | var descriptor = { 111 | type: 'object', 112 | fields: { 113 | name: {type: 'string', pattern: /^[0-9]+$/} 114 | } 115 | } 116 | var schema = new Schema(descriptor); 117 | schema.validate({name: 'alpha'}, function(err, res) { 118 | expect(res.errors.length).to.eql(1); 119 | expect(res.errors[0].message).to.eql( 120 | 'name value alpha does not match pattern /^[0-9]+$/'); 121 | done(); 122 | }); 123 | } 124 | ); 125 | 126 | it('should error on string consisting of whitespace', 127 | function(done) { 128 | var descriptor = { 129 | type: 'object', 130 | fields: { 131 | name: {type: 'string', whitespace: true} 132 | } 133 | } 134 | var schema = new Schema(descriptor); 135 | schema.validate({name: ' '}, function(err, res) { 136 | expect(res.errors.length).to.eql(1); 137 | expect(res.errors[0].message).to.eql('name cannot be empty'); 138 | done(); 139 | }); 140 | } 141 | ); 142 | 143 | it('should error on empty string', 144 | function(done) { 145 | var descriptor = { 146 | type: 'object', 147 | fields: { 148 | name: {type: 'string', required: true, whitespace: true} 149 | } 150 | } 151 | var schema = new Schema(descriptor); 152 | schema.validate({name: ''}, function(err, res) { 153 | expect(res.errors.length).to.eql(1); 154 | expect(res.errors[0].message).to.eql('name cannot be empty'); 155 | done(); 156 | }); 157 | } 158 | ); 159 | 160 | it('should validate on required string field', function(done) { 161 | var descriptor = { 162 | type: 'object', 163 | fields: { 164 | name: {type: 'string', required: true, whitespace: true} 165 | } 166 | } 167 | var schema = new Schema(descriptor); 168 | schema.validate({name: 'field'}, function(err, res) { 169 | expect(err).to.eql(null); 170 | expect(res).to.eql(null); 171 | done(); 172 | }); 173 | }); 174 | 175 | 176 | it('should validate on required string field in range', 177 | function(done) { 178 | var descriptor = { 179 | type: 'object', 180 | fields: { 181 | name: {type: 'string', required: true, min: 6, max: 20} 182 | } 183 | } 184 | var schema = new Schema(descriptor); 185 | schema.validate({name: 'valid field'}, function(err, res) { 186 | expect(err).to.eql(null); 187 | expect(res).to.eql(null); 188 | done(); 189 | }); 190 | } 191 | ); 192 | 193 | }); 194 | -------------------------------------------------------------------------------- /test/spec/transform.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , descriptor = require('../fixtures/schema/transform'); 4 | 5 | describe('async-validate:', function() { 6 | 7 | it('should transform by stripping whitespace', function(done) { 8 | var schema = new Schema(descriptor) 9 | , source = {name: ' user '}; 10 | 11 | schema.validate(source, function(err, res) { 12 | expect(err).to.eql(null); 13 | expect(res).to.eql(null); 14 | expect(source.name).to.eql('user'); 15 | done(); 16 | }); 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /test/spec/undef.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index'); 3 | 4 | describe('async-validate:', function() { 5 | 6 | var descriptor = { 7 | type: 'object', 8 | fields: { 9 | name: {type: 'string'}, 10 | age: {type: 'integer', required: false} 11 | } 12 | } 13 | 14 | var string = { 15 | type: 'object', 16 | fields: { 17 | mock: {type: 'string', required: false} 18 | } 19 | } 20 | 21 | var func = { 22 | type: 'object', 23 | fields: { 24 | mock: {type: 'function', required: false} 25 | } 26 | } 27 | 28 | 29 | var regexp = { 30 | type: 'object', 31 | fields: { 32 | mock: {type: 'regexp', required: false} 33 | } 34 | } 35 | 36 | var float = { 37 | type: 'object', 38 | fields: { 39 | mock: {type: 'float', required: false} 40 | } 41 | } 42 | 43 | var number = { 44 | type: 'object', 45 | fields: { 46 | mock: {type: 'number', required: false} 47 | } 48 | } 49 | 50 | var object = { 51 | type: 'object', 52 | fields: { 53 | mock: {type: 'object', required: false} 54 | } 55 | } 56 | 57 | var arr = { 58 | type: 'object', 59 | fields: { 60 | mock: {type: 'array', required: false} 61 | } 62 | } 63 | 64 | var bool = { 65 | type: 'object', 66 | fields: { 67 | mock: {type: 'boolean', required: false} 68 | } 69 | } 70 | 71 | var date = { 72 | type: 'object', 73 | fields: { 74 | mock: {type: 'date', required: false} 75 | } 76 | } 77 | 78 | var enumerable = { 79 | type: 'object', 80 | fields: { 81 | mock: {type: 'enum', required: false} 82 | } 83 | } 84 | 85 | it('should error on invalid integer field if not required (first/single)', 86 | function(done) { 87 | var schema = new Schema(descriptor); 88 | var source = {age: 'abc', name : 'User'}; 89 | var opts = {first : true, single: true}; 90 | schema.validate(source, opts, function(err, res) { 91 | expect(res.errors.length).to.eql(1); 92 | expect(res.errors[0].message).to.eql('age is not an integer'); 93 | expect(res.errors[0].field).to.eql('age'); 94 | done(); 95 | }); 96 | } 97 | ); 98 | 99 | it('should allow undefined integer field if not required', function(done) { 100 | var schema = new Schema(descriptor); 101 | var source = {age: undefined, name : 'User'}; 102 | var opts = {first : false, single: true}; 103 | schema.validate(source, opts, function(err, res) { 104 | expect(err).to.eql(null); 105 | expect(res).to.eql(null); 106 | done(); 107 | }); 108 | }); 109 | 110 | it('should allow undefined integer field if not required (first)', 111 | function(done) { 112 | var schema = new Schema(descriptor); 113 | var source = {age: undefined, name : 'User'}; 114 | var opts = {first : true, single: true}; 115 | schema.validate(source, opts, function(err, res) { 116 | expect(err).to.eql(null); 117 | expect(res).to.eql(null); 118 | done(); 119 | }); 120 | } 121 | ); 122 | 123 | it('should allow undefined array field if not required', function(done) { 124 | var schema = new Schema(arr); 125 | var source = {mock: undefined}; 126 | var opts = {}; 127 | schema.validate(source, opts, function(err, res) { 128 | expect(err).to.eql(null); 129 | expect(res).to.eql(null); 130 | done(); 131 | }); 132 | }); 133 | 134 | it('should allow undefined boolean field if not required', function(done) { 135 | var schema = new Schema(bool); 136 | var source = {mock: undefined}; 137 | var opts = {}; 138 | schema.validate(source, opts, function(err, res) { 139 | expect(err).to.eql(null); 140 | expect(res).to.eql(null); 141 | done(); 142 | }); 143 | }); 144 | 145 | it('should allow undefined date field if not required', function(done) { 146 | var schema = new Schema(date); 147 | var source = {mock: undefined}; 148 | var opts = {}; 149 | schema.validate(source, opts, function(err, res) { 150 | expect(err).to.eql(null); 151 | expect(res).to.eql(null); 152 | done(); 153 | }); 154 | }); 155 | 156 | it('should allow undefined enum field if not required', function(done) { 157 | var schema = new Schema(enumerable); 158 | var source = {mock: undefined}; 159 | var opts = {}; 160 | schema.validate(source, opts, function(err, res) { 161 | expect(err).to.eql(null); 162 | expect(res).to.eql(null); 163 | done(); 164 | }); 165 | }); 166 | 167 | it('should allow undefined float field if not required', function(done) { 168 | var schema = new Schema(float); 169 | var source = {mock: undefined}; 170 | var opts = {}; 171 | schema.validate(source, opts, function(err, res) { 172 | expect(err).to.eql(null); 173 | expect(res).to.eql(null); 174 | done(); 175 | }); 176 | }); 177 | 178 | it('should allow undefined function field if not required', function(done) { 179 | var schema = new Schema(func); 180 | var source = {mock: undefined}; 181 | var opts = {}; 182 | schema.validate(source, opts, function(err, res) { 183 | expect(err).to.eql(null); 184 | expect(res).to.eql(null); 185 | done(); 186 | }); 187 | }); 188 | 189 | it('should allow undefined number field if not required', function(done) { 190 | var schema = new Schema(number); 191 | var source = {mock: undefined}; 192 | var opts = {}; 193 | schema.validate(source, opts, function(err, res) { 194 | expect(err).to.eql(null); 195 | expect(res).to.eql(null); 196 | done(); 197 | }); 198 | }); 199 | 200 | it('should allow undefined object field if not required', function(done) { 201 | var schema = new Schema(object); 202 | var source = {mock: undefined}; 203 | var opts = {}; 204 | schema.validate(source, opts, function(err, res) { 205 | expect(err).to.eql(null); 206 | expect(res).to.eql(null); 207 | done(); 208 | }); 209 | }); 210 | 211 | it('should allow undefined regexp field if not required', function(done) { 212 | var schema = new Schema(regexp); 213 | var source = {mock: undefined}; 214 | var opts = {}; 215 | schema.validate(source, opts, function(err, res) { 216 | expect(err).to.eql(null); 217 | expect(res).to.eql(null); 218 | done(); 219 | }); 220 | }); 221 | 222 | it('should allow undefined string field if not required', function(done) { 223 | var schema = new Schema(string); 224 | var source = {mock: undefined}; 225 | var opts = {}; 226 | schema.validate(source, opts, function(err, res) { 227 | expect(err).to.eql(null); 228 | expect(res).to.eql(null); 229 | done(); 230 | }); 231 | }); 232 | 233 | }); 234 | -------------------------------------------------------------------------------- /test/spec/vars.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Schema = require('../../index') 3 | , Model = require('../fixtures/model') 4 | , descriptor = require('../fixtures/schema/vars'); 5 | 6 | describe('async-validate:', function() { 7 | 8 | before(function(done) { 9 | // load plugin definition 10 | Schema.plugin([require('../fixtures/vars-plugin')]); 11 | done(); 12 | }); 13 | 14 | it('should error with vars option and missing id', function(done) { 15 | var schema = new Schema(descriptor) 16 | , opts = {vars: {model: new Model()}} 17 | , source = {id: 'qux'}; 18 | schema.validate(source, opts, function(err, res) { 19 | expect(res.errors.length).to.eql(1); 20 | expect(res.errors[0].message).to.eql( 21 | 'user not found for id qux'); 22 | done(); 23 | }); 24 | }); 25 | 26 | it('should validate with vars option', function(done) { 27 | var schema = new Schema(descriptor) 28 | , opts = {vars: {model: new Model()}} 29 | , source = {id: 'foo'}; 30 | schema.validate(source, opts, function(err, res) { 31 | expect(err).to.eql(null); 32 | expect(res).to.eql(null); 33 | done(); 34 | }); 35 | }); 36 | }); 37 | --------------------------------------------------------------------------------