├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example └── webservice.js ├── lib └── revalidator.js ├── package.json └── test └── validator-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_install: 3 | - curl --location http://git.io/1OcIZA | bash -s 4 | node_js: 5 | - "0.8" 6 | - "0.10" 7 | - "0.11" 8 | 9 | notifications: 10 | email: 11 | - travis@nodejitsu.com 12 | irc: "irc.freenode.org#nodejitsu" 13 | 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 0.1.3 / 2012-10-17 3 | ================== 4 | 5 | * Fixed case problem with types 6 | 7 | 0.1.2 / 2012-06-27 8 | ================== 9 | 10 | * Added host-name String format 11 | * Added support for additionalProperties 12 | * Added few default validation messages for formats 13 | 14 | 0.1.1 / 2012-04-16 15 | ================== 16 | 17 | * Added default and custom error message support 18 | * Added suport for conform function 19 | * Updated date-time format 20 | 21 | 0.1.0 / 2011-11-09 22 | ================= 23 | 24 | * Initial release 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright (c) 2009-2010 Alexis Sellier, Charlie Robbins, & the Contributors. 180 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # revalidator [![Build Status](https://secure.travis-ci.org/flatiron/revalidator.png)](http://travis-ci.org/flatiron/revalidator) 2 | 3 | A cross-browser / node.js validator with [JSONSchema](http://tools.ietf.org/html/draft-zyp-json-schema-04) compatibility as the primary goal. 4 | 5 | ## Example 6 | The core of `revalidator` is simple and succinct: `revalidator.validate(obj, schema)`: 7 | 8 | ``` js 9 | var revalidator = require('revalidator'); 10 | 11 | console.dir(revalidator.validate(someObject, { 12 | properties: { 13 | url: { 14 | description: 'the url the object should be stored at', 15 | type: 'string', 16 | pattern: '^/[^#%&*{}\\:<>?\/+]+$', 17 | required: true 18 | }, 19 | challenge: { 20 | description: 'a means of protecting data (insufficient for production, used as example)', 21 | type: 'string', 22 | minLength: 5 23 | }, 24 | body: { 25 | description: 'what to store at the url', 26 | type: 'any', 27 | default: null 28 | } 29 | } 30 | })); 31 | ``` 32 | 33 | This will return with a value indicating if the `obj` conforms to the `schema`. If it does not, a descriptive object will be returned containing the errors encountered with validation. 34 | 35 | ``` js 36 | { 37 | valid: true // or false 38 | errors: [/* Array of errors if valid is false */] 39 | } 40 | ``` 41 | 42 | In the browser, the validation function is exposed on `window.validate` by simply including `revalidator.js`. 43 | 44 | ## Installation 45 | 46 | ### Installing npm (node package manager) 47 | ``` bash 48 | $ curl http://npmjs.org/install.sh | sh 49 | ``` 50 | 51 | ### Installing revalidator 52 | ``` bash 53 | $ [sudo] npm install revalidator 54 | ``` 55 | 56 | ## Usage 57 | 58 | `revalidator` takes json-schema as input to validate objects. 59 | 60 | ### revalidator.validate (obj, schema, options) 61 | 62 | This will return with a value indicating if the `obj` conforms to the `schema`. If it does not, a descriptive object will be returned containing the errors encountered with validation. 63 | 64 | ``` js 65 | { 66 | valid: true // or false 67 | errors: [/* Array of errors if valid is false */] 68 | } 69 | ``` 70 | 71 | #### Available Options 72 | 73 | * __validateFormats__: Enforce format constraints (_default true_) 74 | * __validateFormatsStrict__: When `validateFormats` is _true_ treat unrecognized formats as validation errors (_default false_) 75 | * __validateFormatExtensions__: When `validateFormats` is _true_ also validate formats defined in `validate.formatExtensions` (_default true_) 76 | * __additionalProperties__: When `additionalProperties` is _true_ allow additional unvisited properties on the object. (_default true_) 77 | * __cast__: Enforce casting of some types (for integers/numbers are only supported) when it's possible, e.g. `"42" => 42`, but `"forty2" => "forty2"` for the `integer` type. 78 | 79 | ### Schema 80 | For a property an `value` is that which is given as input for validation where as an `expected value` is the value of the below fields 81 | 82 | #### required 83 | If true, the value should not be undefined 84 | 85 | ```js 86 | { required: true } 87 | ``` 88 | 89 | #### allowEmpty 90 | If false, the value must not be an empty string 91 | 92 | ```js 93 | { allowEmpty: false } 94 | ``` 95 | 96 | #### type 97 | The `type of value` should be equal to the expected value 98 | 99 | ```js 100 | { type: 'string' } 101 | { type: 'number' } 102 | { type: 'integer' } 103 | { type: 'array' } 104 | { type: 'boolean' } 105 | { type: 'object' } 106 | { type: 'null' } 107 | { type: 'any' } 108 | { type: ['boolean', 'string'] } 109 | ``` 110 | 111 | #### pattern 112 | The expected value regex needs to be satisfied by the value 113 | 114 | ```js 115 | { pattern: /^[a-z]+$/ } 116 | ``` 117 | 118 | #### maxLength 119 | The length of value must be greater than or equal to expected value 120 | 121 | ```js 122 | { maxLength: 8 } 123 | ``` 124 | 125 | #### minLength 126 | The length of value must be lesser than or equal to expected value 127 | 128 | ```js 129 | { minLength: 8 } 130 | ``` 131 | 132 | #### minimum 133 | Value must be greater than or equal to the expected value 134 | 135 | ```js 136 | { minimum: 10 } 137 | ``` 138 | 139 | #### maximum 140 | Value must be lesser than or equal to the expected value 141 | 142 | ```js 143 | { maximum: 10 } 144 | ``` 145 | 146 | #### allowEmpty 147 | Value may not be empty 148 | 149 | ```js 150 | { allowEmpty: false } 151 | ``` 152 | 153 | #### exclusiveMinimum 154 | Value must be greater than expected value 155 | 156 | ```js 157 | { exclusiveMinimum: 9 } 158 | ``` 159 | 160 | #### exclusiveMaximum 161 | Value must be lesser than expected value 162 | 163 | ```js 164 | { exclusiveMaximum: 11 } 165 | ``` 166 | 167 | #### divisibleBy 168 | Value must be divisible by expected value 169 | 170 | ```js 171 | { divisibleBy: 5 } 172 | { divisibleBy: 0.5 } 173 | ``` 174 | 175 | #### minItems 176 | Value must contain more than expected number of items 177 | 178 | ```js 179 | { minItems: 2 } 180 | ``` 181 | 182 | #### maxItems 183 | Value must contain fewer than expected number of items 184 | 185 | ```js 186 | { maxItems: 5 } 187 | ``` 188 | 189 | #### uniqueItems 190 | Value must hold a unique set of values 191 | 192 | ```js 193 | { uniqueItems: true } 194 | ``` 195 | 196 | #### enum 197 | Value must be present in the array of expected values 198 | 199 | ```js 200 | { enum: ['month', 'year'] } 201 | ``` 202 | 203 | #### format 204 | Value must be a valid format 205 | 206 | ```js 207 | { format: 'url' } 208 | { format: 'email' } 209 | { format: 'ip-address' } 210 | { format: 'ipv6' } 211 | { format: 'date-time' } 212 | { format: 'date' } 213 | { format: 'time' } 214 | { format: 'color' } 215 | { format: 'host-name' } 216 | { format: 'utc-millisec' } 217 | { format: 'regex' } 218 | ``` 219 | 220 | #### conform 221 | Value must conform to constraint denoted by expected value 222 | 223 | ```js 224 | { conform: function (v) { 225 | if (v%3==1) return true; 226 | return false; 227 | } 228 | } 229 | ``` 230 | 231 | #### dependencies 232 | Value is valid only if the dependent value is valid 233 | 234 | ```js 235 | { 236 | town: { required: true, dependencies: 'country' }, 237 | country: { maxLength: 3, required: true } 238 | } 239 | ``` 240 | 241 | ### Nested Schema 242 | We also allow nested schema 243 | 244 | ```js 245 | { 246 | properties: { 247 | title: { 248 | type: 'string', 249 | maxLength: 140, 250 | required: true 251 | }, 252 | author: { 253 | type: 'object', 254 | required: true, 255 | properties: { 256 | name: { 257 | type: 'string', 258 | required: true 259 | }, 260 | email: { 261 | type: 'string', 262 | format: 'email' 263 | } 264 | } 265 | } 266 | } 267 | } 268 | ``` 269 | 270 | ### Custom Messages 271 | We also allow custom messages for different constraints 272 | 273 | ```js 274 | { 275 | type: 'string', 276 | format: 'url' 277 | messages: { 278 | type: 'Not a string type', 279 | format: 'Expected format is a url' 280 | } 281 | ``` 282 | 283 | ```js 284 | { 285 | conform: function () { ... }, 286 | message: 'This can be used as a global message' 287 | } 288 | ``` 289 | 290 | ## Tests 291 | All tests are written with [vows][0] and should be run with [npm][1]: 292 | 293 | ``` bash 294 | $ npm test 295 | ``` 296 | 297 | #### Author: [Charlie Robbins](https://github.com/indexzero), [Alexis Sellier](http://cloudhead.io) 298 | #### Contributors: [Fedor Indutny](http://github.com/indutny), [Bradley Meck](http://github.com/bmeck), [Laurie Harper](http://laurie.holoweb.net/), [Martijn Swaagman](http://www.martijnswaagman.nl) 299 | #### License: Apache 2.0 300 | 301 | [0]: http://vowsjs.org 302 | [1]: http://npmjs.org 303 | -------------------------------------------------------------------------------- /example/webservice.js: -------------------------------------------------------------------------------- 1 | // 2 | // (C) 2011, Alexis Sellier, Charlie Robbins, & the Contributors. 3 | // Apache 2.0 4 | // 5 | // A simple web service for storing JSON data via REST 6 | // 7 | // GET - View Object 8 | // POST - Create Object 9 | // PUT - Update Object 10 | // DELETE - Delete Object 11 | // 12 | 13 | var revalidator = require('../'), 14 | http = require('http'), 15 | // 16 | // Keep our objects in a simple memory store 17 | // 18 | memoryStore = {}, 19 | // 20 | // Set up our request schema 21 | // 22 | schema = { 23 | properties: { 24 | url: { 25 | description: 'the url the object should be stored at', 26 | type: 'string', 27 | pattern: '^/[^#%&*{}\\:<>?\/+]+$', 28 | required: true 29 | }, 30 | challenge: { 31 | description: 'a means of protecting data (insufficient for production, used as example)', 32 | type: 'string', 33 | minLength: 5 34 | }, 35 | body: { 36 | description: 'what to store at the url', 37 | type: 'any', 38 | default: null 39 | } 40 | } 41 | } 42 | 43 | var server = http.createServer(function validateRestRequest (req, res) { 44 | req.method = req.method.toUpperCase(); 45 | 46 | // 47 | // Log the requests 48 | // 49 | console.log(req.method, req.url); 50 | 51 | // 52 | // Buffer the request so it can be parsed as JSON 53 | // 54 | var requestBody = []; 55 | req.on('data', function addDataToBody (data) { 56 | requestBody.push(data); 57 | }); 58 | 59 | // 60 | // Once the request has ended work with the body 61 | // 62 | req.on('end', function dealWithRest () { 63 | 64 | // 65 | // Parse the JSON 66 | // 67 | requestBody = requestBody.join(''); 68 | if ({POST: 1, PUT: 1}[req.method]) { 69 | try { 70 | requestBody = JSON.parse(requestBody); 71 | } 72 | catch (e) { 73 | res.writeHead(400); 74 | res.end(e); 75 | return; 76 | } 77 | } 78 | else { 79 | requestBody = {}; 80 | } 81 | 82 | // 83 | // If this was sent to a url but the body url was not declared 84 | // Make sure the body get the requested url so that our schema 85 | // validates before we work on it 86 | // 87 | if (!requestBody.url) { 88 | requestBody.url = req.url; 89 | } 90 | 91 | // 92 | // Don't let users override the main API endpoint 93 | // 94 | if (requestBody.url === '/') { 95 | res.writeHead(400); 96 | res.end('Cannot override the API endpoint "/"'); 97 | return; 98 | } 99 | 100 | // 101 | // See if our request and target are out of sync 102 | // This lets us double check the url we are about to take up 103 | // if we choose to send the request to the url directly 104 | // 105 | if (req.url !== '/' && requestBody.url !== req.url) { 106 | res.writeHead(400); 107 | res.end('Requested url and actual url do not match'); 108 | return; 109 | } 110 | 111 | // 112 | // Validate the schema 113 | // 114 | var validation = revalidator.validate(requestBody, schema); 115 | if (!validation.valid) { 116 | res.writeHead(400); 117 | res.end(validation.errors.join('\n')); 118 | return; 119 | } 120 | 121 | // 122 | // Grab the current value from storage and 123 | // check if it is a valid state for REST 124 | // 125 | var storedValue = memoryStore[requestBody.url]; 126 | if (req.method === 'POST') { 127 | if (storedValue) { 128 | res.writeHead(400); 129 | res.end('ALREADY EXISTS'); 130 | return; 131 | } 132 | } 133 | else if (!storedValue) { 134 | res.writeHead(404); 135 | res.end('DOES NOT EXIST'); 136 | return; 137 | } 138 | 139 | // 140 | // Check our challenge 141 | // 142 | if (storedValue && requestBody.challenge != storedValue.challenge) { 143 | res.writeHead(403); 144 | res.end('NOT AUTHORIZED'); 145 | return; 146 | } 147 | 148 | // 149 | // Since revalidator only checks and does not manipulate 150 | // our object we need to set up the defaults our selves 151 | // For an easier solution to this please look at Flatiron's 152 | // `Resourceful` project 153 | // 154 | if (requestBody.body === undefined) { 155 | requestBody.body = schema.properties.body.default; 156 | } 157 | 158 | // 159 | // Use REST to determine how to manipulate the stored 160 | // values 161 | // 162 | switch (req.method) { 163 | 164 | case "GET": 165 | res.writeHead(200); 166 | var result = storedValue.body; 167 | res.end(JSON.stringify(result)); 168 | return; 169 | 170 | case "POST": 171 | res.writeHead(201); 172 | res.end(); 173 | memoryStore[requestBody.url] = requestBody; 174 | return; 175 | 176 | case "DELETE": 177 | delete memoryStore[requestBody.url]; 178 | res.writeHead(200); 179 | res.end(); 180 | return; 181 | 182 | case "PUT": 183 | memoryStore[requestBody.url] = requestBody; 184 | res.writeHead(200); 185 | res.end(); 186 | return; 187 | 188 | default: 189 | res.writeHead(400); 190 | res.end('Invalid Http Verb'); 191 | return; 192 | } 193 | }); 194 | }) 195 | // 196 | // Listen to various ports depending on environment we are being run on 197 | // 198 | server.listen(process.env.PORT || process.env.C9_PORT || 1337, function reportListening () { 199 | 200 | console.log('JSON REST Service listening on port', this.address().port); 201 | console.log('Requests can be sent via REST to "/" if they conform to the following schema:'); 202 | console.log(JSON.stringify(schema, null, ' ')); 203 | 204 | }); 205 | -------------------------------------------------------------------------------- /lib/revalidator.js: -------------------------------------------------------------------------------- 1 | (function (exports) { 2 | exports.validate = validate; 3 | exports.mixin = mixin; 4 | 5 | // 6 | // ### function validate (object, schema, options) 7 | // #### {Object} object the object to validate. 8 | // #### {Object} schema (optional) the JSON Schema to validate against. 9 | // #### {Object} options (optional) options controlling the validation 10 | // process. See {@link #validate.defaults) for details. 11 | // Validate object against a JSON Schema. 12 | // If object is self-describing (i.e. has a 13 | // $schema property), it will also be validated 14 | // against the referenced schema. [TODO]: This behaviour bay be 15 | // suppressed by setting the {@link #validate.options.???} 16 | // option to ???.[/TODO] 17 | // 18 | // If schema is not specified, and object 19 | // is not self-describing, validation always passes. 20 | // 21 | // Note: in order to pass options but no schema, 22 | // schema must be specified in the call to 23 | // validate(); otherwise, options will 24 | // be interpreted as the schema. schema may be passed 25 | // as null, undefinded, or the empty object 26 | // ({}) in this case. 27 | // 28 | function validate(object, schema, options) { 29 | options = mixin({}, validate.defaults, options); 30 | var errors = []; 31 | 32 | if (schema.type === 'array') 33 | validateProperty(object, object, '', schema, options, errors); 34 | else 35 | validateObject(object, schema, options, errors); 36 | 37 | // 38 | // TODO: self-described validation 39 | // if (! options.selfDescribing) { ... } 40 | // 41 | 42 | return { 43 | valid: !(errors.length), 44 | errors: errors 45 | }; 46 | }; 47 | 48 | /** 49 | * Default validation options. Defaults can be overridden by 50 | * passing an 'options' hash to {@link #validate}. They can 51 | * also be set globally be changing the values in 52 | * validate.defaults directly. 53 | */ 54 | validate.defaults = { 55 | /** 56 | *

57 | * Enforce 'format' constraints. 58 | *

59 | * Default: true 60 | *

61 | */ 62 | validateFormats: true, 63 | /** 64 | *

65 | * When {@link #validateFormats} is true, 66 | * treat unrecognized formats as validation errors. 67 | *

68 | * Default: false 69 | *

70 | * 71 | * @see validation.formats for default supported formats. 72 | */ 73 | validateFormatsStrict: false, 74 | /** 75 | *

76 | * When {@link #validateFormats} is true, 77 | * also validate formats defined in {@link #validate.formatExtensions}. 78 | *

79 | * Default: true 80 | *

81 | */ 82 | validateFormatExtensions: true, 83 | /** 84 | *

85 | * When {@link #additionalProperties} is true, 86 | * allow additional unvisited properties on the object. 87 | *

88 | * Default: true 89 | *

90 | */ 91 | additionalProperties: true 92 | }; 93 | 94 | /** 95 | * Default messages to include with validation errors. 96 | */ 97 | validate.messages = { 98 | required: "is required", 99 | allowEmpty: "must not be empty", 100 | minLength: "is too short (minimum is %{expected} characters)", 101 | maxLength: "is too long (maximum is %{expected} characters)", 102 | pattern: "invalid input", 103 | minimum: "must be greater than or equal to %{expected}", 104 | maximum: "must be less than or equal to %{expected}", 105 | exclusiveMinimum: "must be greater than %{expected}", 106 | exclusiveMaximum: "must be less than %{expected}", 107 | divisibleBy: "must be divisible by %{expected}", 108 | minItems: "must contain more than %{expected} items", 109 | maxItems: "must contain less than %{expected} items", 110 | uniqueItems: "must hold a unique set of values", 111 | format: "is not a valid %{expected}", 112 | conform: "must conform to given constraint", 113 | type: "must be of %{expected} type", 114 | additionalProperties: "must not exist" 115 | }; 116 | validate.messages['enum'] = "must be present in given enumerator"; 117 | 118 | /** 119 | * 120 | */ 121 | validate.formats = { 122 | 'email': /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i, 123 | 'ip-address': /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i, 124 | 'ipv6': /^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}$/, 125 | 'date-time': /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,3})?Z$/, 126 | 'date': /^\d{4}-\d{2}-\d{2}$/, 127 | 'time': /^\d{2}:\d{2}:\d{2}$/, 128 | 'color': /^#[a-z0-9]{6}|#[a-z0-9]{3}|(?:rgb\(\s*(?:[+-]?\d+%?)\s*,\s*(?:[+-]?\d+%?)\s*,\s*(?:[+-]?\d+%?)\s*\))aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow$/i, 129 | //'style': (not supported) 130 | //'phone': (not supported) 131 | //'uri': (not supported) 132 | 'host-name': /^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])/, 133 | 'utc-millisec': { 134 | test: function (value) { 135 | return typeof(value) === 'number' && value >= 0; 136 | } 137 | }, 138 | 'regex': { 139 | test: function (value) { 140 | try { new RegExp(value) } 141 | catch (e) { return false } 142 | 143 | return true; 144 | } 145 | } 146 | }; 147 | 148 | /** 149 | * 150 | */ 151 | validate.formatExtensions = { 152 | 'url': /^(https?|ftp|git):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i 153 | }; 154 | 155 | function mixin(obj) { 156 | var sources = Array.prototype.slice.call(arguments, 1); 157 | while (sources.length) { 158 | var source = sources.shift(); 159 | if (!source) { continue } 160 | 161 | if (typeof(source) !== 'object') { 162 | throw new TypeError('mixin non-object'); 163 | } 164 | 165 | for (var p in source) { 166 | if (source.hasOwnProperty(p)) { 167 | obj[p] = source[p]; 168 | } 169 | } 170 | } 171 | 172 | return obj; 173 | } 174 | 175 | function validateObject(object, schema, options, errors) { 176 | var props, allProps = Object.keys(object), 177 | visitedProps = []; 178 | 179 | // see 5.2 180 | if (schema.properties) { 181 | props = schema.properties; 182 | for (var p in props) { 183 | if (props.hasOwnProperty(p)) { 184 | visitedProps.push(p); 185 | validateProperty(object, object[p], p, props[p], options, errors); 186 | } 187 | } 188 | } 189 | 190 | // see 5.3 191 | if (schema.patternProperties) { 192 | props = schema.patternProperties; 193 | for (var p in props) { 194 | if (props.hasOwnProperty(p)) { 195 | var re = new RegExp(p); 196 | 197 | // Find all object properties that are matching `re` 198 | for (var k in object) { 199 | if (object.hasOwnProperty(k)) { 200 | if (re.exec(k) !== null) { 201 | validateProperty(object, object[k], k, props[p], options, errors); 202 | visitedProps.push(k); 203 | } 204 | } 205 | } 206 | } 207 | } 208 | } 209 | 210 | //if additionalProperties is not defined set default value 211 | if (schema.additionalProperties === undefined) { 212 | schema.additionalProperties = options.additionalProperties; 213 | } 214 | 215 | // see 5.4 216 | if (undefined !== schema.additionalProperties) { 217 | var i, l; 218 | 219 | var unvisitedProps = allProps.filter(function(k){ 220 | return -1 === visitedProps.indexOf(k); 221 | }); 222 | 223 | // Prevent additional properties; each unvisited property is therefore an error 224 | if (schema.additionalProperties === false && unvisitedProps.length > 0) { 225 | for (i = 0, l = unvisitedProps.length; i < l; i++) { 226 | error("additionalProperties", unvisitedProps[i], object[unvisitedProps[i]], false, errors); 227 | } 228 | } 229 | // additionalProperties is a schema and validate unvisited properties against that schema 230 | else if (typeof schema.additionalProperties == "object" && unvisitedProps.length > 0) { 231 | for (i = 0, l = unvisitedProps.length; i < l; i++) { 232 | validateProperty(object, object[unvisitedProps[i]], unvisitedProps[i], schema.unvisitedProperties, options, errors); 233 | } 234 | } 235 | } 236 | 237 | } 238 | 239 | function validateProperty(object, value, property, schema, options, errors) { 240 | var format, 241 | valid, 242 | spec, 243 | type; 244 | 245 | function constrain(name, value, assert) { 246 | if (schema[name] !== undefined && !assert(value, schema[name])) { 247 | error(name, property, value, schema, errors); 248 | } 249 | } 250 | 251 | if (value === undefined) { 252 | if (schema.required && schema.type !== 'any') { 253 | return error('required', property, undefined, schema, errors); 254 | } else { 255 | return; 256 | } 257 | } 258 | 259 | if (options.cast) { 260 | if (('integer' === schema.type || 'number' === schema.type) && value == +value) { 261 | value = +value; 262 | object[property] = value; 263 | } 264 | 265 | if ('boolean' === schema.type) { 266 | if ('true' === value || '1' === value || 1 === value) { 267 | value = true; 268 | object[property] = value; 269 | } 270 | 271 | if ('false' === value || '0' === value || 0 === value) { 272 | value = false; 273 | object[property] = value; 274 | } 275 | } 276 | } 277 | 278 | if (schema.format && options.validateFormats) { 279 | format = schema.format; 280 | 281 | if (options.validateFormatExtensions) { spec = validate.formatExtensions[format] } 282 | if (!spec) { spec = validate.formats[format] } 283 | if (!spec) { 284 | if (options.validateFormatsStrict) { 285 | return error('format', property, value, schema, errors); 286 | } 287 | } 288 | else { 289 | if (!spec.test(value)) { 290 | return error('format', property, value, schema, errors); 291 | } 292 | } 293 | } 294 | 295 | if (schema['enum'] && schema['enum'].indexOf(value) === -1) { 296 | error('enum', property, value, schema, errors); 297 | } 298 | 299 | // Dependencies (see 5.8) 300 | if (typeof schema.dependencies === 'string' && 301 | object[schema.dependencies] === undefined) { 302 | error('dependencies', property, null, schema, errors); 303 | } 304 | 305 | if (isArray(schema.dependencies)) { 306 | for (var i = 0, l = schema.dependencies.length; i < l; i++) { 307 | if (object[schema.dependencies[i]] === undefined) { 308 | error('dependencies', property, null, schema, errors); 309 | } 310 | } 311 | } 312 | 313 | if (typeof schema.dependencies === 'object') { 314 | validateObject(object, schema.dependencies, options, errors); 315 | } 316 | 317 | checkType(value, schema.type, function(err, type) { 318 | if (err) return error('type', property, typeof value, schema, errors); 319 | 320 | constrain('conform', value, function (a, e) { return e(a, object) }); 321 | 322 | switch (type || (isArray(value) ? 'array' : typeof value)) { 323 | case 'string': 324 | constrain('allowEmpty', value, function (a, e) { return e ? e : a !== '' }); 325 | constrain('minLength', value.length, function (a, e) { return a >= e }); 326 | constrain('maxLength', value.length, function (a, e) { return a <= e }); 327 | constrain('pattern', value, function (a, e) { 328 | e = typeof e === 'string' 329 | ? e = new RegExp(e) 330 | : e; 331 | return e.test(a) 332 | }); 333 | break; 334 | case 'integer': 335 | case 'number': 336 | constrain('minimum', value, function (a, e) { return a >= e }); 337 | constrain('maximum', value, function (a, e) { return a <= e }); 338 | constrain('exclusiveMinimum', value, function (a, e) { return a > e }); 339 | constrain('exclusiveMaximum', value, function (a, e) { return a < e }); 340 | constrain('divisibleBy', value, function (a, e) { 341 | var multiplier = Math.max((a - Math.floor(a)).toString().length - 2, (e - Math.floor(e)).toString().length - 2); 342 | multiplier = multiplier > 0 ? Math.pow(10, multiplier) : 1; 343 | return (a * multiplier) % (e * multiplier) === 0 344 | }); 345 | break; 346 | case 'array': 347 | constrain('items', value, function (a, e) { 348 | var nestedErrors; 349 | for (var i = 0, l = a.length; i < l; i++) { 350 | nestedErrors = []; 351 | validateProperty(object, a[i], property, e, options, nestedErrors); 352 | nestedErrors.forEach(function (err) { 353 | err.property = 354 | (property ? property + '.' : '') + 355 | i + 356 | (err.property ? '.' + err.property.replace(property + '.', '') : ''); 357 | }); 358 | nestedErrors.unshift(0, 0); 359 | Array.prototype.splice.apply(errors, nestedErrors); 360 | } 361 | return true; 362 | }); 363 | constrain('minItems', value, function (a, e) { return a.length >= e }); 364 | constrain('maxItems', value, function (a, e) { return a.length <= e }); 365 | constrain('uniqueItems', value, function (a, e) { 366 | if (!e) return true; 367 | 368 | var h = {}; 369 | 370 | for (var i = 0, l = a.length; i < l; i++) { 371 | var key = JSON.stringify(a[i]); 372 | if (h[key]) return false; 373 | h[key] = true; 374 | } 375 | 376 | return true; 377 | }); 378 | break; 379 | case 'object': 380 | // Recursive validation 381 | if (schema.properties || schema.patternProperties || schema.additionalProperties) { 382 | var nestedErrors = []; 383 | validateObject(value, schema, options, nestedErrors); 384 | nestedErrors.forEach(function (e) { 385 | e.property = property + '.' + e.property 386 | }); 387 | nestedErrors.unshift(0, 0); 388 | Array.prototype.splice.apply(errors, nestedErrors); 389 | } 390 | break; 391 | } 392 | }); 393 | } 394 | 395 | function checkType(val, type, callback) { 396 | var result = false, 397 | types = isArray(type) ? type : [type]; 398 | 399 | // No type - no check 400 | if (type === undefined) return callback(null, type); 401 | 402 | // Go through available types 403 | // And fine first matching 404 | for (var i = 0, l = types.length; i < l; i++) { 405 | type = types[i].toLowerCase().trim(); 406 | if (type === 'string' ? typeof val === 'string' : 407 | type === 'array' ? isArray(val) : 408 | type === 'object' ? val && typeof val === 'object' && 409 | !isArray(val) : 410 | type === 'number' ? typeof val === 'number' : 411 | type === 'integer' ? typeof val === 'number' && Math.floor(val) === val : 412 | type === 'null' ? val === null : 413 | type === 'boolean'? typeof val === 'boolean' : 414 | type === 'date' ? isDate(val) : 415 | type === 'any' ? typeof val !== 'undefined' : false) { 416 | return callback(null, type); 417 | } 418 | } 419 | 420 | callback(true); 421 | } 422 | 423 | function error(attribute, property, actual, schema, errors) { 424 | var lookup = { expected: schema[attribute], actual: actual, attribute: attribute, property: property }; 425 | var message = schema.messages && schema.messages[attribute] || schema.message || validate.messages[attribute] || "no default message"; 426 | message = message.replace(/%\{([a-z]+)\}/ig, function (_, match) { return lookup[match.toLowerCase()] || ''; }); 427 | errors.push({ 428 | attribute: attribute, 429 | property: property, 430 | expected: schema[attribute], 431 | actual: actual, 432 | message: message 433 | }); 434 | } 435 | 436 | function isArray(value) { 437 | return Object.prototype.toString.call(value) === '[object Array]'; 438 | } 439 | 440 | function isDate(value) { 441 | return Object.prototype.toString.call(value) === '[object Date]'; 442 | } 443 | 444 | })(typeof module === 'object' && module && module.exports ? module.exports : window); 445 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "revalidator", 3 | "version": "0.3.1", 4 | "description": "A cross-browser / node.js validator powered by JSON Schema", 5 | "license": "Apache-2.0", 6 | "author": "Charlie Robbins ", 7 | "maintainers": [ 8 | "cloudhead ", 9 | "indutny " 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "http://github.com/flatiron/revalidator.git" 14 | }, 15 | "devDependencies": { 16 | "vows": "0.8.x" 17 | }, 18 | "main": "./lib/revalidator", 19 | "scripts": { 20 | "test": "vows test/*-test.js --spec" 21 | }, 22 | "engines": { 23 | "node": ">= 0.8.0" 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /test/validator-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | vows = require('vows'), 3 | revalidator = require('../lib/revalidator'); 4 | 5 | //- this is a flawed deep clone implemnetation but works for all required tests 6 | function clone(object) { 7 | if (object instanceof Array) { 8 | return object.map(function (element) { 9 | return clone(element); 10 | }); 11 | } 12 | 13 | return Object.keys(object).reduce(function (obj, k) { 14 | if (object[k].constructor === Object) { 15 | obj[k] = clone(object[k]); 16 | } else if (Array.isArray(object[k])) { 17 | obj[k] = []; 18 | for (var i = object[k].length - 1; i >= 0; i--) { 19 | obj[k][i] = (object[k][i].constructor === Object) ? clone(object[k][i]) : object[k][i]; 20 | } 21 | } else { 22 | obj[k] = object[k]; 23 | } 24 | return obj; 25 | }, {}); 26 | } 27 | 28 | 29 | function assertInvalid(res) { 30 | assert.isObject(res); 31 | assert.strictEqual(res.valid, false); 32 | } 33 | 34 | function assertValid(res) { 35 | assert.isObject(res); 36 | assert.strictEqual(res.valid, true); 37 | } 38 | 39 | function assertHasError(attr, field) { 40 | return function (res) { 41 | assert.notEqual(res.errors.length, 0); 42 | assert.ok(res.errors.some(function (e) { 43 | return e.attribute === attr && (field ? e.property === field : true); 44 | })); 45 | }; 46 | } 47 | 48 | function assertHasErrorMsg(attr, msg) { 49 | return function (res) { 50 | assert.notEqual(res.errors.length, 0); 51 | assert.ok(res.errors.some(function (e) { 52 | return e.attribute === attr && e.message === msg; 53 | })); 54 | }; 55 | } 56 | 57 | function assertValidates(passingValue, failingValue, attributes) { 58 | var schema = { 59 | name: 'Resource', 60 | properties: { field: {} } 61 | }; 62 | 63 | var failing; 64 | 65 | if (!attributes) { 66 | attributes = failingValue; 67 | failing = false; 68 | } else { 69 | failing = true; 70 | } 71 | 72 | var attr = Object.keys(attributes)[0]; 73 | revalidator.mixin(schema.properties.field, attributes); 74 | 75 | var result = { 76 | "when the object conforms": { 77 | topic: function () { 78 | return revalidator.validate({ field: passingValue }, schema); 79 | }, 80 | "return an object with `valid` set to true": assertValid 81 | } 82 | }; 83 | 84 | if (failing) { 85 | result["when the object does not conform"] ={ 86 | topic: function () { 87 | return revalidator.validate({ field: failingValue }, schema); 88 | }, 89 | "return an object with `valid` set to false": assertInvalid, 90 | "and an error concerning the attribute": assertHasError(Object.keys(attributes)[0], 'field') 91 | }; 92 | } 93 | 94 | return result; 95 | } 96 | 97 | vows.describe('revalidator', { 98 | "Validating": { 99 | "with :'string'": assertValidates ('hello', 42, { type: "string" }), 100 | "with :'number'": assertValidates (42, 'hello', { type: "number" }), 101 | "with :'integer'": assertValidates (42, 42.5, { type: "integer" }), 102 | "with :'integer' decimal": assertValidates (10000000000, 10000000000.5, { type: "integer" }), 103 | "with :'array'": assertValidates ([4, 2], 'hi', { type: "array" }), 104 | "with :'object'": assertValidates ({}, [], { type: "object" }), 105 | "with :'boolean'": assertValidates (false, 42, { type: "boolean" }), 106 | "with :bool,num": assertValidates (false, 'hello', { type: ["boolean", "number"] }), 107 | "with :bool,num 2": assertValidates (544, null, { type: ["boolean", "number"] }), 108 | "with :'null'": assertValidates (null, false, { type: "null" }), 109 | "with :'any'": assertValidates (9, { type: "any" }), 110 | "with :'date'": assertValidates (new Date(), 'hello', { type: "date" }), 111 | "with ": assertValidates ("kaboom", "42", { pattern: /^[a-z]+$/ }), 112 | "with ": assertValidates ("boom", "kaboom", { maxLength: 4 }), 113 | "with ": assertValidates ("kaboom", "boom", { minLength: 6 }), 114 | "with ": assertValidates ("hello", "", { allowEmpty: false }), 115 | "with ": assertValidates ( 512, 43, { minimum: 473 }), 116 | "with ": assertValidates ( 512, 1949, { maximum: 678 }), 117 | "with ": assertValidates ( 10, 9, { divisibleBy: 5 }), 118 | "with decimal": assertValidates ( 0.2, 0.009, { divisibleBy: 0.01 }), 119 | "with ": assertValidates ("orange", "cigar", { enum: ["orange", "apple", "pear"] }), 120 | "with :'url'": assertValidates ('http://test.com/', 'hello', { format: 'url' }), 121 | "with ": { 122 | topic: { 123 | properties: { 124 | town: { dependencies: "country" }, 125 | country: { } 126 | } 127 | }, 128 | "when the object conforms": { 129 | topic: function (schema) { 130 | return revalidator.validate({ town: "luna", country: "moon" }, schema); 131 | }, 132 | "return an object with `valid` set to true": assertValid 133 | }, 134 | "when the object does not conform": { 135 | topic: function (schema) { 136 | return revalidator.validate({ town: "luna" }, schema); 137 | }, 138 | "return an object with `valid` set to false": assertInvalid, 139 | "and an error concerning the attribute": assertHasError('dependencies') 140 | } 141 | }, 142 | "with as array": { 143 | topic: { 144 | properties: { 145 | town: { dependencies: ["country", "planet"] }, 146 | country: { }, 147 | planet: { } 148 | } 149 | }, 150 | "when the object conforms": { 151 | topic: function (schema) { 152 | return revalidator.validate({ town: "luna", country: "moon", planet: "mars" }, schema); 153 | }, 154 | "return an object with `valid` set to true": assertValid 155 | }, 156 | "when the object does not conform": { 157 | topic: function (schema) { 158 | return revalidator.validate({ town: "luna", planet: "mars" }, schema); 159 | }, 160 | "return an object with `valid` set to false": assertInvalid, 161 | "and an error concerning the attribute": assertHasError('dependencies') 162 | } 163 | }, 164 | "with as schema": { 165 | topic: { 166 | properties: { 167 | town: { 168 | type: 'string', 169 | dependencies: { 170 | properties: { x: { type: "number" } } 171 | } 172 | }, 173 | country: { } 174 | } 175 | }, 176 | "when the object conforms": { 177 | topic: function (schema) { 178 | return revalidator.validate({ town: "luna", x: 1 }, schema); 179 | }, 180 | "return an object with `valid` set to true": assertValid, 181 | }, 182 | "when the object does not conform": { 183 | topic: function (schema) { 184 | return revalidator.validate({ town: "luna", x: 'no' }, schema); 185 | }, 186 | "return an object with `valid` set to false": assertInvalid 187 | } 188 | }, 189 | "with :'integer' and": { 190 | " constraints": assertValidates ( 512, 43, { minimum: 473, type: 'integer' }), 191 | " constraints": assertValidates ( 512, 1949, { maximum: 678, type: 'integer' }), 192 | " constraints": assertValidates ( 10, 9, { divisibleBy: 5, type: 'integer' }) 193 | }, 194 | "with :false": { 195 | topic: { 196 | properties: { 197 | town: { type: 'string' } 198 | }, 199 | additionalProperties: false 200 | }, 201 | "when the object conforms": { 202 | topic: function (schema) { 203 | return revalidator.validate({ town: "luna" }, schema); 204 | }, 205 | "return an object with `valid` set to true": assertValid 206 | }, 207 | "when the object does not conform": { 208 | topic: function (schema) { 209 | return revalidator.validate({ town: "luna", area: 'park' }, schema); 210 | }, 211 | "return an object with `valid` set to false": assertInvalid 212 | } 213 | }, 214 | "with option :false": { 215 | topic: { 216 | properties: { 217 | town: { type: 'string' } 218 | } 219 | }, 220 | "when the object conforms": { 221 | topic: function (schema) { 222 | return revalidator.validate({ town: "luna" }, schema, {additionalProperties: false}); 223 | }, 224 | "return an object with `valid` set to true": assertValid 225 | }, 226 | "when the object does not conform": { 227 | topic: function (schema) { 228 | return revalidator.validate({ town: "luna", area: 'park' }, schema, {additionalProperties: false}); 229 | }, 230 | "return an object with `valid` set to false": assertInvalid 231 | }, 232 | "but overridden to true at schema": { 233 | topic: { 234 | properties: { 235 | town: { type: 'string' } 236 | }, 237 | additionalProperties: true 238 | }, 239 | "when the object does not conform": { 240 | topic: function (schema) { 241 | return revalidator.validate({ town: "luna", area: 'park' }, schema, {additionalProperties: false}); 242 | }, 243 | "return an object with `valid` set to true": assertValid 244 | } 245 | } 246 | } 247 | } 248 | }) 249 | .addBatch({ 250 | "An array schema": { 251 | topic: { 252 | type: 'array', 253 | items: { 254 | type: 'number' 255 | } 256 | }, 257 | "and a valid object object": { 258 | topic: [1,2,3], 259 | "can be validated with `revalidator.validate`": { 260 | "and if it conforms": { 261 | topic: function (object, schema) { 262 | return revalidator.validate(object, schema); 263 | }, 264 | "return an object with the `valid` property set to true": assertValid, 265 | "return an object with the `errors` property as an empty array": function (res) { 266 | assert.isArray(res.errors); 267 | assert.isEmpty(res.errors); 268 | } 269 | }, 270 | } 271 | }, 272 | "and an invalid object": { 273 | topic: [1,'a',3], 274 | "can be validated with `revalidator.validate`": { 275 | "and if it conforms": { 276 | topic: function (object, schema) { 277 | return revalidator.validate(object, schema); 278 | }, 279 | "return an object with the `valid` property set to false": assertInvalid, 280 | "and an error concerning the `type` attribute": assertHasError('type', '1') 281 | }, 282 | } 283 | } 284 | }, 285 | "A grid schema": { 286 | topic: { 287 | type: 'array', 288 | items: { 289 | type: 'array', 290 | maxItems: '2', 291 | items: { 292 | type: 'null', 293 | } 294 | } 295 | }, 296 | "and a valid object object": { 297 | topic: [[null,null]], 298 | "can be validated with `revalidator.validate`": { 299 | "and if it conforms": { 300 | topic: function (object, schema) { 301 | return revalidator.validate(object, schema); 302 | }, 303 | "return an object with the `valid` property set to true": assertValid, 304 | "return an object with the `errors` property as an empty array": function (res) { 305 | assert.isArray(res.errors); 306 | assert.isEmpty(res.errors); 307 | } 308 | }, 309 | } 310 | }, 311 | "and an invalid object": { 312 | topic: [[null, null, null], [1,'2',true], [null, null, {foo: 'bar'}]], 313 | "can be validated with `revalidator.validate`": { 314 | "and if it conforms": { 315 | topic: function (object, schema) { 316 | return revalidator.validate(object, schema); 317 | }, 318 | "return an object with the `valid` property set to false": assertInvalid, 319 | "and an error concerning the `type` attribute at 1.1": assertHasError('type', '1.0'), 320 | "and an error concerning the `type` attribute at 1.2": assertHasError('type', '1.1'), 321 | "and an error concerning the `type` attribute at 1.3": assertHasError('type', '1.2'), 322 | "and an error concerning the `type` attribute at 2.2": assertHasError('type', '2.2'), 323 | "and an error concerning the `maxItems` attribute at 0": assertHasError('maxItems', '0'), 324 | "and an error concerning the `maxItems` attribute at 1": assertHasError('maxItems', '1'), 325 | "and an error concerning the `maxItems` attribute at 2": assertHasError('maxItems', '2') 326 | }, 327 | } 328 | }, 329 | }, 330 | 'A schema with an array as root element': { 331 | topic: { 332 | name: 'Array of Articles', 333 | type: 'array', 334 | items: { 335 | type: 'object', 336 | properties: { 337 | title: { 338 | type: 'string', 339 | maxLength: 140, 340 | conditions: { 341 | optional: function () { 342 | return !this.published; 343 | } 344 | } 345 | }, 346 | date: { type: 'string', format: 'date', messages: { format: "must be a valid %{expected} and nothing else" } }, 347 | body: { type: 'string' }, 348 | tags: { 349 | type: 'array', 350 | uniqueItems: true, 351 | minItems: 2, 352 | items: { 353 | type: 'string', 354 | pattern: /[a-z ]+/ 355 | } 356 | }, 357 | tuple: { 358 | type: 'array', 359 | minItems: 2, 360 | maxItems: 2, 361 | items: { 362 | type: ['string', 'number'] 363 | } 364 | }, 365 | author: { type: 'string', pattern: /^[\w ]+$/i, required: true, messages: { required: "is essential for survival" } }, 366 | published: { type: 'boolean', 'default': false }, 367 | category: { type: 'string' }, 368 | palindrome: {type: 'string', conform: function(val) { 369 | return val == val.split("").reverse().join(""); } 370 | }, 371 | name: { type: 'string', default: '', conform: function(val, data) { 372 | return (val === data.author); } 373 | } 374 | }, 375 | patternProperties: { 376 | '^_': { 377 | type: 'boolean', default: false 378 | } 379 | } 380 | } 381 | }, 382 | "and an array": { 383 | topic: [{ 384 | title: 'Gimme some Gurus', 385 | date: '2012-02-04', 386 | body: "And I will pwn your codex.", 387 | tags: ['energy drinks', 'code'], 388 | tuple: ['string0', 103], 389 | author: 'cloudhead', 390 | published: true, 391 | category: 'misc', 392 | palindrome: 'dennis sinned', 393 | name: 'cloudhead', 394 | _flag: true 395 | },{ 396 | title: 'Gimme some Gurus', 397 | date: '2012-02-04', 398 | body: "And I will pwn your codex.", 399 | tags: ['energy drinks', 'code'], 400 | tuple: ['string0', 103], 401 | author: 'cloudhead', 402 | published: true, 403 | category: 'misc', 404 | palindrome: 'dennis sinned', 405 | name: 'cloudhead', 406 | _flag: true 407 | }], 408 | "can be validated with `revalidator.validate`": { 409 | "and if it conforms": { 410 | topic: function (object, schema) { 411 | return revalidator.validate(object, schema); 412 | }, 413 | "return an object with the `valid` property set to true": assertValid, 414 | "return an object with the `errors` property as an empty array": function (res) { 415 | assert.isArray(res.errors); 416 | assert.isEmpty(res.errors); 417 | } 418 | }, 419 | "and if it has a missing required property": { 420 | topic: function (object, schema) { 421 | object = clone(object); 422 | delete object[1].author; 423 | return revalidator.validate(object, schema); 424 | }, 425 | "return an object with `valid` set to false": assertInvalid, 426 | "and an error concerning the 'required' attribute": assertHasError('required'), 427 | "and the error message defined": assertHasErrorMsg('required', "is essential for survival") 428 | }, 429 | "and if it has a missing non-required property": { 430 | topic: function (object, schema) { 431 | object = clone(object); 432 | delete object[1].category; 433 | return revalidator.validate(object, schema); 434 | }, 435 | "return an object with `valid` set to false": assertValid 436 | }, 437 | "and if it has a incorrect pattern property": { 438 | topic: function (object, schema) { 439 | object = clone(object); 440 | object[1]._additionalFlag = 'text'; 441 | return revalidator.validate(object, schema); 442 | }, 443 | "return an object with `valid` set to false": assertInvalid 444 | }, 445 | "and if it has a incorrect unique array property": { 446 | topic: function (object, schema) { 447 | object = clone(object); 448 | object[1].tags = ['a', 'a']; 449 | return revalidator.validate(object, schema); 450 | }, 451 | "return an object with `valid` set to false": assertInvalid 452 | }, 453 | "and if it has a incorrect array property (wrong values)": { 454 | topic: function (object, schema) { 455 | object = clone(object); 456 | object[1].tags = ['a', '____']; 457 | return revalidator.validate(object, schema); 458 | }, 459 | "return an object with `valid` set to false": assertInvalid 460 | }, 461 | "and if it has a incorrect array property (< minItems)": { 462 | topic: function (object, schema) { 463 | object = clone(object); 464 | object[1].tags = ['x']; 465 | return revalidator.validate(object, schema); 466 | }, 467 | "return an object with `valid` set to false": assertInvalid 468 | }, 469 | "and if it has a incorrect format (date)": { 470 | topic: function (object, schema) { 471 | object = clone(object); 472 | object[1].date = 'bad date'; 473 | return revalidator.validate(object, schema); 474 | }, 475 | "return an object with `valid` set to false": assertInvalid, 476 | "and the error message defined": assertHasErrorMsg('format', "must be a valid date and nothing else") 477 | }, 478 | "and if it is not a palindrome (conform function)": { 479 | topic: function (object, schema) { 480 | object = clone(object); 481 | object[1].palindrome = 'bad palindrome'; 482 | return revalidator.validate(object, schema); 483 | }, 484 | "return an object with `valid` set to false": assertInvalid 485 | }, 486 | "and if it didn't validate a pattern": { 487 | topic: function (object, schema) { 488 | object = clone(object); 489 | object[1].author = 'email@address.com'; 490 | return revalidator.validate(object, schema); 491 | }, 492 | "return an object with `valid` set to false": assertInvalid, 493 | "and an error concerning the 'pattern' attribute": assertHasError('pattern') 494 | } 495 | } 496 | } 497 | } 498 | }).addBatch({ 499 | "A schema": { 500 | topic: { 501 | name: 'Article', 502 | properties: { 503 | title: { 504 | type: 'string', 505 | maxLength: 140, 506 | conditions: { 507 | optional: function () { 508 | return !this.published; 509 | } 510 | } 511 | }, 512 | date: { type: 'string', format: 'date', messages: { format: "must be a valid %{expected} and nothing else" } }, 513 | body: { type: 'string' }, 514 | tags: { 515 | type: 'array', 516 | uniqueItems: true, 517 | minItems: 2, 518 | items: { 519 | type: 'string', 520 | pattern: /[a-z ]+/ 521 | } 522 | }, 523 | tuple: { 524 | type: 'array', 525 | minItems: 2, 526 | maxItems: 2, 527 | items: { 528 | type: ['string', 'number'] 529 | } 530 | }, 531 | publisher: { 532 | type: 'object', 533 | properties: { 534 | name: { type: 'string' }, 535 | agents: { 536 | type: 'array', 537 | items: { 538 | type: 'object', 539 | properties: { name: { type: 'string' } } 540 | } 541 | } 542 | } 543 | }, 544 | author: { type: 'string', pattern: /^[\w ]+$/i, required: true, messages: { required: "is essential for survival" } }, 545 | published: { type: 'boolean', 'default': false }, 546 | category: { type: 'string' }, 547 | palindrome: {type: 'string', conform: function(val) { 548 | return val == val.split("").reverse().join(""); } 549 | }, 550 | name: { type: 'string', default: '', conform: function(val, data) { 551 | return (val === data.author); } 552 | } 553 | }, 554 | patternProperties: { 555 | '^_': { 556 | type: 'boolean', default: false 557 | } 558 | } 559 | }, 560 | "and an object": { 561 | topic: { 562 | title: 'Gimme some Gurus', 563 | date: '2012-02-04', 564 | body: "And I will pwn your codex.", 565 | tags: ['energy drinks', 'code'], 566 | tuple: ['string0', 103], 567 | publisher:{ 568 | name: 'jungletours', 569 | agents: [ 570 | { name: 'sandro' }, 571 | { name: 'jose' } 572 | ] 573 | }, 574 | author: 'cloudhead', 575 | published: true, 576 | category: 'misc', 577 | palindrome: 'dennis sinned', 578 | name: 'cloudhead', 579 | _flag: true 580 | }, 581 | "can be validated with `revalidator.validate`": { 582 | "and if it conforms": { 583 | topic: function (object, schema) { 584 | return revalidator.validate(object, schema); 585 | }, 586 | "return an object with the `valid` property set to true": assertValid, 587 | "return an object with the `errors` property as an empty array": function (res) { 588 | assert.isArray(res.errors); 589 | assert.isEmpty(res.errors); 590 | } 591 | }, 592 | "and if it has a nested object which does not conform": { 593 | topic: function (object, schema) { 594 | object = clone(object); 595 | object.publisher.name = null; 596 | return revalidator.validate(object, schema); 597 | }, 598 | "return an object with `valid` set to false": assertInvalid, 599 | "and an error concerning the 'required' attribute": assertHasError('type', 'publisher.name') 600 | }, 601 | "and if it has an object within an array which does not conform": { 602 | topic: function (object, schema) { 603 | object = clone(object); 604 | object.publisher.agents[1] = {name: null}; 605 | return revalidator.validate(object, schema); 606 | }, 607 | "return an object with `valid` set to false": assertInvalid, 608 | "and an error concerning the 'required' attribute": assertHasError('type', 'publisher.agents.1.name') 609 | }, 610 | "and if it has a missing required property": { 611 | topic: function (object, schema) { 612 | object = clone(object); 613 | delete object.author; 614 | return revalidator.validate(object, schema); 615 | }, 616 | "return an object with `valid` set to false": assertInvalid, 617 | "and an error concerning the 'required' attribute": assertHasError('required'), 618 | "and the error message defined": assertHasErrorMsg('required', "is essential for survival") 619 | }, 620 | "and if it has a missing non-required property": { 621 | topic: function (object, schema) { 622 | object = clone(object); 623 | delete object.category; 624 | return revalidator.validate(object, schema); 625 | }, 626 | "return an object with `valid` set to false": assertValid 627 | }, 628 | "and if it has a incorrect pattern property": { 629 | topic: function (object, schema) { 630 | object = clone(object); 631 | object._additionalFlag = 'text'; 632 | return revalidator.validate(object, schema); 633 | }, 634 | "return an object with `valid` set to false": assertInvalid 635 | }, 636 | "and if it has a incorrect unique array property": { 637 | topic: function (object, schema) { 638 | object = clone(object); 639 | object.tags = ['a', 'a']; 640 | return revalidator.validate(object, schema); 641 | }, 642 | "return an object with `valid` set to false": assertInvalid 643 | }, 644 | "and if it has a correct array property (uniqueItems false)": { 645 | topic: function (object, schema) { 646 | object = clone(object); 647 | schema = clone(schema); 648 | schema.properties.tags.uniqueItems = false; 649 | object.tags = ['a', 'a']; 650 | return revalidator.validate(object, schema); 651 | }, 652 | "return an object with `valid` set to false": assertValid 653 | }, 654 | "and if it has a incorrect array property (wrong values)": { 655 | topic: function (object, schema) { 656 | object = clone(object); 657 | object.tags = ['a', '____']; 658 | return revalidator.validate(object, schema); 659 | }, 660 | "return an object with `valid` set to false": assertInvalid 661 | }, 662 | "and if it has a incorrect array property (< minItems)": { 663 | topic: function (object, schema) { 664 | object = clone(object); 665 | object.tags = ['x']; 666 | return revalidator.validate(object, schema); 667 | }, 668 | "return an object with `valid` set to false": assertInvalid 669 | }, 670 | "and if it has a incorrect format (date)": { 671 | topic: function (object, schema) { 672 | object = clone(object); 673 | object.date = 'bad date'; 674 | return revalidator.validate(object, schema); 675 | }, 676 | "return an object with `valid` set to false": assertInvalid, 677 | "and the error message defined": assertHasErrorMsg('format', "must be a valid date and nothing else") 678 | }, 679 | "and if it is not a palindrome (conform function)": { 680 | topic: function (object, schema) { 681 | object = clone(object); 682 | object.palindrome = 'bad palindrome'; 683 | return revalidator.validate(object, schema); 684 | }, 685 | "return an object with `valid` set to false": assertInvalid 686 | }, 687 | "and if it didn't validate a pattern": { 688 | topic: function (object, schema) { 689 | object = clone(object); 690 | object.author = 'email@address.com'; 691 | return revalidator.validate(object, schema); 692 | }, 693 | "return an object with `valid` set to false": assertInvalid, 694 | "and an error concerning the 'pattern' attribute": assertHasError('pattern') 695 | }, 696 | } 697 | }, 698 | "with option": { 699 | topic: { 700 | properties: { 701 | answer: { type: "integer" }, 702 | is_ready: { type: "boolean" } 703 | } 704 | }, 705 | "and property": { 706 | "is castable string": { 707 | topic: function (schema) { 708 | return revalidator.validate({ answer: "42" }, schema, { cast: true }); 709 | }, 710 | "return an object with `valid` set to true": assertValid 711 | }, 712 | "is uncastable string": { 713 | topic: function (schema) { 714 | return revalidator.validate({ answer: "forty2" }, schema, { cast: true }); 715 | }, 716 | "return an object with `valid` set to false": assertInvalid 717 | }, 718 | "is casted to integer": { 719 | topic: function (schema) { 720 | var object = { answer: "42" }; 721 | revalidator.validate(object, schema, { cast: true }); 722 | return object; 723 | }, 724 | "return an object with `answer` set to 42": function(res) { assert.strictEqual(res.answer, 42); } 725 | } 726 | }, 727 | "and property": { 728 | "is castable 'true/false' string": { 729 | topic: function (schema) { 730 | return revalidator.validate({ is_ready: "true" }, schema, { cast: true }); 731 | }, 732 | "return an object with `valid` set to true": assertValid 733 | }, 734 | "is castable '1/0' string": { 735 | topic: function (schema) { 736 | return revalidator.validate({ is_ready: "1" }, schema, { cast: true }); 737 | }, 738 | "return an object with `valid` set to true": assertValid 739 | }, 740 | "is castable `1/0` integer": { 741 | topic: function (schema) { 742 | return revalidator.validate({ is_ready: 1 }, schema, { cast: true }); 743 | }, 744 | "return an object with `valid` set to true": assertValid 745 | }, 746 | "is uncastable string": { 747 | topic: function (schema) { 748 | return revalidator.validate({ is_ready: "not yet" }, schema, { cast: true }); 749 | }, 750 | "return an object with `valid` set to false": assertInvalid 751 | }, 752 | "is uncastable number": { 753 | topic: function (schema) { 754 | return revalidator.validate({ is_ready: 42 }, schema, { cast: true }); 755 | }, 756 | "return an object with `valid` set to false": assertInvalid 757 | }, 758 | "is casted to boolean": { 759 | topic: function (schema) { 760 | var object = { is_ready: "true" }; 761 | revalidator.validate(object, schema, { cast: true }); 762 | return object; 763 | }, 764 | "return an object with `is_ready` set to true": function(res) { assert.strictEqual(res.is_ready, true); } 765 | } 766 | }, 767 | "default true": { 768 | topic: function(schema) { 769 | revalidator.validate.defaults.cast = true; 770 | return schema; 771 | }, 772 | "and no direct option passed to validate": { 773 | "and castable number": { 774 | topic: function (schema) { 775 | return revalidator.validate({ answer: "42" }, schema); 776 | }, 777 | "return an object with `valid` set to true": assertValid 778 | } 779 | }, 780 | "and direct false passed to validate": { 781 | "and castable number": { 782 | topic: function (schema) { 783 | return revalidator.validate({ answer: "42" }, schema, { cast: false }); 784 | }, 785 | "return an object with `valid` set to false": assertInvalid 786 | } 787 | } 788 | } 789 | } 790 | } 791 | }).export(module); 792 | --------------------------------------------------------------------------------