├── .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 [](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 |
--------------------------------------------------------------------------------