├── index.js ├── .travis.yml ├── images ├── joi.png └── validation.png ├── AUTHORS ├── Makefile ├── .gitignore ├── test ├── function.js ├── helper.js ├── date.js ├── boolean.js ├── binary.js ├── errors.js ├── ref.js ├── array.js ├── alternatives.js ├── number.js ├── any.js └── object.js ├── lib ├── function.js ├── ref.js ├── boolean.js ├── cast.js ├── date.js ├── binary.js ├── index.js ├── number.js ├── language.js ├── alternatives.js ├── errors.js ├── array.js ├── string.js ├── object.js └── any.js ├── package.json ├── CONTRIBUTING.md ├── LICENSE └── README.md /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib'); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 5 | 6 | -------------------------------------------------------------------------------- /images/joi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/joi/master/images/joi.png -------------------------------------------------------------------------------- /images/validation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/joi/master/images/validation.png -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Van Nguyen 2 | Eran Hammer (http://hueniverse.com) 3 | Wyatt Preul (http://jsgeek.com) -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @node node_modules/lab/bin/lab 3 | test-cov: 4 | @node node_modules/lab/bin/lab -t 100 5 | test-cov-html: 6 | @node node_modules/lab/bin/lab -r html -o coverage.html 7 | 8 | .PHONY: test test-cov test-cov-html 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | npm-debug.log 4 | dump.rdb 5 | node_modules 6 | results.tap 7 | results.xml 8 | npm-shrinkwrap.json 9 | config.json 10 | .DS_Store 11 | */.DS_Store 12 | */*/.DS_Store 13 | ._* 14 | */._* 15 | */*/._* 16 | coverage.* 17 | lib-cov 18 | complexity.md 19 | -------------------------------------------------------------------------------- /test/function.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Lab = require('lab'); 4 | var Joi = require('../lib'); 5 | var Validate = require('./helper'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | // Test shortcuts 14 | 15 | var expect = Lab.expect; 16 | var before = Lab.before; 17 | var after = Lab.after; 18 | var describe = Lab.experiment; 19 | var it = Lab.test; 20 | 21 | 22 | describe('func', function () { 23 | 24 | it('should validate a function', function (done) { 25 | 26 | Validate(Joi.func().required(), [ 27 | [function () { }, true], 28 | ['', false] 29 | ]); 30 | done(); 31 | }); 32 | }); 33 | 34 | -------------------------------------------------------------------------------- /lib/function.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Any = require('./any'); 4 | var Errors = require('./errors'); 5 | var Hoek = require('hoek'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | internals.Function = function () { 14 | 15 | Any.call(this); 16 | this._type = 'func'; 17 | }; 18 | 19 | Hoek.inherits(internals.Function, Any); 20 | 21 | 22 | internals.Function.prototype._base = function (value, state, options) { 23 | 24 | return { 25 | value: value, 26 | errors: (typeof value === 'function') ? null : Errors.create('function.base', null, state, options) 27 | }; 28 | }; 29 | 30 | 31 | module.exports = new internals.Function(); -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Lab = require('lab'); 4 | var Joi = require('../'); 5 | 6 | 7 | // Declare internals 8 | 9 | var internals = {}; 10 | 11 | 12 | // Test shortcuts 13 | 14 | var expect = Lab.expect; 15 | var before = Lab.before; 16 | var after = Lab.after; 17 | var describe = Lab.experiment; 18 | var it = Lab.test; 19 | 20 | 21 | module.exports = function (schema, config) { 22 | 23 | var compiled = Joi.compile(schema); 24 | for (var i in config) { 25 | compiled.validate(config[i][0], function (err, value) { 26 | 27 | if (err !== null && config[i][1]) { 28 | console.log(err); 29 | } 30 | 31 | if (err === null && !config[i][1]) { 32 | console.log(config[i][0]); 33 | } 34 | 35 | expect(err === null).to.equal(config[i][1]); 36 | }); 37 | } 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /lib/ref.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Hoek = require('hoek'); 4 | 5 | 6 | // Declare internals 7 | 8 | var internals = {}; 9 | 10 | 11 | exports.create = function (key, options) { 12 | 13 | Hoek.assert(key, 'Missing reference key'); 14 | Hoek.assert(typeof key === 'string', 'Invalid reference key:', key); 15 | 16 | var settings = Hoek.clone(options); // options can be reused and modified 17 | 18 | var ref = function (value) { 19 | 20 | return Hoek.reach(value, key, settings); 21 | }; 22 | 23 | ref.isJoi = true; 24 | ref.key = key; 25 | ref.path = key.split((settings && settings.separator) || '.'); 26 | ref.depth = ref.path.length; 27 | ref.root = ref.path[0]; 28 | 29 | ref.toString = function () { 30 | 31 | return 'ref:' + key; 32 | }; 33 | 34 | return ref; 35 | }; 36 | 37 | 38 | exports.isRef = function (ref) { 39 | 40 | return typeof ref === 'function' && ref.isJoi; 41 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joi", 3 | "description": "Object schema validation", 4 | "version": "4.1.0", 5 | "repository": "git://github.com/spumko/joi", 6 | "main": "index", 7 | "keywords": [ 8 | "schema", 9 | "validation" 10 | ], 11 | "engines": { 12 | "node": ">=0.10.22" 13 | }, 14 | "dependencies": { 15 | "hoek": "^2.1.x", 16 | "topo": "1.x.x" 17 | }, 18 | "devDependencies": { 19 | "lab": "3.x.x" 20 | }, 21 | "scripts": { 22 | "test": "make test-cov" 23 | }, 24 | "testling": { 25 | "files": "test/*.js", 26 | "browsers": [ 27 | "ie/6..latest", 28 | "chrome/22..latest", 29 | "firefox/16..latest", 30 | "safari/latest", 31 | "opera/11.0..latest", 32 | "iphone/6", 33 | "ipad/6", 34 | "android-browser/latest" 35 | ] 36 | }, 37 | "licenses": [ 38 | { 39 | "type": "BSD", 40 | "url": "http://github.com/spumko/joi/raw/master/LICENSE" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | We welcome contributions from the community and are pleased to have them. Please follow this guide when logging issues or making code changes. 3 | 4 | ## Logging Issues 5 | All issues should be created using the [new issue form](https://github.com/spumko/joi/issues/new). Clearly describe the issue including steps to reproduce if there are any. Also, make sure to indicate the earliest version that has the issue being reported. 6 | 7 | ## Patching Code 8 | Code changes are welcome and should follow the guidelines below. 9 | 10 | * Fork the repository on GitHub. 11 | * Fix the issue ensuring that your code follows the [style guide](https://github.com/spumko/hapi/blob/master/docs/Style.md). 12 | * Add tests for your new code ensuring that you have 100% code coverage (we can help you reach 100% but will not merge without it). 13 | * Run `npm test` to generate a report of test coverage 14 | * [Pull requests](http://help.github.com/send-pull-requests/) should be made to the [master branch](https://github.com/spumko/joi/tree/master). -------------------------------------------------------------------------------- /lib/boolean.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Any = require('./any'); 4 | var Errors = require('./errors'); 5 | var Hoek = require('hoek'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | internals.Boolean = function () { 14 | 15 | Any.call(this); 16 | this._type = 'boolean'; 17 | }; 18 | 19 | Hoek.inherits(internals.Boolean, Any); 20 | 21 | 22 | internals.Boolean.prototype._base = function (value, state, options) { 23 | 24 | var result = { 25 | value: value 26 | }; 27 | 28 | if (typeof value === 'string' && 29 | options.convert) { 30 | 31 | var lower = value.toLowerCase(); 32 | result.value = (lower === 'true' || lower === 'yes' || lower === 'on' ? true 33 | : (lower === 'false' || lower === 'no' || lower === 'off' ? false : value)); 34 | } 35 | 36 | result.errors = (typeof result.value === 'boolean') ? null : Errors.create('boolean.base', null, state, options); 37 | return result; 38 | }; 39 | 40 | 41 | module.exports = new internals.Boolean(); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2013, Walmart. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Walmart nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL WALMART BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /lib/cast.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Hoek = require('hoek'); 4 | var Ref = require('./ref'); 5 | // Type modules are delay-loaded to prevent circular dependencies 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = { 11 | any: null, 12 | date: require('./date'), 13 | string: require('./string'), 14 | number: require('./number'), 15 | boolean: require('./boolean'), 16 | alt: null, 17 | object: null 18 | }; 19 | 20 | 21 | exports.schema = function (config) { 22 | 23 | internals.any = internals.any || new (require('./any'))(); 24 | internals.alt = internals.alt || require('./alternatives'); 25 | internals.object = internals.object || require('./object'); 26 | 27 | if (config && 28 | typeof config === 'object') { 29 | 30 | if (config.isJoi) { 31 | return config; 32 | } 33 | 34 | if (Array.isArray(config)) { 35 | return internals.alt.try(config); 36 | } 37 | 38 | if (config instanceof RegExp) { 39 | return internals.string.regex(config); 40 | } 41 | 42 | if (config instanceof Date) { 43 | return internals.date.valid(config); 44 | } 45 | 46 | return internals.object.keys(config); 47 | } 48 | 49 | if (typeof config === 'string') { 50 | return internals.string.valid(config); 51 | } 52 | 53 | if (typeof config === 'number') { 54 | return internals.number.valid(config); 55 | } 56 | 57 | if (typeof config === 'boolean') { 58 | return internals.boolean.valid(config); 59 | } 60 | 61 | if (Ref.isRef(config)) { 62 | return internals.any.valid(config); 63 | } 64 | 65 | Hoek.assert(config === null, 'Invalid schema content:', config); 66 | 67 | return internals.any.valid(null); 68 | }; 69 | 70 | 71 | exports.ref = function (id) { 72 | 73 | return Ref.isRef(id) ? id : Ref.create(id); 74 | }; -------------------------------------------------------------------------------- /lib/date.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Any = require('./any'); 4 | var Errors = require('./errors'); 5 | var Hoek = require('hoek'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | internals.Date = function () { 14 | 15 | Any.call(this); 16 | this._type = 'date'; 17 | }; 18 | 19 | Hoek.inherits(internals.Date, Any); 20 | 21 | 22 | internals.Date.prototype._base = function (value, state, options) { 23 | 24 | var result = { 25 | value: (options.convert && internals.toDate(value)) || value 26 | }; 27 | 28 | result.errors = (result.value instanceof Date) ? null : Errors.create('date.base', null, state, options); 29 | return result; 30 | }; 31 | 32 | 33 | internals.toDate = function (value) { 34 | 35 | if (value instanceof Date) { 36 | return value; 37 | } 38 | 39 | if (typeof value === 'string' || 40 | Hoek.isInteger(value)) { 41 | 42 | var date = new Date(value); 43 | if (!isNaN(date.getTime())) { 44 | return date; 45 | } 46 | } 47 | 48 | return null; 49 | }; 50 | 51 | 52 | internals.Date.prototype.min = function (date) { 53 | 54 | date = internals.toDate(date); 55 | Hoek.assert(date, 'Invalid date format'); 56 | 57 | return this._test('min', date, function (value, state, options) { 58 | 59 | if (value.getTime() >= date.getTime()) { 60 | return null; 61 | } 62 | 63 | return Errors.create('date.min', { limit: date }, state, options); 64 | }); 65 | }; 66 | 67 | 68 | internals.Date.prototype.max = function (date) { 69 | 70 | date = internals.toDate(date); 71 | Hoek.assert(date, 'Invalid date format'); 72 | 73 | return this._test('max', date, function (value, state, options) { 74 | 75 | if (value.getTime() <= date.getTime()) { 76 | return null; 77 | } 78 | 79 | return Errors.create('date.max', { limit: date }, state, options); 80 | }); 81 | }; 82 | 83 | 84 | module.exports = new internals.Date(); -------------------------------------------------------------------------------- /test/date.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Lab = require('lab'); 4 | var Joi = require('../lib'); 5 | var Validate = require('./helper'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | // Test shortcuts 14 | 15 | var expect = Lab.expect; 16 | var before = Lab.before; 17 | var after = Lab.after; 18 | var describe = Lab.experiment; 19 | var it = Lab.test; 20 | 21 | 22 | describe('date', function () { 23 | 24 | it('fails on boolean', function (done) { 25 | 26 | var schema = Joi.date(); 27 | Validate(schema, [ 28 | [true, false], 29 | [false, false] 30 | ]); 31 | 32 | done(); 33 | }); 34 | 35 | it('matches specific date', function (done) { 36 | 37 | var now = Date.now(); 38 | Joi.date().valid(new Date(now)).validate(new Date(now), function (err, value) { 39 | 40 | expect(err).to.not.exist; 41 | done(); 42 | }); 43 | }); 44 | 45 | it('errors on invalid input and convert disabled', function (done) { 46 | 47 | Joi.date().options({ convert: false }).validate('1-1-2013', function (err, value) { 48 | 49 | expect(err).to.exist; 50 | expect(err.message).to.equal('value must be a number of milliseconds or valid date string'); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('validates date', function (done) { 56 | 57 | Joi.date().validate(new Date(), function (err, value) { 58 | 59 | expect(err).to.not.exist; 60 | done(); 61 | }); 62 | }); 63 | 64 | describe('#validate', function () { 65 | 66 | it('validates min', function (done) { 67 | 68 | Validate(Joi.date().min('1-1-2012'), [ 69 | ['1-1-2013', true], 70 | ['1-1-2012', true], 71 | [0, false], 72 | ['1-1-2000', false] 73 | ]); 74 | done(); 75 | }); 76 | 77 | it('validates max', function (done) { 78 | 79 | Validate(Joi.date().max('1-1-2013'), [ 80 | ['1-1-2013', true], 81 | ['1-1-2012', true], 82 | [0, true], 83 | ['1-1-2014', false] 84 | ]); 85 | done(); 86 | }); 87 | }); 88 | }); -------------------------------------------------------------------------------- /lib/binary.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Any = require('./any'); 4 | var Errors = require('./errors'); 5 | var Hoek = require('hoek'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | // Declare internals 14 | 15 | var internals = {}; 16 | 17 | 18 | internals.Binary = function () { 19 | 20 | Any.call(this); 21 | this._type = 'binary'; 22 | }; 23 | 24 | Hoek.inherits(internals.Binary, Any); 25 | 26 | 27 | internals.Binary.prototype._base = function (value, state, options) { 28 | 29 | var result = { 30 | value: value 31 | }; 32 | 33 | if (typeof value === 'string' && 34 | options.convert) { 35 | 36 | try { 37 | var converted = new Buffer(value, this._flags.encoding); 38 | result.value = converted; 39 | } 40 | catch (e) { } 41 | } 42 | 43 | result.errors = Buffer.isBuffer(result.value) ? null : Errors.create('binary.base', null, state, options); 44 | return result; 45 | }; 46 | 47 | 48 | internals.Binary.prototype.encoding = function (encoding) { 49 | 50 | var obj = this.clone(); 51 | obj._flags.encoding = encoding; 52 | return obj; 53 | }; 54 | 55 | 56 | internals.Binary.prototype.min = function (limit) { 57 | 58 | Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); 59 | 60 | return this._test('min', limit, function (value, state, options) { 61 | 62 | if (value.length >= limit) { 63 | return null; 64 | } 65 | 66 | return Errors.create('binary.min', { limit: limit }, state, options); 67 | }); 68 | }; 69 | 70 | 71 | internals.Binary.prototype.max = function (limit) { 72 | 73 | Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); 74 | 75 | return this._test('max', limit, function (value, state, options) { 76 | 77 | if (value.length <= limit) { 78 | return null; 79 | } 80 | 81 | return Errors.create('binary.max', { limit: limit }, state, options); 82 | }); 83 | }; 84 | 85 | 86 | internals.Binary.prototype.length = function (limit) { 87 | 88 | Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); 89 | 90 | return this._test('length', limit, function (value, state, options) { 91 | 92 | if (value.length === limit) { 93 | return null; 94 | } 95 | 96 | return Errors.create('binary.length', { limit: limit }, state, options); 97 | }); 98 | }; 99 | 100 | 101 | module.exports = new internals.Binary(); -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Any = require('./any'); 4 | var Cast = require('./cast'); 5 | var Ref = require('./ref'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = { 11 | alternatives: require('./alternatives'), 12 | array: require('./array'), 13 | boolean: require('./boolean'), 14 | binary: require('./binary'), 15 | date: require('./date'), 16 | func: require('./function'), 17 | number: require('./number'), 18 | object: require('./object'), 19 | string: require('./string') 20 | }; 21 | 22 | 23 | internals.root = function () { 24 | 25 | var any = new Any(); 26 | 27 | var root = any.clone(); 28 | root.any = function () { 29 | 30 | return any; 31 | }; 32 | 33 | root.alternatives = root.alt = function () { 34 | 35 | return arguments.length ? internals.alternatives.try.apply(internals.alternatives, arguments) : internals.alternatives; 36 | }; 37 | 38 | root.array = function () { 39 | 40 | return internals.array; 41 | }; 42 | 43 | root.boolean = root.bool = function () { 44 | 45 | return internals.boolean; 46 | }; 47 | 48 | root.binary = function () { 49 | 50 | return internals.binary; 51 | }; 52 | 53 | root.date = function () { 54 | 55 | return internals.date; 56 | }; 57 | 58 | root.func = function () { 59 | 60 | return internals.func; 61 | }; 62 | 63 | root.number = function () { 64 | 65 | return internals.number; 66 | }; 67 | 68 | root.object = function () { 69 | 70 | return arguments.length ? internals.object.keys.apply(internals.object, arguments) : internals.object; 71 | }; 72 | 73 | root.string = function () { 74 | 75 | return internals.string; 76 | }; 77 | 78 | root.ref = function () { 79 | 80 | return Ref.create.apply(null, arguments); 81 | }; 82 | 83 | root.validate = function (value /*, [schema], [options], callback */) { 84 | 85 | var callback = arguments[arguments.length - 1]; 86 | 87 | if (arguments.length === 2) { 88 | return any.validate(value, callback); 89 | } 90 | 91 | var options = arguments.length === 4 ? arguments[2] : {}; 92 | var schema = Cast.schema(arguments[1]); 93 | 94 | return schema._validateWithOptions(value, options, callback); 95 | }; 96 | 97 | root.describe = function () { 98 | 99 | var schema = arguments.length ? Cast.schema(arguments[0]) : any; 100 | return schema.describe(); 101 | }; 102 | 103 | root.compile = function (schema) { 104 | 105 | return Cast.schema(schema); 106 | }; 107 | 108 | return root; 109 | }; 110 | 111 | 112 | module.exports = internals.root(); 113 | -------------------------------------------------------------------------------- /lib/number.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Any = require('./any'); 4 | var Errors = require('./errors'); 5 | var Hoek = require('hoek'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | internals.Number = function () { 14 | 15 | Any.call(this); 16 | this._type = 'number'; 17 | }; 18 | 19 | Hoek.inherits(internals.Number, Any); 20 | 21 | 22 | internals.Number.prototype._base = function (value, state, options) { 23 | 24 | var result = { 25 | errors: null, 26 | value: value 27 | }; 28 | 29 | if (typeof value === 'string' && 30 | options.convert) { 31 | 32 | var number = parseFloat(value); 33 | result.value = (isNaN(number) || !isFinite(value)) ? NaN : number; 34 | } 35 | 36 | result.errors = (typeof result.value === 'number' && !isNaN(result.value)) ? null : Errors.create('number.base', null, state, options); 37 | return result; 38 | }; 39 | 40 | 41 | internals.Number.prototype.min = function (limit) { 42 | 43 | Hoek.assert(Hoek.isInteger(limit), 'limit must be an integer'); 44 | 45 | return this._test('min', limit, function (value, state, options) { 46 | 47 | if (value >= limit) { 48 | return null; 49 | } 50 | 51 | return Errors.create('number.min', { limit: limit }, state, options); 52 | }); 53 | }; 54 | 55 | 56 | internals.Number.prototype.max = function (limit) { 57 | 58 | Hoek.assert(Hoek.isInteger(limit), 'limit must be an integer'); 59 | 60 | return this._test('max', limit, function (value, state, options) { 61 | 62 | if (value <= limit) { 63 | return null; 64 | } 65 | 66 | return Errors.create('number.max', { limit: limit }, state, options); 67 | }); 68 | }; 69 | 70 | 71 | internals.Number.prototype.integer = function () { 72 | 73 | return this._test('integer', undefined, function (value, state, options) { 74 | 75 | return Hoek.isInteger(value) ? null : Errors.create('number.integer', null, state, options); 76 | }); 77 | }; 78 | 79 | 80 | internals.Number.prototype.negative = function () { 81 | 82 | return this._test('negative', undefined, function (value, state, options) { 83 | 84 | if (value < 0) { 85 | return null; 86 | } 87 | 88 | return Errors.create('number.negative', null, state, options); 89 | }); 90 | }; 91 | 92 | 93 | internals.Number.prototype.positive = function () { 94 | 95 | return this._test('positive', undefined, function (value, state, options) { 96 | 97 | if (value > 0) { 98 | return null; 99 | } 100 | 101 | return Errors.create('number.positive', null, state, options); 102 | }); 103 | }; 104 | 105 | 106 | module.exports = new internals.Number(); 107 | -------------------------------------------------------------------------------- /lib/language.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | 4 | // Declare internals 5 | 6 | var internals = {}; 7 | 8 | 9 | exports.errors = { 10 | root: 'value', 11 | any: { 12 | unknown: 'is not allowed', 13 | invalid: 'contains an invalid value', 14 | empty: 'is not allowed to be empty', 15 | required: 'is required', 16 | allowOnly: 'must be one of {{valids}}' 17 | }, 18 | alternatives: { 19 | base: 'not matching any of the allowed alternatives' 20 | }, 21 | array: { 22 | base: 'must be an array', 23 | includes: 'position {{pos}} does not match any of the allowed types', 24 | includesOne: 'position {{pos}} fails because {{reason}}', 25 | excludes: 'position {{pos}} contains an excluded value', 26 | min: 'must contain at least {{limit}} items', 27 | max: 'must contain less than or equal to {{limit}} items', 28 | length: 'must contain {{limit}} items' 29 | }, 30 | boolean: { 31 | base: 'must be a boolean' 32 | }, 33 | binary: { 34 | base: 'must be a buffer or a string', 35 | min: 'must be at least {{limit}} bytes', 36 | max: 'must be less than or equal to {{limit}} bytes', 37 | length: 'must be {{limit}} bytes' 38 | }, 39 | date: { 40 | base: 'must be a number of milliseconds or valid date string', 41 | min: 'must be larger than or equal to {{limit}}', 42 | max: 'must be less than or equal to {{limit}}' 43 | }, 44 | function: { 45 | base: 'must be a Function' 46 | }, 47 | object: { 48 | base: 'must be an object', 49 | min: 'must have at least {{limit}} children', 50 | max: 'must have less than or equal to {{limit}} children', 51 | length: 'must have {{limit}} children', 52 | allowUnknown: 'is not allowed', 53 | with: 'missing required peer {{peer}}', 54 | without: 'conflict with forbidden peer {{peer}}', 55 | missing: 'must contain at least one of {{peers}}', 56 | xor: 'contains a conflict between exclusive peers {{peers}}', 57 | or: 'must contain at least one of {{peers}}', 58 | and: 'contains {{present}} without its required peers {{missing}}', 59 | assert: 'validation failed because {{ref}} failed to {{message}}', 60 | rename: { 61 | multiple: 'cannot rename child {{from}} because multiple renames are disabled and another key was already renamed to {{to}}', 62 | override: 'cannot rename child {{from}} because override is disabled and target {{to}} exists' 63 | } 64 | }, 65 | number: { 66 | base: 'must be a number', 67 | min: 'must be larger than or equal to {{limit}}', 68 | max: 'must be less than or equal to {{limit}}', 69 | float: 'must be a float or double', 70 | integer: 'must be an integer', 71 | negative: 'must be a negative number', 72 | positive: 'must be a positive number' 73 | }, 74 | string: { 75 | base: 'must be a string', 76 | min: 'length must be at least {{limit}} characters long', 77 | max: 'length must be less than or equal to {{limit}} characters long', 78 | length: 'length must be {{limit}} characters long', 79 | alphanum: 'must only contain alpha-numeric characters', 80 | token: 'must only contain alpha-numeric and underscore characters', 81 | regex: 'fails to match the required pattern', 82 | email: 'must be a valid email', 83 | isoDate: 'must be a valid ISO 8601 date', 84 | guid: 'must be a valid GUID', 85 | hostname: 'must be a valid hostname' 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /test/boolean.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Lab = require('lab'); 4 | var Joi = require('../lib'); 5 | var Validate = require('./helper'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | // Test shortcuts 14 | 15 | var expect = Lab.expect; 16 | var before = Lab.before; 17 | var after = Lab.after; 18 | var describe = Lab.experiment; 19 | var it = Lab.test; 20 | 21 | 22 | describe('boolean', function () { 23 | 24 | it('converts a string to a boolean', function (done) { 25 | 26 | Joi.boolean().validate('true', function (err, value) { 27 | 28 | expect(err).to.not.exist; 29 | expect(value).to.equal(true); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('errors on a number', function (done) { 35 | 36 | Joi.boolean().validate(1, function (err, value) { 37 | 38 | expect(err).to.exist; 39 | expect(value).to.equal(1); 40 | done(); 41 | }); 42 | }); 43 | 44 | describe('#validate', function () { 45 | 46 | it('converts string values and validates', function (done) { 47 | 48 | var rule = Joi.boolean(); 49 | Validate(rule, [ 50 | ['1234', false], 51 | [false, true], 52 | [true, true], 53 | [null, false], 54 | ['on', true], 55 | ['off', true], 56 | ['true', true], 57 | ['false', true], 58 | ['yes', true], 59 | ['no', true] 60 | ]); done(); 61 | }); 62 | 63 | it('should handle work with required', function (done) { 64 | 65 | var rule = Joi.boolean().required(); 66 | Validate(rule, [ 67 | ['1234', false], 68 | ['true', true], 69 | [false, true], 70 | [true, true], 71 | [null, false] 72 | ]); done(); 73 | }); 74 | 75 | it('should handle work with allow', function (done) { 76 | 77 | var rule = Joi.boolean().allow(false); 78 | Validate(rule, [ 79 | ['1234', false], 80 | [false, true], 81 | [null, false] 82 | ]); done(); 83 | }); 84 | 85 | it('should handle work with invalid', function (done) { 86 | 87 | var rule = Joi.boolean().invalid(false); 88 | Validate(rule, [ 89 | ['1234', false], 90 | [false, false], 91 | [true, true], 92 | [null, false] 93 | ]); done(); 94 | }); 95 | 96 | it('should handle work with invalid and null allowed', function (done) { 97 | 98 | var rule = Joi.boolean().invalid(false).allow(null); 99 | Validate(rule, [ 100 | ['1234', false], 101 | [false, false], 102 | [true, true], 103 | [null, true] 104 | ]); done(); 105 | }); 106 | 107 | it('should handle work with allow and invalid', function (done) { 108 | 109 | var rule = Joi.boolean().invalid(true).allow(false); 110 | Validate(rule, [ 111 | ['1234', false], 112 | [false, true], 113 | [true, false], 114 | [null, false] 115 | ]); done(); 116 | }); 117 | 118 | it('should handle work with allow, invalid, and null allowed', function (done) { 119 | 120 | var rule = Joi.boolean().invalid(true).allow(false).allow(null); 121 | Validate(rule, [ 122 | ['1234', false], 123 | [false, true], 124 | [true, false], 125 | [null, true] 126 | ]); done(); 127 | }); 128 | }); 129 | }); 130 | 131 | -------------------------------------------------------------------------------- /lib/alternatives.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Hoek = require('hoek'); 4 | var Any = require('./any'); 5 | var Cast = require('./cast'); 6 | var Errors = require('./errors'); 7 | 8 | 9 | // Declare internals 10 | 11 | var internals = {}; 12 | 13 | 14 | internals.Alternatives = function () { 15 | 16 | Any.call(this); 17 | this._type = 'alternatives'; 18 | this._invalids.remove(null); 19 | 20 | this._inner = []; 21 | }; 22 | 23 | Hoek.inherits(internals.Alternatives, Any); 24 | 25 | 26 | internals.Alternatives.prototype._base = function (value, state, options) { 27 | 28 | var errors = []; 29 | for (var i = 0, il = this._inner.length; i < il; ++i) { 30 | var item = this._inner[i]; 31 | var schema = item.schema; 32 | if (!schema) { 33 | var failed = item.is._validate(item.ref(state.parent), null, options, state.parent).errors; 34 | schema = failed ? item.otherwise : item.then; 35 | if (!schema) { 36 | continue; 37 | } 38 | } 39 | 40 | var result = schema._validate(value, state, options); 41 | if (!result.errors) { // Found a valid match 42 | return result; 43 | } 44 | 45 | errors = errors.concat(result.errors); 46 | } 47 | 48 | return { errors: errors.length ? errors : Errors.create('alternatives.base', null, state, options) }; 49 | }; 50 | 51 | 52 | internals.Alternatives.prototype.try = function (/* schemas */) { 53 | 54 | 55 | var schemas = Hoek.flatten(Array.prototype.slice.call(arguments)); 56 | Hoek.assert(schemas.length, 'Cannot add other alternatives without at least one schema'); 57 | 58 | var obj = this.clone(); 59 | 60 | for (var i = 0, il = schemas.length; i < il; ++i) { 61 | var cast = Cast.schema(schemas[i]); 62 | if (cast._refs.length) { 63 | obj._refs = obj._refs.concat(cast._refs) 64 | } 65 | obj._inner.push({ schema: cast }); 66 | } 67 | 68 | return obj; 69 | }; 70 | 71 | 72 | internals.Alternatives.prototype.when = function (ref, options) { 73 | 74 | Hoek.assert(ref, 'Missing reference'); 75 | Hoek.assert(options, 'Missing options'); 76 | Hoek.assert(typeof options === 'object', 'Invalid options'); 77 | Hoek.assert(options.is, 'Missing "is" directive'); 78 | Hoek.assert(options.then !== undefined || options.otherwise !== undefined, 'options must have at least one of "then" or "otherwise"'); 79 | 80 | var obj = this.clone(); 81 | 82 | var item = { 83 | ref: Cast.ref(ref), 84 | is: Cast.schema(options.is), 85 | then: options.then !== undefined ? Cast.schema(options.then) : undefined, 86 | otherwise: options.otherwise !== undefined ? Cast.schema(options.otherwise) : undefined 87 | }; 88 | 89 | obj._refs = obj._refs.concat(item.ref.root, item.is._refs); 90 | 91 | if (item.then && item.then._refs) { 92 | obj._refs = obj._refs.concat(item.then._refs); 93 | } 94 | 95 | if (item.otherwise && item.otherwise._refs) { 96 | obj._refs = obj._refs.concat(item.otherwise._refs); 97 | } 98 | 99 | obj._inner.push(item); 100 | 101 | return obj; 102 | }; 103 | 104 | 105 | internals.Alternatives.prototype.describe = function () { 106 | 107 | var descriptions = []; 108 | for (var i = 0, il = this._inner.length; i < il; ++i) { 109 | var item = this._inner[i]; 110 | if (item.schema) { 111 | 112 | // try() 113 | 114 | descriptions.push(item.schema.describe()); 115 | } 116 | else { 117 | 118 | // when() 119 | 120 | var when = { 121 | ref: item.ref.key, 122 | is: item.is.describe() 123 | }; 124 | 125 | if (item.then) { 126 | when.then = item.then.describe(); 127 | } 128 | 129 | if (item.otherwise) { 130 | when.otherwise = item.otherwise.describe(); 131 | } 132 | 133 | descriptions.push(when); 134 | } 135 | } 136 | 137 | return descriptions; 138 | }; 139 | 140 | 141 | module.exports = new internals.Alternatives(); 142 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Hoek = require('hoek'); 4 | var Language = require('./language'); 5 | 6 | 7 | // Declare internals 8 | 9 | var internals = {}; 10 | 11 | 12 | internals.Err = function (type, context, state, options) { 13 | 14 | this.type = type; 15 | this.context = context || {}; 16 | this.context.key = state.key; 17 | this.path = state.path; 18 | this.options = options; 19 | }; 20 | 21 | 22 | internals.Err.prototype.toString = function () { 23 | 24 | var self = this; 25 | 26 | var localized = this.options.language; 27 | this.context.key = this.context.key || localized.root || Language.errors.root; 28 | 29 | var format = Hoek.reach(localized, this.type) || Hoek.reach(Language.errors, this.type); 30 | var hasKey = false; 31 | var message = format.replace(/\{\{\s*([^\s}]+?)\s*\}\}/ig, function (match, name) { 32 | 33 | hasKey = hasKey || name === 'key'; 34 | var value = Hoek.reach(self.context, name); 35 | return Array.isArray(value) ? value.join(', ') : value.toString(); 36 | }); 37 | 38 | return hasKey ? message : this.context.key + ' ' + message; 39 | }; 40 | 41 | 42 | exports.create = function (type, context, state, options) { 43 | 44 | return new internals.Err(type, context, state, options); 45 | }; 46 | 47 | 48 | exports.process = function (errors, object) { 49 | 50 | if (!errors || !errors.length) { 51 | return null; 52 | } 53 | 54 | var details = []; 55 | for (var i = 0, il = errors.length; i < il; ++i) { 56 | var item = errors[i]; 57 | details.push({ 58 | message: item.toString(), 59 | path: item.path || item.context.key, 60 | type: item.type 61 | }); 62 | } 63 | 64 | // Construct error 65 | 66 | var message = ''; 67 | details.forEach(function (error) { 68 | 69 | message += (message ? '. ' : '') + error.message; 70 | }); 71 | 72 | var error = new Error(message); 73 | error.details = details; 74 | error._object = object; 75 | error.annotate = internals.annotate; 76 | return error; 77 | }; 78 | 79 | 80 | internals.annotate = function () { 81 | 82 | var obj = Hoek.clone(this._object || {}); 83 | 84 | var lookup = {}; 85 | var el = this.details.length; 86 | for (var e = el - 1; e >= 0; --e) { // Reverse order to process deepest child first 87 | var pos = el - e; 88 | var error = this.details[e]; 89 | var path = error.path.split('.'); 90 | var ref = obj; 91 | for (var i = 0, il = path.length; i < il && ref; ++i) { 92 | var seg = path[i]; 93 | if (i + 1 < il) { 94 | ref = ref[seg]; 95 | } 96 | else { 97 | var value = ref[seg]; 98 | if (value !== undefined) { 99 | delete ref[seg]; 100 | var label = seg + '_$key$_' + pos + '_$end$_'; 101 | ref[label] = value; 102 | lookup[error.path] = label; 103 | } 104 | else if (lookup[error.path]) { 105 | var replacement = lookup[error.path]; 106 | var appended = replacement.replace('_$end$_', ', ' + pos + '_$end$_'); 107 | ref[appended] = ref[replacement]; 108 | lookup[error.path] = appended; 109 | delete ref[replacement]; 110 | } 111 | else { 112 | ref['_$miss$_' + seg + '|' + pos + '_$end$_'] = '__missing__'; 113 | } 114 | } 115 | } 116 | } 117 | 118 | var annotated = JSON.stringify(obj, null, 2); 119 | 120 | annotated = annotated.replace(/_\$key\$_([, \d]+)_\$end\$_\"/g, function ($0, $1) { 121 | 122 | return '" \u001b[31m[' + $1 + ']\u001b[0m'; 123 | }); 124 | 125 | var message = annotated.replace(/\"_\$miss\$_([^\|]+)\|(\d+)_\$end\$_\"\: \"__missing__\"/g, function ($0, $1, $2) { 126 | 127 | return '\u001b[41m"' + $1 + '"\u001b[0m\u001b[31m [' + $2 + ']: -- missing --\u001b[0m'; 128 | }); 129 | 130 | message += '\n\u001b[31m'; 131 | 132 | for (e = 0; e < el; ++e) { 133 | message += '\n[' + (e + 1) + '] ' + this.details[e].message; 134 | } 135 | 136 | message += '\u001b[0m'; 137 | 138 | return message; 139 | }; 140 | 141 | -------------------------------------------------------------------------------- /lib/array.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Sys = require('sys'); 4 | var Any = require('./any'); 5 | var Cast = require('./cast'); 6 | var Errors = require('./errors'); 7 | var Hoek = require('hoek'); 8 | 9 | 10 | // Declare internals 11 | 12 | var internals = {}; 13 | 14 | 15 | internals.Array = function () { 16 | 17 | Any.call(this); 18 | this._type = 'array'; 19 | }; 20 | 21 | Hoek.inherits(internals.Array, Any); 22 | 23 | 24 | internals.Array.prototype._base = function (value, state, options) { 25 | 26 | var result = { 27 | value: value 28 | }; 29 | 30 | if (typeof value === 'string' && 31 | options.convert) { 32 | 33 | try { 34 | var converted = JSON.parse(value); 35 | if (Array.isArray(converted)) { 36 | result.value = converted; 37 | } 38 | } 39 | catch (e) { } 40 | } 41 | 42 | result.errors = Array.isArray(result.value) ? null : Errors.create('array.base', null, state, options); 43 | return result; 44 | }; 45 | 46 | 47 | internals.Array.prototype.includes = function () { 48 | 49 | var inclusions = Hoek.flatten(Array.prototype.slice.call(arguments)).map(function (type) { 50 | 51 | return Cast.schema(type); 52 | }); 53 | 54 | return this._test('includes', inclusions, function (value, state, options) { 55 | 56 | for (var v = 0, vl = value.length; v < vl; ++v) { 57 | var item = value[v]; 58 | var isValid = false; 59 | var localState = { key: v, path: (state.path ? state.path + '.' : '') + v, parent: value, reference: state.reference }; 60 | 61 | for (var i = 0, il = inclusions.length; i < il; ++i) { 62 | var result = inclusions[i]._validate(item, localState, options); 63 | if (!result.errors) { 64 | value[v] = result.value; 65 | isValid = true; 66 | break; 67 | } 68 | 69 | // Return the actual error if only one inclusion defined 70 | 71 | if (il === 1) { 72 | return Errors.create('array.includesOne', { pos: v, reason: result.errors }, { key: state.key, path: localState.path }, options); 73 | } 74 | } 75 | 76 | if (!isValid) { 77 | return Errors.create('array.includes', { pos: v }, { key: state.key, path: localState.path }, options); 78 | } 79 | } 80 | 81 | return null; 82 | }); 83 | }; 84 | 85 | 86 | internals.Array.prototype.excludes = function () { 87 | 88 | var exclusions = Hoek.flatten(Array.prototype.slice.call(arguments)).map(function (type) { 89 | 90 | return Cast.schema(type); 91 | }); 92 | 93 | return this._test('excludes', exclusions, function (value, state, options) { 94 | 95 | for (var v = 0, vl = value.length; v < vl; ++v) { 96 | var item = value[v]; 97 | var localState = { key: v, path: (state.path ? state.path + '.' : '') + v, parent: value, reference: state.reference }; 98 | 99 | for (var i = 0, il = exclusions.length; i < il; ++i) { 100 | var result = exclusions[i]._validate(item, localState, {}); // Not passing options to use defaults 101 | if (!result.errors) { 102 | return Errors.create('array.excludes', { pos: v }, { key: state.key, path: localState.path }, options); 103 | } 104 | } 105 | } 106 | 107 | return null; 108 | }); 109 | }; 110 | 111 | 112 | internals.Array.prototype.min = function (limit) { 113 | 114 | Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); 115 | 116 | return this._test('min', limit, function (value, state, options) { 117 | 118 | if (value.length >= limit) { 119 | return null; 120 | } 121 | 122 | return Errors.create('array.min', { limit: limit }, state, options); 123 | }); 124 | }; 125 | 126 | 127 | internals.Array.prototype.max = function (limit) { 128 | 129 | Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); 130 | 131 | return this._test('max', limit, function (value, state, options) { 132 | 133 | if (value.length <= limit) { 134 | return null; 135 | } 136 | 137 | return Errors.create('array.max', { limit: limit }, state, options); 138 | }); 139 | }; 140 | 141 | 142 | internals.Array.prototype.length = function (limit) { 143 | 144 | Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); 145 | 146 | return this._test('length', limit, function (value, state, options) { 147 | 148 | if (value.length === limit) { 149 | return null; 150 | } 151 | 152 | return Errors.create('array.length', { limit: limit }, state, options); 153 | }); 154 | }; 155 | 156 | 157 | module.exports = new internals.Array(); -------------------------------------------------------------------------------- /test/binary.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Lab = require('lab'); 4 | var Joi = require('../lib'); 5 | var Validate = require('./helper'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | // Test shortcuts 14 | 15 | var expect = Lab.expect; 16 | var before = Lab.before; 17 | var after = Lab.after; 18 | var describe = Lab.experiment; 19 | var it = Lab.test; 20 | 21 | 22 | describe('binary', function () { 23 | 24 | it('should convert a string to a buffer', function (done) { 25 | 26 | var result = Joi.binary().validate('test', function (err, value) { 27 | 28 | expect(err).to.not.exist; 29 | expect(value instanceof Buffer).to.equal(true); 30 | expect(value.length).to.equal(4); 31 | expect(value.toString('utf8')).to.equal('test'); 32 | done(); 33 | }); 34 | }); 35 | 36 | describe('#validate', function () { 37 | 38 | it('should return an error when a non-buffer or non-string is used', function (done) { 39 | 40 | Joi.binary().validate(5, function (err, value) { 41 | 42 | expect(err).to.exist; 43 | expect(err.message).to.equal('value must be a buffer or a string'); 44 | done(); 45 | }); 46 | }); 47 | 48 | it('should accept a buffer object', function (done) { 49 | 50 | var schema = { 51 | buffer: Joi.binary 52 | }; 53 | 54 | Joi.binary().validate(new Buffer('hello world'), function (err, value) { 55 | 56 | expect(err).to.not.exist; 57 | expect(value.toString('utf8')).to.equal('hello world'); 58 | done(); 59 | }); 60 | }); 61 | }); 62 | 63 | describe('#encoding', function () { 64 | 65 | it('applies encoding', function (done) { 66 | 67 | var schema = Joi.binary().encoding('base64'); 68 | var input = new Buffer('abcdef'); 69 | schema.validate(input.toString('base64'), function (err, value) { 70 | 71 | expect(err).to.not.exist; 72 | expect(value instanceof Buffer).to.equal(true); 73 | expect(value.toString()).to.equal('abcdef'); 74 | done(); 75 | }); 76 | }); 77 | }); 78 | 79 | describe('#min', function () { 80 | 81 | it('validates buffer size', function (done) { 82 | 83 | var schema = Joi.binary().min(5); 84 | Validate(schema, [ 85 | [new Buffer('testing'), true], 86 | [new Buffer('test'), false] 87 | ]); 88 | done(); 89 | }); 90 | 91 | it('throws when min is not a number', function (done) { 92 | 93 | expect(function () { 94 | 95 | Joi.binary().min('a'); 96 | }).to.throw('limit must be a positive integer'); 97 | done(); 98 | }); 99 | 100 | it('throws when min is not an integer', function (done) { 101 | 102 | expect(function () { 103 | 104 | Joi.binary().min(1.2); 105 | }).to.throw('limit must be a positive integer'); 106 | done(); 107 | }); 108 | }); 109 | 110 | describe('#max', function () { 111 | 112 | it('validates buffer size', function (done) { 113 | 114 | var schema = Joi.binary().max(5); 115 | Validate(schema, [ 116 | [new Buffer('testing'), false], 117 | [new Buffer('test'), true] 118 | ]); 119 | done(); 120 | }); 121 | 122 | it('throws when max is not a number', function (done) { 123 | 124 | expect(function () { 125 | 126 | Joi.binary().max('a'); 127 | }).to.throw('limit must be a positive integer'); 128 | done(); 129 | }); 130 | 131 | it('throws when max is not an integer', function (done) { 132 | 133 | expect(function () { 134 | 135 | Joi.binary().max(1.2); 136 | }).to.throw('limit must be a positive integer'); 137 | done(); 138 | }); 139 | }); 140 | 141 | describe('#length', function () { 142 | 143 | it('validates buffer size', function (done) { 144 | 145 | var schema = Joi.binary().length(4); 146 | Validate(schema, [ 147 | [new Buffer('test'), true], 148 | [new Buffer('testing'), false] 149 | ]); 150 | done(); 151 | }); 152 | 153 | it('throws when length is not a number', function (done) { 154 | 155 | expect(function () { 156 | 157 | Joi.binary().length('a'); 158 | }).to.throw('limit must be a positive integer'); 159 | done(); 160 | }); 161 | 162 | it('throws when length is not an integer', function (done) { 163 | 164 | expect(function () { 165 | 166 | Joi.binary().length(1.2); 167 | }).to.throw('limit must be a positive integer'); 168 | done(); 169 | }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /lib/string.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Net = require('net'); 4 | var Any = require('./any'); 5 | var Errors = require('./errors'); 6 | var Hoek = require('hoek'); 7 | 8 | 9 | // Declare internals 10 | 11 | var internals = {}; 12 | 13 | 14 | internals.String = function () { 15 | 16 | Any.call(this); 17 | this._type = 'string'; 18 | this._invalids.add(''); 19 | }; 20 | 21 | Hoek.inherits(internals.String, Any); 22 | 23 | 24 | internals.String.prototype._base = function (value, state, options) { 25 | 26 | return { 27 | value: value, 28 | errors: (value && typeof value === 'string') ? null : Errors.create('string.base', null, state, options) 29 | }; 30 | }; 31 | 32 | 33 | internals.String.prototype.insensitive = function () { 34 | 35 | var obj = this.clone(); 36 | obj._flags.insensitive = true; 37 | return obj; 38 | }; 39 | 40 | 41 | internals.String.prototype.min = function (limit, encoding) { 42 | 43 | Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); 44 | 45 | return this._test('min', limit, function (value, state, options) { 46 | 47 | var length = encoding ? Buffer.byteLength(value, encoding) : value.length; 48 | if (length >= limit) { 49 | return null; 50 | } 51 | 52 | return Errors.create('string.min', { limit: limit }, state, options); 53 | }); 54 | }; 55 | 56 | 57 | internals.String.prototype.max = function (limit, encoding) { 58 | 59 | Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); 60 | 61 | return this._test('max', limit, function (value, state, options) { 62 | 63 | var length = encoding ? Buffer.byteLength(value, encoding) : value.length; 64 | if (length <= limit) { 65 | return null; 66 | } 67 | 68 | return Errors.create('string.max', { limit: limit }, state, options); 69 | }); 70 | }; 71 | 72 | 73 | internals.String.prototype.length = function (limit, encoding) { 74 | 75 | Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); 76 | 77 | return this._test('length', limit, function (value, state, options) { 78 | 79 | var length = encoding ? Buffer.byteLength(value, encoding) : value.length; 80 | if (length === limit) { 81 | return null; 82 | } 83 | 84 | return Errors.create('string.length', { limit: limit }, state, options); 85 | }); 86 | }; 87 | 88 | 89 | internals.String.prototype.regex = function (pattern) { 90 | 91 | Hoek.assert(pattern instanceof RegExp, 'pattern must be a RegExp'); 92 | 93 | return this._test('regex', pattern, function (value, state, options) { 94 | 95 | if (pattern.test(value)) { 96 | return null; 97 | } 98 | 99 | return Errors.create('string.regex', null, state, options); 100 | }); 101 | }; 102 | 103 | 104 | internals.String.prototype.alphanum = function () { 105 | 106 | return this._test('alphanum', undefined, function (value, state, options) { 107 | 108 | if (/^[a-zA-Z0-9]+$/.test(value)) { 109 | return null; 110 | } 111 | 112 | return Errors.create('string.alphanum', null, state, options); 113 | }); 114 | }; 115 | 116 | 117 | internals.String.prototype.token = function () { 118 | 119 | return this._test('token', undefined, function (value, state, options) { 120 | 121 | if (/^\w+$/.test(value)) { 122 | return null; 123 | } 124 | 125 | return Errors.create('string.token', null, state, options); 126 | }); 127 | }; 128 | 129 | 130 | internals.String.prototype.email = function () { 131 | 132 | var regex = /^(?:[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+\.)*[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+@(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!\.)){0,61}[a-zA-Z0-9]?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!$)){0,61}[a-zA-Z0-9]?)|(?:\[(?:(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\]))$/; 133 | 134 | return this._test('email', undefined, function (value, state, options) { 135 | 136 | if (regex.test(value)) { 137 | return null; 138 | } 139 | 140 | return Errors.create('string.email', null, state, options); 141 | }); 142 | }; 143 | 144 | 145 | internals.String.prototype.isoDate = function () { 146 | 147 | var regex = /^(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))$/; 148 | 149 | return this._test('isoDate', undefined, function (value, state, options) { 150 | 151 | if (regex.test(value)) { 152 | return null; 153 | } 154 | 155 | return Errors.create('string.isoDate', null, state, options); 156 | }); 157 | }; 158 | 159 | 160 | internals.String.prototype.guid = function () { 161 | 162 | var regex = /^[A-F0-9]{8}(?:-?[A-F0-9]{4}){3}-?[A-F0-9]{12}$/i; 163 | var regex2 = /^\{[A-F0-9]{8}(?:-?[A-F0-9]{4}){3}-?[A-F0-9]{12}\}$/i; 164 | 165 | return this._test('guid', undefined, function (value, state, options) { 166 | 167 | if (regex.test(value) || regex2.test(value)) { 168 | return null; 169 | } 170 | 171 | return Errors.create('string.guid', null, state, options); 172 | }); 173 | }; 174 | 175 | 176 | internals.String.prototype.hostname = function () { 177 | 178 | var regex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/; 179 | 180 | return this._test('hostname', undefined, function (value, state, options) { 181 | 182 | if ((value.length <= 255 && regex.test(value)) || 183 | Net.isIPv6(value)) { 184 | 185 | return null; 186 | } 187 | 188 | return Errors.create("string.hostname", null, state, options); 189 | }); 190 | }; 191 | 192 | 193 | module.exports = new internals.String(); -------------------------------------------------------------------------------- /test/errors.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Lab = require('lab'); 4 | var Joi = require('../lib'); 5 | 6 | 7 | // Declare internals 8 | 9 | var internals = {}; 10 | 11 | 12 | // Test shortcuts 13 | 14 | var expect = Lab.expect; 15 | var before = Lab.before; 16 | var after = Lab.after; 17 | var describe = Lab.experiment; 18 | var it = Lab.test; 19 | 20 | 21 | describe('errors', function () { 22 | 23 | it('supports custom errors when validating types', function (done) { 24 | 25 | var schema = Joi.object({ 26 | email: Joi.string().email(), 27 | date: Joi.date(), 28 | alphanum: Joi.string().alphanum(), 29 | min: Joi.string().min(3), 30 | max: Joi.string().max(3), 31 | required: Joi.string().required(), 32 | xor: Joi.string(), 33 | renamed: Joi.string().valid('456'), 34 | notEmpty: Joi.string().required() 35 | }).rename('renamed', 'required').without('required', 'xor').without('xor', 'required'); 36 | 37 | var input = { 38 | email: 'invalid-email', 39 | date: 'invalid-date', 40 | alphanum: '\b\n\f\r\t', 41 | min: 'ab', 42 | max: 'abcd', 43 | required: 'hello', 44 | xor: '123', 45 | renamed: '456', 46 | notEmpty: '' 47 | }; 48 | 49 | var lang = { 50 | any: { 51 | empty: '3' 52 | }, 53 | date: { 54 | base: '18' 55 | }, 56 | string: { 57 | base: '13', 58 | min: '14', 59 | max: '15', 60 | alphanum: '16', 61 | email: '19' 62 | }, 63 | object: { 64 | without: '7', 65 | rename: { 66 | override: '11' 67 | } 68 | } 69 | }; 70 | 71 | Joi.validate(input, schema, { abortEarly: false, language: lang }, function (err, value) { 72 | 73 | expect(err).to.exist; 74 | expect(err.message).to.equal('value 11. required 7. xor 7. email 19. date 18. alphanum 16. min 14. max 15. notEmpty 3. notEmpty 13'); 75 | done(); 76 | }); 77 | }); 78 | 79 | it('does not prefix with key when language uses context.key', function (done) { 80 | 81 | Joi.valid('sad').options({ language: { any: { allowOnly: 'my hero {{key}} is not {{valids}}' } } }).validate(5, function (err, value) { 82 | 83 | expect(err.message).to.equal('my hero value is not sad'); 84 | done(); 85 | }); 86 | }); 87 | 88 | it('returns error type in validation error', function (done) { 89 | 90 | var input = { 91 | notNumber: '', 92 | notString: true, 93 | notBoolean: 9 94 | }; 95 | 96 | var schema = { 97 | notNumber: Joi.number().required(), 98 | notString: Joi.string().required(), 99 | notBoolean: Joi.boolean().required() 100 | } 101 | 102 | Joi.validate(input, schema, { abortEarly: false }, function (err, value) { 103 | 104 | expect(err).to.exist; 105 | expect(err.details).to.have.length(3); 106 | expect(err.details[0].type).to.equal('number.base'); 107 | expect(err.details[1].type).to.equal('string.base'); 108 | expect(err.details[2].type).to.equal('boolean.base'); 109 | done(); 110 | }); 111 | }); 112 | 113 | it('returns a full path to an error value on an array (includes)', function (done) { 114 | 115 | var schema = Joi.array().includes(Joi.array().includes({ x: Joi.number() })); 116 | var input = [ 117 | [{ x: 1 }], 118 | [{ x: 1 }, { x: 'a' }] 119 | ]; 120 | 121 | schema.validate(input, function (err, value) { 122 | 123 | expect(err).to.exist; 124 | expect(err.details[0].path).to.equal('1'); 125 | done(); 126 | }); 127 | }); 128 | 129 | it('returns a full path to an error value on an array (excludes)', function (done) { 130 | 131 | var schema = Joi.array().includes(Joi.array().excludes({ x: Joi.string() })); 132 | var input = [ 133 | [{ x: 1 }], 134 | [{ x: 1 }, { x: 'a' }] 135 | ]; 136 | 137 | schema.validate(input, function (err, value) { 138 | 139 | expect(err).to.exist; 140 | expect(err.details[0].path).to.equal('1'); 141 | done(); 142 | }); 143 | }); 144 | 145 | it('returns a full path to an error value on an object', function (done) { 146 | 147 | var schema = { 148 | x: Joi.array().includes({ x: Joi.number() }) 149 | }; 150 | 151 | var input = { 152 | x: [{ x: 1 }, { x: 'a' }] 153 | }; 154 | 155 | Joi.validate(input, schema, function (err, value) { 156 | 157 | expect(err).to.exist; 158 | expect(err.details[0].path).to.equal('x.1'); 159 | done(); 160 | }); 161 | }); 162 | 163 | it('overrides root key language', function (done) { 164 | 165 | Joi.string().options({ language: { root: 'blah' } }).validate(4, function (err, value) { 166 | 167 | expect(err.message).to.equal('blah must be a string'); 168 | done(); 169 | }); 170 | }); 171 | 172 | describe('#annotate', function () { 173 | 174 | it('annotates error', function (done) { 175 | 176 | var object = { 177 | a: 'm', 178 | y: { 179 | b: { 180 | c: 10 181 | } 182 | } 183 | }; 184 | 185 | var schema = { 186 | a: Joi.string().valid('a', 'b', 'c', 'd'), 187 | y: Joi.object({ 188 | u: Joi.string().valid(['e', 'f', 'g', 'h']).required(), 189 | b: Joi.string().valid('i', 'j').allow(false), 190 | d: Joi.object({ 191 | x: Joi.string().valid('k', 'l').required(), 192 | c: Joi.number() 193 | }) 194 | }) 195 | }; 196 | 197 | Joi.validate(object, schema, { abortEarly: false }, function (err, value) { 198 | 199 | expect(err).to.exist; 200 | expect(err.annotate()).to.equal('{\n \"y\": {\n \"b\" \u001b[31m[1]\u001b[0m: {\n \"c\": 10\n },\n \u001b[41m\"u\"\u001b[0m\u001b[31m [2]: -- missing --\u001b[0m\n },\n \"a\" \u001b[31m[3]\u001b[0m: \"m\"\n}\n\u001b[31m\n[1] a must be one of a, b, c, d\n[2] u is required\n[3] b must be a string\u001b[0m'); 201 | done(); 202 | }); 203 | }); 204 | 205 | it('displays alternatives fail as a single line', function (done) { 206 | 207 | var schema = { 208 | x: [ 209 | Joi.string(), 210 | Joi.number(), 211 | Joi.date() 212 | ] 213 | }; 214 | 215 | Joi.validate({ x: true }, schema, function (err, value) { 216 | 217 | expect(err).to.exist; 218 | expect(err.annotate()).to.equal('{\n \"x\" \u001b[31m[1, 2, 3]\u001b[0m: true\n}\n\u001b[31m\n[1] x must be a string\n[2] x must be a number\n[3] x must be a number of milliseconds or valid date string\u001b[0m'); 219 | done(); 220 | }); 221 | }); 222 | }); 223 | }); 224 | -------------------------------------------------------------------------------- /test/ref.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Lab = require('lab'); 4 | var Joi = require('../lib'); 5 | var Validate = require('./helper'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | // Test shortcuts 14 | 15 | var expect = Lab.expect; 16 | var before = Lab.before; 17 | var after = Lab.after; 18 | var describe = Lab.experiment; 19 | var it = Lab.test; 20 | 21 | 22 | describe('ref', function () { 23 | 24 | it('uses ref as a valid value', function (done) { 25 | 26 | var schema = Joi.object({ 27 | a: Joi.ref('b'), 28 | b: Joi.any() 29 | }); 30 | 31 | schema.validate({ a: 5, b: 6 }, function (err, value) { 32 | 33 | expect(err).to.exist; 34 | expect(err.message).to.equal('a must be one of ref:b'); 35 | 36 | Validate(schema, [ 37 | [{ a: 5 }, false], 38 | [{ b: 5 }, true], 39 | [{ a: 5, b: 5 }, true], 40 | [{ a: '5', b: '5' }, true] 41 | ]); 42 | 43 | done(); 44 | }); 45 | }); 46 | 47 | it('uses ref with nested keys as a valid value', function (done) { 48 | 49 | var schema = Joi.object({ 50 | a: Joi.ref('b.c'), 51 | b: { 52 | c: Joi.any() 53 | } 54 | }); 55 | 56 | schema.validate({ a: 5, b: { c: 6 } }, function (err, value) { 57 | 58 | expect(err).to.exist; 59 | expect(err.message).to.equal('a must be one of ref:b.c'); 60 | 61 | Validate(schema, [ 62 | [{ a: 5 }, false], 63 | [{ b: { c: 5 } }, true], 64 | [{ a: 5, b: 5 }, false], 65 | [{ a: '5', b: { c: '5' } }, true] 66 | ]); 67 | 68 | done(); 69 | }); 70 | }); 71 | 72 | it('uses ref with combined nested keys in sub child', function (done) { 73 | 74 | var ref = Joi.ref('b.c'); 75 | expect(ref.root).to.equal('b'); 76 | 77 | var schema = Joi.object({ 78 | a: ref, 79 | b: { 80 | c: Joi.any() 81 | } 82 | }); 83 | 84 | var input = { a: 5, b: { c: 5 } }; 85 | schema.validate(input, function (err, value) { 86 | 87 | expect(err).to.not.exist; 88 | 89 | var parent = Joi.object({ 90 | e: schema 91 | }); 92 | 93 | parent.validate({ e: input }, function (err, value) { 94 | 95 | expect(err).to.not.exist; 96 | done(); 97 | }); 98 | }); 99 | }); 100 | 101 | it('uses ref reach options', function (done) { 102 | 103 | var ref = Joi.ref('b/c', { separator: '/' }); 104 | expect(ref.root).to.equal('b'); 105 | 106 | var schema = Joi.object({ 107 | a: ref, 108 | b: { 109 | c: Joi.any() 110 | } 111 | }); 112 | 113 | schema.validate({ a: 5, b: { c: 5 } }, function (err, value) { 114 | 115 | expect(err).to.not.exist; 116 | done(); 117 | }); 118 | }); 119 | 120 | it('ignores the order in which keys are defined', function (done) { 121 | 122 | var ab = Joi.object({ 123 | a: { 124 | c: Joi.number() 125 | }, 126 | b: Joi.ref('a.c') 127 | }); 128 | 129 | ab.validate({ a: { c: '5' }, b: 5 }, function (err, value) { 130 | 131 | expect(err).to.not.exist; 132 | 133 | var ba = Joi.object({ 134 | b: Joi.ref('a.c'), 135 | a: { 136 | c: Joi.number() 137 | } 138 | }); 139 | 140 | ba.validate({ a: { c: '5' }, b: 5 }, function (err, value) { 141 | 142 | expect(err).to.not.exist; 143 | done(); 144 | }); 145 | }); 146 | }); 147 | 148 | it('uses ref as default value', function (done) { 149 | 150 | var schema = Joi.object({ 151 | a: Joi.default(Joi.ref('b')), 152 | b: Joi.any() 153 | }); 154 | 155 | schema.validate({ b: 6 }, function (err, value) { 156 | 157 | expect(err).to.not.exist; 158 | expect(value).to.deep.equal({ a: 6, b: 6 }); 159 | done(); 160 | }); 161 | }); 162 | 163 | it('uses ref as default value regardless of order', function (done) { 164 | 165 | var ab = Joi.object({ 166 | a: Joi.default(Joi.ref('b')), 167 | b: Joi.number() 168 | }); 169 | 170 | ab.validate({ b: '6' }, function (err, value) { 171 | 172 | expect(err).to.not.exist; 173 | expect(value).to.deep.equal({ a: 6, b: 6 }); 174 | 175 | var ba = Joi.object({ 176 | b: Joi.number(), 177 | a: Joi.default(Joi.ref('b')) 178 | }); 179 | 180 | ba.validate({ b: '6' }, function (err, value) { 181 | 182 | expect(err).to.not.exist; 183 | expect(value).to.deep.equal({ a: 6, b: 6 }); 184 | done(); 185 | }); 186 | }); 187 | }); 188 | 189 | it('ignores the order in which keys are defined with alternatives', function (done) { 190 | 191 | var a = { c: Joi.number() }; 192 | var b = [Joi.ref('a.c'), Joi.ref('c')]; 193 | var c = Joi.number(); 194 | 195 | Validate({ a: a, b: b, c: c }, [ 196 | [{ a: {} }, true], 197 | [{ a: { c: '5' }, b: 5 }, true], 198 | [{ a: { c: '5' }, b: 6, c: '6' }, true], 199 | [{ a: { c: '5' }, b: 7, c: '6' }, false] 200 | ]); 201 | 202 | Validate({ b: b, a: a, c: c }, [ 203 | [{ a: {} }, true], 204 | [{ a: { c: '5' }, b: 5 }, true], 205 | [{ a: { c: '5' }, b: 6, c: '6' }, true], 206 | [{ a: { c: '5' }, b: 7, c: '6' }, false] 207 | ]); 208 | 209 | Validate({ b: b, c: c, a: a }, [ 210 | [{ a: {} }, true], 211 | [{ a: { c: '5' }, b: 5 }, true], 212 | [{ a: { c: '5' }, b: 6, c: '6' }, true], 213 | [{ a: { c: '5' }, b: 7, c: '6' }, false] 214 | ]); 215 | 216 | Validate({ a: a, c: c, b: b }, [ 217 | [{ a: {} }, true], 218 | [{ a: { c: '5' }, b: 5 }, true], 219 | [{ a: { c: '5' }, b: 6, c: '6' }, true], 220 | [{ a: { c: '5' }, b: 7, c: '6' }, false] 221 | ]); 222 | 223 | Validate({ c: c, a: a, b: b }, [ 224 | [{ a: {} }, true], 225 | [{ a: { c: '5' }, b: 5 }, true], 226 | [{ a: { c: '5' }, b: 6, c: '6' }, true], 227 | [{ a: { c: '5' }, b: 7, c: '6' }, false] 228 | ]); 229 | 230 | Validate({ c: c, b: b, a: a }, [ 231 | [{ a: {} }, true], 232 | [{ a: { c: '5' }, b: 5 }, true], 233 | [{ a: { c: '5' }, b: 6, c: '6' }, true], 234 | [{ a: { c: '5' }, b: 7, c: '6' }, false] 235 | ]); 236 | 237 | done(); 238 | }); 239 | 240 | describe('#create', function () { 241 | 242 | it('throws when key is missing', function (done) { 243 | 244 | expect(function () { 245 | 246 | Joi.ref(); 247 | }).to.throw('Missing reference key'); 248 | done(); 249 | }); 250 | 251 | it('throws when key is missing', function (done) { 252 | 253 | expect(function () { 254 | 255 | Joi.ref(5); 256 | }).to.throw('Invalid reference key: 5'); 257 | done(); 258 | }); 259 | 260 | it('finds root with default separator', function (done) { 261 | 262 | expect(Joi.ref('a.b.c').root).to.equal('a'); 263 | done(); 264 | }); 265 | 266 | it('finds root with default separator and options', function (done) { 267 | 268 | expect(Joi.ref('a.b.c', {}).root).to.equal('a'); 269 | done(); 270 | }); 271 | 272 | it('finds root with custom separator', function (done) { 273 | 274 | expect(Joi.ref('a+b+c', { separator: '+' }).root).to.equal('a'); 275 | done(); 276 | }); 277 | }); 278 | }); -------------------------------------------------------------------------------- /test/array.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Lab = require('lab'); 4 | var Joi = require('../lib'); 5 | var Validate = require('./helper'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | // Test shortcuts 14 | 15 | var expect = Lab.expect; 16 | var before = Lab.before; 17 | var after = Lab.after; 18 | var describe = Lab.experiment; 19 | var it = Lab.test; 20 | 21 | 22 | describe('array', function () { 23 | 24 | it('converts a string to an array', function (done) { 25 | 26 | Joi.array().validate('[1,2,3]', function (err, value) { 27 | 28 | expect(err).to.not.exist; 29 | expect(value.length).to.equal(3); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('errors on non-array string', function (done) { 35 | 36 | Joi.array().validate('{ "something": false }', function (err, value) { 37 | 38 | expect(err).to.exist; 39 | expect(err.message).to.equal('value must be an array'); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('errors on number', function (done) { 45 | 46 | Joi.array().validate(3, function (err, value) { 47 | 48 | expect(err).to.exist; 49 | expect(value).to.equal(3); 50 | done(); 51 | }); 52 | }); 53 | 54 | it('converts a non-array string with number type', function (done) { 55 | 56 | Joi.array().validate('3', function (err, value) { 57 | 58 | expect(err).to.exist; 59 | expect(value).to.equal('3'); 60 | done(); 61 | }); 62 | }); 63 | 64 | it('errors on a non-array string', function (done) { 65 | 66 | Joi.array().validate('asdf', function (err, value) { 67 | 68 | expect(err).to.exist; 69 | expect(value).to.equal('asdf'); 70 | done(); 71 | }); 72 | }); 73 | 74 | describe('#includes', function () { 75 | 76 | it('converts members', function (done) { 77 | 78 | var schema = Joi.array().includes(Joi.number()); 79 | var input = ['1', '2', '3']; 80 | schema.validate(input, function (err, value) { 81 | 82 | expect(err).to.not.exist; 83 | expect(value).to.deep.equal([1, 2, 3]); 84 | done(); 85 | }); 86 | }); 87 | 88 | it('allows zero size', function (done) { 89 | 90 | var schema = Joi.object({ 91 | test: Joi.array().includes(Joi.object({ 92 | foo: Joi.string().required() 93 | })) 94 | }); 95 | var input = { test: [] }; 96 | 97 | schema.validate(input, function (err, value) { 98 | 99 | expect(err).to.not.exist; 100 | done(); 101 | }); 102 | }); 103 | 104 | it('returns the first error when only one inclusion', function (done) { 105 | 106 | var schema = Joi.object({ 107 | test: Joi.array().includes(Joi.object({ 108 | foo: Joi.string().required() 109 | })) 110 | }); 111 | var input = { test: [{ foo: 'a' }, { bar: 2 }] }; 112 | 113 | schema.validate(input, function (err, value) { 114 | 115 | expect(err.message).to.equal('test position 1 fails because foo is required'); 116 | done(); 117 | }); 118 | }); 119 | }); 120 | 121 | describe('#min', function () { 122 | 123 | it('validates array size', function (done) { 124 | 125 | var schema = Joi.array().min(2); 126 | Validate(schema, [ 127 | [[1, 2], true], 128 | [[1], false] 129 | ]); 130 | done(); 131 | }); 132 | 133 | it('throws when limit is not a number', function (done) { 134 | 135 | expect(function () { 136 | 137 | Joi.array().min('a'); 138 | }).to.throw('limit must be a positive integer'); 139 | done(); 140 | }); 141 | 142 | it('throws when limit is not an integer', function (done) { 143 | 144 | expect(function () { 145 | 146 | Joi.array().min(1.2); 147 | }).to.throw('limit must be a positive integer'); 148 | done(); 149 | }); 150 | }); 151 | 152 | describe('#max', function () { 153 | 154 | it('validates array size', function (done) { 155 | 156 | var schema = Joi.array().max(1); 157 | Validate(schema, [ 158 | [[1, 2], false], 159 | [[1], true] 160 | ]); 161 | done(); 162 | }); 163 | 164 | it('throws when limit is not a number', function (done) { 165 | 166 | expect(function () { 167 | 168 | Joi.array().max('a'); 169 | }).to.throw('limit must be a positive integer'); 170 | done(); 171 | }); 172 | 173 | it('throws when limit is not an integer', function (done) { 174 | 175 | expect(function () { 176 | 177 | Joi.array().max(1.2); 178 | }).to.throw('limit must be a positive integer'); 179 | done(); 180 | }); 181 | }); 182 | 183 | describe('#length', function () { 184 | 185 | it('validates array size', function (done) { 186 | 187 | var schema = Joi.array().length(2); 188 | Validate(schema, [ 189 | [[1, 2], true], 190 | [[1], false] 191 | ]); 192 | done(); 193 | }); 194 | 195 | it('throws when limit is not a number', function (done) { 196 | 197 | expect(function () { 198 | 199 | Joi.array().length('a'); 200 | }).to.throw('limit must be a positive integer'); 201 | done(); 202 | }); 203 | 204 | it('throws when limit is not an integer', function (done) { 205 | 206 | expect(function () { 207 | 208 | Joi.array().length(1.2); 209 | }).to.throw('limit must be a positive integer'); 210 | done(); 211 | }); 212 | }); 213 | 214 | describe('#validate', function () { 215 | 216 | it('should, by default, allow undefined, allow empty array', function (done) { 217 | 218 | Validate(Joi.array(), [ 219 | [undefined, true], 220 | [[], true] 221 | ]); 222 | done(); 223 | }); 224 | 225 | it('should, when .required(), deny undefined', function (done) { 226 | 227 | Validate(Joi.array().required(), [ 228 | [undefined, false] 229 | ]); 230 | done(); 231 | }); 232 | 233 | it('allows empty arrays', function (done) { 234 | 235 | Validate(Joi.array(), [ 236 | [undefined, true], 237 | [[], true] 238 | ]); 239 | done(); 240 | }); 241 | 242 | it('should exclude values when excludes is called', function (done) { 243 | 244 | Validate(Joi.array().excludes(Joi.string()), [ 245 | [['2', '1'], false], 246 | [['1'], false], 247 | [[2], true] 248 | ]); 249 | done(); 250 | }); 251 | 252 | it('should allow types to be excluded', function (done) { 253 | 254 | var schema = Joi.array().excludes(Joi.number()); 255 | 256 | var n = [1, 2, 'hippo']; 257 | schema.validate(n, function (err, value) { 258 | 259 | expect(err).to.exist; 260 | 261 | var m = ['x', 'y', 'z']; 262 | schema.validate(m, function (err2, value) { 263 | 264 | expect(err2).to.not.exist; 265 | done(); 266 | }); 267 | }); 268 | }); 269 | 270 | it('should validate array of Numbers', function (done) { 271 | 272 | Validate(Joi.array().includes(Joi.number()), [ 273 | [[1, 2, 3], true], 274 | [[50, 100, 1000], true], 275 | [['a', 1, 2], false] 276 | ]); 277 | done(); 278 | }); 279 | 280 | it('should validate array of mixed Numbers & Strings', function (done) { 281 | 282 | Validate(Joi.array().includes(Joi.number(), Joi.string()), [ 283 | [[1, 2, 3], true], 284 | [[50, 100, 1000], true], 285 | [[1, 'a', 5, 10], true], 286 | [['joi', 'everydaylowprices', 5000], true] 287 | ]); 288 | done(); 289 | }); 290 | 291 | it('should validate array of objects with schema', function (done) { 292 | 293 | Validate(Joi.array().includes(Joi.object({ h1: Joi.number().required() })), [ 294 | [[{ h1: 1 }, { h1: 2 }, { h1: 3 }], true], 295 | [[{ h2: 1, h3: 'somestring' }, { h1: 2 }, { h1: 3 }], false], 296 | [[1, 2, [1]], false] 297 | ]); 298 | done(); 299 | }); 300 | 301 | it('should not validate array of unallowed mixed types (Array)', function (done) { 302 | 303 | Validate(Joi.array().includes(Joi.number()), [ 304 | [[1, 2, 3], true], 305 | [[1, 2, [1]], false] 306 | ]); 307 | done(); 308 | }); 309 | 310 | it('errors on invalid number rule using includes', function (done) { 311 | 312 | var schema = Joi.object({ 313 | arr: Joi.array().includes(Joi.number().integer()) 314 | }); 315 | 316 | var input = { arr: [1, 2, 2.1] }; 317 | schema.validate(input, function (err, value) { 318 | 319 | expect(err).to.exist; 320 | expect(err.message).to.equal('arr position 2 fails because 2 must be an integer'); 321 | done(); 322 | }); 323 | }); 324 | 325 | it('validates an array within an object', function (done) { 326 | 327 | var schema = Joi.object({ 328 | array: Joi.array().includes(Joi.string().min(5), Joi.number().min(3)) 329 | }).options({ convert: false }); 330 | 331 | Validate(schema, [ 332 | [{ array: ['12345'] }, true], 333 | [{ array: ['1'] }, false], 334 | [{ array: [3] }, true], 335 | [{ array: ['12345', 3] }, true] 336 | ]); 337 | done(); 338 | }); 339 | }); 340 | }); 341 | -------------------------------------------------------------------------------- /test/alternatives.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Lab = require('lab'); 4 | var Joi = require('..'); 5 | var Validate = require('./helper'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | // Test shortcuts 14 | 15 | var expect = Lab.expect; 16 | var before = Lab.before; 17 | var after = Lab.after; 18 | var describe = Lab.experiment; 19 | var it = Lab.test; 20 | 21 | 22 | describe('alternatives', function () { 23 | 24 | it('fails when no alternatives are provided', function (done) { 25 | 26 | Joi.alternatives().validate('a', function (err, value) { 27 | 28 | expect(err).to.exist; 29 | expect(err.message).to.equal('value not matching any of the allowed alternatives'); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('allows undefined when no alternatives are provided', function (done) { 35 | 36 | Joi.alternatives().validate(undefined, function (err, value) { 37 | 38 | expect(err).to.not.exist; 39 | done(); 40 | }); 41 | }); 42 | 43 | it('applies modifiers when higher priority converts', function (done) { 44 | 45 | var schema = Joi.object({ 46 | a: [ 47 | Joi.number(), 48 | Joi.string() 49 | ] 50 | }); 51 | 52 | schema.validate({ a: '5' }, function (err, value) { 53 | 54 | expect(err).to.not.exist; 55 | expect(value.a).to.equal(5); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('applies modifiers when lower priority valid is a match', function (done) { 61 | 62 | var schema = Joi.object({ 63 | a: [ 64 | Joi.number(), 65 | Joi.valid('5') 66 | ] 67 | }); 68 | 69 | schema.validate({ a: '5' }, function (err, value) { 70 | 71 | expect(err).to.not.exist; 72 | expect(value.a).to.equal(5); 73 | done(); 74 | }); 75 | }); 76 | 77 | it('does not apply modifier if alternative fails', function (done) { 78 | 79 | var schema = Joi.object({ 80 | a: [ 81 | Joi.object({ c: Joi.any(), d: Joi.number() }).rename('b', 'c'), 82 | { b: Joi.any(), d: Joi.string() } 83 | ] 84 | }); 85 | 86 | var input = { a: { b: 'any', d: 'string' } }; 87 | schema.validate(input, function (err, value) { 88 | 89 | expect(err).to.not.exist; 90 | expect(value.a.b).to.equal('any'); 91 | done(); 92 | }); 93 | }); 94 | 95 | describe('#try', function () { 96 | 97 | it('throws when missing alternatives', function (done) { 98 | 99 | expect(function () { 100 | 101 | Joi.alternatives().try(); 102 | }).to.throw('Cannot add other alternatives without at least one schema'); 103 | done(); 104 | }); 105 | }); 106 | 107 | describe('#when', function () { 108 | 109 | it('validates conditional alternatives', function (done) { 110 | 111 | var schema = { 112 | a: Joi.alternatives().when('b', { is: 5, then: 'x', otherwise: 'y' }) 113 | .try('z'), 114 | b: Joi.any() 115 | }; 116 | 117 | Validate(schema, [ 118 | [{ a: 'x', b: 5 }, true], 119 | [{ a: 'x', b: 6 }, false], 120 | [{ a: 'y', b: 5 }, false], 121 | [{ a: 'y', b: 6 }, true], 122 | [{ a: 'z', b: 5 }, true], 123 | [{ a: 'z', b: 6 }, true] 124 | ]); 125 | 126 | done(); 127 | }); 128 | 129 | it('validates only then', function (done) { 130 | 131 | var schema = { 132 | a: Joi.alternatives().when('b', { is: 5, then: 'x' }) 133 | .try('z'), 134 | b: Joi.any() 135 | }; 136 | 137 | Validate(schema, [ 138 | [{ a: 'x', b: 5 }, true], 139 | [{ a: 'x', b: 6 }, false], 140 | [{ a: 'y', b: 5 }, false], 141 | [{ a: 'y', b: 6 }, false], 142 | [{ a: 'z', b: 5 }, true], 143 | [{ a: 'z', b: 6 }, true] 144 | ]); 145 | 146 | done(); 147 | }); 148 | 149 | it('validates only otherwise', function (done) { 150 | 151 | var schema = { 152 | a: Joi.alternatives().when('b', { is: 5, otherwise: 'y' }) 153 | .try('z'), 154 | b: Joi.any() 155 | }; 156 | 157 | Validate(schema, [ 158 | [{ a: 'x', b: 5 }, false], 159 | [{ a: 'x', b: 6 }, false], 160 | [{ a: 'y', b: 5 }, false], 161 | [{ a: 'y', b: 6 }, true], 162 | [{ a: 'z', b: 5 }, true], 163 | [{ a: 'z', b: 6 }, true] 164 | ]); 165 | 166 | done(); 167 | }); 168 | 169 | it('validates when is has ref', function (done) { 170 | 171 | var schema = { 172 | a: Joi.alternatives().when('b', { is: Joi.ref('c'), then: 'x' }), 173 | b: Joi.any(), 174 | c: Joi.number() 175 | }; 176 | 177 | Validate(schema, [ 178 | [{ a: 'x', b: 5, c: '5' }, true], 179 | [{ a: 'x', b: 5, c: '1' }, false], 180 | [{ a: 'x', b: '5', c: '5' }, false], 181 | [{ a: 'y', b: 5, c: 5 }, false], 182 | [{ a: 'y' }, false] 183 | ]); 184 | 185 | done(); 186 | }); 187 | 188 | it('validates when then has ref', function (done) { 189 | 190 | var schema = { 191 | a: Joi.alternatives().when('b', { is: 5, then: Joi.ref('c') }), 192 | b: Joi.any(), 193 | c: Joi.number() 194 | }; 195 | 196 | Validate(schema, [ 197 | [{ a: 'x', b: 5, c: '1' }, false], 198 | [{ a: 1, b: 5, c: '1' }, true], 199 | [{ a: '1', b: 5, c: '1' }, false] 200 | ]); 201 | 202 | done(); 203 | }); 204 | 205 | it('validates when otherwise has ref', function (done) { 206 | 207 | var schema = { 208 | a: Joi.alternatives().when('b', { is: 6, otherwise: Joi.ref('c') }), 209 | b: Joi.any(), 210 | c: Joi.number() 211 | }; 212 | 213 | Validate(schema, [ 214 | [{ a: 'x', b: 5, c: '1' }, false], 215 | [{ a: 1, b: 5, c: '1' }, true], 216 | [{ a: '1', b: 5, c: '1' }, false] 217 | ]); 218 | 219 | done(); 220 | }); 221 | }); 222 | 223 | describe('#describe', function () { 224 | 225 | it('describes when', function (done) { 226 | 227 | var schema = { 228 | a: Joi.alternatives().when('b', { is: 5, then: 'x', otherwise: 'y' }) 229 | .try('z'), 230 | b: Joi.any() 231 | }; 232 | 233 | var outcome = { 234 | type: 'object', 235 | valids: [undefined], 236 | invalids: [null], 237 | children: { 238 | b: { 239 | type: 'any', 240 | valids: [undefined], 241 | invalids: [null] 242 | }, 243 | a: [ 244 | { 245 | ref: 'b', 246 | is: { 247 | type: 'number', 248 | flags: { 249 | allowOnly: true 250 | }, 251 | valids: [undefined, 5 252 | ], 253 | invalids: [null] 254 | }, 255 | then: { 256 | type: 'string', 257 | flags: { 258 | allowOnly: true 259 | }, 260 | valids: [undefined, 'x'], 261 | invalids: [null, ''] 262 | }, 263 | otherwise: { 264 | type: 'string', 265 | flags: { 266 | allowOnly: true 267 | }, 268 | valids: [undefined, 'y'], 269 | invalids: [null, ''] 270 | } 271 | }, 272 | { 273 | type: 'string', 274 | flags: { 275 | allowOnly: true 276 | }, 277 | valids: [undefined, 'z'], 278 | invalids: [null, ''] 279 | } 280 | ] 281 | } 282 | }; 283 | 284 | expect(Joi.describe(schema)).to.deep.equal(outcome); 285 | done(); 286 | }); 287 | 288 | it('describes when (only then)', function (done) { 289 | 290 | var schema = { 291 | a: Joi.alternatives().when('b', { is: 5, then: 'x' }) 292 | .try('z'), 293 | b: Joi.any() 294 | }; 295 | 296 | var outcome = { 297 | type: 'object', 298 | valids: [undefined], 299 | invalids: [null], 300 | children: { 301 | b: { 302 | type: 'any', 303 | valids: [undefined], 304 | invalids: [null] 305 | }, 306 | a: [ 307 | { 308 | ref: 'b', 309 | is: { 310 | type: 'number', 311 | flags: { 312 | allowOnly: true 313 | }, 314 | valids: [undefined, 5 315 | ], 316 | invalids: [null] 317 | }, 318 | then: { 319 | type: 'string', 320 | flags: { 321 | allowOnly: true 322 | }, 323 | valids: [undefined, 'x'], 324 | invalids: [null, ''] 325 | } 326 | }, 327 | { 328 | type: 'string', 329 | flags: { 330 | allowOnly: true 331 | }, 332 | valids: [undefined, 'z'], 333 | invalids: [null, ''] 334 | } 335 | ] 336 | } 337 | }; 338 | 339 | expect(Joi.describe(schema)).to.deep.equal(outcome); 340 | done(); 341 | }); 342 | 343 | it('describes when (only otherwise)', function (done) { 344 | 345 | var schema = { 346 | a: Joi.alternatives().when('b', { is: 5, otherwise: 'y' }) 347 | .try('z'), 348 | b: Joi.any() 349 | }; 350 | 351 | var outcome = { 352 | type: 'object', 353 | valids: [undefined], 354 | invalids: [null], 355 | children: { 356 | b: { 357 | type: 'any', 358 | valids: [undefined], 359 | invalids: [null] 360 | }, 361 | a: [ 362 | { 363 | ref: 'b', 364 | is: { 365 | type: 'number', 366 | flags: { 367 | allowOnly: true 368 | }, 369 | valids: [undefined, 5 370 | ], 371 | invalids: [null] 372 | }, 373 | otherwise: { 374 | type: 'string', 375 | flags: { 376 | allowOnly: true 377 | }, 378 | valids: [undefined, 'y'], 379 | invalids: [null, ''] 380 | } 381 | }, 382 | { 383 | type: 'string', 384 | flags: { 385 | allowOnly: true 386 | }, 387 | valids: [undefined, 'z'], 388 | invalids: [null, ''] 389 | } 390 | ] 391 | } 392 | }; 393 | 394 | expect(Joi.describe(schema)).to.deep.equal(outcome); 395 | done(); 396 | }); 397 | }); 398 | }); 399 | -------------------------------------------------------------------------------- /test/number.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Lab = require('lab'); 4 | var Joi = require('../lib'); 5 | var Validate = require('./helper'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | // Test shortcuts 14 | 15 | var expect = Lab.expect; 16 | var before = Lab.before; 17 | var after = Lab.after; 18 | var describe = Lab.experiment; 19 | var it = Lab.test; 20 | 21 | 22 | describe('number', function () { 23 | 24 | it('fails on boolean', function (done) { 25 | 26 | var schema = Joi.number(); 27 | Validate(schema, [ 28 | [true, false], 29 | [false, false] 30 | ]); 31 | 32 | done(); 33 | }); 34 | 35 | describe('#validate', function () { 36 | 37 | it('should, by default, allow undefined', function (done) { 38 | 39 | Validate(Joi.number(), [ 40 | [undefined, true] 41 | ]); 42 | done(); 43 | }); 44 | 45 | it('should, when .required(), deny undefined', function (done) { 46 | 47 | Validate(Joi.number().required(), [ 48 | [undefined, false] 49 | ]); 50 | done(); 51 | }); 52 | 53 | it('should return false for denied value', function (done) { 54 | 55 | var text = Joi.number().invalid(50); 56 | text.validate(50, function (err, value) { 57 | 58 | expect(err).to.exist; 59 | done(); 60 | }); 61 | }); 62 | 63 | it('should validate integer', function (done) { 64 | 65 | var t = Joi.number().integer(); 66 | Validate(t, [ 67 | [100, true], 68 | [0, true], 69 | [null, false], 70 | [1.02, false], 71 | [0.01, false] 72 | ]); 73 | done(); 74 | }); 75 | 76 | it('can accept string numbers', function (done) { 77 | 78 | var t = Joi.number(); 79 | Validate(t, [ 80 | ['1', true], 81 | ['100', true], 82 | ['1e3', true], 83 | ['1 some text', false], 84 | ['\t\r', false], 85 | [' ', false], 86 | [' 2', true], 87 | ['\t\r43', true], 88 | ['43 ', true], 89 | ['', false] 90 | ]); 91 | done(); 92 | }); 93 | 94 | it('required validates correctly', function (done) { 95 | 96 | var t = Joi.number().required(); 97 | Validate(t, [ 98 | [NaN, false], 99 | ['100', true] 100 | ]); 101 | done(); 102 | }); 103 | 104 | it('converts an object string to a number', function (done) { 105 | 106 | var config = { a: Joi.number() }; 107 | var obj = { a: '123' }; 108 | 109 | Joi.compile(config).validate(obj, function (err, value) { 110 | 111 | expect(err).to.not.exist; 112 | expect(value.a).to.equal(123); 113 | done(); 114 | }); 115 | }); 116 | 117 | it('converts a string to a number', function (done) { 118 | 119 | Joi.number().validate('1', function (err, value) { 120 | 121 | expect(err).to.not.exist; 122 | expect(value).to.equal(1); 123 | done(); 124 | }); 125 | }); 126 | 127 | it('errors on null', function (done) { 128 | 129 | Joi.number().validate(null, function (err, value) { 130 | 131 | expect(err).to.exist; 132 | expect(value).to.equal(null); 133 | done(); 134 | }); 135 | }); 136 | 137 | it('should handle combination of min and max', function (done) { 138 | 139 | var rule = Joi.number().min(8).max(10); 140 | Validate(rule, [ 141 | [1, false], 142 | [11, false], 143 | [8, true], 144 | [9, true], 145 | [null, false] 146 | ]); 147 | done(); 148 | }); 149 | 150 | it('should handle combination of min, max, and null allowed', function (done) { 151 | 152 | var rule = Joi.number().min(8).max(10).allow(null); 153 | Validate(rule, [ 154 | [1, false], 155 | [11, false], 156 | [8, true], 157 | [9, true], 158 | [null, true] 159 | ]); 160 | done(); 161 | }); 162 | 163 | it('should handle combination of min and positive', function (done) { 164 | 165 | var rule = Joi.number().min(-3).positive(); 166 | Validate(rule, [ 167 | [1, true], 168 | [-2, false], 169 | [8, true], 170 | [null, false] 171 | ]); 172 | done(); 173 | }); 174 | 175 | it('should handle combination of max and positive', function (done) { 176 | 177 | var rule = Joi.number().max(5).positive(); 178 | Validate(rule, [ 179 | [4, true], 180 | [-2, false], 181 | [8, false], 182 | [null, false] 183 | ]); 184 | done(); 185 | }); 186 | 187 | it('should handle combination of min and negative', function (done) { 188 | 189 | var rule = Joi.number().min(-3).negative(); 190 | Validate(rule, [ 191 | [4, false], 192 | [-2, true], 193 | [-4, false], 194 | [null, false] 195 | ]); 196 | done(); 197 | }); 198 | 199 | it('should handle combination of negative and positive', function (done) { 200 | 201 | var rule = Joi.number().negative().positive(); 202 | Validate(rule, [ 203 | [4, false], 204 | [-2, false], 205 | [0, false], 206 | [null, false] 207 | ]); 208 | done(); 209 | }); 210 | 211 | it('should handle combination of negative and allow', function (done) { 212 | 213 | var rule = Joi.number().negative().allow(1); 214 | Validate(rule, [ 215 | [1, true], 216 | [-10, true], 217 | [8, false], 218 | [0, false], 219 | [null, false] 220 | ]); 221 | done(); 222 | }); 223 | 224 | it('should handle combination of positive and allow', function (done) { 225 | 226 | var rule = Joi.number().positive().allow(-1); 227 | Validate(rule, [ 228 | [1, true], 229 | [-1, true], 230 | [8, true], 231 | [-10, false], 232 | [null, false] 233 | ]); 234 | done(); 235 | }); 236 | 237 | it('should handle combination of positive, allow, and null allowed', function (done) { 238 | 239 | var rule = Joi.number().positive().allow(-1).allow(null); 240 | Validate(rule, [ 241 | [1, true], 242 | [-1, true], 243 | [8, true], 244 | [-10, false], 245 | [null, true] 246 | ]); 247 | done(); 248 | }); 249 | 250 | it('should handle combination of negative, allow, and null allowed', function (done) { 251 | 252 | var rule = Joi.number().negative().allow(1).allow(null); 253 | Validate(rule, [ 254 | [1, true], 255 | [-10, true], 256 | [8, false], 257 | [0, false], 258 | [null, true] 259 | ]); 260 | done(); 261 | }); 262 | 263 | it('should handle combination of positive, allow, null allowed, and invalid', function (done) { 264 | 265 | var rule = Joi.number().positive().allow(-1).allow(null).invalid(1); 266 | Validate(rule, [ 267 | [1, false], 268 | [-1, true], 269 | [8, true], 270 | [-10, false], 271 | [null, true] 272 | ]); 273 | done(); 274 | }); 275 | 276 | it('should handle combination of negative, allow, null allowed, and invalid', function (done) { 277 | 278 | var rule = Joi.number().negative().allow(1).allow(null).invalid(-5); 279 | Validate(rule, [ 280 | [1, true], 281 | [-10, true], 282 | [-5, false], 283 | [8, false], 284 | [0, false], 285 | [null, true] 286 | ]); 287 | done(); 288 | }); 289 | 290 | it('should handle combination of min, max, and allow', function (done) { 291 | 292 | var rule = Joi.number().min(8).max(10).allow(1); 293 | Validate(rule, [ 294 | [1, true], 295 | [11, false], 296 | [8, true], 297 | [9, true], 298 | [null, false] 299 | ]); 300 | done(); 301 | }); 302 | 303 | it('should handle combination of min, max, allow, and null allowed', function (done) { 304 | 305 | var rule = Joi.number().min(8).max(10).allow(1).allow(null); 306 | Validate(rule, [ 307 | [1, true], 308 | [11, false], 309 | [8, true], 310 | [9, true], 311 | [null, true] 312 | ]); 313 | done(); 314 | }); 315 | 316 | it('should handle combination of min, max, allow, and invalid', function (done) { 317 | 318 | var rule = Joi.number().min(8).max(10).allow(1).invalid(9); 319 | Validate(rule, [ 320 | [1, true], 321 | [11, false], 322 | [8, true], 323 | [9, false], 324 | [null, false] 325 | ]); 326 | done(); 327 | }); 328 | 329 | it('should handle combination of min, max, allow, invalid, and null allowed', function (done) { 330 | 331 | var rule = Joi.number().min(8).max(10).allow(1).invalid(9).allow(null); 332 | Validate(rule, [ 333 | [1, true], 334 | [11, false], 335 | [8, true], 336 | [9, false], 337 | [null, true] 338 | ]); 339 | done(); 340 | }); 341 | 342 | it('should handle combination of min, max, and integer', function (done) { 343 | 344 | var rule = Joi.number().min(8).max(10).integer(); 345 | Validate(rule, [ 346 | [1, false], 347 | [11, false], 348 | [8, true], 349 | [9, true], 350 | [9.1, false], 351 | [null, false] 352 | ]); 353 | done(); 354 | }); 355 | 356 | it('should handle combination of min, max, integer, and allow', function (done) { 357 | 358 | var rule = Joi.number().min(8).max(10).integer().allow(9.1); 359 | Validate(rule, [ 360 | [1, false], 361 | [11, false], 362 | [8, true], 363 | [9, true], 364 | [9.1, true], 365 | [9.2, false], 366 | [null, false] 367 | ]); 368 | done(); 369 | }); 370 | 371 | it('should handle combination of min, max, integer, allow, and invalid', function (done) { 372 | 373 | var rule = Joi.number().min(8).max(10).integer().allow(9.1).invalid(8); 374 | Validate(rule, [ 375 | [1, false], 376 | [11, false], 377 | [8, false], 378 | [9, true], 379 | [9.1, true], 380 | [9.2, false], 381 | [null, false] 382 | ]); 383 | done(); 384 | }); 385 | 386 | it('should handle combination of min, max, integer, allow, invalid, and null allowed', function (done) { 387 | 388 | var rule = Joi.number().min(8).max(10).integer().allow(9.1).invalid(8).allow(null); 389 | Validate(rule, [ 390 | [1, false], 391 | [11, false], 392 | [8, false], 393 | [9, true], 394 | [9.1, true], 395 | [9.2, false], 396 | [null, true] 397 | ]); 398 | done(); 399 | }); 400 | }); 401 | 402 | it('should instantiate separate copies on invocation', function (done) { 403 | 404 | var result1 = Joi.number().min(5); 405 | var result2 = Joi.number().max(5); 406 | 407 | expect(Object.keys(result1)).to.not.equal(Object.keys(result2)); 408 | done(); 409 | }); 410 | 411 | it('should show resulting object with #valueOf', function (done) { 412 | 413 | var result = Joi.number().min(5); 414 | expect(result.valueOf()).to.exist; 415 | done(); 416 | }); 417 | 418 | describe('error message', function () { 419 | 420 | it('should display correctly for int type', function (done) { 421 | 422 | var t = Joi.number().integer(); 423 | Joi.compile(t).validate('1.1', function (err, value) { 424 | 425 | expect(err.message).to.contain('integer'); 426 | done(); 427 | }); 428 | }); 429 | }); 430 | 431 | describe('#min', function () { 432 | 433 | it('throws when limit is not a number', function (done) { 434 | 435 | expect(function () { 436 | 437 | Joi.number().min('a'); 438 | }).to.throw('limit must be an integer'); 439 | done(); 440 | }); 441 | 442 | it('supports 64bit numbers', function (done) { 443 | 444 | var schema = Joi.number().min(1394035612500); 445 | var input = 1394035612552 446 | 447 | schema.validate(input, function (err, value) { 448 | 449 | expect(err).to.not.exist; 450 | expect(value).to.equal(input); 451 | done(); 452 | }); 453 | }); 454 | }); 455 | 456 | describe('#max', function () { 457 | 458 | it('throws when limit is not a number', function (done) { 459 | 460 | expect(function () { 461 | 462 | Joi.number().max('a'); 463 | }).to.throw('limit must be an integer'); 464 | done(); 465 | }); 466 | }); 467 | }); -------------------------------------------------------------------------------- /lib/object.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Hoek = require('hoek'); 4 | var Topo = require('topo'); 5 | var Any = require('./any'); 6 | var Cast = require('./cast'); 7 | var Ref = require('./ref'); 8 | var Errors = require('./errors'); 9 | 10 | 11 | // Declare internals 12 | 13 | var internals = {}; 14 | 15 | 16 | internals.Object = function () { 17 | 18 | Any.call(this); 19 | this._type = 'object'; 20 | this._inner = null; 21 | }; 22 | 23 | Hoek.inherits(internals.Object, Any); 24 | 25 | 26 | internals.Object.prototype._base = function (value, state, options) { 27 | 28 | var target = value; 29 | var errors = []; 30 | var finish = function () { 31 | 32 | return { 33 | value: target, 34 | errors: errors.length ? errors : null 35 | }; 36 | }; 37 | 38 | if (typeof value === 'string' && 39 | options.convert) { 40 | 41 | try { 42 | value = JSON.parse(value); 43 | } 44 | catch (err) { } 45 | } 46 | 47 | if (!value || 48 | typeof value !== 'object' || 49 | Array.isArray(value)) { 50 | 51 | errors.push(Errors.create('object.base', null, state, options)); 52 | return finish(); 53 | } 54 | 55 | // Ensure target is a local copy (parsed) or shallow copy 56 | 57 | if (target === value) { 58 | target = {}; 59 | target.__proto__ = Object.getPrototypeOf(value); 60 | var valueKeys = Object.keys(value); 61 | for (var t = 0, tl = valueKeys.length; t < tl; ++t) { 62 | target[valueKeys[t]] = value[valueKeys[t]]; 63 | } 64 | } 65 | else { 66 | target = value; 67 | } 68 | 69 | // Rename keys 70 | 71 | var renamed = {}; 72 | for (var r = 0, rl = this._renames.length; r < rl; ++r) { 73 | var item = this._renames[r]; 74 | 75 | if (!item.options.multiple && 76 | renamed[item.to]) { 77 | 78 | errors.push(Errors.create('object.rename.multiple', { from: item.from, to: item.to }, state, options)); 79 | if (options.abortEarly) { 80 | return finish(); 81 | } 82 | } 83 | 84 | if (target.hasOwnProperty(item.to) && 85 | !item.options.override && 86 | !renamed[item.to]) { 87 | 88 | errors.push(Errors.create('object.rename.override', { from: item.from, to: item.to }, state, options)); 89 | if (options.abortEarly) { 90 | return finish(); 91 | } 92 | } 93 | 94 | target[item.to] = target[item.from]; 95 | renamed[item.to] = true; 96 | 97 | if (!item.options.alias) { 98 | delete target[item.from]; 99 | } 100 | } 101 | 102 | // Validate dependencies 103 | 104 | for (var d = 0, dl = this._dependencies.length; d < dl; ++d) { 105 | var dep = this._dependencies[d]; 106 | var err = internals[dep.type](dep.key && value[dep.key], dep.peers, target, { key: dep.key, path: (state.path ? state.path + '.' : '') + dep.key }, options); 107 | if (err) { 108 | errors.push(err); 109 | if (options.abortEarly) { 110 | return finish(); 111 | } 112 | } 113 | } 114 | 115 | // Validate schema 116 | 117 | if (!this._inner) { // null allows any keys 118 | return finish(); 119 | } 120 | 121 | var unprocessed = Hoek.mapToObject(Object.keys(target)); 122 | var key; 123 | 124 | for (var i = 0, il = this._inner.length; i < il; ++i) { 125 | var child = this._inner[i]; 126 | var key = child.key; 127 | var item = target[key]; 128 | 129 | delete unprocessed[key]; 130 | 131 | var localState = { key: key, path: (state.path ? state.path + '.' : '') + key, parent: target, reference: state.reference }; 132 | var result = child.schema._validate(item, localState, options); 133 | if (result.errors) { 134 | errors = errors.concat(result.errors); 135 | if (options.abortEarly) { 136 | return finish(); 137 | } 138 | } 139 | 140 | if (result.value !== undefined) { 141 | target[key] = result.value; 142 | } 143 | } 144 | 145 | var unprocessedKeys = Object.keys(unprocessed); 146 | if (unprocessedKeys.length) { 147 | if (options.stripUnknown || 148 | options.skipFunctions) { 149 | 150 | var hasFunctions = false; 151 | for (var k = 0, kl = unprocessedKeys.length; k < kl; ++k) { 152 | key = unprocessedKeys[k]; 153 | if (options.stripUnknown) { 154 | delete target[key]; 155 | } 156 | else if (typeof target[key] === 'function') { 157 | delete unprocessed[key]; 158 | hasFunctions = true; 159 | } 160 | } 161 | 162 | if (options.stripUnknown) { 163 | return finish(); 164 | } 165 | 166 | if (hasFunctions) { 167 | unprocessedKeys = Object.keys(unprocessed); 168 | } 169 | } 170 | 171 | if (unprocessedKeys.length && 172 | (this._flags.allowUnknown !== undefined ? !this._flags.allowUnknown : !options.allowUnknown)) { 173 | 174 | for (var e = 0, el = unprocessedKeys.length; e < el; ++e) { 175 | errors.push(Errors.create('object.allowUnknown', null, { key: unprocessedKeys[e], path: state.path }, options)); 176 | } 177 | } 178 | } 179 | 180 | return finish(); 181 | }; 182 | 183 | 184 | internals.Object.prototype.keys = function (schema) { 185 | 186 | Hoek.assert(schema === null || schema === undefined || typeof schema === 'object', 'Object schema must be a valid object'); 187 | Hoek.assert(!schema || !schema.isJoi, 'Object schema cannot be a joi schema'); 188 | 189 | var obj = this.clone(); 190 | 191 | if (!schema) { 192 | obj._inner = null; 193 | return obj; 194 | } 195 | 196 | var children = Object.keys(schema); 197 | 198 | if (!children.length) { 199 | obj._inner = []; 200 | return obj; 201 | } 202 | 203 | var topo = new Topo(); 204 | if (obj._inner) { 205 | for (var i = 0, il = obj._inner.length; i < il; ++i) { 206 | var child = obj._inner[i]; 207 | topo.add(child, { after: child._refs, group: child.key }); 208 | } 209 | } 210 | 211 | for (var c = 0, cl = children.length; c < cl; ++c) { 212 | var key = children[c]; 213 | var child = schema[key]; 214 | var cast = Cast.schema(child); 215 | topo.add({ key: key, schema: cast }, { after: cast._refs, group: key }); 216 | } 217 | 218 | obj._inner = topo.nodes; 219 | 220 | return obj; 221 | }; 222 | 223 | 224 | internals.Object.prototype.unknown = function (allow) { 225 | 226 | var obj = this.clone(); 227 | obj._flags.allowUnknown = (allow !== false); 228 | return obj; 229 | }; 230 | 231 | 232 | internals.Object.prototype.length = function (limit) { 233 | 234 | Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); 235 | 236 | return this._test('length', limit, function (value, state, options) { 237 | 238 | if (Object.keys(value).length === limit) { 239 | return null; 240 | } 241 | 242 | return Errors.create('object.length', { limit: limit }, state, options); 243 | }); 244 | }; 245 | 246 | 247 | internals.Object.prototype.min = function (limit) { 248 | 249 | Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); 250 | 251 | return this._test('min', limit, function (value, state, options) { 252 | 253 | if (Object.keys(value).length >= limit) { 254 | return null; 255 | } 256 | 257 | return Errors.create('object.min', { limit: limit }, state, options); 258 | }); 259 | }; 260 | 261 | 262 | internals.Object.prototype.max = function (limit) { 263 | 264 | Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer'); 265 | 266 | return this._test('max', limit, function (value, state, options) { 267 | 268 | if (Object.keys(value).length <= limit) { 269 | return null; 270 | } 271 | 272 | return Errors.create('object.max', { limit: limit }, state, options); 273 | }); 274 | }; 275 | 276 | 277 | internals.Object.prototype.with = function (key, peers) { 278 | 279 | return this._dependency('with', key, peers); 280 | }; 281 | 282 | 283 | internals.Object.prototype.without = function (key, peers) { 284 | 285 | return this._dependency('without', key, peers); 286 | }; 287 | 288 | 289 | internals.Object.prototype.xor = function () { 290 | 291 | var peers = Hoek.flatten(Array.prototype.slice.call(arguments)); 292 | return this._dependency('xor', null, peers); 293 | }; 294 | 295 | 296 | internals.Object.prototype.or = function () { 297 | 298 | var peers = Hoek.flatten(Array.prototype.slice.call(arguments)); 299 | return this._dependency('or', null, peers); 300 | }; 301 | 302 | 303 | internals.Object.prototype.and = function () { 304 | 305 | var peers = Hoek.flatten(Array.prototype.slice.call(arguments)); 306 | return this._dependency('and', null, peers); 307 | }; 308 | 309 | 310 | internals.renameDefaults = { 311 | alias: false, // Keep old value in place 312 | multiple: false, // Allow renaming multiple keys into the same target 313 | override: false // Overrides an existing key 314 | }; 315 | 316 | 317 | internals.Object.prototype.rename = function (from, to, options) { 318 | 319 | Hoek.assert(from, 'Rename missing the from argument'); 320 | Hoek.assert(to, 'Rename missing the to argument'); 321 | Hoek.assert(to !== from, 'Cannot rename key to same name:', from); 322 | 323 | for (var i = 0, il = this._renames.length; i < il; ++i) { 324 | Hoek.assert(this._renames[i].from !== from, 'Cannot rename the same key multiple times'); 325 | } 326 | 327 | var obj = this.clone(); 328 | 329 | obj._renames.push({ 330 | from: from, 331 | to: to, 332 | options: Hoek.applyToDefaults(internals.renameDefaults, options || {}) 333 | }); 334 | 335 | return obj; 336 | }; 337 | 338 | 339 | internals.Object.prototype._dependency = function (type, key, peers) { 340 | 341 | peers = [].concat(peers); 342 | for (var i = 0, li = peers.length; i < li; i++) { 343 | Hoek.assert(typeof peers[i] === 'string', type, 'peers must be a string or array of strings'); 344 | } 345 | 346 | var obj = this.clone(); 347 | obj._dependencies.push({ type: type, key: key, peers: peers }); 348 | return obj; 349 | }; 350 | 351 | 352 | internals.with = function (value, peers, parent, state, options) { 353 | 354 | if (value === undefined) { 355 | return null; 356 | } 357 | 358 | for (var i = 0, il = peers.length; i < il; ++i) { 359 | var peer = peers[i]; 360 | if (!parent.hasOwnProperty(peer) || 361 | parent[peer] === undefined) { 362 | return Errors.create('object.with', { peer: peer }, state, options); 363 | } 364 | } 365 | 366 | return null; 367 | }; 368 | 369 | 370 | internals.without = function (value, peers, parent, state, options) { 371 | 372 | if (value === undefined) { 373 | return null; 374 | } 375 | 376 | for (var i = 0, il = peers.length; i < il; ++i) { 377 | var peer = peers[i]; 378 | if (parent.hasOwnProperty(peer) && 379 | parent[peer] !== undefined) { 380 | 381 | return Errors.create('object.without', { peer: peer }, state, options); 382 | } 383 | } 384 | 385 | return null; 386 | }; 387 | 388 | 389 | internals.xor = function (value, peers, parent, state, options) { 390 | 391 | var present = []; 392 | for (var i = 0, il = peers.length; i < il; ++i) { 393 | var peer = peers[i]; 394 | if (parent.hasOwnProperty(peer) && 395 | parent[peer] !== undefined) { 396 | 397 | present.push(peer); 398 | } 399 | } 400 | 401 | if (present.length === 1) { 402 | return null; 403 | } 404 | 405 | if (present.length === 0) { 406 | return Errors.create('object.missing', { peers: peers }, state, options); 407 | } 408 | 409 | return Errors.create('object.xor', { peers: peers }, state, options); 410 | }; 411 | 412 | 413 | internals.or = function (value, peers, parent, state, options) { 414 | 415 | for (var i = 0, il = peers.length; i < il; ++i) { 416 | var peer = peers[i]; 417 | if (parent.hasOwnProperty(peer) && 418 | parent[peer] !== undefined) { 419 | return null; 420 | } 421 | } 422 | 423 | return Errors.create('object.missing', { peers: peers }, state, options); 424 | }; 425 | 426 | 427 | internals.and = function (value, peers, parent, state, options) { 428 | 429 | var missing = []; 430 | var present = []; 431 | var count = peers.length; 432 | for (var i = 0; i < count; ++i) { 433 | var peer = peers[i]; 434 | if (!parent.hasOwnProperty(peer) || 435 | parent[peer] === undefined) { 436 | 437 | missing.push(peer); 438 | } 439 | else { 440 | present.push(peer); 441 | } 442 | } 443 | 444 | var aon = (missing.length === count || present.length === count); 445 | return !aon ? Errors.create('object.and', { present: present, missing: missing }, state, options) : null; 446 | }; 447 | 448 | 449 | internals.Object.prototype.describe = function () { 450 | 451 | var description = Any.prototype.describe.call(this); 452 | 453 | if (this._inner) { 454 | description.children = {}; 455 | for (var i = 0, il = this._inner.length; i < il; ++i) { 456 | var child = this._inner[i]; 457 | description.children[child.key] = child.schema.describe(); 458 | } 459 | } 460 | 461 | if (this._dependencies.length) { 462 | description.dependencies = Hoek.clone(this._dependencies); 463 | } 464 | 465 | return description; 466 | }; 467 | 468 | 469 | internals.Object.prototype.assert = function (ref, schema, message) { 470 | 471 | ref = Cast.ref(ref); 472 | Hoek.assert(ref.depth > 1, 'Cannot use assertions for root level references - use direct key rules instead'); 473 | 474 | var cast = Cast.schema(schema); 475 | 476 | return this._test('assert', { cast: cast, ref: ref }, function (value, state, options) { 477 | 478 | var result = cast._validate(ref(value), null, options, value); 479 | if (!result.errors) { 480 | return null; 481 | } 482 | 483 | return Errors.create('object.assert', { ref: ref.path.join('.'), message: message }, state, options); 484 | }); 485 | }; 486 | 487 | 488 | module.exports = new internals.Object(); -------------------------------------------------------------------------------- /lib/any.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Path = require('path'); 4 | var Hoek = require('hoek'); 5 | var Ref = require('./ref'); 6 | var Errors = require('./errors'); 7 | var Alternatives = null; // Delay-loaded to prevent circular dependencies 8 | var Cast = null; 9 | 10 | 11 | // Declare internals 12 | 13 | var internals = {}; 14 | 15 | 16 | internals.defaults = { 17 | abortEarly: true, 18 | convert: true, 19 | allowUnknown: false, 20 | skipFunctions: false, 21 | stripUnknown: false, 22 | language: {} 23 | }; 24 | 25 | 26 | module.exports = internals.Any = function () { 27 | 28 | this.isJoi = true; 29 | this._type = 'any'; 30 | this._settings = null; 31 | this._valids = new internals.Set([undefined]); 32 | this._invalids = new internals.Set([null]); 33 | this._tests = []; 34 | this._flags = {}; // insensitive (false), allowOnly (false), default (undefined), encoding (undefined), allowUnknown (undefined) 35 | 36 | this._description = null; 37 | this._unit = null; 38 | this._notes = []; 39 | this._tags = []; 40 | this._examples = []; 41 | 42 | this._inner = null; 43 | this._renames = []; 44 | this._dependencies = []; 45 | this._refs = []; 46 | }; 47 | 48 | 49 | internals.Any.prototype.clone = function () { 50 | 51 | var obj = {}; 52 | obj.__proto__ = Object.getPrototypeOf(this); 53 | 54 | obj.isJoi = true; 55 | obj._type = this._type; 56 | obj._settings = Hoek.clone(this._settings); 57 | obj._valids = Hoek.clone(this._valids); 58 | obj._invalids = Hoek.clone(this._invalids); 59 | obj._tests = this._tests.slice(); 60 | obj._flags = Hoek.clone(this._flags); 61 | 62 | obj._description = this._description; 63 | obj._unit = this._unit; 64 | obj._notes = this._notes.slice(); 65 | obj._tags = this._tags.slice(); 66 | obj._examples = this._examples.slice(); 67 | 68 | obj._inner = this._inner ? this._inner.slice() : null; 69 | obj._renames = this._renames.slice(); 70 | obj._dependencies = this._dependencies.slice(); 71 | obj._refs = this._refs.slice(); 72 | 73 | return obj; 74 | }; 75 | 76 | 77 | internals.Any.prototype.concat = function (schema) { 78 | 79 | Hoek.assert(schema && schema.isJoi, 'Invalid schema object'); 80 | Hoek.assert(schema._type === 'any' || schema._type === this._type, 'Cannot merge with another type:', schema._type); 81 | 82 | var obj = this.clone(); 83 | 84 | obj._settings = obj._settings ? Hoek.merge(obj._settings, schema._settings) : schema._settings; 85 | obj._valids.merge(schema._valids); 86 | obj._invalids.merge(schema._invalids); 87 | obj._tests = obj._tests.concat(schema._tests); 88 | Hoek.merge(obj._flags, schema._flags); 89 | 90 | obj._description = schema._description || obj._description; 91 | obj._unit = schema._unit || obj._unit; 92 | obj._notes = obj._notes.concat(schema._notes); 93 | obj._tags = obj._tags.concat(schema._tags); 94 | obj._examples = obj._examples.concat(schema._examples); 95 | 96 | obj._inner = obj._inner ? (schema._inner ? obj._inner.concat(schema._inner) : obj._inner) : schema._inner; 97 | obj._renames = obj._renames.concat(schema._renames); 98 | obj._dependencies = obj._dependencies.concat(schema._dependencies); 99 | obj._refs = obj._refs.concat(schema._refs); 100 | 101 | return obj; 102 | }; 103 | 104 | 105 | internals.Any.prototype._test = function (name, arg, func) { 106 | 107 | Hoek.assert(!this._flags.allowOnly, 'Cannot define rules when valid values specified'); 108 | 109 | var obj = this.clone(); 110 | obj._tests.push({ func: func, name: name, arg: arg }); 111 | return obj; 112 | }; 113 | 114 | 115 | internals.Any.prototype.options = function (options) { 116 | 117 | var obj = this.clone(); 118 | obj._settings = Hoek.applyToDefaults(obj._settings || {}, options); 119 | return obj; 120 | }; 121 | 122 | 123 | internals.Any.prototype.strict = function () { 124 | 125 | var obj = this.clone(); 126 | obj._settings = obj._settings || {}; 127 | obj._settings.convert = false; 128 | return obj; 129 | }; 130 | 131 | 132 | internals.Any.prototype._allow = function () { 133 | 134 | var values = Hoek.flatten(Array.prototype.slice.call(arguments)); 135 | for (var i = 0, il = values.length; i < il; ++i) { 136 | var value = values[i]; 137 | this._invalids.remove(value); 138 | this._valids.add(value, this._refs); 139 | } 140 | }; 141 | 142 | 143 | internals.Any.prototype.allow = function () { 144 | 145 | var obj = this.clone(); 146 | obj._allow.apply(obj, arguments); 147 | return obj; 148 | }; 149 | 150 | 151 | internals.Any.prototype.valid = internals.Any.prototype.equal = function () { 152 | 153 | Hoek.assert(!this._tests.length, 'Cannot set valid values when rules specified'); 154 | 155 | var obj = this.allow.apply(this, arguments); 156 | obj._flags.allowOnly = true; 157 | return obj; 158 | }; 159 | 160 | 161 | internals.Any.prototype.invalid = internals.Any.prototype.not = function (value) { 162 | 163 | var obj = this.clone(); 164 | var values = Hoek.flatten(Array.prototype.slice.call(arguments)); 165 | for (var i = 0, il = values.length; i < il; ++i) { 166 | var value = values[i]; 167 | obj._valids.remove(value); 168 | obj._invalids.add(value, this._refs); 169 | } 170 | 171 | return obj; 172 | }; 173 | 174 | 175 | internals.Any.prototype.required = function () { 176 | 177 | var obj = this.clone(); 178 | obj._valids.remove(undefined); 179 | obj._invalids.add(undefined); 180 | return obj; 181 | }; 182 | 183 | 184 | internals.Any.prototype.optional = function () { 185 | 186 | var obj = this.clone(); 187 | obj._invalids.remove(undefined); 188 | obj._valids.add(undefined); 189 | return obj; 190 | }; 191 | 192 | 193 | internals.Any.prototype.default = function (value) { 194 | 195 | var obj = this.clone(); 196 | obj._flags.default = value; 197 | if (Ref.isRef(value)) { 198 | obj._refs.push(value.root); 199 | } 200 | return obj; 201 | }; 202 | 203 | 204 | internals.Any.prototype.when = function (ref, options) { 205 | 206 | Hoek.assert(options && typeof options === 'object', 'Invalid options'); 207 | Hoek.assert(options.then !== undefined || options.otherwise !== undefined, 'options must have at least one of "then" or "otherwise"'); 208 | 209 | Cast = Cast || require('./cast'); 210 | var then = options.then ? this.concat(Cast.schema(options.then)) : this; 211 | var otherwise = options.otherwise ? this.concat(Cast.schema(options.otherwise)) : this; 212 | 213 | Alternatives = Alternatives || require('./alternatives'); 214 | return Alternatives.when(ref, { is: options.is, then: then, otherwise: otherwise }); 215 | }; 216 | 217 | 218 | internals.Any.prototype.description = function (desc) { 219 | 220 | Hoek.assert(desc && typeof desc === 'string', 'Description must be a non-empty string'); 221 | 222 | var obj = this.clone(); 223 | obj._description = desc; 224 | return obj; 225 | }; 226 | 227 | 228 | internals.Any.prototype.notes = function (notes) { 229 | 230 | Hoek.assert(notes && (typeof notes === 'string' || Array.isArray(notes)), 'Notes must be a non-empty string or array'); 231 | 232 | var obj = this.clone(); 233 | obj._notes = obj._notes.concat(notes); 234 | return obj; 235 | }; 236 | 237 | 238 | internals.Any.prototype.tags = function (tags) { 239 | 240 | Hoek.assert(tags && (typeof tags === 'string' || Array.isArray(tags)), 'Tags must be a non-empty string or array'); 241 | 242 | var obj = this.clone(); 243 | obj._tags = obj._tags.concat(tags); 244 | return obj; 245 | }; 246 | 247 | 248 | internals.Any.prototype.example = function (value) { 249 | 250 | Hoek.assert(arguments.length, 'Missing example'); 251 | var result = this._validate(value, null, internals.defaults); 252 | Hoek.assert(!result.errors, 'Bad example:', result.errors && Errors.process(result.errors, value)); 253 | 254 | var obj = this.clone(); 255 | obj._examples = obj._examples.concat(value); 256 | return obj; 257 | }; 258 | 259 | 260 | internals.Any.prototype.unit = function (name) { 261 | 262 | Hoek.assert(name && typeof name === 'string', 'Unit name must be a non-empty string'); 263 | 264 | var obj = this.clone(); 265 | obj._unit = name; 266 | return obj; 267 | }; 268 | 269 | 270 | internals.Any.prototype._validate = function (value, state, options, reference) { 271 | 272 | var self = this; 273 | 274 | // Setup state and settings 275 | 276 | state = state || { key: '', path: '', parent: null, reference: reference }; 277 | 278 | if (this._settings) { 279 | options = Hoek.applyToDefaults(options, this._settings); 280 | } 281 | 282 | var errors = []; 283 | var finish = function () { 284 | 285 | return { 286 | value: (value !== undefined) ? value : (Ref.isRef(self._flags.default) ? self._flags.default(state.parent) : self._flags.default), 287 | errors: errors.length ? errors : null 288 | }; 289 | }; 290 | 291 | // Check allowed and denied values using the original value 292 | 293 | if (this._valids.has(value, state, this._flags.insensitive)) { 294 | return finish(); 295 | } 296 | 297 | if (this._invalids.has(value, state, this._flags.insensitive)) { 298 | errors.push(Errors.create(value === '' ? 'any.empty' : (value === undefined ? 'any.required' : 'any.invalid'), null, state, options)); 299 | if (options.abortEarly || 300 | value === undefined) { // No reason to keep validating missing value 301 | 302 | return finish(); 303 | } 304 | } 305 | 306 | // Convert value and validate type 307 | 308 | if (this._base) { 309 | var base = this._base.call(this, value, state, options); 310 | if (base.errors) { 311 | value = base.value; 312 | errors = errors.concat(base.errors); 313 | return finish(); // Base error always aborts early 314 | } 315 | 316 | if (base.value !== value) { 317 | value = base.value; 318 | 319 | // Check allowed and denied values using the converted value 320 | 321 | if (this._valids.has(value, state, this._flags.insensitive)) { 322 | return finish(); 323 | } 324 | 325 | if (this._invalids.has(value, state, this._flags.insensitive)) { 326 | errors.push(Errors.create('any.invalid', null, state, options)); 327 | if (options.abortEarly) { 328 | return finish(); 329 | } 330 | } 331 | } 332 | } 333 | 334 | // Required values did not match 335 | 336 | if (this._flags.allowOnly) { 337 | errors.push(Errors.create('any.allowOnly', { valids: this._valids.toString(false) }, state, options)); 338 | if (options.abortEarly) { 339 | return finish(); 340 | } 341 | } 342 | 343 | // Validate tests 344 | 345 | for (var i = 0, il = this._tests.length; i < il; ++i) { 346 | var test = this._tests[i]; 347 | var err = test.func.call(this, value, state, options); 348 | if (err) { 349 | errors.push(err); 350 | if (options.abortEarly) { 351 | return finish(); 352 | } 353 | } 354 | } 355 | 356 | return finish(); 357 | }; 358 | 359 | 360 | internals.Any.prototype._validateWithOptions = function (value, options, callback) { 361 | 362 | var settings = Hoek.applyToDefaults(internals.defaults, options); 363 | var result = this._validate(value, null, settings); 364 | var errors = Errors.process(result.errors, value); 365 | 366 | return callback(errors, result.value); 367 | }; 368 | 369 | 370 | internals.Any.prototype.validate = function (value, callback) { 371 | 372 | var result = this._validate(value, null, internals.defaults); 373 | var errors = Errors.process(result.errors, value); 374 | 375 | return callback(errors, result.value); 376 | }; 377 | 378 | 379 | internals.Any.prototype.describe = function () { 380 | 381 | var description = { 382 | type: this._type 383 | }; 384 | 385 | if (Object.keys(this._flags).length) { 386 | description.flags = this._flags; 387 | } 388 | 389 | if (this._description) { 390 | description.description = this._description; 391 | } 392 | 393 | if (this._notes.length) { 394 | description.notes = this._notes; 395 | } 396 | 397 | if (this._tags.length) { 398 | description.tags = this._tags; 399 | } 400 | 401 | if (this._examples.length) { 402 | description.examples = this._examples; 403 | } 404 | 405 | if (this._unit) { 406 | description.unit = this._unit; 407 | } 408 | 409 | var valids = this._valids.values(); 410 | if (valids.length) { 411 | description.valids = valids; 412 | } 413 | 414 | var invalids = this._invalids.values(); 415 | if (invalids.length) { 416 | description.invalids = invalids; 417 | } 418 | 419 | description.rules = []; 420 | 421 | for (var i = 0, il = this._tests.length; i < il; ++i) { 422 | var validator = this._tests[i]; 423 | var item = { name: validator.name }; 424 | if (validator.arg) { 425 | item.arg = validator.arg; 426 | } 427 | description.rules.push(item); 428 | } 429 | 430 | if (!description.rules.length) { 431 | delete description.rules; 432 | } 433 | 434 | return description; 435 | }; 436 | 437 | 438 | // Set 439 | 440 | internals.Set = function (values) { 441 | 442 | this._set = []; 443 | 444 | for (var i = 0, il = values.length; i < il; ++i) { 445 | this.add(values[i]); 446 | } 447 | }; 448 | 449 | 450 | internals.Set.prototype.add = function (value, refs) { 451 | 452 | Hoek.assert(value === null || value === undefined || value instanceof Date || Ref.isRef(value) || (typeof value !== 'function' && typeof value !== 'object'), 'Value cannot be an object or function'); 453 | 454 | if (typeof value !== 'function' && 455 | this.has(value, null, false)) { 456 | 457 | return; 458 | } 459 | 460 | if (Ref.isRef(value)) { 461 | refs.push(value.root); 462 | } 463 | 464 | this._set.push(value); 465 | }; 466 | 467 | 468 | internals.Set.prototype.merge = function (set) { 469 | 470 | for (var i = 0, il = set._set.length; i < il; ++i) { 471 | this.add(set._set[i]); 472 | } 473 | }; 474 | 475 | 476 | internals.Set.prototype.remove = function (value) { 477 | 478 | this._set = this._set.filter(function (item) { 479 | 480 | return value !== item; 481 | }); 482 | }; 483 | 484 | 485 | internals.Set.prototype.has = function (value, state, insensitive) { 486 | 487 | for (var i = 0, il = this._set.length; i < il; ++i) { 488 | var item = this._set[i]; 489 | 490 | if (Ref.isRef(item)) { 491 | item = item(state.reference || state.parent); 492 | } 493 | 494 | if (typeof value !== typeof item) { 495 | continue; 496 | } 497 | 498 | if (value === item || 499 | (value instanceof Date && item instanceof Date && value.getTime() === item.getTime()) || 500 | (insensitive && typeof value === 'string' && value.toLowerCase() === item.toLowerCase())) { 501 | 502 | return true; 503 | } 504 | } 505 | 506 | return false; 507 | }; 508 | 509 | 510 | internals.Set.prototype.values = function () { 511 | 512 | return this._set.slice(); 513 | }; 514 | 515 | 516 | internals.Set.prototype.toString = function (includeUndefined) { 517 | 518 | var list = ''; 519 | for (var i = 0, il = this._set.length; i < il; ++i) { 520 | var item = this._set[i]; 521 | if (item !== undefined || includeUndefined) { 522 | list += (list ? ', ' : '') + internals.stringify(item); 523 | } 524 | } 525 | 526 | return list; 527 | }; 528 | 529 | 530 | internals.stringify = function (value) { 531 | 532 | if (value === undefined) { 533 | return 'undefined'; 534 | } 535 | 536 | if (value === null) { 537 | return 'null'; 538 | } 539 | 540 | if (typeof value === 'string') { 541 | return value; 542 | } 543 | 544 | return value.toString(); 545 | }; 546 | -------------------------------------------------------------------------------- /test/any.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Lab = require('lab'); 4 | var Joi = require('../lib'); 5 | var Validate = require('./helper'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | // Test shortcuts 14 | 15 | var expect = Lab.expect; 16 | var before = Lab.before; 17 | var after = Lab.after; 18 | var describe = Lab.experiment; 19 | var it = Lab.test; 20 | 21 | 22 | describe('any', function () { 23 | 24 | describe('#equal', function () { 25 | 26 | it('validates valid values', function (done) { 27 | 28 | Validate(Joi.equal(4), [ 29 | [4, true], 30 | [5, false] 31 | ]); 32 | 33 | done(); 34 | }); 35 | }); 36 | 37 | describe('#not', function () { 38 | 39 | it('validates invalid values', function (done) { 40 | 41 | Validate(Joi.not(5), [ 42 | [4, true], 43 | [5, false] 44 | ]); 45 | 46 | done(); 47 | }); 48 | }); 49 | 50 | describe('#strict', function () { 51 | 52 | it('validates without converting', function (done) { 53 | 54 | var schema = Joi.object({ 55 | array: Joi.array().includes(Joi.string().min(5), Joi.number().min(3)) 56 | }).strict(); 57 | 58 | Validate(schema, [ 59 | [{ array: ['12345'] }, true], 60 | [{ array: ['1'] }, false], 61 | [{ array: [3] }, true], 62 | [{ array: ['12345', 3] }, true] 63 | ]); done(); 64 | }); 65 | }); 66 | 67 | describe('#options', function () { 68 | 69 | it('adds to existing options', function (done) { 70 | 71 | var schema = Joi.object({ b: Joi.number().strict().options({ convert: true }) }); 72 | var input = { b: '2' }; 73 | schema.validate(input, function (err, value) { 74 | 75 | expect(err).to.not.exist; 76 | expect(value.b).to.equal(2); 77 | done(); 78 | }); 79 | }); 80 | }); 81 | 82 | describe('#strict', function () { 83 | 84 | it('adds to existing options', function (done) { 85 | 86 | var schema = Joi.object({ b: Joi.number().options({ convert: true }).strict() }); 87 | var input = { b: '2' }; 88 | schema.validate(input, function (err, value) { 89 | 90 | expect(err).to.exist; 91 | expect(value.b).to.equal('2'); 92 | done(); 93 | }); 94 | }); 95 | }); 96 | 97 | describe('#default', function () { 98 | 99 | it('sets the value', function (done) { 100 | 101 | var schema = Joi.object({ foo: Joi.string().default('test') }); 102 | var input = {}; 103 | 104 | schema.validate(input, function (err, value) { 105 | 106 | expect(err).to.not.exist; 107 | expect(value.foo).to.equal('test'); 108 | 109 | done(); 110 | }); 111 | }); 112 | 113 | it('should not overide a value when value is given', function (done) { 114 | 115 | var schema = Joi.object({ foo: Joi.string().default('bar') }); 116 | var input = { foo: 'test' }; 117 | 118 | schema.validate(input, function (err, value) { 119 | 120 | expect(err).to.not.exist; 121 | expect(value.foo).to.equal('test'); 122 | 123 | done(); 124 | }); 125 | }); 126 | }); 127 | 128 | describe('#description', function () { 129 | 130 | it('sets the description', function (done) { 131 | 132 | var b = Joi.description('my description'); 133 | expect(b._description).to.equal('my description'); 134 | 135 | done(); 136 | }); 137 | 138 | it('throws when description is missing', function (done) { 139 | 140 | expect(function () { 141 | 142 | Joi.description(); 143 | }).to.throw('Description must be a non-empty string'); 144 | done(); 145 | }); 146 | }); 147 | 148 | describe('#notes', function () { 149 | 150 | it('sets the notes', function (done) { 151 | 152 | var b = Joi.notes(['a']).notes('my notes'); 153 | expect(b._notes).to.deep.equal(['a', 'my notes']); 154 | 155 | done(); 156 | }); 157 | 158 | it('throws when notes are missing', function (done) { 159 | 160 | expect(function () { 161 | 162 | Joi.notes(); 163 | }).to.throw('Notes must be a non-empty string or array'); 164 | done(); 165 | }); 166 | 167 | it('throws when notes are invalid', function (done) { 168 | 169 | expect(function () { 170 | 171 | Joi.notes(5); 172 | }).to.throw('Notes must be a non-empty string or array'); 173 | done(); 174 | }); 175 | }); 176 | 177 | describe('#tags', function () { 178 | 179 | it('sets the tags', function (done) { 180 | 181 | var b = Joi.tags(['tag1', 'tag2']).tags('tag3'); 182 | expect(b._tags).to.include('tag1'); 183 | expect(b._tags).to.include('tag2'); 184 | expect(b._tags).to.include('tag3'); 185 | 186 | done(); 187 | }); 188 | 189 | it('throws when tags are missing', function (done) { 190 | 191 | expect(function () { 192 | 193 | Joi.tags(); 194 | }).to.throw('Tags must be a non-empty string or array'); 195 | done(); 196 | }); 197 | 198 | it('throws when tags are invalid', function (done) { 199 | 200 | expect(function () { 201 | 202 | Joi.tags(5); 203 | }).to.throw('Tags must be a non-empty string or array'); 204 | done(); 205 | }); 206 | }); 207 | 208 | describe('#example', function () { 209 | 210 | it('sets an example', function (done) { 211 | 212 | var schema = Joi.valid(5, 6, 7).example(5); 213 | expect(schema._examples).to.include(5); 214 | expect(schema.describe().examples).to.deep.equal([5]); 215 | done(); 216 | }); 217 | 218 | it('throws when tags are missing', function (done) { 219 | 220 | expect(function () { 221 | 222 | Joi.example(); 223 | }).to.throw('Missing example'); 224 | done(); 225 | }); 226 | 227 | it('throws when example fails own rules', function (done) { 228 | 229 | expect(function () { 230 | 231 | var schema = Joi.valid(5, 6, 7).example(4); 232 | }).to.throw('Bad example: value must be one of 5, 6, 7'); 233 | done(); 234 | }); 235 | }); 236 | 237 | describe('#unit', function () { 238 | 239 | it('sets the unit', function (done) { 240 | 241 | var b = Joi.unit('milliseconds'); 242 | expect(b._unit).to.equal('milliseconds'); 243 | expect(b.describe().unit).to.equal('milliseconds'); 244 | done(); 245 | }); 246 | 247 | it('throws when unit is missing', function (done) { 248 | 249 | expect(function () { 250 | 251 | Joi.unit(); 252 | }).to.throw('Unit name must be a non-empty string'); 253 | done(); 254 | }); 255 | }); 256 | 257 | describe('#_validate', function () { 258 | 259 | it('checks value after conversion', function (done) { 260 | 261 | var schema = Joi.number().invalid(2); 262 | Joi.validate('2', schema, { abortEarly: false }, function (err, value) { 263 | 264 | expect(err).to.exist; 265 | done(); 266 | }); 267 | }); 268 | }); 269 | 270 | describe('#concat', function () { 271 | 272 | it('throws when schema is not any', function (done) { 273 | 274 | expect(function () { 275 | 276 | Joi.string().concat(Joi.number()); 277 | }).to.throw('Cannot merge with another type: number'); 278 | done(); 279 | }); 280 | 281 | it('throws when schema is missing', function (done) { 282 | 283 | expect(function () { 284 | 285 | Joi.string().concat(); 286 | }).to.throw('Invalid schema object'); 287 | done(); 288 | }); 289 | 290 | it('throws when schema is invalid', function (done) { 291 | 292 | expect(function () { 293 | 294 | Joi.string().concat(1); 295 | }).to.throw('Invalid schema object'); 296 | done(); 297 | }); 298 | 299 | it('merges two schemas (settings)', function (done) { 300 | 301 | var a = Joi.number().options({ convert: true }); 302 | var b = Joi.options({ convert: false }); 303 | 304 | Validate(a, [[1, true], ['1', true]]); 305 | Validate(a.concat(b), [[1, true], ['1', false]]); 306 | done(); 307 | }); 308 | 309 | it('merges two schemas (invalid)', function (done) { 310 | 311 | var a = Joi.string().valid('a'); 312 | var b = Joi.string().valid('b'); 313 | 314 | Validate(a, [['a', true], ['b', false]]); 315 | Validate(b, [['b', true], ['a', false]]); 316 | Validate(a.concat(b), [['a', true], ['b', true]]); 317 | done(); 318 | }); 319 | 320 | it('merges two schemas (invalid)', function (done) { 321 | 322 | var a = Joi.string().invalid('a'); 323 | var b = Joi.invalid('b'); 324 | 325 | Validate(a, [['b', true], ['a', false]]); 326 | Validate(b, [['a', true], ['b', false]]); 327 | Validate(a.concat(b), [['a', false], ['b', false]]); 328 | done(); 329 | }); 330 | 331 | it('merges two schemas (tests)', function (done) { 332 | 333 | var a = Joi.number().min(5); 334 | var b = Joi.number().max(10); 335 | 336 | Validate(a, [[4, false], [11, true]]); 337 | Validate(b, [[6, true], [11, false]]); 338 | Validate(a.concat(b), [[4, false], [6, true], [11, false]]); 339 | done(); 340 | }); 341 | 342 | it('merges two schemas (flags)', function (done) { 343 | 344 | var a = Joi.string().valid('a'); 345 | var b = Joi.string().insensitive(); 346 | 347 | Validate(a, [['a', true], ['A', false], ['b', false]]); 348 | Validate(a.concat(b), [['a', true], ['A', true], ['b', false]]); 349 | done(); 350 | }); 351 | 352 | it('overrides and append information', function (done) { 353 | 354 | var a = Joi.description('a').unit('a').tags('a').example('a'); 355 | var b = Joi.description('b').unit('b').tags('b').example('b'); 356 | 357 | var desc = a.concat(b).describe(); 358 | expect(desc).to.deep.equal({ 359 | type: 'any', 360 | description: 'b', 361 | tags: ['a', 'b'], 362 | examples: ['a', 'b'], 363 | unit: 'b', 364 | valids: [undefined], 365 | invalids: [null] 366 | }); 367 | done(); 368 | }); 369 | 370 | it('merges two objects (any key + specific key)', function (done) { 371 | 372 | var a = Joi.object(); 373 | var b = Joi.object({ b: 1 }); 374 | 375 | Validate(a, [[{ b: 1 }, true], [{ b: 2 }, true]]); 376 | Validate(b, [[{ b: 1 }, true], [{ b: 2 }, false]]); 377 | Validate(a.concat(b), [[{ b: 1 }, true], [{ b: 2 }, false]]); 378 | Validate(b.concat(a), [[{ b: 1 }, true], [{ b: 2 }, false]]); 379 | done(); 380 | }); 381 | 382 | it('merges two objects (no key + any key)', function (done) { 383 | 384 | var a = Joi.object({}); 385 | var b = Joi.object(); 386 | 387 | Validate(a, [[{}, true], [{ b: 2 }, false]]); 388 | Validate(b, [[{}, true], [{ b: 2 }, true]]); 389 | Validate(a.concat(b), [[{}, true], [{ b: 2 }, false]]); 390 | Validate(b.concat(a), [[{}, true], [{ b: 2 }, false]]); 391 | done(); 392 | }); 393 | 394 | it('merges two objects (key + key)', function (done) { 395 | 396 | var a = Joi.object({ a: 1 }); 397 | var b = Joi.object({ b: 2 }); 398 | 399 | Validate(a, [[{ a: 1 }, true], [{ b: 2 }, false]]); 400 | Validate(b, [[{ a: 1 }, false], [{ b: 2 }, true]]); 401 | Validate(a.concat(b), [[{ a: 1 }, true], [{ b: 2 }, true]]); 402 | Validate(b.concat(a), [[{ a: 1 }, true], [{ b: 2 }, true]]); 403 | done(); 404 | }); 405 | 406 | it('merges two objects (renames)', function (done) { 407 | 408 | var a = Joi.object({ a: 1 }).rename('c', 'a'); 409 | var b = Joi.object({ b: 2 }).rename('d', 'b'); 410 | 411 | a.concat(b).validate({ c: 1, d: 2 }, function (err, value) { 412 | 413 | expect(err).to.not.exist; 414 | expect(value).to.deep.equal({ a: 1, b: 2 }); 415 | }); 416 | done(); 417 | }); 418 | 419 | it('merges two objects (deps)', function (done) { 420 | 421 | var a = Joi.object({ a: 1 }); 422 | var b = Joi.object({ b: 2 }).and('b', 'a'); 423 | 424 | a.concat(b).validate({ a: 1, b: 2 }, function (err, value) { 425 | 426 | expect(err).to.not.exist; 427 | }); 428 | done(); 429 | }); 430 | 431 | it('merges two alternatives with references', function (done) { 432 | 433 | var schema = { 434 | a: { c: Joi.number() }, 435 | b: Joi.alternatives(Joi.ref('a.c')).concat(Joi.alternatives(Joi.ref('c'))), 436 | c: Joi.number() 437 | }; 438 | 439 | Validate(schema, [ 440 | [{ a: {} }, true], 441 | [{ a: { c: '5' }, b: 5 }, true], 442 | [{ a: { c: '5' }, b: 6, c: '6' }, true], 443 | [{ a: { c: '5' }, b: 7, c: '6' }, false] 444 | ]); 445 | 446 | done(); 447 | }); 448 | }); 449 | 450 | describe('#when', function () { 451 | 452 | it('throws when options are invalid', function (done) { 453 | 454 | expect(function () { 455 | 456 | Joi.when('a'); 457 | }).to.throw('Invalid options'); 458 | 459 | done(); 460 | }); 461 | 462 | it('forks type into alternatives', function (done) { 463 | 464 | var schema = { 465 | a: Joi.any(), 466 | b: Joi.string().valid('x').when('a', { is: 5, then: Joi.valid('y'), otherwise: Joi.valid('z') }) 467 | }; 468 | 469 | Validate(schema, [ 470 | [{ a: 5, b: 'x' }, true], 471 | [{ a: 5, b: 'y' }, true], 472 | [{ a: 5, b: 'z' }, false], 473 | [{ a: 1, b: 'x' }, true], 474 | [{ a: 1, b: 'y' }, false], 475 | [{ a: 1, b: 'z' }, true], 476 | [{ a: 5, b: 'a' }, false], 477 | [{ b: 'a' }, false] 478 | ]); 479 | 480 | done(); 481 | }); 482 | 483 | it('forks type into alternatives (only then)', function (done) { 484 | 485 | var schema = { 486 | a: Joi.any(), 487 | b: Joi.string().valid('x').when('a', { is: 5, then: Joi.valid('y') }) 488 | }; 489 | 490 | Validate(schema, [ 491 | [{ a: 5, b: 'x' }, true], 492 | [{ a: 5, b: 'y' }, true], 493 | [{ a: 5, b: 'z' }, false], 494 | [{ a: 1, b: 'x' }, true], 495 | [{ a: 1, b: 'y' }, false], 496 | [{ a: 1, b: 'z' }, false], 497 | [{ a: 5, b: 'a' }, false], 498 | [{ b: 'a' }, false] 499 | ]); 500 | 501 | done(); 502 | }); 503 | 504 | it('forks type into alternatives (only otherwise)', function (done) { 505 | 506 | var schema = { 507 | a: Joi.any(), 508 | b: Joi.string().valid('x').when('a', { is: 5, otherwise: Joi.valid('z') }) 509 | }; 510 | 511 | Validate(schema, [ 512 | [{ a: 5, b: 'x' }, true], 513 | [{ a: 5, b: 'y' }, false], 514 | [{ a: 5, b: 'z' }, false], 515 | [{ a: 1, b: 'x' }, true], 516 | [{ a: 1, b: 'y' }, false], 517 | [{ a: 1, b: 'z' }, true], 518 | [{ a: 5, b: 'a' }, false], 519 | [{ b: 'a' }, false] 520 | ]); 521 | 522 | done(); 523 | }); 524 | }); 525 | 526 | describe('Set', function () { 527 | 528 | describe('#add', function () { 529 | 530 | it('throws when adding a non ref function', function (done) { 531 | 532 | expect(function () { 533 | 534 | Joi.valid(function () { }); 535 | }).to.throw('Value cannot be an object or function'); 536 | done(); 537 | }); 538 | 539 | it('throws when adding an object function', function (done) { 540 | 541 | expect(function () { 542 | 543 | Joi.valid({}); 544 | }).to.throw('Value cannot be an object or function'); 545 | done(); 546 | }); 547 | }); 548 | 549 | describe('#values', function () { 550 | 551 | it('returns array', function (done) { 552 | 553 | var a = Joi.any(); 554 | var b = a.required(); 555 | expect(a._valids.values().length).to.equal(1); 556 | expect(b._valids.values().length).to.equal(0); 557 | expect(a._invalids.values().length).to.equal(1); 558 | expect(b._invalids.values().length).to.equal(2); 559 | done(); 560 | }); 561 | }); 562 | 563 | describe('#toString', function () { 564 | 565 | it('includes undefined', function (done) { 566 | 567 | var b = Joi.any(); 568 | expect(b._valids.toString(true)).to.equal('undefined'); 569 | done(); 570 | }); 571 | }); 572 | }); 573 | }); 574 | 575 | -------------------------------------------------------------------------------- /test/object.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | 3 | var Lab = require('lab'); 4 | var Joi = require('../lib'); 5 | var Validate = require('./helper'); 6 | 7 | 8 | // Declare internals 9 | 10 | var internals = {}; 11 | 12 | 13 | // Test shortcuts 14 | 15 | var expect = Lab.expect; 16 | var before = Lab.before; 17 | var after = Lab.after; 18 | var describe = Lab.experiment; 19 | var it = Lab.test; 20 | 21 | 22 | describe('object', function () { 23 | 24 | it('converts a json string to an object', function (done) { 25 | 26 | Joi.object().validate('{"hi":true}', function (err, value) { 27 | 28 | expect(err).to.not.exist; 29 | expect(value.hi).to.equal(true); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('errors on non-object string', function (done) { 35 | 36 | Joi.object().validate('a string', function (err, value) { 37 | 38 | expect(err).to.exist; 39 | expect(value).to.equal('a string'); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('should validate an object', function (done) { 45 | 46 | var schema = Joi.object().required(); 47 | Validate(schema, [ 48 | [{}, true], 49 | [{ hi: true }, true], 50 | ['', false] 51 | ]); 52 | done(); 53 | }); 54 | 55 | it('allows any key when schema is undefined', function (done) { 56 | 57 | Joi.object().validate({ a: 4 }, function (err, value) { 58 | 59 | expect(err).to.not.exist; 60 | 61 | Joi.object(undefined).validate({ a: 4 }, function (err, value) { 62 | 63 | expect(err).to.not.exist; 64 | done(); 65 | }); 66 | }); 67 | }); 68 | 69 | it('allows any key when schema is null', function (done) { 70 | 71 | Joi.object(null).validate({ a: 4 }, function (err, value) { 72 | 73 | expect(err).to.not.exist; 74 | done(); 75 | }); 76 | }); 77 | 78 | it('throws on invalid object schema', function (done) { 79 | 80 | expect(function () { 81 | 82 | Joi.object(4); 83 | }).to.throw('Object schema must be a valid object'); 84 | done(); 85 | }); 86 | 87 | it('throws on joi object schema', function (done) { 88 | 89 | expect(function () { 90 | 91 | Joi.object(Joi.object()); 92 | }).to.throw('Object schema cannot be a joi schema'); 93 | done(); 94 | }); 95 | 96 | it('skips conversion when value is undefined', function (done) { 97 | 98 | Joi.object({ a: Joi.object() }).validate(undefined, function (err, value) { 99 | 100 | expect(err).to.not.exist; 101 | expect(value).to.not.exist; 102 | done(); 103 | }); 104 | }); 105 | 106 | it('errors on array', function (done) { 107 | 108 | Joi.object().validate([1, 2, 3], function (err, value) { 109 | 110 | expect(err).to.exist; 111 | done(); 112 | }); 113 | }); 114 | 115 | it('should prevent extra keys from existing by default', function (done) { 116 | 117 | var schema = Joi.object({ item: Joi.string().required() }).required(); 118 | Validate(schema, [ 119 | [{ item: 'something' }, true], 120 | [{ item: 'something', item2: 'something else' }, false], 121 | ['', false] 122 | ]); 123 | done(); 124 | }); 125 | 126 | it('should validate count when min is set', function (done) { 127 | 128 | var schema = Joi.object().min(3); 129 | Validate(schema, [ 130 | [{ item: 'something' }, false], 131 | [{ item: 'something', item2: 'something else' }, false], 132 | [{ item: 'something', item2: 'something else', item3: 'something something else' }, true], 133 | ['', false] 134 | ]); 135 | done(); 136 | }); 137 | 138 | it('should validate count when max is set', function (done) { 139 | 140 | var schema = Joi.object().max(2); 141 | Validate(schema, [ 142 | [{ item: 'something' }, true], 143 | [{ item: 'something', item2: 'something else' }, true], 144 | [{ item: 'something', item2: 'something else', item3: 'something something else' }, false], 145 | ['', false] 146 | ]); 147 | done(); 148 | }); 149 | 150 | it('should validate count when min and max is set', function (done) { 151 | 152 | var schema = Joi.object().max(3).min(2); 153 | Validate(schema, [ 154 | [{ item: 'something' }, false], 155 | [{ item: 'something', item2: 'something else' }, true], 156 | [{ item: 'something', item2: 'something else', item3: 'something something else' }, true], 157 | [{ item: 'something', item2: 'something else', item3: 'something something else', item4: 'item4' }, false], 158 | ['', false] 159 | ]); 160 | done(); 161 | }); 162 | 163 | it('should validate count when length is set', function (done) { 164 | 165 | var schema = Joi.object().length(2); 166 | Validate(schema, [ 167 | [{ item: 'something' }, false], 168 | [{ item: 'something', item2: 'something else' }, true], 169 | [{ item: 'something', item2: 'something else', item3: 'something something else' }, false], 170 | ['', false] 171 | ]); 172 | done(); 173 | }); 174 | 175 | it('should traverse an object and validate all properties in the top level', function (done) { 176 | 177 | var schema = Joi.object({ 178 | num: Joi.number() 179 | }); 180 | 181 | Validate(schema, [ 182 | [{ num: 1 }, true], 183 | [{ num: [1, 2, 3] }, false] 184 | ]); 185 | done(); 186 | }); 187 | 188 | it('should traverse an object and child objects and validate all properties', function (done) { 189 | 190 | var schema = Joi.object({ 191 | num: Joi.number(), 192 | obj: Joi.object({ 193 | item: Joi.string() 194 | }) 195 | }); 196 | 197 | Validate(schema, [ 198 | [{ num: 1 }, true], 199 | [{ num: [1, 2, 3] }, false], 200 | [{ num: 1, obj: { item: 'something' } }, true], 201 | [{ num: 1, obj: { item: 123 } }, false] 202 | ]); 203 | done(); 204 | }); 205 | 206 | it('should traverse an object several levels', function (done) { 207 | 208 | var schema = Joi.object({ 209 | obj: Joi.object({ 210 | obj: Joi.object({ 211 | obj: Joi.object({ 212 | item: Joi.boolean() 213 | }) 214 | }) 215 | }) 216 | }); 217 | 218 | Validate(schema, [ 219 | [{ num: 1 }, false], 220 | [{ obj: {} }, true], 221 | [{ obj: { obj: {} } }, true], 222 | [{ obj: { obj: { obj: {} } } }, true], 223 | [{ obj: { obj: { obj: { item: true } } } }, true], 224 | [{ obj: { obj: { obj: { item: 10 } } } }, false] 225 | ]); 226 | done(); 227 | }); 228 | 229 | it('should traverse an object several levels with required levels', function (done) { 230 | 231 | var schema = Joi.object({ 232 | obj: Joi.object({ 233 | obj: Joi.object({ 234 | obj: Joi.object({ 235 | item: Joi.boolean() 236 | }) 237 | }).required() 238 | }) 239 | }); 240 | 241 | Validate(schema, [ 242 | [null, false], 243 | [undefined, true], 244 | [{}, true], 245 | [{ obj: {} }, false], 246 | [{ obj: { obj: {} } }, true], 247 | [{ obj: { obj: { obj: {} } } }, true], 248 | [{ obj: { obj: { obj: { item: true } } } }, true], 249 | [{ obj: { obj: { obj: { item: 10 } } } }, false] 250 | ]); 251 | done(); 252 | }); 253 | 254 | it('should traverse an object several levels with required levels (without Joi.obj())', function (done) { 255 | 256 | var schema = { 257 | obj: { 258 | obj: { 259 | obj: { 260 | item: Joi.boolean().required() 261 | } 262 | } 263 | } 264 | }; 265 | 266 | Validate(schema, [ 267 | [null, false], 268 | [undefined, true], 269 | [{}, true], 270 | [{ obj: {} }, true], 271 | [{ obj: { obj: {} } }, true], 272 | [{ obj: { obj: { obj: {} } } }, false], 273 | [{ obj: { obj: { obj: { item: true } } } }, true], 274 | [{ obj: { obj: { obj: { item: 10 } } } }, false] 275 | ]); 276 | done(); 277 | }); 278 | 279 | it('errors on unknown keys when functions allows', function (done) { 280 | 281 | var schema = Joi.object({ a: Joi.number() }).options({ skipFunctions: true }); 282 | var obj = { a: 5, b: 'value' }; 283 | schema.validate(obj, function (err, value) { 284 | 285 | expect(err).to.exist; 286 | done(); 287 | }); 288 | }); 289 | 290 | it('validates both valid() and with()', function (done) { 291 | 292 | var schema = Joi.object({ 293 | first: Joi.valid('value'), 294 | second: Joi.any() 295 | }).with('first', 'second'); 296 | 297 | Validate(schema, [[{ first: 'value' }, false]]); 298 | done(); 299 | }); 300 | 301 | describe('#keys', function () { 302 | 303 | it('allows any key', function (done) { 304 | 305 | var a = Joi.object({ a: 4 }); 306 | var b = a.keys(); 307 | a.validate({ b: 3 }, function (err, value) { 308 | 309 | expect(err).to.exist; 310 | b.validate({ b: 3 }, function (err, value) { 311 | 312 | expect(err).to.not.exist; 313 | done(); 314 | }); 315 | }); 316 | }); 317 | 318 | it('forbids all keys', function (done) { 319 | 320 | var a = Joi.object(); 321 | var b = a.keys({}); 322 | a.validate({ b: 3 }, function (err, value) { 323 | 324 | expect(err).to.not.exist; 325 | b.validate({ b: 3 }, function (err, value) { 326 | 327 | expect(err).to.exist; 328 | done(); 329 | }); 330 | }); 331 | }); 332 | 333 | it('adds to existing keys', function (done) { 334 | 335 | var a = Joi.object({ a: 1 }); 336 | var b = a.keys({ b: 2 }); 337 | a.validate({ a: 1, b: 2 }, function (err, value) { 338 | 339 | expect(err).to.exist; 340 | b.validate({ a: 1, b: 2 }, function (err, value) { 341 | 342 | expect(err).to.not.exist; 343 | done(); 344 | }); 345 | }); 346 | }); 347 | }); 348 | 349 | describe('#unknown', function () { 350 | 351 | it('allows local unknown without applying to children', function (done) { 352 | 353 | var schema = Joi.object({ 354 | a: { 355 | b: Joi.number() 356 | } 357 | }).unknown(); 358 | 359 | Validate(schema, [ 360 | [{ a: { b: 5 } }, true], 361 | [{ a: { b: 'x' } }, false], 362 | [{ a: { b: 5 }, c: 'ignore' }, true], 363 | [{ a: { b: 5, c: 'ignore' } }, false] 364 | ]); 365 | 366 | done(); 367 | }); 368 | 369 | it('forbids local unknown without applying to children', function (done) { 370 | 371 | var schema = Joi.object({ 372 | a: Joi.object({ 373 | b: Joi.number() 374 | }).unknown() 375 | }).options({ allowUnknown: false }); 376 | 377 | Validate(schema, [ 378 | [{ a: { b: 5 } }, true], 379 | [{ a: { b: 'x' } }, false], 380 | [{ a: { b: 5 }, c: 'ignore' }, false], 381 | [{ a: { b: 5, c: 'ignore' } }, true] 382 | ]); 383 | 384 | done(); 385 | }); 386 | }); 387 | 388 | describe('#rename', function () { 389 | 390 | it('allows renaming multiple times with multiple enabled', function (done) { 391 | 392 | var schema = Joi.object({ 393 | test: Joi.string() 394 | }).rename('test1', 'test').rename('test2', 'test', { multiple: true }); 395 | 396 | Joi.compile(schema).validate({ test1: 'a', test2: 'b' }, function (err, value) { 397 | 398 | expect(err).to.not.exist; 399 | done(); 400 | }); 401 | }); 402 | 403 | it('errors renaming multiple times with multiple disabled', function (done) { 404 | 405 | var schema = Joi.object({ 406 | test: Joi.string() 407 | }).rename('test1', 'test').rename('test2', 'test'); 408 | 409 | Joi.compile(schema).validate({ test1: 'a', test2: 'b' }, function (err, value) { 410 | 411 | expect(err.message).to.equal('value cannot rename child test2 because multiple renames are disabled and another key was already renamed to test'); 412 | done(); 413 | }); 414 | }); 415 | 416 | it('errors multiple times when abortEarly is false', function (done) { 417 | 418 | Joi.object().rename('a', 'b').rename('c', 'b').rename('d', 'b').options({ abortEarly: false }).validate({ a: 1, c: 1, d: 1 }, function (err, value) { 419 | 420 | expect(err).to.exist; 421 | expect(err.message).to.equal('value cannot rename child c because multiple renames are disabled and another key was already renamed to b. value cannot rename child d because multiple renames are disabled and another key was already renamed to b'); 422 | done(); 423 | }); 424 | }); 425 | 426 | it('aliases a key', function (done) { 427 | 428 | var schema = Joi.object({ 429 | a: Joi.number(), 430 | b: Joi.number() 431 | }).rename('a', 'b', { alias: true }); 432 | 433 | var obj = { a: 10 }; 434 | 435 | Joi.compile(schema).validate(obj, function (err, value) { 436 | 437 | expect(err).to.not.exist; 438 | expect(value.a).to.equal(10); 439 | expect(value.b).to.equal(10); 440 | done(); 441 | }); 442 | }); 443 | 444 | it('with override disabled should not allow overwriting existing value', function (done) { 445 | 446 | var schema = Joi.object({ 447 | test1: Joi.string() 448 | }).rename('test', 'test1'); 449 | 450 | schema.validate({ test: 'b', test1: 'a' }, function (err, value) { 451 | 452 | expect(err.message).to.equal('value cannot rename child test because override is disabled and target test1 exists'); 453 | done(); 454 | }); 455 | }); 456 | 457 | it('with override enabled should allow overwriting existing value', function (done) { 458 | 459 | var schema = Joi.object({ 460 | test1: Joi.string() 461 | }).rename('test', 'test1', { override: true }); 462 | 463 | schema.validate({ test: 'b', test1: 'a' }, function (err, value) { 464 | 465 | expect(err).to.not.exist; 466 | done(); 467 | }); 468 | }); 469 | 470 | it('renames when data is nested in an array via includes', function (done) { 471 | 472 | var schema = { 473 | arr: Joi.array().includes(Joi.object({ 474 | one: Joi.string(), 475 | two: Joi.string() 476 | }).rename('uno', 'one').rename('dos', 'two')) 477 | }; 478 | 479 | var data = { arr: [{ uno: '1', dos: '2' }] }; 480 | Joi.object(schema).validate(data, function (err, value) { 481 | 482 | expect(err).to.not.exist; 483 | expect(value.arr[0].one).to.equal('1'); 484 | expect(value.arr[0].two).to.equal('2'); 485 | done(); 486 | }); 487 | }); 488 | 489 | it('applies rename and validation in the correct order regardless of key order', function (done) { 490 | 491 | var schema1 = Joi.object({ 492 | a: Joi.number() 493 | }).rename('b', 'a'); 494 | 495 | var input1 = { b: '5' }; 496 | 497 | schema1.validate(input1, function (err1, value1) { 498 | 499 | expect(err1).to.not.exist; 500 | expect(value1.b).to.not.exist; 501 | expect(value1.a).to.equal(5); 502 | 503 | var schema2 = Joi.object({ a: Joi.number(), b: Joi.any() }).rename('b', 'a'); 504 | var input2 = { b: '5' }; 505 | 506 | schema2.validate(input2, function (err2, value2) { 507 | 508 | expect(err2).to.not.exist; 509 | expect(value2.b).to.not.exist; 510 | expect(value2.a).to.equal(5); 511 | 512 | done(); 513 | }); 514 | }); 515 | }); 516 | 517 | it('sets the default value after key is renamed', function (done) { 518 | 519 | var schema = Joi.object({ 520 | foo2: Joi.string().default('test') 521 | }).rename('foo', 'foo2'); 522 | 523 | var input = {}; 524 | 525 | Joi.validate(input, schema, function (err, value) { 526 | 527 | expect(err).to.not.exist; 528 | expect(value.foo2).to.equal('test'); 529 | 530 | done(); 531 | }); 532 | }); 533 | }); 534 | 535 | describe('#describe', function () { 536 | 537 | it('return empty description when no schema defined', function (done) { 538 | 539 | var schema = Joi.object(); 540 | var desc = schema.describe(); 541 | expect(desc).to.deep.equal({ 542 | type: 'object', 543 | valids: [undefined], 544 | invalids: [null] 545 | }); 546 | done(); 547 | }); 548 | }); 549 | 550 | describe('#length', function () { 551 | 552 | it('throws when length is not a number', function (done) { 553 | 554 | expect(function () { 555 | 556 | Joi.object().length('a'); 557 | }).to.throw('limit must be a positive integer'); 558 | done(); 559 | }); 560 | }); 561 | 562 | describe('#min', function () { 563 | 564 | it('throws when limit is not a number', function (done) { 565 | 566 | expect(function () { 567 | 568 | Joi.object().min('a'); 569 | }).to.throw('limit must be a positive integer'); 570 | done(); 571 | }); 572 | }); 573 | 574 | describe('#max', function () { 575 | 576 | it('throws when limit is not a number', function (done) { 577 | 578 | expect(function () { 579 | 580 | Joi.object().max('a'); 581 | }).to.throw('limit must be a positive integer'); 582 | done(); 583 | }); 584 | }); 585 | 586 | describe('#with', function () { 587 | 588 | it('should throw an error when a parameter is not a string', function (done) { 589 | 590 | try { 591 | Joi.object().with({}); 592 | var error = false; 593 | } 594 | catch (e) { 595 | error = true; 596 | } 597 | expect(error).to.equal(true); 598 | 599 | try { 600 | Joi.object().with(123); 601 | error = false; 602 | } 603 | catch (e) { 604 | error = true; 605 | } 606 | expect(error).to.equal(true); 607 | done(); 608 | }); 609 | }); 610 | 611 | describe('#without', function () { 612 | 613 | it('should throw an error when a parameter is not a string', function (done) { 614 | 615 | try { 616 | Joi.object().without({}); 617 | var error = false; 618 | } 619 | catch (e) { 620 | error = true; 621 | } 622 | expect(error).to.equal(true); 623 | 624 | try { 625 | Joi.object().without(123); 626 | error = false; 627 | } 628 | catch (e) { 629 | error = true; 630 | } 631 | expect(error).to.equal(true); 632 | done(); 633 | }); 634 | }); 635 | 636 | describe('#xor', function () { 637 | 638 | it('should throw an error when a parameter is not a string', function (done) { 639 | 640 | try { 641 | Joi.object().xor({}); 642 | var error = false; 643 | } 644 | catch (e) { 645 | error = true; 646 | } 647 | expect(error).to.equal(true); 648 | 649 | try { 650 | Joi.object().xor(123); 651 | error = false; 652 | } 653 | catch (e) { 654 | error = true; 655 | } 656 | expect(error).to.equal(true); 657 | done(); 658 | }); 659 | }); 660 | 661 | describe('#or', function () { 662 | 663 | it('should throw an error when a parameter is not a string', function (done) { 664 | 665 | try { 666 | Joi.object().or({}); 667 | var error = false; 668 | } 669 | catch (e) { 670 | error = true; 671 | } 672 | expect(error).to.equal(true); 673 | 674 | try { 675 | Joi.object().or(123); 676 | error = false; 677 | } 678 | catch (e) { 679 | error = true; 680 | } 681 | expect(error).to.equal(true); 682 | done(); 683 | }); 684 | 685 | it('errors multiple levels deep', function (done) { 686 | 687 | Joi.object({ 688 | a: { 689 | b: Joi.object().or('x', 'y') 690 | } 691 | }).validate({ a: { b: { c: 1 } } }, function (err, value) { 692 | 693 | expect(err).to.exist; 694 | expect(err.message).to.equal('value must contain at least one of x, y'); 695 | done(); 696 | }); 697 | }); 698 | }); 699 | 700 | describe('#assert', function () { 701 | 702 | it('validates upwards reference', function (done) { 703 | 704 | var schema = Joi.object({ 705 | a: { 706 | b: Joi.string(), 707 | c: Joi.number() 708 | }, 709 | d: { 710 | e: Joi.any() 711 | } 712 | }).assert(Joi.ref('d/e', { separator: '/' }), Joi.ref('a.c'), 'equal to a.c'); 713 | 714 | schema.validate({ a: { b: 'x', c: 5 }, d: { e: 6 } }, function (err, value) { 715 | 716 | expect(err).to.exist; 717 | expect(err.message).to.equal('value validation failed because d.e failed to equal to a.c'); 718 | 719 | Validate(schema, [ 720 | [{ a: { b: 'x', c: 5 }, d: { e: 5 } }, true] 721 | ]); 722 | 723 | done(); 724 | }); 725 | }); 726 | 727 | it('validates upwards reference with implicit context', function (done) { 728 | 729 | var schema = Joi.object({ 730 | a: { 731 | b: Joi.string(), 732 | c: Joi.number() 733 | }, 734 | d: { 735 | e: Joi.any() 736 | } 737 | }).assert('d.e', Joi.ref('a.c'), 'equal to a.c'); 738 | 739 | schema.validate({ a: { b: 'x', c: 5 }, d: { e: 6 } }, function (err, value) { 740 | 741 | expect(err).to.exist; 742 | expect(err.message).to.equal('value validation failed because d.e failed to equal to a.c'); 743 | 744 | Validate(schema, [ 745 | [{ a: { b: 'x', c: 5 }, d: { e: 5 } }, true] 746 | ]); 747 | 748 | done(); 749 | }); 750 | }); 751 | 752 | it('throws when context is at root level', function (done) { 753 | 754 | expect(function () { 755 | 756 | var schema = Joi.object({ 757 | a: { 758 | b: Joi.string(), 759 | c: Joi.number() 760 | }, 761 | d: { 762 | e: Joi.any() 763 | } 764 | }).assert('a', Joi.ref('d.e'), 'equal to d.e'); 765 | }).to.throw('Cannot use assertions for root level references - use direct key rules instead'); 766 | done(); 767 | }); 768 | }); 769 | }); 770 | 771 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![joi Logo](https://raw.github.com/spumko/joi/master/images/joi.png) 3 | 4 | Object schema description language and validator for JavaScript objects. 5 | 6 | Current version: **4.1.x** 7 | 8 | [![Build Status](https://secure.travis-ci.org/spumko/joi.png)](http://travis-ci.org/spumko/joi) 9 | 10 | [![Browser Support](https://ci.testling.com/spumko/joi.png)](https://ci.testling.com/spumko/joi) 11 | 12 | ## Table of Contents 13 | 14 | 15 | - [Example](#example) 16 | - [Usage](#usage) 17 | - [`validate(value, schema, [options], callback)`](#validatevalue-schema-options-callback) 18 | - [`compile(schema)`](#compileschema) 19 | - [`any`](#any) 20 | - [`any.allow(value)`](#anyallowvalue) 21 | - [`any.valid(value)`](#anyvalidvalue) 22 | - [`any.invalid(value)`](#anyinvalidvalue) 23 | - [`any.required()`](#anyrequired) 24 | - [`any.optional()`](#anyoptional) 25 | - [`description(desc)`](#descriptiondesc) 26 | - [`any.notes(notes)`](#anynotesnotes) 27 | - [`any.tags(tags)`](#anytagstags) 28 | - [`any.example(value)`](#anyexamplevalue) 29 | - [`any.unit(name)`](#anyunitname) 30 | - [`any.options(options)`](#anyoptionsoptions) 31 | - [`any.strict()`](#anystrict) 32 | - [`any.default(value)`](#anydefault) 33 | - [`any.concat(schema)`](#anyconcatschema) 34 | - [`any.when(ref, options)`](#anywhenref-options) 35 | - [`array`](#array) 36 | - [`array.includes(type)`](#arrayincludestype) 37 | - [`array.excludes(type)`](#arrayexcludestype) 38 | - [`array.min(limit)`](#arrayminlimit) 39 | - [`array.max(limit)`](#arraymaxlimit) 40 | - [`array.length(limit)`](#arraylengthlimit) 41 | - [`binary`](#binary) 42 | - [`binary.encoding(encoding)`](#binaryencodingencoding) 43 | - [`binary.min(limit)`](#binaryminlimit) 44 | - [`binary.max(limit)`](#binarymaxlimit) 45 | - [`binary.length(limit)`](#binarylengthlimit) 46 | - [`boolean()`](#boolean) 47 | - [`date`](#date) 48 | - [`date.min(date)`](#datemindate) 49 | - [`date.max(date)`](#datemaxdate) 50 | - [`func`](#func) 51 | - [`number`](#number) 52 | - [`number.min(limit)`](#numberminlimit) 53 | - [`number.max(limit)`](#numbermaxlimit) 54 | - [`number.integer()`](#numberinteger) 55 | - [`object`](#object) 56 | - [`object.keys([schema])`](#objectkeysschema) 57 | - [`object.min(limit)`](#objectminlimit) 58 | - [`object.max(limit)`](#objectmaxlimit) 59 | - [`object.length(limit)`](#objectlengthlimit) 60 | - [`object.and(peers)`](#objectandpeers) 61 | - [`object.or(peers)`](#objectorpeers) 62 | - [`object.xor(peers)`](#objectxorpeers) 63 | - [`object.with(key, peers)`](#objectwithkey-peers) 64 | - [`object.without(key, peers)`](#objectwithoutkey-peers) 65 | - [`object.rename(from, to, [options])`](#objectrenamefrom-to-options) 66 | - [`object.assert(ref, schema, message)`](#objectassertref-schema-message) 67 | - [`object.unknown([allow])`](#objectunknownallow) 68 | - [`string`](#string) 69 | - [`string.insensitive()`](#stringinsensitive) 70 | - [`string.min(limit, [encoding])`](#stringminlimit-encoding) 71 | - [`string.max(limit, [encoding])`](#stringmaxlimit-encoding) 72 | - [`string.length(limit, [encoding])`](#stringlengthlimit-encoding) 73 | - [`string.regex(pattern)`](#stringregexpattern) 74 | - [`string.alphanum()`](#stringalphanum) 75 | - [`string.token()`](#stringtoken) 76 | - [`string.email()`](#stringemail) 77 | - [`string.guid()`](#stringguid) 78 | - [`string.isoDate()`](#stringisodate) 79 | - [`string.hostname()`](#stringhostname) 80 | - [`alternatives`](#alternatives) 81 | - [`alternatives.try(schemas)`](#alternativestryschemas) 82 | - [`alternatives.when(ref, options)`](#alternativeswhenref-options) 83 | - [`ref(key, [options])`](#refkey-options) 84 | 85 | # Example 86 | 87 | ```javascript 88 | var Joi = require('joi'); 89 | 90 | var schema = Joi.object().keys({ 91 | username: Joi.string().alphanum().min(3).max(30).required(), 92 | password: Joi.string().regex(/[a-zA-Z0-9]{3,30}/), 93 | access_token: [Joi.string(), Joi.number()], 94 | birthyear: Joi.number().integer().min(1900).max(2013), 95 | email: Joi.string().email() 96 | }).with('username', 'birthyear').without('password', 'access_token'); 97 | 98 | Joi.validate({ username: 'abc', birthyear: 1994 }, schema, function (err) { }); // err === null -> valid 99 | ``` 100 | 101 | The above schema defines the following constraints: 102 | * `username` 103 | * a required string 104 | * must contain only alphanumeric characters 105 | * at least 3 characters long but no more than 30 106 | * must be accompanied by `birthyear` 107 | * `password` 108 | * an optional string 109 | * must satisfy the custom regex 110 | * cannot appear together with `access_token` 111 | * `access_token` 112 | * an optional, unconstrained string or number 113 | * `birthyear` 114 | * an integer between 1900 and 2013 115 | * `email` 116 | * a valid email address string 117 | 118 | # Usage 119 | 120 | Usage is a two steps process. First, a schema is constructed using the provided types and constraints: 121 | 122 | ```javascript 123 | var schema = { 124 | a: Joi.string() 125 | }; 126 | ``` 127 | 128 | Note that **joi** schema objects are immutable which means every additional rule added (e.g. `.min(5)`) will return a 129 | new schema object. 130 | 131 | Then the value is validated against the schema: 132 | 133 | ```javascript 134 | Joi.validate({ a: 'a string' }, schema, function (err) { }); 135 | ``` 136 | 137 | If the value is valid, `null` is returned, otherwise an `Error` object. 138 | 139 | The schema can be a plain JavaScript object where every key is assigned a **joi** type, or it can be a **joi** type directly: 140 | 141 | ```javascript 142 | var schema = Joi.string().min(10); 143 | ``` 144 | 145 | If the schema is a **joi** type, the `schema.validate(value, callback)` can be called directly on the type. When passing a non-type schema object, 146 | the module converts it internally to an object() type equivalent to: 147 | 148 | ```javascript 149 | var schema = Joi.object().keys({ 150 | a: Joi.string() 151 | }); 152 | ``` 153 | 154 | When validating a schema: 155 | * Keys are optional by default. 156 | * Strings are utf-8 encoded by default. 157 | * Rules are defined in an additive fashion and evaluated in order after whitelist and blacklist checks. 158 | 159 | ### `validate(value, schema, [options], callback)` 160 | 161 | Validates a value using the given schema and options where: 162 | - `value` - the value being validated. 163 | - `schema` - the validation schema. Can be a **joi** type object or a plain object where every key is assigned a **joi** type object. 164 | - `options` - an optional object with the following optional keys: 165 | - `abortEarly` - when `true`, stops validation on the first error, otherwise returns all the errors found. Defaults to `true`. 166 | - `convert` - when `true`, attempts to cast values to the required types (e.g. a string to a number). Defaults to `true`. 167 | - `allowUnknown` - when `true`, allows object to contain unknown keys which are ignored. Defaults to `false`. 168 | - `skipFunctions` - when `true`, ignores unknown keys with a function value. Defaults to `false`. 169 | - `stripUnknown` - when `true`, unknown keys are deleted (only when value is an object). Defaults to `false`. 170 | - `language` - overrides individual error messages. Defaults to no override (`{}`). 171 | - `callback` - the callback method using the signature `function(err, callback)` where: 172 | - `err` - if validation failed, the error reason, otherwise `null`. 173 | - `value` - the validated value with any type conversions and other modifiers applied (the input is left unchanged). `value` can be 174 | incomplete if validation failed and `abortEarly` is `true`. 175 | 176 | ```javascript 177 | var schema = { 178 | a: Joi.number() 179 | }; 180 | 181 | var value = { 182 | a: '123' 183 | }; 184 | 185 | Joi.validate(value, schema, function (err) { }); 186 | // err -> null 187 | // value.a -> 123 (number, not string) 188 | ``` 189 | 190 | ### `compile(schema)` 191 | 192 | Converts literal schema definition to **joi** schema object (or returns the same back if already a **joi** schema object) where: 193 | - `schema` - the schema definition to compile. 194 | 195 | ```javascript 196 | var definition = ['key', 5, { a: true, b: [/^a/, 'boom'] }]; 197 | var schema = Joi.compile(definition); 198 | 199 | // Same as: 200 | 201 | var schema = Joi.alternatives().try([ 202 | Joi.string().valid('key'), 203 | Joi.number().valid(5), 204 | Joi.object().keys({ 205 | a: Joi.boolean().valid(true), 206 | b: Joi.alternatives().try([ 207 | Joi.string().regex(/^a/), 208 | Joi.string().valid('boom') 209 | ]) 210 | }) 211 | ]); 212 | ``` 213 | 214 | ### `any` 215 | 216 | Generates a schema object that matches any data type. 217 | 218 | ```javascript 219 | var any = Joi.any(); 220 | any.valid('a'); 221 | 222 | any.validate('a', function (err) { }); 223 | ``` 224 | 225 | #### `any.allow(value)` 226 | 227 | Whitelists a value where: 228 | - `value` - the allowed value which can be of any type and will be matched against the validated value before applying any other rules. 229 | `value` can be an array of values, or multiple values can be passed as individual arguments. `value` supports [references](#refkey-options). 230 | 231 | ```javascript 232 | var schema = { 233 | a: Joi.any().allow('a'), 234 | b: Joi.any().allow('b', 'B'), 235 | c: Joi.any().allow(['c', 'C']) 236 | }; 237 | ``` 238 | 239 | #### `any.valid(value)` 240 | 241 | Adds the provided values into the allowed whitelist and marks them as the only valid values allowed where: 242 | - `value` - the allowed value which can be of any type and will be matched against the validated value before applying any other rules. 243 | `value` can be an array of values, or multiple values can be passed as individual arguments. `value` supports [references](#refkey-options). 244 | 245 | ```javascript 246 | var schema = { 247 | a: Joi.any().valid('a'), 248 | b: Joi.any().valid('b', 'B'), 249 | c: Joi.any().valid(['c', 'C']) 250 | }; 251 | ``` 252 | 253 | #### `any.invalid(value)` 254 | 255 | Blacklists a value where: 256 | - `value` - the forbidden value which can be of any type and will be matched against the validated value before applying any other rules. 257 | `value` can be an array of values, or multiple values can be passed as individual arguments. `value` supports [references](#refkey-options). 258 | 259 | ```javascript 260 | var schema = { 261 | a: Joi.any().invalid('a'), 262 | b: Joi.any().invalid('b', 'B'), 263 | c: Joi.any().invalid(['c', 'C']) 264 | }; 265 | ``` 266 | 267 | #### `any.required()` 268 | 269 | Marks a key as required which will not allow `undefined` as value. All keys are optional by default. 270 | 271 | ```javascript 272 | var schema = { 273 | a: Joi.any().required() 274 | }; 275 | ``` 276 | 277 | #### `any.optional()` 278 | 279 | Marks a key as optional which will allow `undefined` as values. Used to annotate the schema for readability as all keys are optional by default. 280 | 281 | ```javascript 282 | var schema = { 283 | a: Joi.any().optional() 284 | }; 285 | ``` 286 | 287 | #### `any.description(desc)` 288 | 289 | Annotates the key where: 290 | - `desc` - the description string. 291 | 292 | ```javascript 293 | var schema = Joi.any().description('this key will match anything you give it'); 294 | ``` 295 | 296 | #### `any.notes(notes)` 297 | 298 | Annotates the key where: 299 | - `notes` - the notes string or array of strings. 300 | 301 | ```javascript 302 | var schema = Joi.any().notes(['this is special', 'this is important']); 303 | ``` 304 | 305 | #### `any.tags(tags)` 306 | 307 | Annotates the key where: 308 | - `tags` - the tag string or array of strings. 309 | 310 | ```javascript 311 | var schema = Joi.any().tags(['api', 'user']); 312 | ``` 313 | 314 | #### `any.example(value)` 315 | 316 | Annotates the key where: 317 | - `value` - an example value. 318 | 319 | If the example fails to pass validation, the function will throw. 320 | 321 | ```javascript 322 | var schema = Joi.string().min(4).example('abcd'); 323 | ``` 324 | 325 | #### `any.unit(name)` 326 | 327 | Annotates the key where: 328 | - `name` - the unit name of the value. 329 | 330 | ```javascript 331 | var schema = Joi.number().unit('milliseconds'); 332 | ``` 333 | 334 | #### `any.options(options)` 335 | 336 | Overrides the global `validate()` options for the current key and any sub-key where: 337 | - `options` - an object with the same optional keys as [`Joi.validate(value, schema, options, callback)`](#joivalidatevalue-schema-options-callback). 338 | 339 | ```javascript 340 | var schema = { 341 | a: Joi.any().options({ convert: false }) 342 | }; 343 | ``` 344 | 345 | #### `any.strict()` 346 | 347 | Sets the `options.convert` options to `false` which prevent type casting for the current key and any child keys. 348 | 349 | ```javascript 350 | var schema = { 351 | a: Joi.any().strict() 352 | }; 353 | ``` 354 | 355 | #### `any.default(value)` 356 | 357 | Sets a default value if the original value is undefined where: 358 | - `value` - the value. `value` supports [references](#refkey-options). 359 | 360 | ```javascript 361 | var schema = { 362 | username: Joi.string().default('new_user') 363 | }; 364 | var input = {}; 365 | Joi.validate(input, schema, function (err) { }); 366 | // input === { username: "new_user" } 367 | ``` 368 | 369 | #### `any.concat(schema)` 370 | 371 | Returns a new type that is the result of adding the rules of one type to another where: 372 | - `schema` - a **joi** type to merge into the current schema. Can only be of the same type as the context type or `any`. 373 | 374 | ```javascript 375 | var a = Joi.string().valid('a'); 376 | var b = Joi.string().valid('b'); 377 | var ab = a.concat(b); 378 | ``` 379 | 380 | #### `any.when(ref, options)` 381 | 382 | Converts the type into an [`alternatives`](#alternatives) type where the conditions are merged into the type definition where: 383 | - `ref` - the key name or [reference](#refkey-options). 384 | - `options` - an object with: 385 | - `is` - the required condition **joi** type. 386 | - `then` - the alternative schema type if the condition is true. Required if `otherwise` is missing. 387 | - `otherwise` - the alternative schema type if the condition is false. Required if `then` is missing. 388 | 389 | ```javascript 390 | var schema = { 391 | a: Joi.any().valid('x').when('b', { is: 5, then: Joi.valid('y'), otherwise: Joi.valid('z') }), 392 | b: Joi.any() 393 | }; 394 | ``` 395 | 396 | ### `array` 397 | 398 | Generates a schema object that matches an array data type. 399 | 400 | Supports the same methods of the [`any()`](#any) type. 401 | 402 | ```javascript 403 | var array = Joi.array; 404 | array.includes(Joi.string().valid('a', 'b')); 405 | 406 | array.validate(['a', 'b', 'a'], function (err) { }); 407 | ``` 408 | 409 | #### `array.includes(type)` 410 | 411 | List the types allowed for the array values where: 412 | - `type` - a **joi** schema object to validate each array item against. `type` can be an array of values, or multiple values can be passed as individual arguments. 413 | 414 | ```javascript 415 | var schema = { 416 | a: Joi.array().includes(Joi.string, Joi.number) 417 | }; 418 | ``` 419 | 420 | #### `array.excludes(type)` 421 | 422 | List the types forbidden for the array values where: 423 | - `type` - a **joi** schema object to validate each array item against. `type` can be an array of values, or multiple values can be passed as individual arguments. 424 | 425 | ```javascript 426 | var schema = { 427 | a: Joi.array().excludes(Joi.object) 428 | }; 429 | ``` 430 | 431 | #### `array.min(limit)` 432 | 433 | Specifies the minimum number of items in the array where: 434 | - `limit` - the lowest number of array items allowed. 435 | 436 | ```javascript 437 | var schema = { 438 | a: Joi.array().min(2) 439 | }; 440 | ``` 441 | 442 | #### `array.max(limit)` 443 | 444 | Specifies the maximum number of items in the array where: 445 | - `limit` - the highest number of array items allowed. 446 | 447 | ```javascript 448 | var schema = { 449 | a: Joi.array().max(10) 450 | }; 451 | ``` 452 | 453 | #### `array.length(limit)` 454 | 455 | Specifies the exact number of items in the array where: 456 | - `limit` - the number of array items allowed. 457 | 458 | ```javascript 459 | var schema = { 460 | a: Joi.array().length(5) 461 | }; 462 | ``` 463 | 464 | ### `boolean` 465 | 466 | Generates a schema object that matches a boolean data type (as well as the strings 'true', 'false', 'yes', and 'no'). Can also be called via `bool()`. 467 | 468 | Supports the same methods of the [`any()`](#any) type. 469 | 470 | ```javascript 471 | var boolean = Joi.boolean(); 472 | boolean.allow(null); 473 | 474 | boolean.validate(true, function (err) { }); 475 | ``` 476 | 477 | ### `binary` 478 | 479 | Generates a schema object that matches a Buffer data type (as well as the strings which will be converted to Buffers). 480 | 481 | Supports the same methods of the [`any()`](#any) type. 482 | 483 | ```javascript 484 | var schema = { 485 | a: Joi.binary() 486 | }; 487 | ``` 488 | 489 | #### `binary.encoding(encoding)` 490 | 491 | Sets the string encoding format if a string input is converted to a buffer where: 492 | - `encoding` - the encoding scheme. 493 | 494 | ```javascript 495 | var schema = Joi.binary().encoding('base64'); 496 | ``` 497 | 498 | #### `binary.min(limit)` 499 | 500 | Specifies the minimum length of the buffer where: 501 | - `limit` - the lowest size of the buffer. 502 | 503 | ```javascript 504 | var schema = { 505 | a: Joi.binary().min(2) 506 | }; 507 | ``` 508 | 509 | #### `binary.max(limit)` 510 | 511 | Specifies the maximum length of the buffer where: 512 | - `limit` - the highest size of the buffer. 513 | 514 | ```javascript 515 | var schema = { 516 | a: Joi.binary().max(10) 517 | }; 518 | ``` 519 | 520 | #### `binary.length(limit)` 521 | 522 | Specifies the exact length of the buffer: 523 | - `limit` - the size of buffer allowed. 524 | 525 | ```javascript 526 | var schema = { 527 | a: Joi.binary().length(5) 528 | }; 529 | ``` 530 | 531 | ### `date` 532 | 533 | Generates a schema object that matches a date type (as well as a JavaScript date string or number of milliseconds). 534 | 535 | Supports the same methods of the [`any()`](#any) type. 536 | 537 | ```javascript 538 | var date = Joi.date(); 539 | date.min('12-20-2012'); 540 | 541 | date.validate('12-21-2012', function (err) { }); 542 | ``` 543 | 544 | #### `date.min(date)` 545 | 546 | Specifies the oldest date allowed where: 547 | - `date` - the oldest date allowed. 548 | 549 | ```javascript 550 | var schema = { 551 | a: Joi.date().min('1-1-1974') 552 | }; 553 | ``` 554 | 555 | #### `date.max(date)` 556 | 557 | Specifies the latest date allowed where: 558 | - `date` - the latest date allowed. 559 | 560 | ```javascript 561 | var schema = { 562 | a: Joi.date().max('12-31-2020') 563 | }; 564 | ``` 565 | 566 | ### `func` 567 | 568 | Generates a schema object that matches a function type. 569 | 570 | Supports the same methods of the [`any()`](#any) type. 571 | 572 | ```javascript 573 | var func = Joi.func(); 574 | func.allow(null); 575 | 576 | func.validate(function () {}, function (err) { }); 577 | ``` 578 | 579 | ### `number` 580 | 581 | Generates a schema object that matches a number data type (as well as strings that can be converted to numbers). 582 | 583 | Supports the same methods of the [`any()`](#any) type. 584 | 585 | ```javascript 586 | var number = Joi.number(); 587 | number.min(1).max(10).integer(); 588 | 589 | number.validate(5, function (err) { }); 590 | ``` 591 | 592 | #### `number.min(limit)` 593 | 594 | Specifies the minimum value where: 595 | - `limit` - the minimum value allowed. 596 | 597 | ```javascript 598 | var schema = { 599 | a: Joi.number().min(2) 600 | }; 601 | ``` 602 | 603 | #### `number.max(limit)` 604 | 605 | Specifies the maximum value where: 606 | - `limit` - the maximum value allowed. 607 | 608 | ```javascript 609 | var schema = { 610 | a: Joi.number().max(10) 611 | }; 612 | ``` 613 | 614 | #### `number.integer()` 615 | 616 | Requires the number to be an integer (no floating point). 617 | 618 | ```javascript 619 | var schema = { 620 | a: Joi.number().integer() 621 | }; 622 | ``` 623 | 624 | ### `object` 625 | 626 | Generates a schema object that matches an object data type (as well as JSON strings that parsed into objects). Defaults 627 | to allowing any child key. 628 | 629 | Supports the same methods of the [`any()`](#any) type. 630 | 631 | ```javascript 632 | var object = Joi.object().keys({ 633 | a: Joi.number().min(1).max(10).integer(), 634 | b: 'some string' 635 | }); 636 | 637 | object.validate({ a: 5 }, function (err) { }); 638 | ``` 639 | 640 | #### `object.keys([schema])` 641 | 642 | Sets the allowed object keys where: 643 | - `schema` - optional object where each key is assinged a **joi** type object. If `schema` is `{}` no keys allowed. 644 | If `schema` is `null` or `undefined`, any key allowed. If `schema` is an object with keys, the keys are added to any 645 | previously defined keys (but narrows the selection if all keys previously allowed). Defaults to 'undefined' which 646 | allows any child key. 647 | 648 | ```javascript 649 | var object = Joi.object().keys({ 650 | a: Joi.number() 651 | b: Joi.string() 652 | }); 653 | ``` 654 | 655 | #### `object.min(limit)` 656 | 657 | Specifies the minimum number of keys in the object where: 658 | - `limit` - the lowest number of keys allowed. 659 | 660 | ```javascript 661 | var schema = { 662 | a: Joi.object().min(2) 663 | }; 664 | ``` 665 | 666 | #### `object.max(limit)` 667 | 668 | Specifies the maximum number of keys in the object where: 669 | - `limit` - the highest number of object keys allowed. 670 | 671 | ```javascript 672 | var schema = { 673 | a: Joi.object().max(10) 674 | }; 675 | ``` 676 | 677 | #### `object.length(limit)` 678 | 679 | Specifies the exact number of keys in the object where: 680 | - `limit` - the number of object keys allowed. 681 | 682 | ```javascript 683 | var schema = { 684 | a: Joi.object().length(5) 685 | }; 686 | ``` 687 | 688 | #### `object.and(peers)` 689 | 690 | Defines am all-or-nothing relationship between keys where if one of the peers is present, all of them are required as 691 | well where: 692 | - `peers` - the key names of which if one present, all are required. `peers` can be a single string value, an 693 | array of string values, or each peer provided as an argument. 694 | 695 | ```javascript 696 | var schema = Joi.object().keys({ 697 | a: Joi.any(), 698 | b: Joi.any() 699 | }).or('a', 'b'); 700 | ``` 701 | 702 | #### `object.or(peers)` 703 | 704 | Defines a relationship between keys where one of the peers is required (and more than one is allowed) where: 705 | - `peers` - the key names of which at least one must appear. `peers` can be a single string value, an 706 | array of string values, or each peer provided as an argument. 707 | 708 | ```javascript 709 | var schema = Joi.object().keys({ 710 | a: Joi.any(), 711 | b: Joi.any() 712 | }).or('a', 'b'); 713 | ``` 714 | 715 | #### `object.xor(peers)` 716 | 717 | Defines an exclusive relationship between a set of keys where one of them is required but not at the same time where: 718 | - `peers` - the exclusive key names that must not appear together but where one of them is required. `peers` can be a single string value, an 719 | array of string values, or each peer provided as an argument. 720 | 721 | ```javascript 722 | var schema = Joi.object().keys({ 723 | a: Joi.any(), 724 | b: Joi.any() 725 | }).xor('a', 'b'); 726 | ``` 727 | 728 | #### `object.with(key, peers)` 729 | 730 | Requires the presence of other keys whenever the specified key is present where: 731 | - `key` - the reference key. 732 | - `peers` - the required peer key names that must appear together with `key`. `peers` can be a single string value or an array of string values. 733 | 734 | Note that unlike [`object.and()`](#objectandpeers), `with()` creates a dependency only between the `key` and each of the `peers`, not 735 | between the `peers` themselves. 736 | 737 | ```javascript 738 | var schema = Joi.object().keys({ 739 | a: Joi.any(), 740 | b: Joi.any() 741 | }).with('a', 'b'); 742 | ``` 743 | 744 | #### `object.without(key, peers)` 745 | 746 | Forbids the presence of other keys whenever the specified is present where: 747 | - `key` - the reference key. 748 | - `peers` - the forbidden peer key names that must not appear together with `key`. `peers` can be a single string value or an array of string values. 749 | 750 | ```javascript 751 | var schema = Joi.object().keys({ 752 | a: Joi.any(), 753 | b: Joi.any() 754 | }).without('a', ['b']); 755 | ``` 756 | 757 | #### `object.rename(from, to, [options])` 758 | 759 | Renames a key to another name (deletes the renamed key) where: 760 | - `from` - the original key name. 761 | - `to` - the new key name. 762 | - `options` - an optional object with the following optional keys: 763 | - `alias` - if `true`, does not delete the old key name, keeping both the new and old keys in place. Defaults to `false`. 764 | - `multiple` - if `true`, allows renaming multiple keys to the same destination where the last rename wins. Defaults to `false`. 765 | - `override` - if `true`, allows renaming a key over an existing key. Defaults to `false`. 766 | 767 | Keys are renamed before any other validation rules are applied. 768 | 769 | ```javascript 770 | var object = Joi.object().keys({ 771 | a: Joi.number() 772 | }).rename('b', 'a'); 773 | 774 | object.validate({ b: 5 }, function (err) { }); 775 | ``` 776 | 777 | #### `object.assert(ref, schema, message)` 778 | 779 | Verifies an assertion where: 780 | - `ref` - the key name or [reference](#refkey-options). 781 | - `schema` - the validation rules required to satisfy the assertion. If the `schema` includes references, they are resolved against 782 | the object value, not the value of the `ref` target. 783 | - `message` - human-readable message used when the assertion fails. 784 | 785 | ```javascript 786 | var schema = Joi.object().keys({ 787 | a: { 788 | b: Joi.string(), 789 | c: Joi.number() 790 | }, 791 | d: { 792 | e: Joi.any 793 | } 794 | }).assert('d.e', Joi.ref('a.c'), 'equal to a.c'); 795 | ``` 796 | 797 | #### `object.unknown([allow])` 798 | 799 | Overrides the handling of unknown keys for the scope of the current object only (does not apply to children) where: 800 | - `allow` - if `false`, unknown keys are not allowed, otherwise unknown keys are ignored. 801 | 802 | ```javascript 803 | var schema = Joi.Object({ a: Joi.any() }).unknown(); 804 | ``` 805 | 806 | ### `string` 807 | 808 | Generates a schema object that matches a string data type. Note that empty strings are not allowed by default and must be enabled with `allow('')`. 809 | 810 | Supports the same methods of the [`any()`](#any) type. 811 | 812 | ```javascript 813 | Joi.string().min(1).max(10); 814 | 815 | string.validate('12345', function (err) { }); 816 | ``` 817 | 818 | #### `string.insensitive()` 819 | 820 | Allows the value to match any whitelist of blacklist item in a case insensitive comparison. 821 | 822 | ```javascript 823 | var schema = Joi.string().valid('a').insensitive(); 824 | ``` 825 | 826 | #### `string.min(limit, [encoding])` 827 | 828 | Specifies the minimum number string characters where: 829 | - `limit` - the minimum number of string characters required. 830 | - `encoding` - is specified, the string length is calculated in bytes using the provided encoding. 831 | 832 | ```javascript 833 | var schema = Joi.string().min(2); 834 | ``` 835 | 836 | #### `string.max(limit, [encoding])` 837 | 838 | Specifies the maximum number of string characters where: 839 | - `limit` - the maximum number of string characters allowed. 840 | - `encoding` - is specified, the string length is calculated in bytes using the provided encoding. 841 | 842 | ```javascript 843 | var schema = Joi.string().max(10); 844 | ``` 845 | 846 | #### `string.length(limit, [encoding])` 847 | 848 | Specifies the exact string length required where: 849 | - `limit` - the required string length. 850 | - `encoding` - is specified, the string length is calculated in bytes using the provided encoding. 851 | 852 | ```javascript 853 | var schema = Joi.string().length(5); 854 | ``` 855 | 856 | #### `string.regex(pattern)` 857 | 858 | Defines a regular expression rule where: 859 | - `pattern` - a regular expression object the string value must match against. 860 | 861 | ```javascript 862 | var schema = Joi.string().regex(/^[abc]+$/); 863 | ``` 864 | 865 | #### `string.alphanum()` 866 | 867 | Requires the string value to only contain a-z, A-Z, and 0-9. 868 | 869 | ```javascript 870 | var schema = Joi.string().alphanum(); 871 | ``` 872 | 873 | #### `string.token()` 874 | 875 | Requires the string value to only contain a-z, A-Z, 0-9, and underscore _. 876 | 877 | ```javascript 878 | var schema = Joi.string().token(); 879 | ``` 880 | 881 | #### `string.email()` 882 | 883 | Requires the string value to be a valid email address. 884 | 885 | ```javascript 886 | var schema = Joi.string().email(); 887 | ``` 888 | 889 | #### `string.guid()` 890 | 891 | Requires the string value to be a valid GUID. 892 | 893 | ```javascript 894 | var schema = Joi.string().guid(); 895 | ``` 896 | 897 | #### `string.isoDate()` 898 | 899 | Requires the string value to be in valid ISO 8601 date format. 900 | 901 | ```javascript 902 | var schema = Joi.string().isoDate(); 903 | ``` 904 | 905 | #### `string.hostname()` 906 | 907 | Requires the string value to be a valid hostname as per [RFC1123](http://tools.ietf.org/html/rfc1123). 908 | 909 | ```javascript 910 | var schema = Joi.string().hostname(); 911 | ``` 912 | 913 | ### `alternatives` 914 | 915 | Generates a type that will match one of the provided alternative schemas via the [`try()`](#alternativestryschemas) 916 | method. If no schemas are added, the type will not match any value except for `undefined`. 917 | 918 | Supports the same methods of the [`any()`](#any) type. 919 | 920 | Alternatives can be expressed using the shorter `[]` notation. 921 | 922 | ```javascript 923 | var alt = Joi.alternatives.try(Joi.number, Joi.string); 924 | // Same as [Joi.number(), Joi.string()] 925 | ``` 926 | 927 | #### `alternatives.try(schemas) 928 | 929 | Adds an alternative schema type for attempting to match against the validated value where: 930 | - `schema` - an array of alternative **joi** types. Also supports providing each type as a separate argument. 931 | 932 | ```javascript 933 | var alt = Joi.alternatives.try(Joi.number(), Joi.string()); 934 | alt.validate('a', function (err) { }); 935 | ``` 936 | 937 | #### `alternatives.when(ref, options)` 938 | 939 | Adds a conditional alternative schema type based on another key value where: 940 | - `ref` - the key name or [reference](#refkey-options). 941 | - `options` - an object with: 942 | - `is` - the required condition **joi** type. 943 | - `then` - the alternative schema type if the condition is true. Required if `otherwise` is missing. 944 | - `otherwise` - the alternative schema type if the condition is false. Required if `then` is missing. 945 | 946 | ```javascript 947 | var schema = { 948 | a: Joi.alternatives().when('b', { is: 5, then: Joi.string(), otherwise: Joi.number() }), 949 | b: Joi.any() 950 | }; 951 | ``` 952 | 953 | ### `ref(key, [options])` 954 | 955 | Generates a reference to the value of the named key. References are resolved at validation time and in order of dependency 956 | so that if one key validation depends on another, the dependent key is validated second after the reference is validated. 957 | References support the following arguments: 958 | - `key` - the reference target. References cannot point up the object tree, only to siebling keys, but they can point to 959 | children (e.g. 'a.b.c') using the `.` separator. 960 | - `options` - optional settings: 961 | - `separator` - override the default `.` hierarchy separator. 962 | 963 | Note that references can only be used where explicitly supported such as in `valid()` or `invalid()` rules. If upwards 964 | (parents) references are needed, use [`object.assert()`](#objectassertref-schema-message). 965 | 966 | ```javascript 967 | var schema = Joi.object().keys({ 968 | a: Joi.ref('b.c'), 969 | b: { 970 | c: Joi.any() 971 | } 972 | }); 973 | 974 | schema.validate({ a: 5, b: { c: 5 } }, function (err, value) {}); 975 | ``` 976 | --------------------------------------------------------------------------------