├── .jshintrc ├── HISTORY.md ├── LICENSE ├── README.md ├── bower.json ├── build.sh ├── index.js ├── js-schema.debug.js ├── js-schema.min.js ├── lib ├── BaseSchema.js ├── extensions │ ├── Array.js │ ├── Boolean.js │ ├── Function.js │ ├── Number.js │ ├── Object.js │ ├── Schema.js │ └── String.js ├── patterns │ ├── anything.js │ ├── class.js │ ├── equality.js │ ├── nothing.js │ ├── object.js │ ├── or.js │ ├── reference.js │ ├── regexp.js │ └── schema.js └── schema.js ├── package.json ├── test.sh └── test ├── extensions ├── errors-array.js ├── errors-boolean.js ├── errors-number.js ├── errors-object.js ├── errors-reference.js └── validate-object.js └── printTestResult.js /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 20, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : false, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 14 | "indent" : 2, // {int} Number of spaces to use for indentation 15 | "latedef" : false, // true: Require variables/functions to be defined before being used 16 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 17 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 18 | "noempty" : true, // true: Prohibit use of empty blocks 19 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 20 | "plusplus" : false, // true: Prohibit use of `++` & `--` 21 | "quotmark" : false, // Quotation mark consistency: 22 | // false : do nothing (default) 23 | // true : ensure whatever is used is consistent 24 | // "single" : require single quotes 25 | // "double" : require double quotes 26 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 27 | "unused" : true, // true: Require all defined variables be used 28 | "strict" : false, // true: Requires all functions run in ES5 Strict Mode 29 | "maxparams" : false, // {int} Max number of formal params allowed per function 30 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 31 | "maxstatements" : false, // {int} Max number statements per function 32 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 33 | "maxlen" : false, // {int} Max number of characters per line 34 | 35 | // Relaxing 36 | "asi" : true, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 37 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 38 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 39 | "eqnull" : true, // true: Tolerate use of `== null` 40 | "es5" : true, // true: Allow ES5 syntax (ex: getters and setters) 41 | "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) 42 | "moz" : true, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 43 | // (ex: `for each`, multiple try/catch, function expression…) 44 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 45 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 46 | "funcscope" : false, // true: Tolerate defining variables inside control statements 47 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 48 | "iterator" : false, // true: Tolerate using the `__iterator__` property 49 | "lastsemic" : true, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 50 | "laxbreak" : true, // true: Tolerate possibly unsafe line breakings 51 | "laxcomma" : true, // true: Tolerate comma-first style coding 52 | "loopfunc" : false, // true: Tolerate functions being defined in loops 53 | "multistr" : false, // true: Tolerate multi-line strings 54 | "proto" : false, // true: Tolerate using the `__proto__` property 55 | "scripturl" : false, // true: Tolerate script-targeted URLs 56 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 57 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 58 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 59 | "validthis" : false, // true: Tolerate using this in a non-constructor function 60 | 61 | // Environments 62 | "browser" : true, // Web Browser (window, document, etc) 63 | "browserify" : true, // Browserify (node.js code in the browser) 64 | "couch" : false, // CouchDB 65 | "devel" : true, // Development/debugging (alert, confirm, etc) 66 | "dojo" : false, // Dojo Toolkit 67 | "jquery" : false, // jQuery 68 | "mootools" : false, // MooTools 69 | "node" : true, // Node.js 70 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 71 | "prototypejs" : false, // Prototype and Scriptaculous 72 | "rhino" : false, // Rhino 73 | "worker" : false, // Web Workers 74 | "wsh" : false, // Windows Scripting Host 75 | "yui" : false, // Yahoo User Interface 76 | 77 | // Custom Globals 78 | "globals" : {} // additional predefined global variables 79 | } 80 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | Version history 2 | =============== 3 | 4 | ### 1.0.1 (2014-05-06) ### 5 | * Add missing concatenated and minified versions 6 | 7 | ### 1.0.0 (2014-05-03) ### 8 | * Proper implementation of regexp and quantified properties 9 | * The library has been stable for a long time now, so it's time to release 1.0. It will also make it possible 10 | to signal API breakage in the future (using semantic versioning). 11 | 12 | ### 0.7.2 (2014-12-02) ### 13 | * Reverting the object pattern fix released in 0.7.1 because of breaking error reporting functionality 14 | 15 | ### 0.7.1 (2014-11-09) ### 16 | * A bugfix for OrSchema `errors()` method (thanks to Kuba Wyrobek) 17 | * Type checking for `Array.of()` that prevents misusages like: 18 | `Array.of(schema, length)` or `Array.of(schema, minLength, maxLength)` (thanks to Kuba Wyrobek) 19 | * A bugfix for the object pattern to handle `{ "+.+" : String }` and similar patterns correctly 20 | (thanks to Alex Ivanov) 21 | * The `String.of()` extension now has `.` as default instead of `[a-zA-Z0-9]` (thanks to Mikael Berg) 22 | 23 | ### 0.7.0 (2014-09-01) ### 24 | * Support for error reporting 25 | * Addition of .jshintrc file for code style consistency 26 | * Addition of unit tests 27 | 28 | ### 0.6.4 (2014-08-11) ### 29 | * Fixing Bower issues 30 | 31 | ### 0.6.3 (2014-05-03) ### 32 | * Bower front-end package manager support 33 | 34 | ### 0.6.2 (2013-02-03) ### 35 | * Fixing Windows and Mac compatibility issues (#3, #5) 36 | 37 | ### 0.6.1 (2012-08-19) ### 38 | * Minor bugfixes 39 | 40 | ### 0.6.0 (2012-08-18) ### 41 | * Support for general purpose referencing (with JSON Schema serialization/deserialization) 42 | * Property description support 43 | * Splitting out random generation into a separate library (molnarg/generate.js) 44 | * Removal of the premature compilation system (optimizations will reappear in a future release) 45 | * Cleaner codebase 46 | * Js-schema is now self-contained: it has no dependencies 47 | * Reduced size: 15kb -> 5kb (minified and gzipped) 48 | 49 | ### 0.5.0 (2012-06-27) ### 50 | * Support for self-referencing 51 | 52 | ### 0.4.1 (2012-06-19) ### 53 | * Browser support 54 | 55 | ### 0.4.0 (2012-06-17) ### 56 | * Regexp object properties 57 | * Enhanced JSON Schema serialization/deserialization support 58 | * Deep equality pattern 59 | 60 | ### 0.3.0 (2012-06-13) ### 61 | * Optional properties in objects 62 | * Introduction of schema.generate() 63 | 64 | ### 0.2.0 (2012-06-04) ### 65 | * Compilation infrastructure 66 | * Pattern documentation 67 | 68 | ### 0.1.1 (2012-05-27) ### 69 | * Basic patterns are in place. 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (C) 2012 Gábor Molnár 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | js-schema 2 | ========= 3 | 4 | js-schema is a new way of describing object schemas in JavaScript. It has a clean and simple syntax, 5 | and it is capable of serializing to/from the popular JSON Schema format. The typical use case is 6 | declarative object validation. 7 | 8 | **Latest release**: 1.0.1 (2015/05/06) 9 | 10 | Features 11 | ======== 12 | 13 | Defining a schema: 14 | 15 | ```javascript 16 | var Duck = schema({ // A duck 17 | swim : Function, // - can swim 18 | quack : Function, // - can quack 19 | age : Number.min(0).max(5), // - is 0 to 5 years old 20 | color : ['yellow', 'brown'] // - has either yellow or brown color 21 | }); 22 | ``` 23 | 24 | The resulting function (`Duck`) can be used to check objects against the declared schema: 25 | 26 | ```javascript 27 | // Some animals 28 | var myDuck = { swim : function() {}, quack : function() {}, age : 2, color : 'yellow' }, 29 | myCat = { walk : function() {}, purr : function() {}, age : 3, color : 'black' }, 30 | animals = [ myDuck, myCat, {}, /*...*/ ]; 31 | 32 | // Simple checks 33 | console.log( Duck(myDuck) ); // true 34 | console.log( Duck(myCat) ); // false 35 | 36 | // Using the schema function with filter 37 | var ducks = animals.filter( Duck ); // every Duck-like animal 38 | var walking = animals.filter( schema({ walk : Function }) ); // every animal that can walk 39 | ``` 40 | 41 | It is also possible to define self-referencing data structures: 42 | 43 | ```javascript 44 | var Tree = schema({ left : [ Number, Tree ], right : [ Number, Tree ] }); 45 | 46 | console.log( Tree({ left : 3, right : 3 }) ); // true 47 | console.log( Tree({ left : 3, right : { left: 5, right: 5 } }) ); // true 48 | console.log( Tree({ left : 3, right : { left: 5, right: 's' } }) ); // false 49 | ``` 50 | 51 | Error reporting: 52 | 53 | ```javascript 54 | Duck.errors({ 55 | swim: function() {}, 56 | quack: function() {}, 57 | age: 6, 58 | color: 'green' 59 | }); 60 | 61 | // { 62 | // age: 'number = 6 is bigger than required maximum = 5', 63 | // color: 'string = green is not reference to string = yellow AND 64 | // string = green is not reference to string = brown' 65 | // } 66 | ``` 67 | 68 | Usage 69 | ===== 70 | 71 | Include js-schema in your project with `var schema = require('js-schema');` in node.js or with 72 | `` in the browser. AMD module loading is also supported. 73 | 74 | The first parameter passed to the `schema` function describes the schema, and the return value 75 | is a new function called validator. Then the validator can be used to check any object against 76 | the described schema as in the example above. 77 | 78 | There are various patterns that can be used to describe a schema. For example, 79 | `schema({n : Number})` returns a validation function which returns true when called 80 | with an object that has a number type property called `n`. This is a combination of the 81 | object pattern and the instanceof pattern. Most of the patterns are pretty intuitive, so 82 | reading a schema description is quite easy even if you are not familiar with js-schema. 83 | Most patterns accept other patterns as parameters, so composition of patterns is very easy. 84 | 85 | Extensions are functions that return validator by themselves without using the `schema` function 86 | as wrapper. These extensions are usually tied to native object constructors, like `Array`, 87 | `Number`, or `String`, and can be used everywhere where a pattern is expected. Examples 88 | include `Array.of(X)`, `Number.min(X)`. 89 | 90 | For serialization to JSON Schema use the `toJSON()` method of any schema (it returns an object) 91 | or call `JSON.stringify(x)` on the schema (to get a string). For deserialization use 92 | `schema.fromJSON(json)`. JSON Schema support is still incomplete, but it can reliably deserialize 93 | JSON Schemas generated by js-schema itself. 94 | 95 | Patterns 96 | ======== 97 | 98 | ### Basic rules ### 99 | 100 | There are 10 basic rules used for describing schemas: 101 | 102 | 1. `Class` (where `Class` is a function, and has a function type property called `schema`) 103 | matches `x` if `Class.schema(x) === true`. 104 | 2. `Class` (where `Class` is a function) matches `x` if `x instanceof Class`. 105 | 3. `/regexp/` matches `x` if `/regexp/.test(x) === true`. 106 | 4. `[object]` matches `x` if `x` is deep equal to `object` 107 | 5. `[pattern1, pattern2, ...]` matches `x` if _any_ of the given patterns match `x`. 108 | 6. `{ 'a' : pattern1, 'b' : pattern2, ... }` matches `x` if `pattern1` matches `x.a`, 109 | `pattern2` matches `x.b`, etc. For details see the object pattern subsection. 110 | 7. `primitive` (where `primitive` is boolean, number, or string) matches `x` if `primitive === x`. 111 | 8. `null` matches `x` if `x` _is_ `null` or `undefined`. 112 | 9. `undefined` matches anything. 113 | 10. `schema.self` references the schema returned by the last use of the `schema` function. 114 | For details see the self-referencing subsection. 115 | 116 | The order is important. When calling `schema(pattern)`, the rules are examined one by one, 117 | starting with the first. If there's a match, js-schema first resolves the sub-patterns, and then 118 | generates the appropriate validator function and returns it. 119 | 120 | ### Example ### 121 | 122 | The following example contains patterns for all of the rules. The comments 123 | denote the number of the rules used and the nesting level of the subpatterns (indentation). 124 | 125 | ```javascript 126 | var Color = function() {}, x = { /* ... */ }; 127 | 128 | var validate = schema({ // (6) 'object' pattern 129 | a : [ Color, 'red', 'blue', [[0,0,0]] ], // (5) 'or' pattern 130 | // (2) 'instanceof' pattern 131 | // (7) 'primitive' pattern 132 | // (4) 'deep equality' pattern 133 | b : Number, // (1) 'class schema' pattern 134 | c : /The meaning of life is \d+/, // (3) 'regexp' pattern 135 | d : undefined, // (9) 'anything' pattern 136 | e : [null, schema.self] // (5) 'or' pattern 137 | // (8) 'nothing' pattern 138 | // (10) 'self' pattern 139 | }); 140 | 141 | console.log( validate(x) ); 142 | ``` 143 | 144 | `validate(x)` returns true if all of these are true: 145 | * `x.a` is either 'red', 'blue', an instance of the Color class, 146 | or an array that is exactly like `[0,0,0]` 147 | * `x.b` conforms to Number.schema (it return true if `x.b instanceof Number`) 148 | * `x.c` is a string that matches the /The meaning of life is \d+/ regexp 149 | * `x` doesn't have a property called `e`, or it does but it is `null` or `undefined`, 150 | or it is an object that matches this schema 151 | 152 | ### The object pattern ### 153 | 154 | The object pattern is more complex than the others. Using the object pattern it is possible to 155 | define optional properties, regexp properties, etc. This extra information can be encoded in 156 | the property names. 157 | 158 | The property names in an object pattern are always regular expressions, and the given schema 159 | applies to instance properties whose name match this regexp. The number of expected matches can 160 | also be specified with `?`, `+` or `*` as the first character of the property name. `?` means 161 | 0 or 1, `*` means 0 or more, and `+` means 1 or more. A single `*` as a property name 162 | matches any instance property that is not matched by other regexps. 163 | 164 | An example of using these: 165 | ```javascript 166 | var x = { /* ... */ }; 167 | 168 | var validate = schema({ 169 | 'name' : String, // x.name must be string 170 | 'colou?r' : String // x must have a string type property called either 171 | // 'color' or 'colour' but not both 172 | '?location' : String, // if x has a property called 'location' then it must be string 173 | '*identifier-.*' : Number, // if the name of a property of x matches /identifier-.*/ then 174 | // it must be a number 175 | '+serialnumber-.*' : Number, // if the name of a property of x matches /serialnumber-.*/ then 176 | // it must be a number and there should be at least one such property 177 | '*' : Boolean // any other property that doesn't match any of these rules 178 | // must be Boolean 179 | }); 180 | 181 | assert( validate(x) === true ); 182 | ``` 183 | 184 | ### Self-referencing ### 185 | 186 | The easiest way to do self-referencing is using `schema.self`. However, to support a more 187 | intuitive notation (as seen in the Tree example above) there is an other way to reference 188 | the schema that is being described. When executing this: 189 | 190 | ```javascript 191 | var Tree = schema({ left : [ Number, Tree ], right : [ Number, Tree ] }); 192 | ``` 193 | 194 | js-schema sees in fact `{ left : [ Number, undefined ], right : [ Number, undefined ] }` as first 195 | parameter, since the value of the `Tree` variable is undefined when the schema function is 196 | called. Consider the meaning of `[ Number, undefined ]` according to the rules described above: 197 | 'this property must be either Number, or anything else'. It doesn't make much sense to include 198 | 'anything else' in an 'or' relation. If js-schema sees `undefined` in an or relation, it assumes 199 | that this is in fact a self-reference. 200 | 201 | Use this feature carefully, because it may easily lead to bugs. Only use it when the return value 202 | of the schema function is assigned to a newly defined variable. 203 | 204 | Extensions 205 | ========== 206 | 207 | ### Numbers ### 208 | 209 | There are five functions that can be used for describing number ranges: `min`, `max`, `below`, 210 | `above` and `step`. All of these are chainable, so for example `Number.min(a).below(b)` matches `x` 211 | if `a <= x && x < b`. The `Number.step(a)` matches `x` if `x` is a divisible by `a`. 212 | 213 | ### Strings ### 214 | 215 | The `String.of` method has three signatures: 216 | - `String.of(charset)` matches `x` if it is a string and contains characters that are included in `charset` 217 | - `String.of(length, charset)` additionally checks the length of the instance and returns true only if it equals to `length`. 218 | - `String.of(minLength, maxLength, charset)` is similar, but checks if the length is in the given interval. 219 | 220 | `charset` must be given in a format that can be directly inserted in a regular expression when 221 | wrapped by `[]`. For example, `'abc'` means a character set containing the first 3 lowercase letters 222 | of the english alphabet, while `'a-zA-C'` means a character set of all english lowercase letters, 223 | and the first 3 uppercase letters. If `charset` is `undefined` or `null` then there's no restriction on 224 | the character set. 225 | 226 | ### Arrays ### 227 | 228 | The `Array.like(array)` matches `x` if `x instanceof Array` and it deep equals `array`. 229 | 230 | The `Array.of` method has three signatures: 231 | - `Array.of(pattern)` matches `x` if `x instanceof Array` and `pattern` matches every element of `x`. 232 | - `Array.of(length, pattern)` additionally checks the length of the instance and returns true only if it equals to `length`. 233 | - `Array.of(minLength, maxLength, pattern)` is similar, but checks if the length is in the given interval. 234 | 235 | ### Objects ### 236 | 237 | `Object.reference(object)` matches `x` if `x === object`. 238 | 239 | `Object.like(object)` matches `x` if `x` deep equals `object`. 240 | 241 | ### Functions ### 242 | 243 | `Function.reference(func)` matches `x` if `x === func`. 244 | 245 | Future plans 246 | ============ 247 | 248 | Better JSON Schema support. js-schema should be able to parse any valid JSON schema and generate 249 | JSON Schema for most of the patterns (this is not possible in general, because of patterns that hold 250 | external references like the 'instanceof' pattern). 251 | 252 | Contributing 253 | ============ 254 | 255 | Feel free to open an issue or send a pull request if you would like to help improving js-schema or find a bug. 256 | 257 | People who made significant contributions so far: 258 | 259 | * [Alan James](https://github.com/alanjames1987) 260 | * [Kuba Wyrobek](https://github.com/parhelium) 261 | * [Mikael Berg](https://github.com/mikberg) 262 | * [Alex Ivanov](https://github.com/alex-ivanov) 263 | 264 | Installation 265 | ============ 266 | 267 | Using [npm](http://npmjs.org): 268 | 269 | npm install js-schema 270 | 271 | Using [bower](http://bower.io): 272 | 273 | bower install js-schema 274 | 275 | Build 276 | ===== 277 | 278 | To build the browser version you will need node.js and the development dependencies that can be 279 | installed with npm (type `npm install ./` in the project directory). `build.sh` 280 | assembles a debug version using browserify and then minifies it using uglify. 281 | 282 | License 283 | ======= 284 | 285 | The MIT License 286 | 287 | Copyright (C) 2012 Gábor Molnár 288 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Gábor Molnár ", 3 | "contributors": [ 4 | "Alan James", 5 | "Kuba Wyrobek", 6 | "Mikael Berg", 7 | "Alex Ivanov" 8 | ], 9 | "name": "js-schema", 10 | "description": "A simple and intuitive object validation library", 11 | "keywords": [ 12 | "json", 13 | "schema", 14 | "JSON Schema", 15 | "validation", 16 | "validator" 17 | ], 18 | "version": "1.0.1", 19 | "homepage": "https://github.com/molnarg/js-schema", 20 | "repository": { 21 | "type": "git", 22 | "url": "git@github.com:molnarg/js-schema.git" 23 | }, 24 | "dependencies": { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $(dirname $0) 4 | 5 | DEBUG="js-schema.debug.js" 6 | MIN="js-schema.min.js" 7 | 8 | if [ -f node_modules/browserify/bin/cmd.js ] 9 | then 10 | BROWSERIFY='node_modules/browserify/bin/cmd.js' 11 | else 12 | BROWSERIFY=browserify 13 | fi 14 | 15 | if [ -f node_modules/uglify-js/bin/uglifyjs ] 16 | then 17 | UGLIFY='node_modules/uglify-js/bin/uglifyjs' 18 | else 19 | UGLIFY=uglifyjs 20 | fi 21 | 22 | 23 | $BROWSERIFY index.js | sed 's|require("/index.js")|window.schema = require("/index.js")|g' > $DEBUG 24 | $UGLIFY $DEBUG >$MIN 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/schema') 2 | 3 | // Patterns 4 | require('./lib/patterns/reference') 5 | require('./lib/patterns/nothing') 6 | require('./lib/patterns/anything') 7 | require('./lib/patterns/object') 8 | require('./lib/patterns/or') 9 | require('./lib/patterns/equality') 10 | require('./lib/patterns/regexp') 11 | require('./lib/patterns/class') 12 | require('./lib/patterns/schema') 13 | 14 | // Extensions 15 | require('./lib/extensions/Boolean') 16 | require('./lib/extensions/Number') 17 | require('./lib/extensions/String') 18 | require('./lib/extensions/Object') 19 | require('./lib/extensions/Array') 20 | require('./lib/extensions/Function') 21 | require('./lib/extensions/Schema') 22 | -------------------------------------------------------------------------------- /js-schema.debug.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 && instance.length < this.min) 171 | return ( 'Array length should not be less than ' + this.min + ' and is ' + instance.length ) 172 | if (this.max < Infinity && instance.length > this.max) 173 | return ( 'Array length should not be more than ' + this.max + ' and is ' + instance.length ) 174 | } 175 | 176 | // Checking conformance to the given item schema 177 | var results = {} 178 | for (var i = 0; i < instance.length; i++) { 179 | var errs = this.itemSchema.errors(instance[i]) 180 | if (errs) { 181 | results[i] = errs 182 | } 183 | } 184 | var resultKeysArray = Object.keys(results) 185 | if (resultKeysArray.length > 0) { 186 | return results 187 | } 188 | 189 | return false 190 | }, 191 | validate: function(instance) { 192 | // Instance must be an instance of Array 193 | if (!(instance instanceof Array)) return false 194 | 195 | // Checking length 196 | if (this.min === this.max) { 197 | if (instance.length !== this.min) return false 198 | 199 | } else { 200 | if (this.min > 0 && instance.length < this.min) return false 201 | if (this.max < Infinity && instance.length > this.max) return false 202 | } 203 | 204 | // Checking conformance to the given item schema 205 | for (var i = 0; i < instance.length; i++) { 206 | if (!this.itemSchema.validate(instance[i])) return false 207 | } 208 | 209 | return true 210 | }, 211 | 212 | toJSON: Schema.session(function() { 213 | var json = Schema.prototype.toJSON.call(this, true) 214 | 215 | if (json['$ref'] != null) return json 216 | 217 | json.type = 'array' 218 | 219 | if (this.min > 0) json.minItems = this.min 220 | if (this.max < Infinity) json.maxItems = this.max 221 | if (this.itemSchema !== anything) json.items = this.itemSchema.toJSON() 222 | 223 | return json 224 | }) 225 | }) 226 | 227 | 228 | Schema.fromJSON.def(function(sch) { 229 | if (!sch || sch.type !== 'array') return 230 | 231 | // Tuple typing is not yet supported 232 | if (sch.items instanceof Array) return 233 | 234 | return new ArraySchema(Schema.fromJSON(sch.items), sch.maxItems, sch.minItems) 235 | }) 236 | 237 | Array.of = function() { 238 | // Possible signatures : (schema) 239 | // (length, schema) 240 | // (minLength, maxLength, schema) 241 | var args = Array.prototype.slice.call(arguments).reverse(); 242 | if(args.length == 3){ 243 | if(!(typeof args[2] === "number")){ 244 | throw new Error("3 arguments were passed to Array.of and 1st argument (minLength) SHOULD be number NOT " + args[2]) 245 | } 246 | if(!(typeof args[1] === "number")){ 247 | throw new Error("3 arguments were passed to Array.of and 2nd argument (maxLength) SHOULD be number NOT " + args[1]) 248 | } 249 | } 250 | if (args.length === 2) { 251 | if(!(typeof args[1] === "number")){ 252 | throw new Error("2 arguments were passed to Array.of and 1nd argument (length) SHOULD be number NOT " + args[1]) 253 | } 254 | args[2] = args[1] 255 | } 256 | return new ArraySchema(Schema.fromJS(args[0]), args[1], args[2]).wrap() 257 | } 258 | 259 | Array.like = function(other) { 260 | return new EqualitySchema(other).wrap() 261 | } 262 | 263 | Array.schema = new ArraySchema().wrap() 264 | 265 | },{"../BaseSchema":2,"../patterns/anything":10,"../patterns/equality":12}],4:[function(require,module,exports){ 266 | var Schema = require('../BaseSchema') 267 | 268 | var BooleanSchema = module.exports = Schema.extensions.BooleanSchema = new Schema.extend({ 269 | errors: function(instance) { 270 | if (!this.validate(instance)) { 271 | return ( instance + ' is not Boolean' ) 272 | } 273 | return false 274 | }, 275 | 276 | validate: function(instance) { 277 | return Object(instance) instanceof Boolean 278 | }, 279 | 280 | toJSON: function() { 281 | return { 282 | type: 'boolean' 283 | } 284 | } 285 | }) 286 | 287 | var booleanSchema = module.exports = new BooleanSchema().wrap() 288 | 289 | Schema.fromJSON.def(function(sch) { 290 | if (!sch || sch.type !== 'boolean') return 291 | 292 | return booleanSchema 293 | }) 294 | 295 | Boolean.schema = booleanSchema 296 | 297 | },{"../BaseSchema":2}],5:[function(require,module,exports){ 298 | var ReferenceSchema = require('../patterns/reference') 299 | 300 | Function.reference = function(f) { 301 | return new ReferenceSchema(f).wrap() 302 | } 303 | 304 | },{"../patterns/reference":16}],6:[function(require,module,exports){ 305 | var Schema = require('../BaseSchema') 306 | 307 | var NumberSchema = module.exports = Schema.extensions.NumberSchema = Schema.extend({ 308 | initialize: function(minimum, exclusiveMinimum, maximum, exclusiveMaximum, divisibleBy) { 309 | this.minimum = minimum != null ? minimum : -Infinity 310 | this.exclusiveMinimum = exclusiveMinimum 311 | this.maximum = minimum != null ? maximum : Infinity 312 | this.exclusiveMaximum = exclusiveMaximum 313 | this.divisibleBy = divisibleBy || 0 314 | }, 315 | 316 | min: function(minimum) { 317 | return new NumberSchema( minimum, false 318 | , this.maximum 319 | , this.exclusiveMaximum 320 | , this.divisibleBy 321 | ).wrap() 322 | }, 323 | 324 | above: function(minimum) { 325 | return new NumberSchema( minimum, true 326 | , this.maximum 327 | , this.exclusiveMaximum 328 | , this.divisibleBy 329 | ).wrap() 330 | }, 331 | 332 | max: function(maximum) { 333 | return new NumberSchema( this.minimum 334 | , this.exclusiveMinimum 335 | , maximum 336 | , false 337 | , this.divisibleBy 338 | ).wrap() 339 | }, 340 | 341 | below: function(maximum) { 342 | return new NumberSchema( this.minimum 343 | , this.exclusiveMinimum 344 | , maximum 345 | , true 346 | , this.divisibleBy 347 | ).wrap() 348 | }, 349 | 350 | step: function(divisibleBy) { 351 | return new NumberSchema( this.minimum 352 | , this.exclusiveMinimum 353 | , this.maximum 354 | , this.exclusiveMaximum 355 | , divisibleBy 356 | ).wrap() 357 | }, 358 | 359 | publicFunctions: ['min', 'above', 'max', 'below', 'step'], 360 | 361 | errors: function(instance) { 362 | var message 363 | if (!(Object(instance) instanceof Number)) { 364 | message = instance + ' is not Number' 365 | } else if (instance < this.minimum) { 366 | message = 'number = ' + instance + ' is smaller than required minimum = ' + this.minimum 367 | } else if (instance > this.maximum) { 368 | message = 'number = ' + instance + ' is bigger than required maximum = ' + this.maximum 369 | } else if (this.divisibleBy !== 0 && instance % this.divisibleBy !== 0) { 370 | message = 'number = ' + instance + ' is not divisibleBy ' + this.divisibleBy 371 | } 372 | 373 | if (message != null) { 374 | return message 375 | } 376 | return false 377 | }, 378 | 379 | validate: function(instance) { 380 | return (Object(instance) instanceof Number) && 381 | (this.exclusiveMinimum ? instance > this.minimum 382 | : instance >= this.minimum) && 383 | (this.exclusiveMaximum ? instance < this.maximum 384 | : instance <= this.maximum) && 385 | (this.divisibleBy === 0 || instance % this.divisibleBy === 0) 386 | }, 387 | 388 | toJSON: function() { 389 | var json = Schema.prototype.toJSON.call(this) 390 | 391 | json.type = ( this.divisibleBy !== 0 && this.divisibleBy % 1 === 0) ? 'integer' : 'number' 392 | 393 | if (this.divisibleBy !== 0 && this.divisibleBy !== 1) json.divisibleBy = this.divisibleBy 394 | 395 | if (this.minimum !== -Infinity) { 396 | json.minimum = this.minimum 397 | if (this.exclusiveMinimum === true) json.exclusiveMinimum = true 398 | } 399 | 400 | if (this.maximum !== Infinity) { 401 | json.maximum = this.maximum 402 | if (this.exclusiveMaximum === true) json.exclusiveMaximum = true 403 | } 404 | 405 | return json 406 | } 407 | }) 408 | 409 | Schema.fromJSON.def(function(sch) { 410 | if (!sch || (sch.type !== 'number' && sch.type !== 'integer')) return 411 | 412 | return new NumberSchema(sch.minimum, sch.exclusiveMinimum, sch.maximum, sch.exclusiveMaximum, sch.divisibleBy || (sch.type === 'integer' ? 1 : 0)) 413 | }) 414 | 415 | Number.schema = new NumberSchema().wrap() 416 | Number.min = Number.schema.min 417 | Number.above = Number.schema.above 418 | Number.max = Number.schema.max 419 | Number.below = Number.schema.below 420 | Number.step = Number.schema.step 421 | 422 | Number.Integer = Number.step(1) 423 | 424 | },{"../BaseSchema":2}],7:[function(require,module,exports){ 425 | var ReferenceSchema = require('../patterns/reference') 426 | , EqualitySchema = require('../patterns/equality') 427 | , ObjectSchema = require('../patterns/object') 428 | 429 | Object.like = function(other) { 430 | return new EqualitySchema(other).wrap() 431 | } 432 | 433 | Object.reference = function(o) { 434 | return new ReferenceSchema(o).wrap() 435 | } 436 | 437 | Object.schema = new ObjectSchema().wrap() 438 | 439 | },{"../patterns/equality":12,"../patterns/object":14,"../patterns/reference":16}],8:[function(require,module,exports){ 440 | var Schema = require('../BaseSchema') 441 | , schema = require('../schema') 442 | 443 | var SchemaReference = module.exports = Schema.extensions.SchemaReference = Schema.extend({ 444 | validate: function() { 445 | throw new Error('Trying to validate unresolved schema reference.') 446 | }, 447 | 448 | resolve: function(schemaDescriptor) { 449 | var schemaObject = Schema.fromJS(schemaDescriptor) 450 | 451 | for (var key in schemaObject) { 452 | if (schemaObject[key] instanceof Function) { 453 | this[key] = schemaObject[key].bind(schemaObject) 454 | } else { 455 | this[key] = schemaObject[key] 456 | } 457 | } 458 | 459 | delete this.resolve 460 | }, 461 | 462 | publicFunctions: [ 'resolve' ] 463 | }) 464 | 465 | schema.reference = function(schemaDescriptor) { 466 | return new SchemaReference() 467 | } 468 | 469 | function renewing(ref) { 470 | ref.resolve = function() { 471 | Schema.self = schema.self = renewing(new SchemaReference()) 472 | return SchemaReference.prototype.resolve.apply(this, arguments) 473 | } 474 | return ref 475 | } 476 | 477 | Schema.self = schema.self = renewing(new SchemaReference()) 478 | 479 | Schema.fromJSON.def(function(sch) { 480 | if (sch.id == null && sch['$ref'] == null) return 481 | 482 | var id, session = Schema.session 483 | 484 | if (!session.deserialized) session.deserialized = { references: {}, subscribers: {} } 485 | 486 | if (sch.id != null) { 487 | // This schema can be referenced in the future with the given ID 488 | id = sch.id 489 | 490 | // Deserializing: 491 | delete sch.id 492 | var schemaObject = Schema.fromJSON(sch) 493 | sch.id = id 494 | 495 | // Storing the schema object and notifying subscribers 496 | session.deserialized.references[id] = schemaObject 497 | ;(session.deserialized.subscribers[id] || []).forEach(function(callback) { 498 | callback(schemaObject) 499 | }) 500 | 501 | return schemaObject 502 | 503 | } else { 504 | // Referencing a schema given somewhere else with the given ID 505 | id = sch['$ref'] 506 | 507 | // If the referenced schema is already known, we are ready 508 | if (session.deserialized.references[id]) return session.deserialized.references[id] 509 | 510 | // If not, returning a reference, and when the schema gets known, resolving the reference 511 | if (!session.deserialized.subscribers[id]) session.deserialized.subscribers[id] = [] 512 | var reference = new SchemaReference() 513 | session.deserialized.subscribers[id].push(reference.resolve.bind(reference)) 514 | 515 | return reference 516 | } 517 | }) 518 | 519 | },{"../BaseSchema":2,"../schema":19}],9:[function(require,module,exports){ 520 | var RegexpSchema = require('../patterns/regexp') 521 | 522 | String.of = function() { 523 | // Possible signatures : (charset) 524 | // (length, charset) 525 | // (minLength, maxLength, charset) 526 | var args = Array.prototype.slice.call(arguments).reverse() 527 | , charset = args[0] ? ('[' + args[0] + ']') : '.' 528 | , max = args[1] 529 | , min = (args.length > 2) ? args[2] : args[1] 530 | , regexp = '^' + charset + '{' + (min || 0) + ',' + (max || '') + '}$' 531 | 532 | return new RegexpSchema(RegExp(regexp)).wrap() 533 | } 534 | 535 | String.schema = new RegexpSchema().wrap() 536 | 537 | },{"../patterns/regexp":17}],10:[function(require,module,exports){ 538 | var Schema = require('../BaseSchema') 539 | 540 | var AnythingSchema = module.exports = Schema.patterns.AnythingSchema = Schema.extend({ 541 | errors: function(instance) { 542 | if (instance == null) 543 | return 'anything cannot be null' 544 | 545 | return false 546 | }, 547 | validate: function(instance) { 548 | return instance != null 549 | }, 550 | 551 | toJSON: function() { 552 | return { type: 'any' } 553 | } 554 | }) 555 | 556 | var anything = AnythingSchema.instance = new AnythingSchema() 557 | 558 | Schema.fromJS.def(function(sch) { 559 | if (sch === undefined) return anything 560 | }) 561 | 562 | Schema.fromJSON.def(function(sch) { 563 | if (sch.type === 'any') return anything 564 | }) 565 | 566 | },{"../BaseSchema":2}],11:[function(require,module,exports){ 567 | var Schema = require('../BaseSchema') 568 | 569 | var ClassSchema = module.exports = Schema.patterns.ClassSchema = Schema.extend({ 570 | initialize: function(constructor) { 571 | this.constructor = constructor 572 | }, 573 | getName: function(obj) { 574 | if (!obj) return obj 575 | if (obj instanceof Object) { 576 | return obj.constructor.name 577 | } else { 578 | return typeof obj + ' = ' + obj 579 | } 580 | }, 581 | errors: function(instance) { 582 | var middleMessage = ' is not instance of ' 583 | 584 | if (instance == null) { 585 | return this.getName(instance) + middleMessage + this.getName(this.constructor) 586 | } 587 | if (!(instance instanceof this.constructor)) { 588 | return this.getName(instance) + middleMessage + this.getName(this.constructor); 589 | } 590 | return false 591 | }, 592 | validate: function(instance) { 593 | return instance instanceof this.constructor 594 | } 595 | }) 596 | 597 | 598 | Schema.fromJS.def(function(constructor) { 599 | if (!(constructor instanceof Function)) return 600 | 601 | if (constructor.schema instanceof Function) { 602 | return constructor.schema.unwrap() 603 | } else { 604 | return new ClassSchema(constructor) 605 | } 606 | }) 607 | 608 | },{"../BaseSchema":2}],12:[function(require,module,exports){ 609 | var Schema = require('../BaseSchema') 610 | 611 | // Object deep equality 612 | var equal = function(a, b) { 613 | // if a or b is primitive, simple comparison 614 | if (Object(a) !== a || Object(b) !== b) return a === b 615 | 616 | // both a and b must be Array, or none of them 617 | if ((a instanceof Array) !== (b instanceof Array)) return false 618 | 619 | // they must have the same number of properties 620 | if (Object.keys(a).length !== Object.keys(b).length) return false 621 | 622 | // and every property should be equal 623 | for (var key in a) { 624 | if (!equal(a[key], b[key])) return false 625 | } 626 | 627 | // if every check succeeded, they are deep equal 628 | return true 629 | } 630 | 631 | var EqualitySchema = module.exports = Schema.patterns.EqualitySchema = Schema.extend({ 632 | initialize: function(object) { 633 | this.object = object 634 | }, 635 | errors: function(instance) { 636 | if (!equal(instance, this.object)) { 637 | return ( instance + ' is not equal to ' + this.object ) 638 | } 639 | return false 640 | }, 641 | validate: function(instance) { 642 | return equal(instance, this.object) 643 | }, 644 | 645 | toJSON: function() { 646 | var json = Schema.prototype.toJSON.call(this) 647 | 648 | json['enum'] = [this.object] 649 | 650 | return json 651 | } 652 | }) 653 | 654 | 655 | Schema.fromJS.def(function(sch) { 656 | if (sch instanceof Array && sch.length === 1) return new EqualitySchema(sch[0]) 657 | }) 658 | 659 | },{"../BaseSchema":2}],13:[function(require,module,exports){ 660 | var Schema = require('../BaseSchema') 661 | 662 | var NothingSchema = module.exports = Schema.patterns.NothingSchema = Schema.extend({ 663 | errors: function(instance) { 664 | return false 665 | }, 666 | validate: function(instance) { 667 | return instance == null 668 | }, 669 | 670 | toJSON: function() { 671 | return { type: 'null' } 672 | } 673 | }) 674 | 675 | var nothing = NothingSchema.instance = new NothingSchema() 676 | 677 | Schema.fromJS.def(function(sch) { 678 | if (sch === null) return nothing 679 | }) 680 | 681 | Schema.fromJSON.def(function(sch) { 682 | if (sch.type === 'null') return nothing 683 | }) 684 | 685 | },{"../BaseSchema":2}],14:[function(require,module,exports){ 686 | var Schema = require('../BaseSchema') 687 | , anything = require('./anything').instance 688 | , nothing = require('./nothing').instance 689 | 690 | var ObjectSchema = module.exports = Schema.patterns.ObjectSchema = Schema.extend({ 691 | initialize: function(properties, other) { 692 | var self = this 693 | 694 | this.other = other || anything 695 | this.properties = properties || [] 696 | 697 | // Sorting properties into two groups 698 | this.stringProps = {}, this.regexpProps = [] 699 | this.properties.forEach(function(property) { 700 | if (typeof property.key === 'string') { 701 | self.stringProps[property.key] = property 702 | } else { 703 | self.regexpProps.push(property) 704 | } 705 | }) 706 | }, 707 | 708 | errors: function(instance) { 709 | var self = this 710 | 711 | if (instance == null) return instance + ' is not Object' 712 | 713 | var error, errors = {} 714 | 715 | // Simple string properties 716 | Object.keys(this.stringProps).forEach(function(key) { 717 | if (key in instance) { 718 | if (error = self.stringProps[key].value.errors(instance[key])) { 719 | errors[key] = error 720 | } 721 | } else if (self.stringProps[key].min > 0) { 722 | errors[key] = 'key is not present in the object' 723 | } 724 | }) 725 | 726 | // Regexp and other properties 727 | if (this.regexpProps.length || this.other !== anything) { 728 | var checked 729 | var occurences = self.regexpProps.map(function() { return 0 }) 730 | 731 | for (var key in instance) { 732 | // Checking the key against every key regexps 733 | checked = false 734 | this.regexpProps.forEach(function (prop, index) { 735 | if (prop.key.test(key)) { 736 | occurences[index] += 1 737 | checked = true 738 | if (error = prop.value.errors(instance[key])) { 739 | errors[key] = error 740 | } 741 | } 742 | }) 743 | 744 | // If the key is not matched by regexps and by simple string checks 745 | // then check it against this.other 746 | if (!checked && !(key in this.stringProps)) { 747 | if (error = this.other.errors(instance[key])) { 748 | errors[key] = error 749 | } 750 | } 751 | } 752 | 753 | // Checking if regexps have the appropriate occurence number in the object 754 | for (var i = 0; i < self.regexpProps.length; i++) { 755 | var prop = self.regexpProps[i] 756 | if (prop.min > occurences[i]) { 757 | errors[prop.key.toString().slice(1,-1)] = 'regexp key matched ' + occurences[i] + 758 | ' times which is lower than allowed (' + prop.min + ')' 759 | } else if (occurences[i] > prop.max) { 760 | errors[prop.key.toString().slice(1,-1)] = 'regexp key matched ' + occurences[i] + 761 | ' times which is higher than allowed (' + prop.max + ')' 762 | } 763 | } 764 | } 765 | 766 | return Object.keys(errors).length ? errors : false 767 | }, 768 | 769 | validate: function(instance) { 770 | var self = this 771 | 772 | if (instance == null) return false 773 | 774 | // Simple string properties 775 | var stringPropsValid = Object.keys(this.stringProps).every(function(key) { 776 | if (key in instance) { 777 | return self.stringProps[key].value.validate(instance[key]) 778 | } else { 779 | return self.stringProps[key].min === 0 780 | } 781 | }) 782 | if (!stringPropsValid) return false 783 | 784 | // If there are no RegExp and other validator, that's all 785 | if (!this.regexpProps.length && this.other === anything) return true 786 | 787 | // Regexp and other properties 788 | var checked 789 | var occurences = self.regexpProps.map(function() { return 0 }) 790 | 791 | for (var key in instance) { 792 | // Checking the key against every key regexps 793 | checked = false 794 | var regexpPropsValid = this.regexpProps.every(function(prop, index) { 795 | if (prop.key.test(key)) { 796 | checked = true 797 | occurences[index] += 1 798 | return prop.value.validate(instance[key]) 799 | } else { 800 | return true 801 | } 802 | }) 803 | if (!regexpPropsValid) return false 804 | 805 | // If the key is not matched by regexps and by simple string checks 806 | // then check it against this.other 807 | if (!checked && !(key in this.stringProps) && !this.other.validate(instance[key])) return false 808 | } 809 | 810 | // Checking if regexps have the appropriate occurence number in the object 811 | for (var i = 0; i < self.regexpProps.length; i++) { 812 | var prop = self.regexpProps[i] 813 | if ((prop.min > occurences[i]) || (occurences[i] > prop.max)) return false 814 | } 815 | 816 | // If all checks passed, the instance conforms to the schema 817 | return true 818 | }, 819 | 820 | toJSON: Schema.session(function() { 821 | var i, property, regexp, json = Schema.prototype.toJSON.call(this, true) 822 | 823 | if (json['$ref'] != null) return json 824 | 825 | json.type = 'object' 826 | 827 | for (i in this.stringProps) { 828 | property = this.stringProps[i] 829 | json.properties = json.properties || {} 830 | json.properties[property.key] = property.value.toJSON() 831 | if (property.min === 1) json.properties[property.key].required = true 832 | if (property.title) json.properties[property.key].title = property.title 833 | } 834 | 835 | for (i = 0; i < this.regexpProps.length; i++) { 836 | property = this.regexpProps[i] 837 | json.patternProperties = json.patternProperties || {} 838 | regexp = property.key.toString() 839 | regexp = regexp.substr(2, regexp.length - 4) 840 | json.patternProperties[regexp] = property.value.toJSON() 841 | if (property.title) json.patternProperties[regexp].title = property.title 842 | } 843 | 844 | if (this.other !== anything) { 845 | json.additionalProperties = (this.other === nothing) ? false : this.other.toJSON() 846 | } 847 | 848 | return json 849 | }) 850 | }) 851 | 852 | // Testing if a given string is a real regexp or just a single string escaped 853 | // If it is just a string escaped, return the string. Otherwise return the regexp 854 | var regexpString = (function() { 855 | // Special characters that should be escaped when describing a regular string in regexp 856 | var shouldBeEscaped = '[](){}^$?*+.'.split('').map(function(element) { 857 | return RegExp('(\\\\)*\\' + element, 'g') 858 | }) 859 | // Special characters that shouldn't be escaped when describing a regular string in regexp 860 | var shouldntBeEscaped = 'bBwWdDsS'.split('').map(function(element) { 861 | return RegExp('(\\\\)*' + element, 'g') 862 | }) 863 | 864 | return function(string) { 865 | var i, j, match 866 | 867 | for (i = 0; i < shouldBeEscaped.length; i++) { 868 | match = string.match(shouldBeEscaped[i]) 869 | if (!match) continue 870 | for (j = 0; j < match.length; j++) { 871 | // If it is not escaped, it must be a regexp (e.g. [, \\[, \\\\[, etc.) 872 | if (match[j].length % 2 === 1) return RegExp('^' + string + '$') 873 | } 874 | } 875 | for (i = 0; i < shouldntBeEscaped.length; i++) { 876 | match = string.match(shouldntBeEscaped[i]) 877 | if (!match) continue 878 | for (j = 0; j < match.length; j++) { 879 | // If it is escaped, it must be a regexp (e.g. \b, \\\b, \\\\\b, etc.) 880 | if (match[j].length % 2 === 0) return RegExp('^' + string + '$') 881 | } 882 | } 883 | 884 | // It is not a real regexp. Removing the escaping. 885 | for (i = 0; i < shouldBeEscaped.length; i++) { 886 | string = string.replace(shouldBeEscaped[i], function(match) { 887 | return match.substr(1) 888 | }) 889 | } 890 | 891 | return string 892 | } 893 | })() 894 | 895 | Schema.fromJS.def(function(object) { 896 | if (!(object instanceof Object)) return 897 | 898 | var other, property, properties = [] 899 | for (var key in object) { 900 | property = { 901 | value: Schema.fromJS(object[key]) 902 | } 903 | 904 | // '*' as property name means 'every other property should match this schema' 905 | if (key === '*') { 906 | other = property.value 907 | continue 908 | } 909 | 910 | // Handling special chars at the beginning of the property name 911 | property.min = (key[0] === '*' || key[0] === '?') ? 0 : 1 912 | property.max = (key[0] === '*' || key[0] === '+') ? Infinity : 1 913 | key = key.replace(/^[*?+]/, '') 914 | 915 | // Handling property title that looks like: { 'a : an important property' : Number } 916 | key = key.replace(/\s*:[^:]+$/, function(match) { 917 | property.title = match.replace(/^\s*:\s*/, '') 918 | return '' 919 | }) 920 | 921 | // Testing if it is regexp-like or not. If it is, then converting to a regexp object 922 | property.key = regexpString(key) 923 | 924 | properties.push(property) 925 | } 926 | 927 | return new ObjectSchema(properties, other) 928 | }) 929 | 930 | Schema.fromJSON.def(function(json) { 931 | if (!json || json.type !== 'object') return 932 | 933 | var key, properties = [] 934 | for (key in json.properties) { 935 | properties.push({ 936 | min: json.properties[key].required ? 1 : 0, 937 | max: 1, 938 | key: key, 939 | value: Schema.fromJSON(json.properties[key]), 940 | title: json.properties[key].title 941 | }) 942 | } 943 | for (key in json.patternProperties) { 944 | properties.push({ 945 | min: 0, 946 | max: Infinity, 947 | key: RegExp('^' + key + '$'), 948 | value: Schema.fromJSON(json.patternProperties[key]), 949 | title: json.patternProperties[key].title 950 | }) 951 | } 952 | 953 | var other 954 | if (json.additionalProperties !== undefined) { 955 | other = json.additionalProperties === false ? nothing : Schema.fromJSON(json.additionalProperties) 956 | } 957 | 958 | return new ObjectSchema(properties, other) 959 | }) 960 | 961 | },{"../BaseSchema":2,"./anything":10,"./nothing":13}],15:[function(require,module,exports){ 962 | var Schema = require('../BaseSchema') 963 | , EqualitySchema = require('../patterns/equality') 964 | 965 | var OrSchema = module.exports = Schema.patterns.OrSchema = Schema.extend({ 966 | initialize: function(schemas) { 967 | this.schemas = schemas 968 | }, 969 | errors: function(instance) { 970 | var self = this 971 | 972 | var errors = [] 973 | if (!this.validate(instance)) { 974 | this.schemas.forEach(function(sch) { 975 | var result = sch.errors(instance) 976 | if (result) { 977 | errors.push(result) 978 | } 979 | }) 980 | if (errors.length > 0) { 981 | return errors; 982 | } 983 | } 984 | return false 985 | }, 986 | validate: function(instance) { 987 | return this.schemas.some(function(sch) { 988 | return sch.validate(instance) 989 | }) 990 | }, 991 | 992 | toJSON: Schema.session(function() { 993 | var json = Schema.prototype.toJSON.call(this, true) 994 | , subjsons = this.schemas.map(function(sch) { 995 | return sch.toJSON() 996 | }) 997 | , onlyEquality = subjsons.every(function(json) { 998 | return json['enum'] instanceof Array && json['enum'].length === 1 999 | }) 1000 | 1001 | if (json['$ref'] != null) return json 1002 | 1003 | if (onlyEquality) { 1004 | json['enum'] = subjsons.map(function(json) { 1005 | return json['enum'][0] 1006 | }) 1007 | 1008 | } else { 1009 | json['type'] = subjsons.map(function(json) { 1010 | var simpleType = typeof json.type === 'string' && Object.keys(json).length === 1 1011 | return simpleType ? json.type : json 1012 | }) 1013 | } 1014 | 1015 | return json 1016 | }) 1017 | }) 1018 | 1019 | 1020 | Schema.fromJS.def(function(schemas) { 1021 | if (schemas instanceof Array) return new OrSchema(schemas.map(function(sch) { 1022 | return sch === undefined ? Schema.self : Schema.fromJS(sch) 1023 | })) 1024 | }) 1025 | 1026 | Schema.fromJSON.def(function(sch) { 1027 | if (!sch) return 1028 | 1029 | if (sch['enum'] instanceof Array) { 1030 | return new OrSchema(sch['enum'].map(function(object) { 1031 | return new EqualitySchema(object) 1032 | })) 1033 | } 1034 | 1035 | if (sch['type'] instanceof Array) { 1036 | return new OrSchema(sch['type'].map(function(type) { 1037 | return Schema.fromJSON(typeof type === 'string' ? { 1038 | type: type 1039 | } : type) 1040 | })) 1041 | } 1042 | }) 1043 | 1044 | },{"../BaseSchema":2,"../patterns/equality":12}],16:[function(require,module,exports){ 1045 | var Schema = require('../BaseSchema') 1046 | 1047 | var ReferenceSchema = module.exports = Schema.patterns.ReferenceSchema = Schema.extend({ 1048 | initialize: function(value) { 1049 | this.value = value 1050 | }, 1051 | getName: function(obj) { 1052 | if (obj instanceof Object) { 1053 | return obj.constructor.name + ' = ' + obj 1054 | } else { 1055 | return typeof obj + ' = ' + obj 1056 | } 1057 | }, 1058 | errors: function(instance) { 1059 | if (instance == null) { 1060 | return ( instance + ' is not a reference' ) 1061 | } 1062 | if (instance !== this.value) { 1063 | var middleMessage = ' is not reference to ' 1064 | return ( this.getName(instance) + middleMessage + this.getName(this.value) ) 1065 | } 1066 | return false 1067 | }, 1068 | validate: function(instance) { 1069 | return instance === this.value 1070 | }, 1071 | 1072 | toJSON: function() { 1073 | var json = Schema.prototype.toJSON.call(this) 1074 | 1075 | json['enum'] = [this.value] 1076 | 1077 | return json 1078 | } 1079 | }) 1080 | 1081 | 1082 | Schema.fromJS.def(function(value) { 1083 | return new ReferenceSchema(value) 1084 | }) 1085 | 1086 | },{"../BaseSchema":2}],17:[function(require,module,exports){ 1087 | var Schema = require('../BaseSchema') 1088 | 1089 | var RegexpSchema = module.exports = Schema.patterns.RegexpSchema = Schema.extend({ 1090 | initialize: function(regexp) { 1091 | this.regexp = regexp 1092 | }, 1093 | errors: function(instance) { 1094 | var message 1095 | if (!(Object(instance) instanceof String)) { 1096 | message = instance + ' is not a String' 1097 | } else if (this.regexp && !this.regexp.test(instance)) { 1098 | message = instance + ' is not matched with RegExp -> ' + this.regexp 1099 | } 1100 | 1101 | if (message) 1102 | return message 1103 | return false 1104 | }, 1105 | validate: function(instance) { 1106 | return Object(instance) instanceof String && (!this.regexp || this.regexp.test(instance)) 1107 | }, 1108 | 1109 | toJSON: function() { 1110 | var json = Schema.prototype.toJSON.call(this) 1111 | 1112 | json.type = 'string' 1113 | 1114 | if (this.regexp) { 1115 | json.pattern = this.regexp.toString() 1116 | json.pattern = json.pattern.substr(1, json.pattern.length - 2) 1117 | } 1118 | 1119 | return json 1120 | } 1121 | }) 1122 | 1123 | Schema.fromJSON.def(function(sch) { 1124 | if (!sch || sch.type !== 'string') return 1125 | 1126 | if ('pattern' in sch) { 1127 | return new RegexpSchema(RegExp('^' + sch.pattern + '$')) 1128 | } else if ('minLength' in sch || 'maxLength' in sch) { 1129 | return new RegexpSchema(RegExp('^.{' + [sch.minLength || 0, sch.maxLength].join(',') + '}$')) 1130 | } else { 1131 | return new RegexpSchema() 1132 | } 1133 | }) 1134 | 1135 | Schema.fromJS.def(function(regexp) { 1136 | if (regexp instanceof RegExp) return new RegexpSchema(regexp) 1137 | }) 1138 | 1139 | },{"../BaseSchema":2}],18:[function(require,module,exports){ 1140 | var Schema = require('../BaseSchema') 1141 | 1142 | Schema.fromJS.def(function(sch) { 1143 | if (sch instanceof Schema) return sch 1144 | }) 1145 | 1146 | },{"../BaseSchema":2}],19:[function(require,module,exports){ 1147 | var Schema = require('./BaseSchema') 1148 | 1149 | schema = module.exports = function(schemaDescription) { 1150 | var doc, schemaObject 1151 | 1152 | if (arguments.length === 2) { 1153 | doc = schemaDescription 1154 | schemaDescription = arguments[1] 1155 | } 1156 | 1157 | if (this instanceof schema) { 1158 | // When called with new, create a schema object and then return the schema function 1159 | var constructor = Schema.extend(schemaDescription) 1160 | schemaObject = new constructor() 1161 | if (doc) schemaObject.doc = doc 1162 | return schemaObject.wrap() 1163 | 1164 | } else { 1165 | // When called as simple function, forward everything to fromJS 1166 | // and then resolve schema.self to the resulting schema object 1167 | schemaObject = Schema.fromJS(schemaDescription) 1168 | schema.self.resolve(schemaObject) 1169 | if (doc) schemaObject.doc = doc 1170 | return schemaObject.wrap() 1171 | } 1172 | } 1173 | 1174 | schema.Schema = Schema 1175 | 1176 | schema.toJSON = function(sch) { 1177 | return Schema.fromJS(sch).toJSON() 1178 | } 1179 | 1180 | schema.fromJS = function(sch) { 1181 | return Schema.fromJS(sch).wrap() 1182 | } 1183 | 1184 | schema.fromJSON = function(sch) { 1185 | return Schema.fromJSON(sch).wrap() 1186 | } 1187 | 1188 | // define js-schema using AMD 1189 | if (typeof define === 'function' && define.amd) { 1190 | define([], function(){ 1191 | return schema; 1192 | }); 1193 | } 1194 | },{"./BaseSchema":2}]},{},[1]); 1195 | -------------------------------------------------------------------------------- /js-schema.min.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0&&instance.lengththis.max)return"Array length should not be more than "+this.max+" and is "+instance.length}var results={};for(var i=0;i0){return results}return false},validate:function(instance){if(!(instance instanceof Array))return false;if(this.min===this.max){if(instance.length!==this.min)return false}else{if(this.min>0&&instance.lengththis.max)return false}for(var i=0;i0)json.minItems=this.min;if(this.maxthis.minimum:instance>=this.minimum)&&(this.exclusiveMaximum?instance2?args[2]:args[1],regexp="^"+charset+"{"+(min||0)+","+(max||"")+"}$";return new RegexpSchema(RegExp(regexp)).wrap()};String.schema=(new RegexpSchema).wrap()},{"../patterns/regexp":17}],10:[function(require,module,exports){var Schema=require("../BaseSchema");var AnythingSchema=module.exports=Schema.patterns.AnythingSchema=Schema.extend({errors:function(instance){if(instance==null)return"anything cannot be null";return false},validate:function(instance){return instance!=null},toJSON:function(){return{type:"any"}}});var anything=AnythingSchema.instance=new AnythingSchema;Schema.fromJS.def(function(sch){if(sch===undefined)return anything});Schema.fromJSON.def(function(sch){if(sch.type==="any")return anything})},{"../BaseSchema":2}],11:[function(require,module,exports){var Schema=require("../BaseSchema");var ClassSchema=module.exports=Schema.patterns.ClassSchema=Schema.extend({initialize:function(constructor){this.constructor=constructor},getName:function(obj){if(!obj)return obj;if(obj instanceof Object){return obj.constructor.name}else{return typeof obj+" = "+obj}},errors:function(instance){var middleMessage=" is not instance of ";if(instance==null){return this.getName(instance)+middleMessage+this.getName(this.constructor)}if(!(instance instanceof this.constructor)){return this.getName(instance)+middleMessage+this.getName(this.constructor)}return false},validate:function(instance){return instance instanceof this.constructor}});Schema.fromJS.def(function(constructor){if(!(constructor instanceof Function))return;if(constructor.schema instanceof Function){return constructor.schema.unwrap()}else{return new ClassSchema(constructor)}})},{"../BaseSchema":2}],12:[function(require,module,exports){var Schema=require("../BaseSchema");var equal=function(a,b){if(Object(a)!==a||Object(b)!==b)return a===b;if(a instanceof Array!==b instanceof Array)return false;if(Object.keys(a).length!==Object.keys(b).length)return false;for(var key in a){if(!equal(a[key],b[key]))return false}return true};var EqualitySchema=module.exports=Schema.patterns.EqualitySchema=Schema.extend({initialize:function(object){this.object=object},errors:function(instance){if(!equal(instance,this.object)){return instance+" is not equal to "+this.object}return false},validate:function(instance){return equal(instance,this.object)},toJSON:function(){var json=Schema.prototype.toJSON.call(this);json["enum"]=[this.object];return json}});Schema.fromJS.def(function(sch){if(sch instanceof Array&&sch.length===1)return new EqualitySchema(sch[0])})},{"../BaseSchema":2}],13:[function(require,module,exports){var Schema=require("../BaseSchema");var NothingSchema=module.exports=Schema.patterns.NothingSchema=Schema.extend({errors:function(instance){return false},validate:function(instance){return instance==null},toJSON:function(){return{type:"null"}}});var nothing=NothingSchema.instance=new NothingSchema;Schema.fromJS.def(function(sch){if(sch===null)return nothing});Schema.fromJSON.def(function(sch){if(sch.type==="null")return nothing})},{"../BaseSchema":2}],14:[function(require,module,exports){var Schema=require("../BaseSchema"),anything=require("./anything").instance,nothing=require("./nothing").instance;var ObjectSchema=module.exports=Schema.patterns.ObjectSchema=Schema.extend({initialize:function(properties,other){var self=this;this.other=other||anything;this.properties=properties||[];this.stringProps={},this.regexpProps=[];this.properties.forEach(function(property){if(typeof property.key==="string"){self.stringProps[property.key]=property}else{self.regexpProps.push(property)}})},errors:function(instance){var self=this;if(instance==null)return instance+" is not Object";var error,errors={};Object.keys(this.stringProps).forEach(function(key){if(key in instance){if(error=self.stringProps[key].value.errors(instance[key])){errors[key]=error}}else if(self.stringProps[key].min>0){errors[key]="key is not present in the object"}});if(this.regexpProps.length||this.other!==anything){var checked;var occurences=self.regexpProps.map(function(){return 0});for(var key in instance){checked=false;this.regexpProps.forEach(function(prop,index){if(prop.key.test(key)){occurences[index]+=1;checked=true;if(error=prop.value.errors(instance[key])){errors[key]=error}}});if(!checked&&!(key in this.stringProps)){if(error=this.other.errors(instance[key])){errors[key]=error}}}for(var i=0;ioccurences[i]){errors[prop.key.toString().slice(1,-1)]="regexp key matched "+occurences[i]+" times which is lower than allowed ("+prop.min+")"}else if(occurences[i]>prop.max){errors[prop.key.toString().slice(1,-1)]="regexp key matched "+occurences[i]+" times which is higher than allowed ("+prop.max+")"}}}return Object.keys(errors).length?errors:false},validate:function(instance){var self=this;if(instance==null)return false;var stringPropsValid=Object.keys(this.stringProps).every(function(key){if(key in instance){return self.stringProps[key].value.validate(instance[key])}else{return self.stringProps[key].min===0}});if(!stringPropsValid)return false;if(!this.regexpProps.length&&this.other===anything)return true;var checked;var occurences=self.regexpProps.map(function(){return 0});for(var key in instance){checked=false;var regexpPropsValid=this.regexpProps.every(function(prop,index){if(prop.key.test(key)){checked=true;occurences[index]+=1;return prop.value.validate(instance[key])}else{return true}});if(!regexpPropsValid)return false;if(!checked&&!(key in this.stringProps)&&!this.other.validate(instance[key]))return false}for(var i=0;ioccurences[i]||occurences[i]>prop.max)return false}return true},toJSON:Schema.session(function(){var i,property,regexp,json=Schema.prototype.toJSON.call(this,true);if(json["$ref"]!=null)return json;json.type="object";for(i in this.stringProps){property=this.stringProps[i];json.properties=json.properties||{};json.properties[property.key]=property.value.toJSON();if(property.min===1)json.properties[property.key].required=true;if(property.title)json.properties[property.key].title=property.title}for(i=0;i0){return errors}}return false},validate:function(instance){return this.schemas.some(function(sch){return sch.validate(instance)})},toJSON:Schema.session(function(){var json=Schema.prototype.toJSON.call(this,true),subjsons=this.schemas.map(function(sch){return sch.toJSON()}),onlyEquality=subjsons.every(function(json){return json["enum"]instanceof Array&&json["enum"].length===1});if(json["$ref"]!=null)return json;if(onlyEquality){json["enum"]=subjsons.map(function(json){return json["enum"][0]})}else{json["type"]=subjsons.map(function(json){var simpleType=typeof json.type==="string"&&Object.keys(json).length===1;return simpleType?json.type:json})}return json})});Schema.fromJS.def(function(schemas){if(schemas instanceof Array)return new OrSchema(schemas.map(function(sch){return sch===undefined?Schema.self:Schema.fromJS(sch)}))});Schema.fromJSON.def(function(sch){if(!sch)return;if(sch["enum"]instanceof Array){return new OrSchema(sch["enum"].map(function(object){return new EqualitySchema(object)}))}if(sch["type"]instanceof Array){return new OrSchema(sch["type"].map(function(type){return Schema.fromJSON(typeof type==="string"?{type:type}:type)}))}})},{"../BaseSchema":2,"../patterns/equality":12}],16:[function(require,module,exports){var Schema=require("../BaseSchema");var ReferenceSchema=module.exports=Schema.patterns.ReferenceSchema=Schema.extend({initialize:function(value){this.value=value},getName:function(obj){if(obj instanceof Object){return obj.constructor.name+" = "+obj}else{return typeof obj+" = "+obj}},errors:function(instance){if(instance==null){return instance+" is not a reference"}if(instance!==this.value){var middleMessage=" is not reference to ";return this.getName(instance)+middleMessage+this.getName(this.value)}return false},validate:function(instance){return instance===this.value},toJSON:function(){var json=Schema.prototype.toJSON.call(this);json["enum"]=[this.value];return json}});Schema.fromJS.def(function(value){return new ReferenceSchema(value)})},{"../BaseSchema":2}],17:[function(require,module,exports){var Schema=require("../BaseSchema");var RegexpSchema=module.exports=Schema.patterns.RegexpSchema=Schema.extend({initialize:function(regexp){this.regexp=regexp},errors:function(instance){var message;if(!(Object(instance)instanceof String)){message=instance+" is not a String"}else if(this.regexp&&!this.regexp.test(instance)){message=instance+" is not matched with RegExp -> "+this.regexp}if(message)return message;return false},validate:function(instance){return Object(instance)instanceof String&&(!this.regexp||this.regexp.test(instance))},toJSON:function(){var json=Schema.prototype.toJSON.call(this);json.type="string";if(this.regexp){json.pattern=this.regexp.toString();json.pattern=json.pattern.substr(1,json.pattern.length-2)}return json}});Schema.fromJSON.def(function(sch){if(!sch||sch.type!=="string")return;if("pattern"in sch){return new RegexpSchema(RegExp("^"+sch.pattern+"$"))}else if("minLength"in sch||"maxLength"in sch){return new RegexpSchema(RegExp("^.{"+[sch.minLength||0,sch.maxLength].join(",")+"}$"))}else{return new RegexpSchema}});Schema.fromJS.def(function(regexp){if(regexp instanceof RegExp)return new RegexpSchema(regexp)})},{"../BaseSchema":2}],18:[function(require,module,exports){var Schema=require("../BaseSchema");Schema.fromJS.def(function(sch){if(sch instanceof Schema)return sch})},{"../BaseSchema":2}],19:[function(require,module,exports){var Schema=require("./BaseSchema");schema=module.exports=function(schemaDescription){var doc,schemaObject;if(arguments.length===2){doc=schemaDescription;schemaDescription=arguments[1]}if(this instanceof schema){var constructor=Schema.extend(schemaDescription);schemaObject=new constructor;if(doc)schemaObject.doc=doc;return schemaObject.wrap()}else{schemaObject=Schema.fromJS(schemaDescription);schema.self.resolve(schemaObject);if(doc)schemaObject.doc=doc;return schemaObject.wrap()}};schema.Schema=Schema;schema.toJSON=function(sch){return Schema.fromJS(sch).toJSON()};schema.fromJS=function(sch){return Schema.fromJS(sch).wrap()};schema.fromJSON=function(sch){return Schema.fromJSON(sch).wrap()};if(typeof define==="function"&&define.amd){define([],function(){return schema})}},{"./BaseSchema":2}]},{},[1]); -------------------------------------------------------------------------------- /lib/BaseSchema.js: -------------------------------------------------------------------------------- 1 | var Schema = module.exports = function() {} 2 | 3 | Schema.prototype = { 4 | wrap: function() { 5 | if (this.wrapped) return this.validate 6 | this.wrapped = true 7 | 8 | var publicFunctions = [ 'toJSON', 'unwrap', 'errors' ] 9 | publicFunctions = publicFunctions.concat(this.publicFunctions || []) 10 | 11 | for (var i = 0; i < publicFunctions.length; i++) { 12 | if (!this[publicFunctions[i]]) continue 13 | this.validate[publicFunctions[i]] = this[publicFunctions[i]].bind(this) 14 | } 15 | 16 | return this.validate 17 | }, 18 | 19 | unwrap: function() { 20 | return this 21 | }, 22 | 23 | toJSON: session(function(makeReference) { 24 | var json, session = Schema.session 25 | 26 | // Initializing session if it isnt 27 | if (!session.serialized) session.serialized = { objects: [], jsons: [], ids: [] } 28 | 29 | var index = session.serialized.objects.indexOf(this) 30 | if (makeReference && index !== -1) { 31 | // This was already serialized, returning a JSON schema reference ($ref) 32 | json = session.serialized.jsons[index] 33 | 34 | // If there was no id given, generating one now 35 | if (json.id == null) { 36 | do { 37 | json.id = 'id-' + Math.floor(Math.random() * 100000) 38 | } while (session.serialized.ids.indexOf(json.id) !== -1) 39 | session.serialized.ids.push(json.id) 40 | } 41 | 42 | json = { '$ref': json.id } 43 | 44 | } else { 45 | // This was not serialized yet, serializing now 46 | json = {} 47 | 48 | if (this.doc != null) json.description = this.doc 49 | 50 | // Registering that this was serialized and storing the json 51 | session.serialized.objects.push(this) 52 | session.serialized.jsons.push(json) 53 | } 54 | 55 | return json 56 | }) 57 | } 58 | 59 | Schema.extend = function(descriptor) { 60 | if (!descriptor.validate) { 61 | throw new Error('Schema objects must have a validate function.') 62 | } 63 | 64 | var constructor = function() { 65 | var self = this; 66 | if (this.initialize) 67 | this.initialize.apply(this, arguments) 68 | 69 | this.validate = this.validate.bind(this) 70 | this.validate.schema = this.validate 71 | } 72 | 73 | var prototype = Object.create(Schema.prototype) 74 | for (var key in descriptor) prototype[key] = descriptor[key] 75 | constructor.prototype = prototype 76 | 77 | return constructor 78 | } 79 | 80 | 81 | var active = false 82 | function session(f) { 83 | return function() { 84 | if (active) { 85 | // There's an active session, just forwarding to the original function 86 | return f.apply(this, arguments) 87 | 88 | } else { 89 | // The initiator is the one who handles the active flag, and clears the session when it's over 90 | active = true 91 | 92 | var result = f.apply(this, arguments) 93 | 94 | // Cleanup 95 | for (var i in session) delete session[i] 96 | active = false 97 | 98 | return result 99 | } 100 | } 101 | } 102 | Schema.session = session 103 | 104 | function lastDefinedResult(functions, arg) { 105 | var i = functions.length, result; 106 | while (i--) { 107 | result = functions[i](arg) 108 | if (result != null) return result 109 | } 110 | } 111 | 112 | var fromJSdefs = [] 113 | Schema.fromJS = lastDefinedResult.bind(null, fromJSdefs) 114 | Schema.fromJS.def = Array.prototype.push.bind(fromJSdefs) 115 | 116 | var fromJSONdefs = [] 117 | Schema.fromJSON = session(lastDefinedResult.bind(null, fromJSONdefs)) 118 | Schema.fromJSON.def = Array.prototype.push.bind(fromJSONdefs) 119 | 120 | Schema.patterns = {} 121 | Schema.extensions = {} 122 | -------------------------------------------------------------------------------- /lib/extensions/Array.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../BaseSchema') 2 | , EqualitySchema = require('../patterns/equality') 3 | , anything = require('../patterns/anything').instance 4 | 5 | var ArraySchema = module.exports = Schema.extensions.ArraySchema = Schema.extend({ 6 | initialize: function(itemSchema, max, min) { 7 | this.itemSchema = itemSchema || anything 8 | this.min = min || 0 9 | this.max = max || Infinity 10 | }, 11 | errors: function(instance) { 12 | var self = this 13 | // Instance must be an instance of Array 14 | if (!(instance instanceof Array)) 15 | return ( instance + ' is not an instance of Array') 16 | 17 | // Checking length 18 | if (this.min === this.max) { 19 | if (instance.length !== this.min) 20 | return ( 'Array length should be equal to ' + this.min + ' and is ' + instance.length ) 21 | 22 | } else { 23 | if (this.min > 0 && instance.length < this.min) 24 | return ( 'Array length should not be less than ' + this.min + ' and is ' + instance.length ) 25 | if (this.max < Infinity && instance.length > this.max) 26 | return ( 'Array length should not be more than ' + this.max + ' and is ' + instance.length ) 27 | } 28 | 29 | // Checking conformance to the given item schema 30 | var results = {} 31 | for (var i = 0; i < instance.length; i++) { 32 | var errs = this.itemSchema.errors(instance[i]) 33 | if (errs) { 34 | results[i] = errs 35 | } 36 | } 37 | var resultKeysArray = Object.keys(results) 38 | if (resultKeysArray.length > 0) { 39 | return results 40 | } 41 | 42 | return false 43 | }, 44 | validate: function(instance) { 45 | // Instance must be an instance of Array 46 | if (!(instance instanceof Array)) return false 47 | 48 | // Checking length 49 | if (this.min === this.max) { 50 | if (instance.length !== this.min) return false 51 | 52 | } else { 53 | if (this.min > 0 && instance.length < this.min) return false 54 | if (this.max < Infinity && instance.length > this.max) return false 55 | } 56 | 57 | // Checking conformance to the given item schema 58 | for (var i = 0; i < instance.length; i++) { 59 | if (!this.itemSchema.validate(instance[i])) return false 60 | } 61 | 62 | return true 63 | }, 64 | 65 | toJSON: Schema.session(function() { 66 | var json = Schema.prototype.toJSON.call(this, true) 67 | 68 | if (json['$ref'] != null) return json 69 | 70 | json.type = 'array' 71 | 72 | if (this.min > 0) json.minItems = this.min 73 | if (this.max < Infinity) json.maxItems = this.max 74 | if (this.itemSchema !== anything) json.items = this.itemSchema.toJSON() 75 | 76 | return json 77 | }) 78 | }) 79 | 80 | 81 | Schema.fromJSON.def(function(sch) { 82 | if (!sch || sch.type !== 'array') return 83 | 84 | // Tuple typing is not yet supported 85 | if (sch.items instanceof Array) return 86 | 87 | return new ArraySchema(Schema.fromJSON(sch.items), sch.maxItems, sch.minItems) 88 | }) 89 | 90 | Array.of = function() { 91 | // Possible signatures : (schema) 92 | // (length, schema) 93 | // (minLength, maxLength, schema) 94 | var args = Array.prototype.slice.call(arguments).reverse(); 95 | if(args.length == 3){ 96 | if(!(typeof args[2] === "number")){ 97 | throw new Error("3 arguments were passed to Array.of and 1st argument (minLength) SHOULD be number NOT " + args[2]) 98 | } 99 | if(!(typeof args[1] === "number")){ 100 | throw new Error("3 arguments were passed to Array.of and 2nd argument (maxLength) SHOULD be number NOT " + args[1]) 101 | } 102 | } 103 | if (args.length === 2) { 104 | if(!(typeof args[1] === "number")){ 105 | throw new Error("2 arguments were passed to Array.of and 1nd argument (length) SHOULD be number NOT " + args[1]) 106 | } 107 | args[2] = args[1] 108 | } 109 | return new ArraySchema(Schema.fromJS(args[0]), args[1], args[2]).wrap() 110 | } 111 | 112 | Array.like = function(other) { 113 | return new EqualitySchema(other).wrap() 114 | } 115 | 116 | Array.schema = new ArraySchema().wrap() 117 | -------------------------------------------------------------------------------- /lib/extensions/Boolean.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../BaseSchema') 2 | 3 | var BooleanSchema = module.exports = Schema.extensions.BooleanSchema = new Schema.extend({ 4 | errors: function(instance) { 5 | if (!this.validate(instance)) { 6 | return ( instance + ' is not Boolean' ) 7 | } 8 | return false 9 | }, 10 | 11 | validate: function(instance) { 12 | return Object(instance) instanceof Boolean 13 | }, 14 | 15 | toJSON: function() { 16 | return { 17 | type: 'boolean' 18 | } 19 | } 20 | }) 21 | 22 | var booleanSchema = module.exports = new BooleanSchema().wrap() 23 | 24 | Schema.fromJSON.def(function(sch) { 25 | if (!sch || sch.type !== 'boolean') return 26 | 27 | return booleanSchema 28 | }) 29 | 30 | Boolean.schema = booleanSchema 31 | -------------------------------------------------------------------------------- /lib/extensions/Function.js: -------------------------------------------------------------------------------- 1 | var ReferenceSchema = require('../patterns/reference') 2 | 3 | Function.reference = function(f) { 4 | return new ReferenceSchema(f).wrap() 5 | } 6 | -------------------------------------------------------------------------------- /lib/extensions/Number.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../BaseSchema') 2 | 3 | var NumberSchema = module.exports = Schema.extensions.NumberSchema = Schema.extend({ 4 | initialize: function(minimum, exclusiveMinimum, maximum, exclusiveMaximum, divisibleBy) { 5 | this.minimum = minimum != null ? minimum : -Infinity 6 | this.exclusiveMinimum = exclusiveMinimum 7 | this.maximum = minimum != null ? maximum : Infinity 8 | this.exclusiveMaximum = exclusiveMaximum 9 | this.divisibleBy = divisibleBy || 0 10 | }, 11 | 12 | min: function(minimum) { 13 | return new NumberSchema( minimum, false 14 | , this.maximum 15 | , this.exclusiveMaximum 16 | , this.divisibleBy 17 | ).wrap() 18 | }, 19 | 20 | above: function(minimum) { 21 | return new NumberSchema( minimum, true 22 | , this.maximum 23 | , this.exclusiveMaximum 24 | , this.divisibleBy 25 | ).wrap() 26 | }, 27 | 28 | max: function(maximum) { 29 | return new NumberSchema( this.minimum 30 | , this.exclusiveMinimum 31 | , maximum 32 | , false 33 | , this.divisibleBy 34 | ).wrap() 35 | }, 36 | 37 | below: function(maximum) { 38 | return new NumberSchema( this.minimum 39 | , this.exclusiveMinimum 40 | , maximum 41 | , true 42 | , this.divisibleBy 43 | ).wrap() 44 | }, 45 | 46 | step: function(divisibleBy) { 47 | return new NumberSchema( this.minimum 48 | , this.exclusiveMinimum 49 | , this.maximum 50 | , this.exclusiveMaximum 51 | , divisibleBy 52 | ).wrap() 53 | }, 54 | 55 | publicFunctions: ['min', 'above', 'max', 'below', 'step'], 56 | 57 | errors: function(instance) { 58 | var message 59 | if (!(Object(instance) instanceof Number)) { 60 | message = instance + ' is not Number' 61 | } else if (instance < this.minimum) { 62 | message = 'number = ' + instance + ' is smaller than required minimum = ' + this.minimum 63 | } else if (instance > this.maximum) { 64 | message = 'number = ' + instance + ' is bigger than required maximum = ' + this.maximum 65 | } else if (this.divisibleBy !== 0 && instance % this.divisibleBy !== 0) { 66 | message = 'number = ' + instance + ' is not divisibleBy ' + this.divisibleBy 67 | } 68 | 69 | if (message != null) { 70 | return message 71 | } 72 | return false 73 | }, 74 | 75 | validate: function(instance) { 76 | return (Object(instance) instanceof Number) && 77 | (this.exclusiveMinimum ? instance > this.minimum 78 | : instance >= this.minimum) && 79 | (this.exclusiveMaximum ? instance < this.maximum 80 | : instance <= this.maximum) && 81 | (this.divisibleBy === 0 || instance % this.divisibleBy === 0) 82 | }, 83 | 84 | toJSON: function() { 85 | var json = Schema.prototype.toJSON.call(this) 86 | 87 | json.type = ( this.divisibleBy !== 0 && this.divisibleBy % 1 === 0) ? 'integer' : 'number' 88 | 89 | if (this.divisibleBy !== 0 && this.divisibleBy !== 1) json.divisibleBy = this.divisibleBy 90 | 91 | if (this.minimum !== -Infinity) { 92 | json.minimum = this.minimum 93 | if (this.exclusiveMinimum === true) json.exclusiveMinimum = true 94 | } 95 | 96 | if (this.maximum !== Infinity) { 97 | json.maximum = this.maximum 98 | if (this.exclusiveMaximum === true) json.exclusiveMaximum = true 99 | } 100 | 101 | return json 102 | } 103 | }) 104 | 105 | Schema.fromJSON.def(function(sch) { 106 | if (!sch || (sch.type !== 'number' && sch.type !== 'integer')) return 107 | 108 | return new NumberSchema(sch.minimum, sch.exclusiveMinimum, sch.maximum, sch.exclusiveMaximum, sch.divisibleBy || (sch.type === 'integer' ? 1 : 0)) 109 | }) 110 | 111 | Number.schema = new NumberSchema().wrap() 112 | Number.min = Number.schema.min 113 | Number.above = Number.schema.above 114 | Number.max = Number.schema.max 115 | Number.below = Number.schema.below 116 | Number.step = Number.schema.step 117 | 118 | Number.Integer = Number.step(1) 119 | -------------------------------------------------------------------------------- /lib/extensions/Object.js: -------------------------------------------------------------------------------- 1 | var ReferenceSchema = require('../patterns/reference') 2 | , EqualitySchema = require('../patterns/equality') 3 | , ObjectSchema = require('../patterns/object') 4 | 5 | Object.like = function(other) { 6 | return new EqualitySchema(other).wrap() 7 | } 8 | 9 | Object.reference = function(o) { 10 | return new ReferenceSchema(o).wrap() 11 | } 12 | 13 | Object.schema = new ObjectSchema().wrap() 14 | -------------------------------------------------------------------------------- /lib/extensions/Schema.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../BaseSchema') 2 | , schema = require('../schema') 3 | 4 | var SchemaReference = module.exports = Schema.extensions.SchemaReference = Schema.extend({ 5 | validate: function() { 6 | throw new Error('Trying to validate unresolved schema reference.') 7 | }, 8 | 9 | resolve: function(schemaDescriptor) { 10 | var schemaObject = Schema.fromJS(schemaDescriptor) 11 | 12 | for (var key in schemaObject) { 13 | if (schemaObject[key] instanceof Function) { 14 | this[key] = schemaObject[key].bind(schemaObject) 15 | } else { 16 | this[key] = schemaObject[key] 17 | } 18 | } 19 | 20 | delete this.resolve 21 | }, 22 | 23 | publicFunctions: [ 'resolve' ] 24 | }) 25 | 26 | schema.reference = function(schemaDescriptor) { 27 | return new SchemaReference() 28 | } 29 | 30 | function renewing(ref) { 31 | ref.resolve = function() { 32 | Schema.self = schema.self = renewing(new SchemaReference()) 33 | return SchemaReference.prototype.resolve.apply(this, arguments) 34 | } 35 | return ref 36 | } 37 | 38 | Schema.self = schema.self = renewing(new SchemaReference()) 39 | 40 | Schema.fromJSON.def(function(sch) { 41 | if (sch.id == null && sch['$ref'] == null) return 42 | 43 | var id, session = Schema.session 44 | 45 | if (!session.deserialized) session.deserialized = { references: {}, subscribers: {} } 46 | 47 | if (sch.id != null) { 48 | // This schema can be referenced in the future with the given ID 49 | id = sch.id 50 | 51 | // Deserializing: 52 | delete sch.id 53 | var schemaObject = Schema.fromJSON(sch) 54 | sch.id = id 55 | 56 | // Storing the schema object and notifying subscribers 57 | session.deserialized.references[id] = schemaObject 58 | ;(session.deserialized.subscribers[id] || []).forEach(function(callback) { 59 | callback(schemaObject) 60 | }) 61 | 62 | return schemaObject 63 | 64 | } else { 65 | // Referencing a schema given somewhere else with the given ID 66 | id = sch['$ref'] 67 | 68 | // If the referenced schema is already known, we are ready 69 | if (session.deserialized.references[id]) return session.deserialized.references[id] 70 | 71 | // If not, returning a reference, and when the schema gets known, resolving the reference 72 | if (!session.deserialized.subscribers[id]) session.deserialized.subscribers[id] = [] 73 | var reference = new SchemaReference() 74 | session.deserialized.subscribers[id].push(reference.resolve.bind(reference)) 75 | 76 | return reference 77 | } 78 | }) 79 | -------------------------------------------------------------------------------- /lib/extensions/String.js: -------------------------------------------------------------------------------- 1 | var RegexpSchema = require('../patterns/regexp') 2 | 3 | String.of = function() { 4 | // Possible signatures : (charset) 5 | // (length, charset) 6 | // (minLength, maxLength, charset) 7 | var args = Array.prototype.slice.call(arguments).reverse() 8 | , charset = args[0] ? ('[' + args[0] + ']') : '.' 9 | , max = args[1] 10 | , min = (args.length > 2) ? args[2] : args[1] 11 | , regexp = '^' + charset + '{' + (min || 0) + ',' + (max || '') + '}$' 12 | 13 | return new RegexpSchema(RegExp(regexp)).wrap() 14 | } 15 | 16 | String.schema = new RegexpSchema().wrap() 17 | -------------------------------------------------------------------------------- /lib/patterns/anything.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../BaseSchema') 2 | 3 | var AnythingSchema = module.exports = Schema.patterns.AnythingSchema = Schema.extend({ 4 | errors: function(instance) { 5 | if (instance == null) 6 | return 'anything cannot be null' 7 | 8 | return false 9 | }, 10 | validate: function(instance) { 11 | return instance != null 12 | }, 13 | 14 | toJSON: function() { 15 | return { type: 'any' } 16 | } 17 | }) 18 | 19 | var anything = AnythingSchema.instance = new AnythingSchema() 20 | 21 | Schema.fromJS.def(function(sch) { 22 | if (sch === undefined) return anything 23 | }) 24 | 25 | Schema.fromJSON.def(function(sch) { 26 | if (sch.type === 'any') return anything 27 | }) 28 | -------------------------------------------------------------------------------- /lib/patterns/class.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../BaseSchema') 2 | 3 | var ClassSchema = module.exports = Schema.patterns.ClassSchema = Schema.extend({ 4 | initialize: function(constructor) { 5 | this.constructor = constructor 6 | }, 7 | getName: function(obj) { 8 | if (!obj) return obj 9 | if (obj instanceof Object) { 10 | return obj.constructor.name 11 | } else { 12 | return typeof obj + ' = ' + obj 13 | } 14 | }, 15 | errors: function(instance) { 16 | var middleMessage = ' is not instance of ' 17 | 18 | if (instance == null) { 19 | return this.getName(instance) + middleMessage + this.getName(this.constructor) 20 | } 21 | if (!(instance instanceof this.constructor)) { 22 | return this.getName(instance) + middleMessage + this.getName(this.constructor); 23 | } 24 | return false 25 | }, 26 | validate: function(instance) { 27 | return instance instanceof this.constructor 28 | } 29 | }) 30 | 31 | 32 | Schema.fromJS.def(function(constructor) { 33 | if (!(constructor instanceof Function)) return 34 | 35 | if (constructor.schema instanceof Function) { 36 | return constructor.schema.unwrap() 37 | } else { 38 | return new ClassSchema(constructor) 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /lib/patterns/equality.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../BaseSchema') 2 | 3 | // Object deep equality 4 | var equal = function(a, b) { 5 | // if a or b is primitive, simple comparison 6 | if (Object(a) !== a || Object(b) !== b) return a === b 7 | 8 | // both a and b must be Array, or none of them 9 | if ((a instanceof Array) !== (b instanceof Array)) return false 10 | 11 | // they must have the same number of properties 12 | if (Object.keys(a).length !== Object.keys(b).length) return false 13 | 14 | // and every property should be equal 15 | for (var key in a) { 16 | if (!equal(a[key], b[key])) return false 17 | } 18 | 19 | // if every check succeeded, they are deep equal 20 | return true 21 | } 22 | 23 | var EqualitySchema = module.exports = Schema.patterns.EqualitySchema = Schema.extend({ 24 | initialize: function(object) { 25 | this.object = object 26 | }, 27 | errors: function(instance) { 28 | if (!equal(instance, this.object)) { 29 | return ( instance + ' is not equal to ' + this.object ) 30 | } 31 | return false 32 | }, 33 | validate: function(instance) { 34 | return equal(instance, this.object) 35 | }, 36 | 37 | toJSON: function() { 38 | var json = Schema.prototype.toJSON.call(this) 39 | 40 | json['enum'] = [this.object] 41 | 42 | return json 43 | } 44 | }) 45 | 46 | 47 | Schema.fromJS.def(function(sch) { 48 | if (sch instanceof Array && sch.length === 1) return new EqualitySchema(sch[0]) 49 | }) 50 | -------------------------------------------------------------------------------- /lib/patterns/nothing.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../BaseSchema') 2 | 3 | var NothingSchema = module.exports = Schema.patterns.NothingSchema = Schema.extend({ 4 | errors: function(instance) { 5 | return false 6 | }, 7 | validate: function(instance) { 8 | return instance == null 9 | }, 10 | 11 | toJSON: function() { 12 | return { type: 'null' } 13 | } 14 | }) 15 | 16 | var nothing = NothingSchema.instance = new NothingSchema() 17 | 18 | Schema.fromJS.def(function(sch) { 19 | if (sch === null) return nothing 20 | }) 21 | 22 | Schema.fromJSON.def(function(sch) { 23 | if (sch.type === 'null') return nothing 24 | }) 25 | -------------------------------------------------------------------------------- /lib/patterns/object.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../BaseSchema') 2 | , anything = require('./anything').instance 3 | , nothing = require('./nothing').instance 4 | 5 | var ObjectSchema = module.exports = Schema.patterns.ObjectSchema = Schema.extend({ 6 | initialize: function(properties, other) { 7 | var self = this 8 | 9 | this.other = other || anything 10 | this.properties = properties || [] 11 | 12 | // Sorting properties into two groups 13 | this.stringProps = {}, this.regexpProps = [] 14 | this.properties.forEach(function(property) { 15 | if (typeof property.key === 'string') { 16 | self.stringProps[property.key] = property 17 | } else { 18 | self.regexpProps.push(property) 19 | } 20 | }) 21 | }, 22 | 23 | errors: function(instance) { 24 | var self = this 25 | 26 | if (instance == null) return instance + ' is not Object' 27 | 28 | var error, errors = {} 29 | 30 | // Simple string properties 31 | Object.keys(this.stringProps).forEach(function(key) { 32 | if (key in instance) { 33 | if (error = self.stringProps[key].value.errors(instance[key])) { 34 | errors[key] = error 35 | } 36 | } else if (self.stringProps[key].min > 0) { 37 | errors[key] = 'key is not present in the object' 38 | } 39 | }) 40 | 41 | // Regexp and other properties 42 | if (this.regexpProps.length || this.other !== anything) { 43 | var checked 44 | var occurences = self.regexpProps.map(function() { return 0 }) 45 | 46 | for (var key in instance) { 47 | // Checking the key against every key regexps 48 | checked = false 49 | this.regexpProps.forEach(function (prop, index) { 50 | if (prop.key.test(key)) { 51 | occurences[index] += 1 52 | checked = true 53 | if (error = prop.value.errors(instance[key])) { 54 | errors[key] = error 55 | } 56 | } 57 | }) 58 | 59 | // If the key is not matched by regexps and by simple string checks 60 | // then check it against this.other 61 | if (!checked && !(key in this.stringProps)) { 62 | if (error = this.other.errors(instance[key])) { 63 | errors[key] = error 64 | } 65 | } 66 | } 67 | 68 | // Checking if regexps have the appropriate occurence number in the object 69 | for (var i = 0; i < self.regexpProps.length; i++) { 70 | var prop = self.regexpProps[i] 71 | if (prop.min > occurences[i]) { 72 | errors[prop.key.toString().slice(1,-1)] = 'regexp key matched ' + occurences[i] + 73 | ' times which is lower than allowed (' + prop.min + ')' 74 | } else if (occurences[i] > prop.max) { 75 | errors[prop.key.toString().slice(1,-1)] = 'regexp key matched ' + occurences[i] + 76 | ' times which is higher than allowed (' + prop.max + ')' 77 | } 78 | } 79 | } 80 | 81 | return Object.keys(errors).length ? errors : false 82 | }, 83 | 84 | validate: function(instance) { 85 | var self = this 86 | 87 | if (instance == null) return false 88 | 89 | // Simple string properties 90 | var stringPropsValid = Object.keys(this.stringProps).every(function(key) { 91 | if (key in instance) { 92 | return self.stringProps[key].value.validate(instance[key]) 93 | } else { 94 | return self.stringProps[key].min === 0 95 | } 96 | }) 97 | if (!stringPropsValid) return false 98 | 99 | // If there are no RegExp and other validator, that's all 100 | if (!this.regexpProps.length && this.other === anything) return true 101 | 102 | // Regexp and other properties 103 | var checked 104 | var occurences = self.regexpProps.map(function() { return 0 }) 105 | 106 | for (var key in instance) { 107 | // Checking the key against every key regexps 108 | checked = false 109 | var regexpPropsValid = this.regexpProps.every(function(prop, index) { 110 | if (prop.key.test(key)) { 111 | checked = true 112 | occurences[index] += 1 113 | return prop.value.validate(instance[key]) 114 | } else { 115 | return true 116 | } 117 | }) 118 | if (!regexpPropsValid) return false 119 | 120 | // If the key is not matched by regexps and by simple string checks 121 | // then check it against this.other 122 | if (!checked && !(key in this.stringProps) && !this.other.validate(instance[key])) return false 123 | } 124 | 125 | // Checking if regexps have the appropriate occurence number in the object 126 | for (var i = 0; i < self.regexpProps.length; i++) { 127 | var prop = self.regexpProps[i] 128 | if ((prop.min > occurences[i]) || (occurences[i] > prop.max)) return false 129 | } 130 | 131 | // If all checks passed, the instance conforms to the schema 132 | return true 133 | }, 134 | 135 | toJSON: Schema.session(function() { 136 | var i, property, regexp, json = Schema.prototype.toJSON.call(this, true) 137 | 138 | if (json['$ref'] != null) return json 139 | 140 | json.type = 'object' 141 | 142 | for (i in this.stringProps) { 143 | property = this.stringProps[i] 144 | json.properties = json.properties || {} 145 | json.properties[property.key] = property.value.toJSON() 146 | if (property.min === 1) json.properties[property.key].required = true 147 | if (property.title) json.properties[property.key].title = property.title 148 | } 149 | 150 | for (i = 0; i < this.regexpProps.length; i++) { 151 | property = this.regexpProps[i] 152 | json.patternProperties = json.patternProperties || {} 153 | regexp = property.key.toString() 154 | regexp = regexp.substr(2, regexp.length - 4) 155 | json.patternProperties[regexp] = property.value.toJSON() 156 | if (property.title) json.patternProperties[regexp].title = property.title 157 | } 158 | 159 | if (this.other !== anything) { 160 | json.additionalProperties = (this.other === nothing) ? false : this.other.toJSON() 161 | } 162 | 163 | return json 164 | }) 165 | }) 166 | 167 | // Testing if a given string is a real regexp or just a single string escaped 168 | // If it is just a string escaped, return the string. Otherwise return the regexp 169 | var regexpString = (function() { 170 | // Special characters that should be escaped when describing a regular string in regexp 171 | var shouldBeEscaped = '[](){}^$?*+.'.split('').map(function(element) { 172 | return RegExp('(\\\\)*\\' + element, 'g') 173 | }) 174 | // Special characters that shouldn't be escaped when describing a regular string in regexp 175 | var shouldntBeEscaped = 'bBwWdDsS'.split('').map(function(element) { 176 | return RegExp('(\\\\)*' + element, 'g') 177 | }) 178 | 179 | return function(string) { 180 | var i, j, match 181 | 182 | for (i = 0; i < shouldBeEscaped.length; i++) { 183 | match = string.match(shouldBeEscaped[i]) 184 | if (!match) continue 185 | for (j = 0; j < match.length; j++) { 186 | // If it is not escaped, it must be a regexp (e.g. [, \\[, \\\\[, etc.) 187 | if (match[j].length % 2 === 1) return RegExp('^' + string + '$') 188 | } 189 | } 190 | for (i = 0; i < shouldntBeEscaped.length; i++) { 191 | match = string.match(shouldntBeEscaped[i]) 192 | if (!match) continue 193 | for (j = 0; j < match.length; j++) { 194 | // If it is escaped, it must be a regexp (e.g. \b, \\\b, \\\\\b, etc.) 195 | if (match[j].length % 2 === 0) return RegExp('^' + string + '$') 196 | } 197 | } 198 | 199 | // It is not a real regexp. Removing the escaping. 200 | for (i = 0; i < shouldBeEscaped.length; i++) { 201 | string = string.replace(shouldBeEscaped[i], function(match) { 202 | return match.substr(1) 203 | }) 204 | } 205 | 206 | return string 207 | } 208 | })() 209 | 210 | Schema.fromJS.def(function(object) { 211 | if (!(object instanceof Object)) return 212 | 213 | var other, property, properties = [] 214 | for (var key in object) { 215 | property = { 216 | value: Schema.fromJS(object[key]) 217 | } 218 | 219 | // '*' as property name means 'every other property should match this schema' 220 | if (key === '*') { 221 | other = property.value 222 | continue 223 | } 224 | 225 | // Handling special chars at the beginning of the property name 226 | property.min = (key[0] === '*' || key[0] === '?') ? 0 : 1 227 | property.max = (key[0] === '*' || key[0] === '+') ? Infinity : 1 228 | key = key.replace(/^[*?+]/, '') 229 | 230 | // Handling property title that looks like: { 'a : an important property' : Number } 231 | key = key.replace(/\s*:[^:]+$/, function(match) { 232 | property.title = match.replace(/^\s*:\s*/, '') 233 | return '' 234 | }) 235 | 236 | // Testing if it is regexp-like or not. If it is, then converting to a regexp object 237 | property.key = regexpString(key) 238 | 239 | properties.push(property) 240 | } 241 | 242 | return new ObjectSchema(properties, other) 243 | }) 244 | 245 | Schema.fromJSON.def(function(json) { 246 | if (!json || json.type !== 'object') return 247 | 248 | var key, properties = [] 249 | for (key in json.properties) { 250 | properties.push({ 251 | min: json.properties[key].required ? 1 : 0, 252 | max: 1, 253 | key: key, 254 | value: Schema.fromJSON(json.properties[key]), 255 | title: json.properties[key].title 256 | }) 257 | } 258 | for (key in json.patternProperties) { 259 | properties.push({ 260 | min: 0, 261 | max: Infinity, 262 | key: RegExp('^' + key + '$'), 263 | value: Schema.fromJSON(json.patternProperties[key]), 264 | title: json.patternProperties[key].title 265 | }) 266 | } 267 | 268 | var other 269 | if (json.additionalProperties !== undefined) { 270 | other = json.additionalProperties === false ? nothing : Schema.fromJSON(json.additionalProperties) 271 | } 272 | 273 | return new ObjectSchema(properties, other) 274 | }) 275 | -------------------------------------------------------------------------------- /lib/patterns/or.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../BaseSchema') 2 | , EqualitySchema = require('../patterns/equality') 3 | 4 | var OrSchema = module.exports = Schema.patterns.OrSchema = Schema.extend({ 5 | initialize: function(schemas) { 6 | this.schemas = schemas 7 | }, 8 | errors: function(instance) { 9 | var self = this 10 | 11 | var errors = [] 12 | if (!this.validate(instance)) { 13 | this.schemas.forEach(function(sch) { 14 | var result = sch.errors(instance) 15 | if (result) { 16 | errors.push(result) 17 | } 18 | }) 19 | if (errors.length > 0) { 20 | return errors; 21 | } 22 | } 23 | return false 24 | }, 25 | validate: function(instance) { 26 | return this.schemas.some(function(sch) { 27 | return sch.validate(instance) 28 | }) 29 | }, 30 | 31 | toJSON: Schema.session(function() { 32 | var json = Schema.prototype.toJSON.call(this, true) 33 | , subjsons = this.schemas.map(function(sch) { 34 | return sch.toJSON() 35 | }) 36 | , onlyEquality = subjsons.every(function(json) { 37 | return json['enum'] instanceof Array && json['enum'].length === 1 38 | }) 39 | 40 | if (json['$ref'] != null) return json 41 | 42 | if (onlyEquality) { 43 | json['enum'] = subjsons.map(function(json) { 44 | return json['enum'][0] 45 | }) 46 | 47 | } else { 48 | json['type'] = subjsons.map(function(json) { 49 | var simpleType = typeof json.type === 'string' && Object.keys(json).length === 1 50 | return simpleType ? json.type : json 51 | }) 52 | } 53 | 54 | return json 55 | }) 56 | }) 57 | 58 | 59 | Schema.fromJS.def(function(schemas) { 60 | if (schemas instanceof Array) return new OrSchema(schemas.map(function(sch) { 61 | return sch === undefined ? Schema.self : Schema.fromJS(sch) 62 | })) 63 | }) 64 | 65 | Schema.fromJSON.def(function(sch) { 66 | if (!sch) return 67 | 68 | if (sch['enum'] instanceof Array) { 69 | return new OrSchema(sch['enum'].map(function(object) { 70 | return new EqualitySchema(object) 71 | })) 72 | } 73 | 74 | if (sch['type'] instanceof Array) { 75 | return new OrSchema(sch['type'].map(function(type) { 76 | return Schema.fromJSON(typeof type === 'string' ? { 77 | type: type 78 | } : type) 79 | })) 80 | } 81 | }) 82 | -------------------------------------------------------------------------------- /lib/patterns/reference.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../BaseSchema') 2 | 3 | var ReferenceSchema = module.exports = Schema.patterns.ReferenceSchema = Schema.extend({ 4 | initialize: function(value) { 5 | this.value = value 6 | }, 7 | getName: function(obj) { 8 | if (obj instanceof Object) { 9 | return obj.constructor.name + ' = ' + obj 10 | } else { 11 | return typeof obj + ' = ' + obj 12 | } 13 | }, 14 | errors: function(instance) { 15 | if (instance == null) { 16 | return ( instance + ' is not a reference' ) 17 | } 18 | if (instance !== this.value) { 19 | var middleMessage = ' is not reference to ' 20 | return ( this.getName(instance) + middleMessage + this.getName(this.value) ) 21 | } 22 | return false 23 | }, 24 | validate: function(instance) { 25 | return instance === this.value 26 | }, 27 | 28 | toJSON: function() { 29 | var json = Schema.prototype.toJSON.call(this) 30 | 31 | json['enum'] = [this.value] 32 | 33 | return json 34 | } 35 | }) 36 | 37 | 38 | Schema.fromJS.def(function(value) { 39 | return new ReferenceSchema(value) 40 | }) 41 | -------------------------------------------------------------------------------- /lib/patterns/regexp.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../BaseSchema') 2 | 3 | var RegexpSchema = module.exports = Schema.patterns.RegexpSchema = Schema.extend({ 4 | initialize: function(regexp) { 5 | this.regexp = regexp 6 | }, 7 | errors: function(instance) { 8 | var message 9 | if (!(Object(instance) instanceof String)) { 10 | message = instance + ' is not a String' 11 | } else if (this.regexp && !this.regexp.test(instance)) { 12 | message = instance + ' is not matched with RegExp -> ' + this.regexp 13 | } 14 | 15 | if (message) 16 | return message 17 | return false 18 | }, 19 | validate: function(instance) { 20 | return Object(instance) instanceof String && (!this.regexp || this.regexp.test(instance)) 21 | }, 22 | 23 | toJSON: function() { 24 | var json = Schema.prototype.toJSON.call(this) 25 | 26 | json.type = 'string' 27 | 28 | if (this.regexp) { 29 | json.pattern = this.regexp.toString() 30 | json.pattern = json.pattern.substr(1, json.pattern.length - 2) 31 | } 32 | 33 | return json 34 | } 35 | }) 36 | 37 | Schema.fromJSON.def(function(sch) { 38 | if (!sch || sch.type !== 'string') return 39 | 40 | if ('pattern' in sch) { 41 | return new RegexpSchema(RegExp('^' + sch.pattern + '$')) 42 | } else if ('minLength' in sch || 'maxLength' in sch) { 43 | return new RegexpSchema(RegExp('^.{' + [sch.minLength || 0, sch.maxLength].join(',') + '}$')) 44 | } else { 45 | return new RegexpSchema() 46 | } 47 | }) 48 | 49 | Schema.fromJS.def(function(regexp) { 50 | if (regexp instanceof RegExp) return new RegexpSchema(regexp) 51 | }) 52 | -------------------------------------------------------------------------------- /lib/patterns/schema.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../BaseSchema') 2 | 3 | Schema.fromJS.def(function(sch) { 4 | if (sch instanceof Schema) return sch 5 | }) 6 | -------------------------------------------------------------------------------- /lib/schema.js: -------------------------------------------------------------------------------- 1 | var Schema = require('./BaseSchema') 2 | 3 | schema = module.exports = function(schemaDescription) { 4 | var doc, schemaObject 5 | 6 | if (arguments.length === 2) { 7 | doc = schemaDescription 8 | schemaDescription = arguments[1] 9 | } 10 | 11 | if (this instanceof schema) { 12 | // When called with new, create a schema object and then return the schema function 13 | var constructor = Schema.extend(schemaDescription) 14 | schemaObject = new constructor() 15 | if (doc) schemaObject.doc = doc 16 | return schemaObject.wrap() 17 | 18 | } else { 19 | // When called as simple function, forward everything to fromJS 20 | // and then resolve schema.self to the resulting schema object 21 | schemaObject = Schema.fromJS(schemaDescription) 22 | schema.self.resolve(schemaObject) 23 | if (doc) schemaObject.doc = doc 24 | return schemaObject.wrap() 25 | } 26 | } 27 | 28 | schema.Schema = Schema 29 | 30 | schema.toJSON = function(sch) { 31 | return Schema.fromJS(sch).toJSON() 32 | } 33 | 34 | schema.fromJS = function(sch) { 35 | return Schema.fromJS(sch).wrap() 36 | } 37 | 38 | schema.fromJSON = function(sch) { 39 | return Schema.fromJSON(sch).wrap() 40 | } 41 | 42 | // define js-schema using AMD 43 | if (typeof define === 'function' && define.amd) { 44 | define([], function(){ 45 | return schema; 46 | }); 47 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Gábor Molnár ", 3 | "contributors": [ 4 | "Alan James", 5 | "Kuba Wyrobek", 6 | "Mikael Berg", 7 | "Alex Ivanov" 8 | ], 9 | "name": "js-schema", 10 | "description": "A simple and intuitive object validation library", 11 | "keywords": [ 12 | "json", 13 | "schema", 14 | "JSON Schema", 15 | "validation", 16 | "validator" 17 | ], 18 | "version": "1.0.1", 19 | "homepage": "https://github.com/molnarg/js-schema", 20 | "repository": { 21 | "type": "git", 22 | "url": "git@github.com:molnarg/js-schema.git" 23 | }, 24 | "devDependencies": { 25 | "browserify": "*", 26 | "uglify-js": "*", 27 | "underscore": "^1.7.0", 28 | "vows": "^0.7.0" 29 | }, 30 | "engines": { 31 | "node": "*" 32 | }, 33 | "main": "./index.js" 34 | } 35 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | vows test/extensions/errors-array.js 4 | vows test/extensions/errors-object.js 5 | vows test/extensions/errors-boolean.js 6 | vows test/extensions/errors-number.js 7 | vows test/extensions/errors-reference.js 8 | vows test/extensions/validate-object.js 9 | -------------------------------------------------------------------------------- /test/extensions/errors-array.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , assert = require('assert') 3 | , printTestResult = require('../printTestResult.js') 4 | , schema = require('../../index.js') 5 | 6 | // Create a Test Suite 7 | vows.describe('Validation Array with errors').addBatch({ 8 | 'Array.of': { 9 | 'Number -> invalid input : [null, undefined, Object, Function]': function() { 10 | var input = [ 11 | null, undefined, Object, Function 12 | ]; 13 | var aSchema = schema(Array.of(Number)); 14 | input.forEach(function(input) { 15 | var result = aSchema.errors(input); 16 | assert(/(.*) is not an instance of Array/.test(result), 'Errors should return "' + input + '" is not an instance of Array'); 17 | }) 18 | }, 19 | 'Number -> valid input : only numbers': function() { 20 | var input = [9, 0, 1, 2, 3, 4, 5, 6]; 21 | var aSchema = schema(Array.of(Number)); 22 | var result = aSchema.errors(input); 23 | assert(result === false, 'errors method should return false'); 24 | } 25 | } 26 | }).export(module) 27 | -------------------------------------------------------------------------------- /test/extensions/errors-boolean.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , assert = require('assert') 3 | , printTestResult = require('../printTestResult.js') 4 | , schema = require('../../index.js') 5 | 6 | // Create a Test Suite 7 | vows.describe('Validation Boolean with errors').addBatch({ 8 | 'Boolean': { 9 | topic: function() { 10 | return { 11 | invalid_inputs: [0, -1, 1, 'false', 'true', 'whatever', NaN, [], {}, /dwa/] 12 | } 13 | }, 14 | 'invalid input -> [0,-1,1,"false","true","whatever",NaN, [],{},/dwa/]': function(topic) { 15 | topic.invalid_inputs.forEach(function(input) { 16 | var result = schema(Boolean).errors(input); 17 | assert(/(.*) is not Boolean/.test(result), 'Errors should throw : ' + input + ' is not Boolean') 18 | assert(/(.*) is not Boolean/.test(result), 'Errors should throw : ' + input + ' is not Boolean') 19 | }) 20 | 21 | } 22 | } 23 | }).export(module) 24 | -------------------------------------------------------------------------------- /test/extensions/errors-number.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , assert = require('assert') 3 | , printTestResult = require('../printTestResult.js') 4 | , schema = require('../../index.js') 5 | 6 | 7 | // Create a Test Suite 8 | vows.describe('Validation Number with errors').addBatch({ 9 | 10 | 'Number': { 11 | topic: function() { 12 | return { 13 | positive_number: 5, 14 | negative_number: -32, 15 | zero: 0 16 | } 17 | }, 18 | 'min() - invalid input': function(topic) { 19 | var result = schema(Number.min(5)).errors(0); 20 | assert(/number = (.*) is smaller than required minimum = (.*)/.test(result), 'Error should return : number = X is smaller than required minimum = Y'); 21 | }, 22 | 'min() - valid input': function(topic) { 23 | var result = schema(Number.min(5)).errors(5); 24 | assert(!result, printTestResult(5, result)) 25 | }, 26 | 'max() - invalid input': function(topic) { 27 | var result = schema(Number.max(5)).errors(9); 28 | assert(/number = (.*) is bigger than required maximum = (.*)/.test(result), 'Error should return : number = X is bigger than required maximum = Y'); 29 | }, 30 | 'max() - valid input': function(topic) { 31 | var result = schema(Number.max(5)).errors(5); 32 | assert(!result, printTestResult(5, result)) 33 | }, 34 | 'min().max() - valid input': function(topic) { 35 | var result = schema(Number.min(0).max(5)).errors(5); 36 | assert(!result, printTestResult(5, result)) 37 | } 38 | } 39 | }).export(module) 40 | -------------------------------------------------------------------------------- /test/extensions/errors-object.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , _ = require('underscore') 3 | , assert = require('assert') 4 | , schema = require('../../index.js') 5 | 6 | 7 | // Create a Test Suite 8 | vows.describe('Testing if errors() precisely informs about the cause').addBatch({ 9 | 'Object : ': { 10 | topic:function(){ 11 | return { 12 | input_invalid_boolean: {test: false}, 13 | input_invalid_function: {test: function () {}}, 14 | input_invalid_object: {test: {}}, 15 | input_invalid_number: {test: 190}, 16 | input_invalid_string: {test: "hello world"} 17 | } 18 | }, 19 | 'Examples from docs':{ 20 | 'Testing Duck':function(t) { 21 | 22 | var Duck = schema({ // A duck 23 | swim: Function, // - can swim 24 | quack: Function, // - can quack 25 | age: Number.min(0).max(5), // - is 0 to 5 years old 26 | color: ['yellow', 'brown'] // - has either yellow or brown color 27 | }); 28 | var instance = { 29 | swim: function () { }, 30 | quack: function () { }, 31 | age: 6, 32 | color: 'green' 33 | } 34 | 35 | assert(instance.age === 6, "Input is broken. Test assumes that instance.age = 6. Instance.age equal to " + instance.age) 36 | assert(instance.color === 'green', "Input is broken. Test assumes that instance.color = 'green'. Instance.test equal to " + instance.color) 37 | 38 | var validation = false === Duck(instance); 39 | assert(validation, 'Validation is broken. Incorrect object '+vows.inspect(instance)+' should NOT be validated.'); 40 | 41 | var errors = Duck.errors(instance); 42 | validation = /number = 6 is bigger than required maximum = 5/.test(errors.age); 43 | errors.color.forEach(function(msg, index,arr){ 44 | assert(/string = green is not reference to (.*)/.test(errors.color[index]), "Incorrect message was returned = " + errors.color[index] ); 45 | }) 46 | 47 | validation = /number = 6 is bigger than required maximum = 5/.test(errors.color); 48 | 49 | } 50 | 51 | }, 52 | 'Number': { 53 | topic:function(common){ 54 | return _.defaults({ 55 | schema: schema({test: Number.min(0).max(10)}), 56 | input_invalid_string: {test: "hello world"}, 57 | input_invalid_number: {test: 190}, 58 | input_valid: {test: 5} 59 | }, common); 60 | }, 61 | 62 | 'Object { test : Number } : String was passed instead Number': function (t) { 63 | var instance = t.input_invalid_string; 64 | assert(_.isString(instance.test), "Input is broken ("+vows.inspect(instance)+"). Test assumes that 'instance.test' is String") 65 | 66 | var validation = false === t.schema(instance); 67 | assert(validation, 'Validation is broken. Incorrect object '+vows.inspect(instance)+' should NOT be validated.'); 68 | 69 | var errors = t.schema.errors(instance); 70 | validation = /hello world is not Number/.test(errors.test); 71 | 72 | assert(validation, 'Function schema.errors returns incorrect message : \n \t\t' + vows.inspect(errors)); 73 | }, 74 | 'Object { test : Number } : Too big Number was passed ': function (t) { 75 | var instance = t.input_invalid_number; 76 | assert(_.isNumber(instance.test), "Input is broken ("+vows.inspect(instance)+"). Test assumes that 'instance.test' is Number") 77 | 78 | var validation = false === t.schema(instance); 79 | assert(validation, 'Validation is broken. Incorrect object should NOT be validated.'); 80 | 81 | var errors = t.schema.errors(instance); 82 | validation = /number = 190 is bigger than required maximum = 10/.test(errors.test); 83 | 84 | assert(validation, 'Function schema.errors returns incorrect message : \n \t\t' + vows.inspect(errors)); 85 | } 86 | }, 87 | 'String': { 88 | topic: function(common){ 89 | return _.defaults({ 90 | schema: schema({test: String}), 91 | input_valid: {test: "some string"} 92 | }, common); 93 | }, 94 | 'Object { test : String } : Boolean | Function | Object | Number was passed instead String': function (t) { 95 | [t.input_invalid_boolean, t.input_invalid_function, t.input_invalid_number, t.input_invalid_object].forEach(function (instance, index, array) { 96 | var validation = false === t.schema(instance); 97 | assert(!_.isString(instance.test), "Input nr "+index+" is broken ("+vows.inspect(instance)+"). Test assumes that 'instance.test' is not String") 98 | assert(validation, 'Validation is broken. Received input '+vows.inspect(t.input_invalid_string)+' should NOT be validated.'); 99 | 100 | var errors = t.schema.errors(instance); 101 | validation = /(.*) is not a String/.test(errors.test); 102 | 103 | assert(validation, 'Function schema.errors returns incorrect message : \n \t\t' + vows.inspect(errors)); 104 | }) 105 | 106 | } 107 | } 108 | } 109 | 110 | }).export(module) 111 | -------------------------------------------------------------------------------- /test/extensions/errors-reference.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , assert = require('assert') 3 | , printTestResult = require('../printTestResult.js') 4 | , schema = require('../../index.js') 5 | , ReferenceSchema = require('../../lib/patterns/reference') 6 | 7 | 8 | var generateTest = function(ref) { 9 | var sch = schema(new ReferenceSchema(ref)); 10 | var result = sch.errors(topic.input); 11 | assert(/Object is not reference to Function = function A(){}/.test(result), printTestResult(ref, result)); 12 | } 13 | // Create a Test Suite 14 | vows.describe('Validation Reference with errors').addBatch({ 15 | 16 | '': { 17 | topic: function() { 18 | var A = function A() { 19 | this.name = 'dwa' 20 | }; 21 | 22 | var instanceA = new A(); 23 | return { 24 | kinds: ['string', 'number', 'date', 'function A()', 'new A()', 'function ()', 'Object'], 25 | inputs: { 26 | 'null': null, 27 | 'undefined': undefined, 28 | 'string': 'other', 29 | 'number': 10, 30 | 'date': new Date(), 31 | 'A': A, 32 | 'new A()': instanceA 33 | }, 34 | schemas: { 35 | 'string': schema(new ReferenceSchema('test')), 36 | 'number': schema(new ReferenceSchema(10)), 37 | 'date': schema(new ReferenceSchema(new Date())), 38 | 'A': schema(new ReferenceSchema(A)), 39 | 'new A()': schema(new ReferenceSchema(instanceA)) 40 | } 41 | } 42 | }, 43 | 'all by all': function(topic) { 44 | inputLabels: for (var inputKey in topic.inputs) { 45 | schemaLabels: for (var schemaKey in topic.schemas) { 46 | var sch = topic.schemas[schemaKey]; 47 | var inp = topic.inputs[inputKey]; 48 | if (schemaKey != inputKey) { 49 | var result = sch.errors(inp); 50 | assert(result) 51 | if (result) { 52 | console.log(' all by all', vows.inspect(result)); 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | }).export(module) 60 | -------------------------------------------------------------------------------- /test/extensions/validate-object.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , assert = require('assert') 3 | , printTestResult = require('../printTestResult.js') 4 | , schema = require('../../index.js') 5 | 6 | 7 | // Create a Test Suite 8 | vows.describe('Validation Object').addBatch({ 9 | 'Object': { 10 | "original validate schema has a bug with regexp field validation.": function () { 11 | //object should have at least one String field 12 | var validation = schema({ 13 | "+.+" : String 14 | }); 15 | var emptyObject = {}; 16 | assert(false === validation(emptyObject)); 17 | 18 | var emptyArray = []; 19 | assert(false === validation(emptyArray)); 20 | }, 21 | "some fields are valid, all the rest are not": function () { 22 | var validation = schema({ 23 | "+check.?" : Number, 24 | "*" : null 25 | }); 26 | var correctObject = { 27 | check1 : 1, 28 | check2 : 2 29 | }; 30 | assert(true, validation(correctObject)); 31 | var incorrectObject = { 32 | check1 : 1, 33 | check2 : 2, 34 | someOther : 3 35 | }; 36 | assert(false === validation(incorrectObject)); 37 | }, 38 | "with allowed other fields": function () { 39 | var anotherValidation = schema({ 40 | "+check.?": Number, 41 | "*": Number 42 | }); 43 | var o1 = { 44 | check1: 1, 45 | check2: 2 46 | }; 47 | var o2 = { 48 | check1: 1, 49 | check2: 2, 50 | someOther: 3 51 | }; 52 | 53 | assert(true === anotherValidation(o1)); 54 | assert(true === anotherValidation(o2)); 55 | }, 56 | "empty object should be valid sometimes": function () { 57 | var allowAll = schema({ 58 | "*": String 59 | }); 60 | assert(true, allowAll({})); 61 | 62 | var allowSome = schema({ 63 | "check": [null, String] 64 | }); 65 | assert(true === allowSome({})); 66 | } 67 | } 68 | }).export(module) 69 | -------------------------------------------------------------------------------- /test/printTestResult.js: -------------------------------------------------------------------------------- 1 | module.exports = function (input, errors, schema){ 2 | input = input || {}; 3 | return [ 4 | "",schema ? "Schema:\n"+JSON.stringify(schema, null, 2) :"", 5 | "","Input:",JSON.stringify(input, null, 2), 6 | "","Errors:",JSON.stringify(errors, null,2) 7 | ].join("\n") 8 | } 9 | --------------------------------------------------------------------------------