├── src ├── test │ ├── expected │ │ ├── empty.json │ │ ├── sample1.json │ │ ├── single-property.json │ │ ├── explicit-object-single-property.json │ │ ├── single-required-property.json │ │ ├── additionalProperties-true.json │ │ ├── additionalProperties-false.json │ │ ├── multiple-properties.json │ │ └── single-required-property-additionalProperties-false.json │ ├── helpers.js │ └── test.js └── lib │ ├── ArrayKeyword.js │ ├── Keyword.js │ ├── StringKeyword.js │ ├── NumberKeyword.js │ ├── ObjectKeyword.js │ ├── Builder.js │ ├── Default.js │ ├── Maximum.js │ ├── Minimum.js │ ├── RefKeyword.js │ ├── UniqueItems.js │ ├── MultipleOf.js │ ├── MaxItems.js │ ├── MinItems.js │ ├── Pattern.js │ ├── MaxLength.js │ ├── MinLength.js │ ├── MaxProperties.js │ ├── MinProperties.js │ ├── Not.js │ ├── ExclusiveMaximum.js │ ├── ExclusiveMinimum.js │ ├── Required.js │ ├── Enum.js │ ├── Definitions.js │ ├── Format.js │ ├── AdditionalItems.js │ ├── AdditionalProperties.js │ ├── AllOf.js │ ├── AnyOf.js │ ├── OneOf.js │ ├── Type.js │ ├── Items.js │ ├── Properties.js │ ├── PatternProperties.js │ ├── Dependencies.js │ ├── index.js │ └── Schema.js ├── .gitignore ├── .npmignore ├── package.json ├── gulpfile.js └── README.md /src/test/expected/empty.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | src/ 3 | -------------------------------------------------------------------------------- /src/test/expected/sample1.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "string" 3 | } -------------------------------------------------------------------------------- /src/test/expected/single-property.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "foo": {} 4 | } 5 | } -------------------------------------------------------------------------------- /src/lib/ArrayKeyword.js: -------------------------------------------------------------------------------- 1 | import Keyword from './Keyword'; 2 | 3 | export default class ArrayKeyword extends Keyword {} -------------------------------------------------------------------------------- /src/lib/Keyword.js: -------------------------------------------------------------------------------- 1 | import Builder from './Builder'; 2 | 3 | export default class Keyword extends Builder {} 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/lib/StringKeyword.js: -------------------------------------------------------------------------------- 1 | import Keyword from './Keyword'; 2 | 3 | export default class StringKeyword extends Keyword {} -------------------------------------------------------------------------------- /src/lib/NumberKeyword.js: -------------------------------------------------------------------------------- 1 | import Keyword from './Keyword'; 2 | 3 | export default class NumberKeyword extends Keyword {} 4 | -------------------------------------------------------------------------------- /src/lib/ObjectKeyword.js: -------------------------------------------------------------------------------- 1 | import Keyword from './Keyword'; 2 | 3 | export default class ObjectKeyword extends Keyword {} 4 | -------------------------------------------------------------------------------- /src/test/expected/explicit-object-single-property.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "foo": {} 5 | } 6 | } -------------------------------------------------------------------------------- /src/lib/Builder.js: -------------------------------------------------------------------------------- 1 | export default class Builder { 2 | json(context) { 3 | throw new Error('json must be overridden'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/test/expected/single-required-property.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "foo": {} 4 | }, 5 | "required": [ 6 | "foo" 7 | ] 8 | } -------------------------------------------------------------------------------- /src/test/expected/additionalProperties-true.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "foo": {} 5 | }, 6 | "additionalProperties": true 7 | } -------------------------------------------------------------------------------- /src/test/expected/additionalProperties-false.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "foo": {} 5 | }, 6 | "additionalProperties": false 7 | } -------------------------------------------------------------------------------- /src/test/expected/multiple-properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "foo": { "type": "string" }, 4 | "bar": { "type": "integer" } 5 | }, 6 | "required": [ "foo" ] 7 | } -------------------------------------------------------------------------------- /src/test/expected/single-required-property-additionalProperties-false.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "foo": {} 4 | }, 5 | "required": [ 6 | "foo" 7 | ], 8 | "additionalProperties": false 9 | } -------------------------------------------------------------------------------- /src/test/helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stringify: stringify, 3 | isEqual: require('lodash').isEqual 4 | }; 5 | 6 | function stringify(obj) { 7 | return JSON.stringify(obj, null, 2); 8 | } 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/lib/Default.js: -------------------------------------------------------------------------------- 1 | import Keyword from './Keyword'; 2 | import Schema from './Schema'; 3 | 4 | export default class Default extends Keyword { 5 | constructor(value) { 6 | super(); 7 | this.value = value; 8 | } 9 | 10 | get value() { 11 | return this._value; 12 | } 13 | 14 | set value(value) { 15 | this._value = value; 16 | } 17 | 18 | json(context) { 19 | context = context || {}; 20 | 21 | const value = (this.value instanceof Schema) 22 | ? this.value.json({}) 23 | : this.value; 24 | 25 | context.default = value; 26 | 27 | return context; 28 | } 29 | } -------------------------------------------------------------------------------- /src/lib/Maximum.js: -------------------------------------------------------------------------------- 1 | import NumberKeyword from './NumberKeyword'; 2 | 3 | export default class Maximum extends NumberKeyword { 4 | constructor(value) { 5 | super(); 6 | this.value = value; 7 | } 8 | 9 | set value(value) { 10 | if (typeof value != 'number') { 11 | throw new Error('value must be a number'); 12 | } 13 | 14 | this._value = value; 15 | } 16 | 17 | get value() { 18 | return this._value; 19 | } 20 | 21 | json(context) { 22 | context = context || {}; 23 | 24 | context.maximum = this.value; 25 | return context; 26 | } 27 | } -------------------------------------------------------------------------------- /src/lib/Minimum.js: -------------------------------------------------------------------------------- 1 | import NumberKeyword from './NumberKeyword'; 2 | 3 | export default class Minimum extends NumberKeyword { 4 | constructor(value) { 5 | super(); 6 | this.value = value; 7 | } 8 | 9 | set value(value) { 10 | if (typeof value != 'number') { 11 | throw new Error('value must be a number'); 12 | } 13 | 14 | this._value = value; 15 | } 16 | 17 | get value() { 18 | return this._value; 19 | } 20 | 21 | json(context) { 22 | context = context || {}; 23 | 24 | context.minimum = this.value; 25 | return context; 26 | } 27 | } -------------------------------------------------------------------------------- /src/lib/RefKeyword.js: -------------------------------------------------------------------------------- 1 | import Keyword from './Keyword'; 2 | 3 | export default class RefKeyword extends Keyword { 4 | constructor(value) { 5 | super(); 6 | this.value = value; 7 | } 8 | 9 | get value() { 10 | return this._value; 11 | } 12 | 13 | set value(value) { 14 | if (typeof value != 'string') { 15 | // TODO better validation 16 | throw new Error('value must be a valid uri string'); 17 | } 18 | 19 | this._value = value; 20 | } 21 | 22 | json(context) { 23 | context = context || {}; 24 | context.$ref = this.value; 25 | return context; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/UniqueItems.js: -------------------------------------------------------------------------------- 1 | import ArrayKeyword from './ArrayKeyword'; 2 | 3 | export default class UniqueItems extends ArrayKeyword { 4 | constructor(value) { 5 | super(); 6 | this.value = value; 7 | } 8 | 9 | get value() { 10 | return this._value; 11 | } 12 | 13 | set value(value) { 14 | if (typeof value == 'boolean') { 15 | this._value = value; 16 | } else { 17 | throw new Error('value must be a boolean value'); 18 | } 19 | } 20 | 21 | json(context) { 22 | context = context || {}; 23 | 24 | context.uniqueItems = this.value; 25 | return context; 26 | } 27 | } -------------------------------------------------------------------------------- /src/lib/MultipleOf.js: -------------------------------------------------------------------------------- 1 | import NumberKeyword from './NumberKeyword'; 2 | 3 | export default class MultipleOf extends NumberKeyword { 4 | constructor(value) { 5 | super(); 6 | this.value = value; 7 | } 8 | 9 | set value(value) { 10 | if (typeof value != 'number' || value <= 0) { 11 | throw new Error('value must be a number greater than 0'); 12 | } 13 | 14 | this._value = value; 15 | } 16 | 17 | get value() { 18 | return this._value; 19 | } 20 | 21 | json(context) { 22 | context = context || {}; 23 | 24 | context.multipleOf = this.value; 25 | return context; 26 | } 27 | } -------------------------------------------------------------------------------- /src/lib/MaxItems.js: -------------------------------------------------------------------------------- 1 | import ArrayKeyword from './ArrayKeyword'; 2 | 3 | export default class MaxItems extends ArrayKeyword { 4 | constructor(value) { 5 | super(); 6 | this.value = value; 7 | } 8 | 9 | get value() { 10 | return this._value; 11 | } 12 | 13 | set value(value) { 14 | if (value >= 0 && Number.isInteger(value)) { 15 | this._value = value; 16 | } else { 17 | throw new Error('value must be an integer and greater than or equal to 0'); 18 | } 19 | } 20 | 21 | json(context) { 22 | context = context || {}; 23 | 24 | context.maxItems = this.value; 25 | return context; 26 | } 27 | } -------------------------------------------------------------------------------- /src/lib/MinItems.js: -------------------------------------------------------------------------------- 1 | import ArrayKeyword from './ArrayKeyword'; 2 | 3 | export default class MinItems extends ArrayKeyword { 4 | constructor(value) { 5 | super(); 6 | this.value = value; 7 | } 8 | 9 | get value() { 10 | return this._value; 11 | } 12 | 13 | set value(value) { 14 | if (value >= 0 && Number.isInteger(value)) { 15 | this._value = value; 16 | } else { 17 | throw new Error('value must be an integer and greater than or equal to 0'); 18 | } 19 | } 20 | 21 | json(context) { 22 | context = context || {}; 23 | 24 | context.minItems = this.value; 25 | return context; 26 | } 27 | } -------------------------------------------------------------------------------- /src/lib/Pattern.js: -------------------------------------------------------------------------------- 1 | import StringKeyword from './StringKeyword'; 2 | 3 | export default class Pattern extends StringKeyword { 4 | constructor(value) { 5 | super(); 6 | this.value = value; 7 | } 8 | 9 | get value() { 10 | return this._value; 11 | } 12 | 13 | set value(value) { 14 | if (typeof value === 'string') { 15 | this._value = value; 16 | } else { 17 | throw new Error('value must be a string and should be a valid regular expression'); 18 | } 19 | } 20 | 21 | json(context) { 22 | context = context || {}; 23 | 24 | context.pattern = this.value; 25 | return context; 26 | } 27 | } -------------------------------------------------------------------------------- /src/lib/MaxLength.js: -------------------------------------------------------------------------------- 1 | import StringKeyword from './StringKeyword'; 2 | 3 | export default class MaxLength extends StringKeyword { 4 | constructor(value) { 5 | super(); 6 | this.value = value; 7 | } 8 | 9 | get value() { 10 | return this._value; 11 | } 12 | 13 | set value(value) { 14 | if (value >= 0 && Number.isInteger(value)) { 15 | this._value = value; 16 | } else { 17 | throw new Error('value must be an integer and greater than or equal to 0'); 18 | } 19 | } 20 | 21 | json(context) { 22 | context = context || {}; 23 | 24 | context.maxLength = this.value; 25 | return context; 26 | } 27 | } -------------------------------------------------------------------------------- /src/lib/MinLength.js: -------------------------------------------------------------------------------- 1 | import StringKeyword from './StringKeyword'; 2 | 3 | export default class MinLength extends StringKeyword { 4 | constructor(value) { 5 | super(); 6 | this.value = value; 7 | } 8 | 9 | get value() { 10 | return this._value; 11 | } 12 | 13 | set value(value) { 14 | if (value >= 0 && Number.isInteger(value)) { 15 | this._value = value; 16 | } else { 17 | throw new Error('value must be an integer and greater than or equal to 0'); 18 | } 19 | } 20 | 21 | json(context) { 22 | context = context || {}; 23 | 24 | context.minLength = this.value; 25 | return context; 26 | } 27 | } -------------------------------------------------------------------------------- /src/lib/MaxProperties.js: -------------------------------------------------------------------------------- 1 | import ObjectKeyword from './ObjectKeyword'; 2 | 3 | export default class MaxProperties extends ObjectKeyword { 4 | constructor(value) { 5 | super(); 6 | this.value = value || 0; 7 | } 8 | 9 | get value() { 10 | return this._value; 11 | } 12 | 13 | set value(value) { 14 | if (Number.isInteger(value) && value >= 0) { 15 | this._value = value; 16 | } else { 17 | throw new Error('value must be an integer greater than or equal to 0') 18 | } 19 | } 20 | 21 | json(context) { 22 | context = context || {}; 23 | context.maxProperties = this.value; 24 | return context; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/MinProperties.js: -------------------------------------------------------------------------------- 1 | import ObjectKeyword from './ObjectKeyword'; 2 | 3 | export default class MinProperties extends ObjectKeyword { 4 | constructor(value) { 5 | super(); 6 | this.value = value || 0; 7 | } 8 | 9 | get value() { 10 | return this._value; 11 | } 12 | 13 | set value(value) { 14 | if (Number.isInteger(value) && value >= 0) { 15 | this._value = value; 16 | } else { 17 | throw new Error('value must be an integer greater than or equal to 0') 18 | } 19 | } 20 | 21 | json(context) { 22 | context = context || {}; 23 | context.minProperties = this.value; 24 | return context; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/Not.js: -------------------------------------------------------------------------------- 1 | import Keyword from './Keyword'; 2 | import Schema from './Schema'; 3 | 4 | export default class Not extends Keyword { 5 | constructor(value) { 6 | super(); 7 | this.value = value; 8 | } 9 | 10 | get value() { 11 | return this._value; 12 | } 13 | 14 | set value(value) { 15 | if (typeof value != 'object' || !(value instanceof Schema)) { 16 | throw new Error('value must be an object and must be a valid JSON schema'); 17 | } 18 | 19 | this._value = value; 20 | } 21 | 22 | json(context) { 23 | context = context || {}; 24 | 25 | const value = (this.value instanceof Schema) 26 | ? this.value.json({}) 27 | : this.value; 28 | 29 | context.not = value; 30 | return context; 31 | } 32 | } -------------------------------------------------------------------------------- /src/lib/ExclusiveMaximum.js: -------------------------------------------------------------------------------- 1 | import NumberKeyword from './NumberKeyword'; 2 | 3 | export default class ExclusiveMaximum extends NumberKeyword { 4 | constructor(value) { 5 | super(); 6 | this.value = value; 7 | } 8 | 9 | get value() { 10 | return this._value; 11 | } 12 | 13 | set value(value) { 14 | if (typeof value == 'boolean') { 15 | this._value = value; 16 | } else { 17 | throw new Error('value must be a boolean value'); 18 | } 19 | } 20 | 21 | json(context) { 22 | context = context || {}; 23 | 24 | if (!context.hasOwnProperty('maximum')) { 25 | throw new Error("maximum must be present with exclusiveMaximum"); 26 | } 27 | 28 | context.exclusiveMaximum = this.value; 29 | return context; 30 | } 31 | } -------------------------------------------------------------------------------- /src/lib/ExclusiveMinimum.js: -------------------------------------------------------------------------------- 1 | import NumberKeyword from './NumberKeyword'; 2 | 3 | export default class ExclusiveMinimum extends NumberKeyword { 4 | constructor(value) { 5 | super(); 6 | this.value = value; 7 | } 8 | 9 | get value() { 10 | return this._value; 11 | } 12 | 13 | set value(value) { 14 | if (typeof value == 'boolean') { 15 | this._value = value; 16 | } else { 17 | throw new Error('value must be a boolean value'); 18 | } 19 | } 20 | 21 | json(context) { 22 | context = context || {}; 23 | 24 | if (!context.hasOwnProperty('minimum')) { 25 | throw new Error('minimum must be present with exclusiveMinimum'); 26 | } 27 | 28 | context.exclusiveMinimum = this.value; 29 | return context; 30 | } 31 | } -------------------------------------------------------------------------------- /src/lib/Required.js: -------------------------------------------------------------------------------- 1 | import ObjectKeyword from './ObjectKeyword'; 2 | 3 | export default class Required extends ObjectKeyword { 4 | constructor(value) { 5 | super(); 6 | 7 | if (!Array.isArray(value)) { 8 | value = Array.prototype.slice.call(arguments); 9 | } 10 | this.value = value; 11 | } 12 | 13 | get value() { 14 | return this._value; 15 | } 16 | 17 | set value(value) { 18 | if (Array.isArray(value) && value.length) { 19 | this._value = value; 20 | } else { 21 | throw new Error('value must be an array of property names with at least one element'); 22 | } 23 | } 24 | 25 | json(context) { 26 | context = context || {}; 27 | context.required = this.value; 28 | return context; 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/lib/Enum.js: -------------------------------------------------------------------------------- 1 | import Keyword from './Keyword'; 2 | 3 | export default class Enum extends Keyword { 4 | constructor(value) { 5 | super(); 6 | 7 | if (!Array.isArray(value)) { 8 | value = Array.prototype.slice.call(arguments); 9 | } 10 | this.value = value; 11 | } 12 | 13 | get value() { 14 | return this._value; 15 | } 16 | 17 | set value(value) { 18 | if (!Array.isArray(value)) { 19 | value = Array.prototype.slice.call(arguments); 20 | } 21 | 22 | if (value.length) { 23 | this._value = value; 24 | } else { 25 | throw new Error('value must be an array with at least one element'); 26 | } 27 | } 28 | 29 | json(context) { 30 | context = context || {}; 31 | context.enum = this.value; 32 | return context; 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/lib/Definitions.js: -------------------------------------------------------------------------------- 1 | import Keyword from './Keyword'; 2 | import Schema from './Schema'; 3 | 4 | export default class Definitions extends Keyword { 5 | constructor(value) { 6 | super(); 7 | this.value = value; 8 | } 9 | 10 | get value() { 11 | return this._value; 12 | } 13 | 14 | set value(value) { 15 | if (typeof value == 'object' && !Array.isArray(value)) { 16 | for (let prop in value) { 17 | if (!(prop instanceof Schema)) { 18 | throw new Error('value properties must be valid Schema instances'); 19 | } 20 | } 21 | this._value = value; 22 | } else { 23 | throw new Error('value must be an object'); 24 | } 25 | } 26 | 27 | json(context) { 28 | context = context || {}; 29 | 30 | context.definitions = value; 31 | return context; 32 | } 33 | } -------------------------------------------------------------------------------- /src/lib/Format.js: -------------------------------------------------------------------------------- 1 | import StringKeyword from './StringKeyword'; 2 | import { includes } from 'lodash'; 3 | 4 | let validFormats = [ 5 | 'date-time', 6 | 'email', 7 | 'hostname', 8 | 'ipv4', 9 | 'ipv6', 10 | 'uri' 11 | ]; 12 | 13 | export default class Format extends StringKeyword { 14 | constructor(value) { 15 | super(); 16 | this.value = value; 17 | } 18 | 19 | get value() { 20 | return this._value; 21 | } 22 | 23 | set value(value) { 24 | if (typeof value !== 'string') { 25 | throw new Error('value must be a string'); 26 | } 27 | 28 | if (!includes(validFormats, value)) { 29 | throw new Error('value must be a valid format'); 30 | } 31 | 32 | this._value = value; 33 | } 34 | 35 | json(context) { 36 | context = context || {}; 37 | 38 | context.format = this.value; 39 | return context; 40 | } 41 | } -------------------------------------------------------------------------------- /src/lib/AdditionalItems.js: -------------------------------------------------------------------------------- 1 | import ArrayKeyword from './ArrayKeyword'; 2 | import Schema from './Schema'; 3 | 4 | export default class AdditionalItems extends ArrayKeyword { 5 | constructor(value) { 6 | super(); 7 | this.value = value; 8 | } 9 | 10 | get value() { 11 | return this._value; 12 | } 13 | 14 | set value(value) { 15 | if (typeof value == 'boolean' || typeof value == 'object' || value instanceof Schema) { 16 | this._value = value; 17 | } else { 18 | throw new Error('value must be a boolean value or a Schema instance'); 19 | } 20 | } 21 | 22 | json(context) { 23 | context = context || {}; 24 | 25 | const value = (this.value instanceof Schema) 26 | ? this.value.json({}) 27 | : this.value; 28 | 29 | context.additionalItems = value; 30 | 31 | return context; 32 | } 33 | } -------------------------------------------------------------------------------- /src/lib/AdditionalProperties.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema'; 2 | import ObjectKeyword from './ObjectKeyword'; 3 | 4 | export default class AdditionalProperties extends ObjectKeyword { 5 | constructor(value) { 6 | super(); 7 | this.value = value; 8 | } 9 | 10 | get value() { 11 | return this._value; 12 | } 13 | 14 | set value(value) { 15 | if (typeof value == 'boolean' || typeof value == 'object' || value instanceof Schema) { 16 | this._value = value; 17 | } else { 18 | throw new Error('value must be a boolean value or a Schema instance'); 19 | } 20 | } 21 | 22 | json(context) { 23 | context = context || {}; 24 | 25 | const value = (this.value instanceof Schema) 26 | ? this.value.json({}) 27 | : this.value; 28 | 29 | context.additionalProperties = value; 30 | 31 | return context; 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/lib/AllOf.js: -------------------------------------------------------------------------------- 1 | import Keyword from './Keyword'; 2 | import Schema from './Schema'; 3 | 4 | export default class AllOf extends Keyword { 5 | constructor(value) { 6 | super(); 7 | this.value = arguments.length > 1 ? Array.prototype.slice.call(arguments) : value; 8 | } 9 | 10 | get value() { 11 | return this._value; 12 | } 13 | 14 | set value(value) { 15 | if (!Array.isArray(value) || !value.length) { 16 | throw new Error('value must be an array with at least one element'); 17 | } 18 | 19 | value.forEach(elem => { 20 | if (typeof elem !== 'object' || !(elem instanceof Schema)) { 21 | throw new Error('value in allOf array must be a Schema instance'); 22 | } 23 | }); 24 | 25 | this._value = value; 26 | } 27 | 28 | json(context) { 29 | context = context || {}; 30 | 31 | if (this.value) { 32 | const props = []; 33 | 34 | this.value.forEach(elem => { 35 | props.push(elem instanceof Schema ? elem.json() : elem); 36 | }); 37 | 38 | context.allOf = props; 39 | } 40 | 41 | return context; 42 | } 43 | } -------------------------------------------------------------------------------- /src/lib/AnyOf.js: -------------------------------------------------------------------------------- 1 | import Keyword from './Keyword'; 2 | import Schema from './Schema'; 3 | 4 | export default class AnyOf extends Keyword { 5 | constructor(value) { 6 | super(); 7 | this.value = arguments.length > 1 ? Array.prototype.slice.call(arguments) : value; 8 | } 9 | 10 | get value() { 11 | return this._value; 12 | } 13 | 14 | set value(value) { 15 | if (!Array.isArray(value) || !value.length) { 16 | throw new Error('value must be an array of values with at least one element'); 17 | } 18 | 19 | value.forEach(elem => { 20 | if (typeof elem != 'object' || !(elem instanceof Schema)) { 21 | throw new Error('array value must be a valid Schema instance'); 22 | } 23 | }); 24 | 25 | this._value = value; 26 | } 27 | 28 | json(context) { 29 | context = context || {}; 30 | if (this.value) { 31 | const props = []; 32 | 33 | this.value.forEach(elem => { 34 | props.push(elem instanceof Schema ? elem.json() : elem) 35 | }); 36 | 37 | context.anyOf = props; 38 | } 39 | 40 | return context; 41 | } 42 | } -------------------------------------------------------------------------------- /src/lib/OneOf.js: -------------------------------------------------------------------------------- 1 | import Keyword from './Keyword'; 2 | import Schema from './Schema'; 3 | 4 | export default class OneOf extends Keyword { 5 | constructor(value) { 6 | super(); 7 | this.value = arguments.length > 1 ? Array.prototype.slice.call(arguments) : value; 8 | } 9 | 10 | get value() { 11 | return this._value; 12 | } 13 | 14 | set value(value) { 15 | if (!Array.isArray(value) || !value.length) { 16 | throw new Error('value must be an array of values with at least one element'); 17 | } 18 | 19 | value.forEach(elem => { 20 | if (typeof elem != 'object' || !(elem instanceof Schema)) { 21 | throw new Error('array values must be valid Schema instances'); 22 | } 23 | }); 24 | 25 | this._value = value; 26 | } 27 | 28 | json(context) { 29 | context = context || {}; 30 | 31 | if (this.value) { 32 | const props = []; 33 | 34 | this.value.forEach(elem => { 35 | props.push(elem instanceof Schema ? elem.json() : elem); 36 | }); 37 | 38 | context.oneOf = props; 39 | } 40 | 41 | return context; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/Type.js: -------------------------------------------------------------------------------- 1 | import Keyword from './Keyword'; 2 | import * as _ from 'lodash'; 3 | 4 | const primitiveTypes = [ 5 | 'array', 6 | 'object', 7 | 'boolean', 8 | 'integer', 9 | 'number', 10 | 'string', 11 | 'null' 12 | ]; 13 | 14 | export default class Type extends Keyword { 15 | constructor(value) { 16 | super(); 17 | this.value = arguments.length > 1 ? Array.prototype.slice.call(arguments) : value; 18 | } 19 | 20 | set value(value) { 21 | if (typeof value != 'string' && !Array.isArray(value)) { 22 | throw new Error('value must be a string or an array of strings'); 23 | } 24 | 25 | if (Array.isArray(value)) { 26 | value.forEach(t => { 27 | if (!_.includes(primitiveTypes, t)) { 28 | throw new Error('value array elements must be a string value that specifies a valid value: ' + t); 29 | } 30 | }); 31 | } 32 | 33 | this._value = value; 34 | } 35 | 36 | get value() { 37 | return this._value; 38 | } 39 | 40 | json(context) { 41 | context = context || {}; 42 | context.type = this.value; 43 | return context; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/Items.js: -------------------------------------------------------------------------------- 1 | import ArrayKeyword from './ArrayKeyword'; 2 | import Schema from './Schema'; 3 | 4 | export default class Items extends ArrayKeyword { 5 | constructor(value) { 6 | super(); 7 | this.value = arguments.length > 1 ? Array.prototype.slice.call(arguments) : value; 8 | } 9 | 10 | get value() { 11 | return this._value; 12 | } 13 | 14 | set value(value) { 15 | if (value instanceof Schema || Array.isArray(value)) { 16 | if (Array.isArray(value)) { 17 | value.forEach(v => { 18 | if (!(v instanceof Schema)) { 19 | throw new Error('array values must be Schema instances'); 20 | } 21 | }); 22 | } 23 | 24 | this._value = value; 25 | 26 | } else { 27 | throw new Error('value must be an array or a Schema instance'); 28 | } 29 | } 30 | 31 | json(context) { 32 | context = context || {}; 33 | 34 | if (this.value) { 35 | let props; 36 | 37 | if (this.value instanceof Schema) { 38 | props = this.value.json(); 39 | } else { 40 | props = []; 41 | 42 | this.value.forEach(elem => { 43 | props.push(elem instanceof Schema ? elem.json() : elem); 44 | }); 45 | } 46 | 47 | context.items = props; 48 | } 49 | 50 | return context; 51 | } 52 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-schema-builder", 3 | "version": "0.0.5", 4 | "description": "Library for generating JSON Schema.", 5 | "main": "./dist/lib", 6 | "scripts": { 7 | "test": "gulp test", 8 | "test-node-0.12": "mocha dist/test -- --harmony", 9 | "clean": "gulp clean" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/atomiqio/json-schema-builder.git" 14 | }, 15 | "keywords": [ 16 | "JSON", 17 | "schema" 18 | ], 19 | "contributors": [ 20 | "Tony Pujals (http://twitter.com/subfuzion)", 21 | "Peter Svetlichny (http://twitter.com/p_svetlichny)" 22 | ], 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/atomiqio/json-schema-builder/issues" 26 | }, 27 | "homepage": "https://github.com/atomiqio/json-schema-builder#readme", 28 | "dependencies": { 29 | "babel-runtime": "^5.4.7", 30 | "lodash": "^3.10.0" 31 | }, 32 | "devDependencies": { 33 | "babel": "^5.4.7", 34 | "chalk": "^1.1.0", 35 | "del": "^1.2.0", 36 | "gulp": "^3.9.0", 37 | "gulp-babel": "^5.1.0", 38 | "gulp-concat": "^2.5.2", 39 | "gulp-mocha": "^2.1.0", 40 | "gulp-sourcemaps": "^1.5.2", 41 | "gulp-util": "^3.0.5", 42 | "json-schema-test-suite": "0.0.10", 43 | "tv4": "^1.1.9", 44 | "z-schema": "^3.11.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/Properties.js: -------------------------------------------------------------------------------- 1 | import Builder from './Builder'; 2 | import Schema from './Schema'; 3 | import ObjectKeyword from './ObjectKeyword'; 4 | 5 | 6 | export default class Properties extends ObjectKeyword { 7 | constructor(value) { 8 | super(); 9 | this.value = value; 10 | } 11 | 12 | get value() { 13 | return this._value; 14 | } 15 | 16 | set value(value) { 17 | if (typeof value == 'object') { 18 | this._value = value; 19 | } else { 20 | throw new Error('value must be an object'); 21 | } 22 | } 23 | 24 | add(name, value) { 25 | if (typeof name == 'object') { 26 | Object.keys(name).forEach(key => { 27 | this.add(key, name[key]); 28 | }); 29 | return; 30 | } 31 | 32 | 33 | if (this.value) { 34 | this.value[name] = value || {}; 35 | } else { 36 | const prop = {}; 37 | prop[name] = value || {}; 38 | this.value = prop; 39 | } 40 | } 41 | 42 | json(context) { 43 | context = context || {}; 44 | 45 | if (this.value) { 46 | const props = {}; 47 | Object.keys(this.value).forEach(key => { 48 | let ctx = {}; 49 | const value = this.value[key]; 50 | props[key] = (value instanceof Builder) 51 | ? this.value[key].json(ctx) 52 | : this.value[key]; 53 | }); 54 | 55 | context.properties = props; 56 | } 57 | 58 | return context; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/PatternProperties.js: -------------------------------------------------------------------------------- 1 | import Builder from './Builder'; 2 | import Schema from './Schema'; 3 | import ObjectKeyword from './ObjectKeyword'; 4 | 5 | export default class PatternProperties extends ObjectKeyword { 6 | constructor(value) { 7 | super(); 8 | this.value = value; 9 | } 10 | 11 | get value() { 12 | return this._value; 13 | } 14 | 15 | set value(value) { 16 | if (typeof value == 'object') { 17 | this._value = value; 18 | } else { 19 | throw new Error('value must be an object'); 20 | } 21 | } 22 | 23 | add(name, value) { 24 | if (typeof name == 'object') { 25 | Object.keys(name).forEach(key => { 26 | this.add(key, name[key]); 27 | }); 28 | return; 29 | } 30 | 31 | if (typeof name != 'string') { 32 | throw new Error('name must be a string and should be a valid regular expression'); 33 | } 34 | 35 | if (typeof value != 'object' && value instanceof Schema) { 36 | throw new Error('value must be a valid Schema instance'); 37 | } 38 | 39 | if (this.value) { 40 | this.value[name] = value; 41 | } else { 42 | const prop = {}; 43 | prop[name] = value; 44 | this.value = prop; 45 | } 46 | } 47 | 48 | json(context) { 49 | context = context || {}; 50 | 51 | if (this.value) { 52 | const props = {}; 53 | Object.keys(this.value).forEach(key => { 54 | const value = this.value[key]; 55 | props[key] = (value instanceof Builder) 56 | ? this.value[key].json() 57 | : this.value[key]; 58 | }); 59 | 60 | context.patternProperties = props; 61 | } 62 | 63 | return context; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var gutil = require('gulp-util'); 3 | var mocha = require('gulp-mocha'); 4 | var babel = require("gulp-babel"); 5 | var sourcemaps = require('gulp-sourcemaps'); 6 | var path = require('path'); 7 | var del = require('del'); 8 | 9 | var babelOptions = { 10 | // http://babeljs.io/docs/usage/experimental/ 11 | stage: 1, 12 | 13 | // http://babeljs.io/docs/usage/runtime/ 14 | optional: ['runtime'], 15 | 16 | // http://babeljs.io/docs/advanced/transformers/#optional 17 | 18 | // whitelist: [], 19 | blacklist: [] 20 | }; 21 | 22 | var paths = { 23 | sourceRoot: path.join(__dirname, 'src'), 24 | src: ['src/**/*.js'], 25 | watch: ['src/**/*.js', 'src/**/*.json'], 26 | dist: 'dist', 27 | test: 'dist/test/test.js' 28 | } 29 | 30 | gulp.task('default', ['test', 'watch']); 31 | 32 | gulp.task('clean', function(cb) { 33 | del(paths.dist, cb); 34 | }); 35 | 36 | gulp.task('babel', ['clean'], function () { 37 | return gulp.src(paths.src) 38 | .pipe(sourcemaps.init()) 39 | .pipe(babel(babelOptions)) 40 | .pipe(sourcemaps.write('.', { sourceRoot: paths.sourceRoot })) 41 | .pipe(gulp.dest(paths.dist)); 42 | }); 43 | 44 | gulp.task('dist', ['babel'], function() { 45 | return gulp.src(['src/**/*', '!**/*.js'], {base: 'src'}) 46 | .pipe(gulp.dest(paths.dist)); 47 | }); 48 | 49 | gulp.task('test', ['dist'], function() { 50 | return gulp.src(paths.test, {read: false}) 51 | .pipe(mocha({ reporter: 'spec' })); 52 | ; //.on('error', gutil.log); 53 | }); 54 | 55 | gulp.task('watch', function () { 56 | gulp.watch(paths.watch, ['test']); 57 | }); 58 | 59 | gulp.task('generate-testsuite', ['dist'], function() { 60 | var testSuite = require('./dist/test/testsuite'); 61 | testSuite.generate('./src/test/testsuite'); 62 | }); 63 | 64 | -------------------------------------------------------------------------------- /src/lib/Dependencies.js: -------------------------------------------------------------------------------- 1 | import ObjectKeyword from './ObjectKeyword'; 2 | import Builder from './Builder'; 3 | import Schema from './Schema'; 4 | import { uniq } from 'lodash'; 5 | 6 | export default class Dependencies extends ObjectKeyword { 7 | constructor(value) { 8 | super(); 9 | this.value = value; 10 | } 11 | 12 | get value() { 13 | return this._value; 14 | } 15 | 16 | set value(value) { 17 | if (typeof value == 'object' && !Array.isArray(value)) { 18 | for (let prop in value) { 19 | if ((typeof prop == 'object' && !(prop instanceof Schema)) && !Array.isArray(prop)) { 20 | throw new Error('value property must be array or Schema instance'); 21 | } 22 | else if (Array.isArray(prop)) { 23 | if (!prop.length) { 24 | throw new Error('array must have at least one item'); 25 | } 26 | 27 | if (uniq(prop).length != prop.length) { 28 | throw new Error('array items must be unique'); 29 | } 30 | 31 | prop.forEach(elem => { 32 | if (typeof elem !== 'string') { 33 | throw new Error('array items must strings'); 34 | } 35 | }); 36 | } 37 | } 38 | } else { 39 | throw new Error('value must be an object'); 40 | } 41 | 42 | this._value = value; 43 | } 44 | 45 | json(context) { 46 | context = context || {}; 47 | 48 | if (this.value) { 49 | const props = {}; 50 | Object.keys(this.value).forEach(key => { 51 | let ctx = {}; 52 | const value = this.value[key]; 53 | props[key] = (value instanceof Builder) 54 | ? this.value[key].json(ctx) 55 | : this.value[key]; 56 | }); 57 | 58 | context.dependencies = props; 59 | } 60 | 61 | return context; 62 | } 63 | } -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema'; 2 | const schema = exports.schema = function () { return new Schema(...arguments) } 3 | 4 | // generic helpers 5 | exports.allOf = function () { return schema().allOf(...arguments) } 6 | exports.anyOf = function () { return schema().anyOf(...arguments) } 7 | exports.default = function () { return schema().default(...arguments) } 8 | exports.enum = function () { return schema().enum(...arguments) } 9 | exports.not = function () { return schema().not(...arguments) } 10 | exports.oneOf = function () { return schema().oneOf(...arguments) } 11 | exports.type = function () { return schema().type(...arguments) } 12 | 13 | // generic helpers - type wrappers 14 | exports.array = function () { return schema().array() } 15 | exports.boolean = function () { return schema().boolean() } 16 | exports.integer = function () { return schema().integer() } 17 | exports.null = function () { return schema().null() } 18 | exports.number = function () { return schema().number() } 19 | exports.object = function () { return schema().object() } 20 | exports.string = function () { return schema().string() } 21 | 22 | 23 | // numeric helpers 24 | exports.exclusiveMaximum = function () { return schema().exclusiveMaximum(...arguments) } 25 | exports.exclusiveMinimum = function () { return schema().exclusiveMinimum(...arguments) } 26 | exports.maximum = function () { return schema().maximum(...arguments) } 27 | exports.minimum = function () { return schema().minimum(...arguments) } 28 | exports.multipleOf = function () { return schema().multipleOf(...arguments) } 29 | 30 | 31 | // array helpers 32 | exports.additionalItems = function () { return schema().additionalItems(...arguments) } 33 | exports.items = function () { return schema().items(...arguments) } 34 | exports.maxItems = function () { return schema().maxItems(...arguments) } 35 | exports.minItems = function () { return schema().minItems(...arguments) } 36 | exports.uniqueItems = function () { return schema().uniqueItems(...arguments) } 37 | 38 | // object helpers 39 | exports.additionalProperties = function () { return schema().additionalProperties(...arguments) } 40 | exports.definitions = function () { return schema().definitions(...arguments) } 41 | exports.dependencies = function () { return schema().dependencies(...arguments) } 42 | exports.maxProperties = function () { return new schema().maxProperties(...arguments) } 43 | exports.minProperties = function () { return new schema().minProperties(...arguments) } 44 | exports.patternProperties = function () { return schema().patternProperties(...arguments) } 45 | exports.properties = function () { return schema().properties(...arguments) } 46 | exports.required = function () { return schema().required(...arguments) } 47 | exports.$ref = function () { return schema().$ref(...arguments) } 48 | 49 | exports.additionalProperty = function () { return schema().additionalProperty(...arguments) } 50 | exports.patternProperty = function () { return schema().patternProperty(...arguments) } 51 | exports.property = function () { return schema().property(...arguments) } 52 | 53 | // string helpers 54 | exports.maxLength = function () { return schema().maxLength(...arguments) } 55 | exports.minLength = function () { return schema().minLength(...arguments) } 56 | exports.pattern = function () { return schema().pattern(...arguments) } 57 | exports.format = function () { return schema().format(...arguments) } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | json-schema-builder 2 | =================== 3 | 4 | [![NPM](https://nodei.co/npm/json-schema-builder.png?compact=true)](https://nodei.co/npm/json-schema-builder/) 5 | 6 | Create syntactically correct [JSON Schema](http://json-schema.org/) with a fluent JavaScript API. 7 | 8 | npm install --save json-schema-builder 9 | 10 | See the [wiki](https://github.com/atomiqio/json-schema-builder/wiki) and [tests](https://github.com/atomiqio/json-schema-builder/blob/master/src/test/test.js) for documentation and examples. 11 | 12 | 13 | Basic Usage 14 | ----------- 15 | 16 | Use json-schema-builder to create an empty schema object: 17 | 18 | var jsb = require('json-schema-builder'); 19 | var schema = jsb.schema(); 20 | 21 | Use `.json()` on any JSON schema builder object to generate actual JSON Schema: 22 | 23 | var doc = schema.json() 24 | 25 | At this point, `doc` is just an empty schema (`{}`) that can be used by a JSON Schema validator to match any JSON instance (a JSON instance is any data in valid JSON format). 26 | 27 | 28 | #### Add a validation constraint to any schema 29 | 30 | Schemas can have validation constraints that restrict the set of JSON instances that can match a schema. There are constraints that apply to any schema. 31 | 32 | One such constraint is `type`. For schemas that have a `type` constraint, there are *additional* constraints that can be applied depending on whether the type is numeric (`number` or `integer`), `string`, `array`, or `object`. 33 | 34 | 35 | ##### type constraint for any schema 36 | 37 | var schema = jsb.schema().type( ) 38 | 39 | where `value` is a string specifying any valid JSON Schema type (`boolean`, `integer`, `number`, `string`, `array`, `object`, and `null`). 40 | 41 | Unless creating an empty schema as shown in the previous section, it is **not** necessary to explicitly invoke `schema()` as shown here. The following example shows the equivalent (and preferred) form: 42 | 43 | var schema = jsb.type('string') 44 | 45 | The `type` constraint can be used to restrict JSON instances to a particular set of acceptable types. The following example demonstrates how to specify a list of types that could be used to validate JSON instances that are either integer or string values: 46 | 47 | var schema = jsb.type( 'integer', 'string' ); 48 | 49 | 50 | `type` has convenient wrappers corresponding to all the valid JSON Schema types: 51 | 52 | var integerSchema = jsb.integer(); // jsb.type('integer') 53 | var numberSchema = jsb.number(); // jsb.type('number') 54 | var booleanSchema = jsb.boolean(); // jsb.type('boolean') 55 | var stringSchema = jsb.string(); // jsb.type('string') 56 | var arraySchema = jsb.array(); // jsb.type('array') 57 | var objectSchema = jsb.object(); // jsb.type('object') 58 | var nullSchema = jsb.null(); // jsb.type('null') 59 | 60 | Using `integerSchema` from this example, `integerSchema.json()` would generate the following JSON Schema document (or fragment): 61 | 62 | { 63 | "type": "integer" 64 | } 65 | 66 | This schema can be used by a validator to match any integer JSON instance (any number without a fraction or exponent part). 67 | 68 | ##### Additional constraints for any schema 69 | 70 | In addition to the `type` constraints, other constraints that can be applied to any schema include `enum`, `allOf`, `anyOf`, `oneOf`, and `not`. 71 | 72 | See [Validation for any instance type](https://github.com/atomiqio/json-schema-builder/wiki/Any). 73 | 74 | ##### Constraints for numeric types 75 | 76 | The following constraints can be applied to numeric types: `multipleOf`, `maximum` and `exclusiveMaximum`, and `minimum` and `exclusiveMinimum`. 77 | 78 | See [Validation for numeric types](https://github.com/atomiqio/json-schema-builder/wiki/Numeric). 79 | 80 | ##### Constraints for string types 81 | 82 | The following constraints can be applied to string types: `maxLength`, `minLength`, and `pattern`. 83 | 84 | See [Validation for string types](https://github.com/atomiqio/json-schema-builder/wiki/String). 85 | 86 | 87 | ##### Constraints for array types 88 | 89 | The following constraints can be applied to array types: `additionalItems and items`, `maxItems`, `minItems`, and `uniqueItems`. 90 | 91 | See [Validation for array types](https://github.com/atomiqio/json-schema-builder/wiki/Array). 92 | 93 | 94 | ##### Constraints for object types 95 | 96 | The following constraints can be applied to object types: `maxProperties`, `minProperties`, `required`, `additionalProperties`, `properties`, `patternProperties`, and `dependencies`. 97 | 98 | See [Validation for object types](https://github.com/atomiqio/json-schema-builder/wiki/Object). 99 | 100 | 101 | ### Saving a schema to file 102 | 103 | There is a convenience `save` method for saving a schema to a file. It generates output as JSON Schema and saves it as a UTF-8, formatted JSON file with 2-space indentation. 104 | 105 | 106 | // save to a file synchronously 107 | schema.save(path, to, filename); 108 | 109 | // save to a file asynchronously 110 | schema.save(filename, function(err) { 111 | ... 112 | }); 113 | 114 | Of course, the output from `schema.json()` can be explicitly persisted any way desired. 115 | 116 | Tests 117 | ----- 118 | 119 | npm test 120 | 121 | `json-schema-builder` provides [tests](https://github.com/atomiqio/json-schema-builder/blob/master/src/test/test.js) to verify the API can generate all the schemas that comprise the standard [JSON Schema Test Suite](https://github.com/json-schema/JSON-Schema-Test-Suite). 122 | -------------------------------------------------------------------------------- /src/lib/Schema.js: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import { writeFile, writeFileSync } from 'fs'; 3 | import { join } from 'path'; 4 | 5 | import AdditionalItems from './AdditionalItems'; 6 | import AdditionalProperties from './AdditionalProperties'; 7 | import AllOf from './AllOf'; 8 | import AnyOf from './AnyOf'; 9 | import Builder from './Builder'; 10 | import Default from './Default'; 11 | import Definitions from './Definitions'; 12 | import Dependencies from './Dependencies'; 13 | import Enum from './Enum'; 14 | import ExclusiveMaximum from './ExclusiveMaximum'; 15 | import ExclusiveMinimum from './ExclusiveMinimum'; 16 | import Format from './Format'; 17 | import Items from './Items'; 18 | import Keyword from './Keyword'; 19 | import Maximum from './Maximum'; 20 | import MaxItems from './MaxItems'; 21 | import MaxLength from './MaxLength'; 22 | import MaxProperties from './MaxProperties'; 23 | import Minimum from './Minimum'; 24 | import MinItems from './MinItems'; 25 | import MinLength from './MinLength'; 26 | import MinProperties from './MinProperties'; 27 | import MultipleOf from './MultipleOf'; 28 | import Not from './Not'; 29 | import OneOf from './OneOf'; 30 | import Pattern from './Pattern'; 31 | import PatternProperties from './PatternProperties'; 32 | import Properties from './Properties'; 33 | import RefKeyword from './RefKeyword'; 34 | import Required from './Required'; 35 | import Type from './Type'; 36 | import UniqueItems from './UniqueItems'; 37 | 38 | function isDefined(value) { 39 | return typeof value !== 'undefined'; 40 | } 41 | 42 | export default class Schema extends Builder { 43 | 44 | constructor() { 45 | super(); 46 | } 47 | 48 | get keywords() { 49 | if (!this._keywords) this._keywords = []; 50 | return this._keywords; 51 | } 52 | 53 | addKeyword(keyword) { 54 | this.keywords.push(keyword); 55 | } 56 | 57 | getKeyword(Class) { 58 | return _.find(this.keywords, keyword => keyword instanceof Class); 59 | } 60 | 61 | getKeywordValue(Class, defaultValue) { 62 | return _.result(_.find(this.keywords, keyword => keyword instanceof Class), 'value', defaultValue); 63 | } 64 | 65 | type() { 66 | if (arguments.length) { 67 | this.addKeyword(new Type(...arguments)); 68 | return this; 69 | } 70 | 71 | return this.getKeywordValue(Type); 72 | } 73 | 74 | // type convenience methods 75 | boolean() { return this.type('boolean'); } 76 | 77 | integer() { return this.type('integer'); } 78 | 79 | number() { return this.type('number'); } 80 | 81 | string() { return this.type('string'); } 82 | 83 | object() { return this.type('object'); } 84 | 85 | array() { return this.type('array'); } 86 | 87 | null() { return this.type('null'); } 88 | 89 | required() { 90 | if (arguments.length) { 91 | this.addKeyword(new Required(...arguments)); 92 | return this; 93 | } 94 | 95 | return this.getKeywordValue(Required); 96 | } 97 | 98 | enum() { 99 | if (arguments.length) { 100 | this.addKeyword(new Enum(...arguments)); 101 | return this; 102 | } 103 | 104 | return this.getKeywordValue(Enum); 105 | } 106 | 107 | properties() { 108 | if (arguments.length) { 109 | this.addKeyword(new Properties(...arguments)); 110 | return this; 111 | } 112 | 113 | return this.getKeywordValue(Properties); 114 | } 115 | 116 | property(name, value, required) { 117 | if (isDefined(name)) { 118 | if (typeof name == 'object') { 119 | required = value; 120 | value = undefined; 121 | Object.keys(name).forEach(key => { 122 | this.property(key, name[key], required); 123 | }); 124 | return this; 125 | } 126 | 127 | const properties = this.getKeyword(Properties); 128 | if (properties) { 129 | properties.add(name, value); 130 | } else { 131 | const prop = {}; 132 | prop[name] = value || {}; 133 | this.properties(prop); 134 | } 135 | 136 | if (required) { 137 | if (this.required()) { 138 | this.required().push(name); 139 | } else { 140 | this.required([name]); 141 | } 142 | } 143 | 144 | return this; 145 | } 146 | 147 | const props = this.properties(); 148 | if (props) { 149 | return props[name]; 150 | } 151 | } 152 | 153 | patternProperties() { 154 | if (arguments.length) { 155 | this.addKeyword(new PatternProperties(...arguments)); 156 | return this; 157 | } 158 | 159 | return this.getKeywordValue(PatternProperties); 160 | } 161 | 162 | patternProperty(name, value) { 163 | if (isDefined(name)) { 164 | if (typeof name == 'object') { 165 | Object.keys(name).forEach(key => { 166 | this.patternProperty(key, name[key]); 167 | }); 168 | return this; 169 | } 170 | 171 | const properties = this.getKeyword(PatternProperties); 172 | if (properties) { 173 | properties.add(name, value); 174 | } else { 175 | const prop = {}; 176 | prop[name] = value || {}; 177 | this.patternProperties(prop); 178 | } 179 | 180 | return this; 181 | } 182 | 183 | const props = this.patternProperties(); 184 | if (props) { 185 | return props[name]; 186 | } 187 | } 188 | 189 | additionalProperties() { 190 | if (arguments.length) { 191 | this.addKeyword(new AdditionalProperties(...arguments)); 192 | return this; 193 | } 194 | 195 | return this.getKeywordValue(AdditionalProperties); 196 | } 197 | 198 | allOf() { 199 | if (arguments.length) { 200 | this.addKeyword(new AllOf(...arguments)); 201 | return this; 202 | } 203 | 204 | return this.getKeywordValue(AllOf); 205 | } 206 | 207 | anyOf() { 208 | if (arguments.length) { 209 | this.addKeyword(new AnyOf(...arguments)); 210 | return this; 211 | } 212 | 213 | return this.getKeywordValue(AnyOf); 214 | } 215 | 216 | oneOf() { 217 | if (arguments.length) { 218 | this.addKeyword(new OneOf(...arguments)); 219 | return this; 220 | } 221 | 222 | return this.getKeywordValue(OneOf); 223 | } 224 | 225 | multipleOf(value) { 226 | if (value) { 227 | this.addKeyword(new MultipleOf(value)); 228 | return this; 229 | } 230 | 231 | return this.getKeywordValue(MultipleOf); 232 | } 233 | 234 | maximum() { 235 | if (arguments.length) { 236 | this.addKeyword(new Maximum(...arguments)); 237 | return this; 238 | } 239 | 240 | return this.getKeywordValue(Maximum); 241 | } 242 | 243 | exclusiveMaximum() { 244 | if (arguments.length) { 245 | this.addKeyword(new ExclusiveMaximum(...arguments)); 246 | return this; 247 | } 248 | 249 | return this.getKeywordValue(ExclusiveMaximum); 250 | } 251 | 252 | minimum() { 253 | if (arguments.length) { 254 | this.addKeyword(new Minimum(...arguments)); 255 | return this; 256 | } 257 | 258 | return this.getKeywordValue(Minimum); 259 | } 260 | 261 | exclusiveMinimum() { 262 | if (arguments.length) { 263 | this.addKeyword(new ExclusiveMinimum(...arguments)); 264 | return this; 265 | } 266 | 267 | return this.getKeywordValue(ExclusiveMinimum); 268 | } 269 | 270 | not() { 271 | if (arguments.length) { 272 | this.addKeyword(new Not(...arguments)); 273 | return this; 274 | } 275 | 276 | return this.getKeywordValue(Not); 277 | } 278 | 279 | maxProperties() { 280 | if (arguments.length) { 281 | this.addKeyword(new MaxProperties(...arguments)); 282 | return this; 283 | } 284 | 285 | return this.getKeywordValue(MaxProperties); 286 | } 287 | 288 | minProperties() { 289 | if (arguments.length) { 290 | this.addKeyword(new MinProperties(...arguments)); 291 | return this; 292 | } 293 | 294 | return this.getKeywordValue(MaxProperties); 295 | } 296 | 297 | additionalItems() { 298 | if (arguments.length) { 299 | this.addKeyword(new AdditionalItems(...arguments)); 300 | return this; 301 | } 302 | 303 | return this.getKeywordValue(AdditionalItems); 304 | } 305 | 306 | items() { 307 | if (arguments.length) { 308 | this.addKeyword(new Items(...arguments)); 309 | return this; 310 | } 311 | 312 | return this.getKeywordValue(Items); 313 | } 314 | 315 | maxItems() { 316 | if (arguments.length) { 317 | this.addKeyword(new MaxItems(...arguments)); 318 | return this; 319 | } 320 | 321 | return this.getKeywordValue(MaxItems); 322 | } 323 | 324 | minItems() { 325 | if (arguments.length) { 326 | this.addKeyword(new MinItems(...arguments)); 327 | return this; 328 | } 329 | 330 | return this.getKeywordValue(MinItems); 331 | } 332 | 333 | uniqueItems() { 334 | if (arguments.length) { 335 | this.addKeyword(new UniqueItems(...arguments)); 336 | return this; 337 | } 338 | 339 | return this.getKeywordValue(UniqueItems); 340 | } 341 | 342 | maxLength() { 343 | if (arguments.length) { 344 | this.addKeyword(new MaxLength(...arguments)); 345 | return this; 346 | } 347 | 348 | return this.getKeywordValue(MaxLength); 349 | } 350 | 351 | minLength() { 352 | if (arguments.length) { 353 | this.addKeyword(new MinLength(...arguments)); 354 | return this; 355 | } 356 | 357 | return this.getKeywordValue(MinLength); 358 | } 359 | 360 | pattern() { 361 | if (arguments.length) { 362 | this.addKeyword(new Pattern(...arguments)); 363 | return this; 364 | } 365 | 366 | return this.getKeywordValue(Pattern); 367 | } 368 | 369 | definitions() { 370 | if (arguments.length) { 371 | this.addKeyword(new Definitions(...arguments)); 372 | return this; 373 | } 374 | 375 | return this.getKeywordValue(Definitions); 376 | } 377 | 378 | dependencies() { 379 | if (arguments.length) { 380 | this.addKeyword(new Dependencies(...arguments)); 381 | return this; 382 | } 383 | 384 | return this.getKeywordValue(Dependencies); 385 | } 386 | 387 | $ref() { 388 | if (arguments.length) { 389 | this.addKeyword(new RefKeyword(...arguments)); 390 | return this; 391 | } 392 | 393 | return this.getKeywordValue(RefKeyword); 394 | } 395 | 396 | json(context) { 397 | context = context || {}; 398 | 399 | this.keywords.forEach(keyword => { 400 | keyword.json(context); 401 | }); 402 | 403 | return context; 404 | } 405 | 406 | format() { 407 | if (arguments.length) { 408 | this.addKeyword(new Format(...arguments)); 409 | return this; 410 | } 411 | 412 | return this.getKeywordValue(Format); 413 | } 414 | 415 | default() { 416 | if (arguments.length) { 417 | this.addKeyword(new Default(...arguments)); 418 | return this; 419 | } 420 | 421 | return this.getKeywordValue(Default); 422 | } 423 | 424 | 425 | save() { 426 | const context = typeof arguments[0] == 'object' ? arguments[0] : null; 427 | const callback = arguments.length && typeof arguments[arguments.length - 1] == 'function' ? arguments[arguments.length - 1] : null; 428 | 429 | if (callback && arguments.length == 1 || !arguments.length) throw new Error('missing filename argument'); 430 | 431 | const begin = context ? 1 : 0; 432 | const end = callback ? arguments.length - 1 : arguments.length; 433 | const args = Array.prototype.slice.call(arguments, begin, end); 434 | const pathname = join(...args); 435 | const json = JSON.stringify(this.json(context), null, 2); 436 | 437 | callback ? writeFile(pathname, json, 'utf8', callback) : writeFileSync(pathname, json, 'utf8'); 438 | } 439 | } 440 | 441 | -------------------------------------------------------------------------------- /src/test/test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import testSuite from 'json-schema-test-suite'; 3 | import * as _ from 'lodash'; 4 | import { isEqual, stringify } from './helpers'; 5 | import * as json from '../lib'; 6 | import { mkdirSync } from 'fs'; 7 | import { join } from 'path'; 8 | import del from 'del'; 9 | 10 | const draft4 = testSuite.draft4(); 11 | 12 | const verbose = false; 13 | 14 | function print() { 15 | if (verbose) { 16 | if (typeof arguments[0] == 'object') { 17 | console.log(JSON.stringify(arguments[0], null, 2)); 18 | } else { 19 | console.log(...arguments); 20 | } 21 | } 22 | } 23 | 24 | function getTestSection(name, description) { 25 | const group = _.findWhere(draft4, { name: name }); 26 | if (!group) throw new Error("can't find schema for: " + name); 27 | const section = _.findWhere(group.schemas, { description: description }); 28 | if (!section) throw new Error("can't find section for: " + name + ' => ' + description); 29 | return section; 30 | } 31 | 32 | function getSchema(name, description) { 33 | return getTestSection(name, description).schema; 34 | } 35 | 36 | function test(name, description, builderFn) { 37 | it(name + ': ' + description, function () { 38 | try { 39 | const expected = getSchema(name, description); 40 | const actual = builderFn().json(); 41 | 42 | if (!isEqual(actual, expected) || verbose) { 43 | print('=============================='); 44 | print('expected =>'); 45 | print(expected); 46 | print('------------------------------'); 47 | print('actual =>'); 48 | print(actual); 49 | } 50 | 51 | assert(isEqual(actual, expected)); 52 | } catch (err) { 53 | print('=============================='); 54 | print('Uncaught error for: %s => %s', name, description); 55 | throw err; 56 | } 57 | }); 58 | } 59 | 60 | test.skip = function () { 61 | it.skip(arguments[0] + ' => ' + arguments[1], function () {}); 62 | } 63 | 64 | describe ('Tests based on standard JSON Schema Test Suite', () => { 65 | 66 | describe('generic keywords (any instance type)', () => { 67 | 68 | describe('enum', () => { 69 | 70 | test('enum', 'simple enum validation', () => { 71 | const schema = json.enum([1, 2, 3]); 72 | assert(schema.enum, [1, 2, 3]); 73 | return schema; 74 | }); 75 | 76 | // equivalent 77 | test('enum', 'simple enum validation', () => { 78 | const schema = json.enum(1, 2, 3); 79 | assert(schema.enum, [1, 2, 3]); 80 | return schema; 81 | }); 82 | 83 | test('enum', 'heterogeneous enum validation', () => { 84 | const schema = json.enum([6, "foo", [], true, { "foo": 12 }]); 85 | assert(schema.enum, [6, "foo", [], true, { "foo": 12 }]); 86 | return schema; 87 | }); 88 | 89 | test('enum', 'enums in properties', () => { 90 | const schema = json 91 | .type('object') 92 | .required(['bar']) 93 | .properties({ 94 | foo: json.enum('foo'), 95 | bar: json.enum('bar') 96 | }); 97 | 98 | return schema; 99 | }); 100 | 101 | // equivalent (adding properties constructed with name and value) 102 | test('enum', 'enums in properties', () => { 103 | const schema = json 104 | .object() 105 | .property('foo', json.enum('foo')) 106 | .property('bar', json.enum('bar'), true); 107 | 108 | return schema; 109 | }); 110 | 111 | // equivalent (adding properties constructed with objects) 112 | test('enum', 'enums in properties', () => { 113 | const schema = json 114 | .object() 115 | .property({ foo: json.enum('foo') }) 116 | .property({ bar: json.enum('bar') }, true); 117 | 118 | return schema; 119 | }); 120 | }); 121 | 122 | describe('type', () => { 123 | 124 | test('type', 'integer type matches integers', () => { 125 | const schema = json.integer(); 126 | assert.equal(schema.type(), 'integer'); 127 | return schema; 128 | }); 129 | 130 | test('type', 'number type matches numbers', () => { 131 | const schema = json.number(); 132 | assert.equal(schema.type(), 'number'); 133 | return schema; 134 | }); 135 | 136 | test('type', 'string type matches strings', () => { 137 | const schema = json.string(); 138 | assert.equal(schema.type(), 'string'); 139 | return schema; 140 | }); 141 | 142 | test('type', 'object type matches objects', () => { 143 | const schema = json.object(); 144 | assert(schema.type, 'object'); 145 | return schema; 146 | }); 147 | 148 | test('type', 'array type matches arrays', () => { 149 | const schema = json.array(); 150 | print(schema); 151 | assert(schema.type, 'array'); 152 | return schema; 153 | }); 154 | 155 | test('type', 'boolean type matches booleans', () => { 156 | const schema = json.boolean(); 157 | assert(schema.type, 'boolean'); 158 | return schema; 159 | }); 160 | 161 | test('type', 'null type matches only the null object', () => { 162 | const schema = json.null(); 163 | assert(schema.type, 'null'); 164 | return schema; 165 | }); 166 | 167 | test('type', 'multiple types can be specified in an array', () => { 168 | const schema = json.type(['integer', 'string']); 169 | return schema; 170 | }); 171 | 172 | }); 173 | 174 | describe('allOf tests', () => { 175 | 176 | test('allOf', 'allOf', () => { 177 | const schema = json.allOf([ 178 | json.property('bar', json.integer(), true), 179 | json.property('foo', json.string(), true)]); 180 | 181 | return schema; 182 | }); 183 | 184 | test('allOf', 'allOf', () => { 185 | const schema = json.allOf( 186 | json.property('bar', json.integer(), true), 187 | json.property('foo', json.string(), true)); 188 | 189 | return schema; 190 | }); 191 | 192 | test('allOf', 'allOf with base schema', () => { 193 | const schema = json 194 | .allOf([ 195 | json.property('foo', json.string(), true), 196 | json.property('baz', json.null(), true)]) 197 | .property('bar', json.integer(), true); 198 | 199 | return schema; 200 | }); 201 | 202 | test('allOf', 'allOf simple types', () => { 203 | const schema = json.allOf([ 204 | json.maximum(30), 205 | json.minimum(20)]); 206 | 207 | return schema; 208 | }); 209 | 210 | }); 211 | 212 | describe('anyOf', () => { 213 | 214 | test('anyOf', 'anyOf', () => { 215 | const schema = json.anyOf([json.integer(), json.minimum(2)]); 216 | return schema; 217 | }); 218 | 219 | // equivalent 220 | test('anyOf', 'anyOf', () => { 221 | const schema = json.anyOf(json.integer(), json.minimum(2)); 222 | return schema; 223 | }); 224 | 225 | test('anyOf', 'anyOf with base schema', () => { 226 | const schema = json.string().anyOf([json.maxLength(2), json.minLength(4)]); 227 | return schema; 228 | }); 229 | 230 | }); 231 | 232 | describe('oneOf', () => { 233 | 234 | test('oneOf', 'oneOf', () => { 235 | const schema = json.oneOf([json.integer(), json.minimum(2)]); 236 | return schema; 237 | }); 238 | 239 | // equivalent 240 | test('oneOf', 'oneOf', () => { 241 | const schema = json.oneOf(json.integer(), json.minimum(2)); 242 | return schema; 243 | }); 244 | 245 | test('oneOf', 'oneOf with base schema', () => { 246 | const schema = json.string().oneOf(json.minLength(2), json.maxLength(4)); 247 | return schema; 248 | }); 249 | 250 | }); 251 | 252 | describe('not', () => { 253 | 254 | test('not', 'not', () => { 255 | const schema = json.not(json.integer()); 256 | return schema; 257 | }); 258 | 259 | }); 260 | 261 | describe('type', () => { 262 | 263 | test('not', 'not multiple types', () => { 264 | const schema = json.not(json.type('integer', 'boolean')); 265 | 266 | return schema; 267 | }); 268 | 269 | test('not', 'not more complex schema', () => { 270 | const schema = json.not(json.object().property('foo', json.string())); 271 | return schema; 272 | }); 273 | 274 | test('not', 'forbidden property', () => { 275 | const schema = json.property('foo', json.not(json.schema())); 276 | return schema; 277 | }); 278 | 279 | }); 280 | 281 | }); 282 | 283 | describe('object keywords', () => { 284 | 285 | describe('dependencies', () => { 286 | 287 | test('dependencies', 'dependencies', () => { 288 | const schema = json.dependencies({ 'bar': ['foo'] }); 289 | return schema; 290 | }); 291 | 292 | test('dependencies', 'multiple dependencies', () => { 293 | const schema = json.dependencies({ 'quux': ['foo', 'bar'] }); 294 | return schema; 295 | }); 296 | 297 | test('dependencies', 'multiple dependencies subschema', ()=> { 298 | const schema = json.dependencies({ 299 | bar: json.properties({ 300 | foo: json.integer(), 301 | bar: json.integer() 302 | }) 303 | }); 304 | 305 | return schema; 306 | }); 307 | 308 | }); 309 | 310 | describe('properties', () => { 311 | 312 | test('properties', 'object properties validation', () => { 313 | const schema = json.properties({ 314 | foo: json.integer(), 315 | bar: json.string() 316 | }); 317 | 318 | return schema; 319 | }); 320 | 321 | // equivalent 322 | test('properties', 'object properties validation', () => { 323 | const schema = json 324 | .property('foo', json.integer()) 325 | .property('bar', json.string()); 326 | 327 | return schema; 328 | }); 329 | 330 | test('properties', 'properties, patternProperties, additionalProperties interaction', () => { 331 | const schema = json 332 | .property('foo', json.array().maxItems(3)) 333 | .property('bar', json.array()) 334 | .patternProperty('f.o', json.minItems(2)) 335 | .additionalProperties(json.integer()); 336 | 337 | return schema; 338 | }); 339 | }); 340 | 341 | describe('patternProperties', () => { 342 | 343 | test('patternProperties', 'patternProperties validates properties matching a regex', () => { 344 | const schema = json.patternProperties({ 'f.*o': json.integer() }); 345 | return schema; 346 | }); 347 | 348 | // equivalent 349 | test('patternProperties', 'patternProperties validates properties matching a regex', () => { 350 | const schema = json.patternProperty('f.*o', json.integer()); 351 | return schema; 352 | }); 353 | 354 | // equivalent 355 | test('patternProperties', 'patternProperties validates properties matching a regex', () => { 356 | const schema = json.patternProperty({ 'f.*o': json.integer() }); 357 | return schema; 358 | }); 359 | 360 | test('patternProperties', 'multiple simultaneous patternProperties are validated', () => { 361 | const schema = json 362 | .patternProperty('a*', json.integer()) 363 | .patternProperty('aaa*', json.maximum(20)) 364 | return schema; 365 | }); 366 | 367 | test('patternProperties', 'regexes are not anchored by default and are case sensitive', () => { 368 | const schema = json 369 | .patternProperty('[0-9]{2,}', json.boolean()) 370 | .patternProperty('X_', json.string()); 371 | 372 | return schema; 373 | }); 374 | }); 375 | 376 | describe('additionalProperties', () => { 377 | 378 | test('additionalProperties', 'additionalProperties being false does not allow other properties', () => { 379 | const schema = json 380 | .properties({ 381 | foo: {}, 382 | bar: {} 383 | }) 384 | .patternProperties({ 385 | '^v': {} 386 | }) 387 | .additionalProperties(false); 388 | 389 | return schema; 390 | }); 391 | 392 | test('additionalProperties', 'additionalProperties allows a schema which should validate', () => { 393 | const schema = json 394 | .properties({ 395 | foo: {}, 396 | bar: {} 397 | }) 398 | .additionalProperties(json.schema().boolean()); 399 | 400 | return schema; 401 | }); 402 | 403 | test('additionalProperties', 'additionalProperties can exist by itself', () => { 404 | const schema = json.additionalProperties(json.boolean()); 405 | return schema; 406 | }); 407 | 408 | test('additionalProperties', 'additionalProperties are allowed by default', () => { 409 | const schema = json 410 | .properties({ 411 | foo: {}, 412 | bar: {} 413 | }); 414 | 415 | return schema; 416 | }); 417 | 418 | }); 419 | 420 | test('maxProperties', 'maxProperties validation', () => { 421 | const schema = json.maxProperties(2); 422 | return schema; 423 | }); 424 | 425 | test('minProperties', 'minProperties validation', () => { 426 | const schema = json.minProperties(1); 427 | return schema; 428 | }); 429 | 430 | test('required', 'required validation', () => { 431 | const schema = json 432 | .property('foo', {}, true) 433 | .property('bar', {}); 434 | return schema; 435 | }); 436 | 437 | describe('definitions', () => { 438 | 439 | test('definitions', 'valid definition', () => { 440 | const schema = json.$ref('http://json-schema.org/draft-04/schema#'); 441 | return schema; 442 | }); 443 | 444 | test('definitions', 'valid definition', () => { 445 | const schema = json.$ref('http://json-schema.org/draft-04/schema#'); 446 | return schema; 447 | }); 448 | 449 | }); 450 | }); 451 | 452 | describe('numeric keywords', () => { 453 | 454 | describe('multipleOf', () => { 455 | 456 | test('multipleOf', 'by int', () => { 457 | const schema = json.multipleOf(2); 458 | return schema; 459 | }); 460 | 461 | test('multipleOf', 'by number', () => { 462 | const schema = json.multipleOf(1.5); 463 | return schema; 464 | }); 465 | 466 | test('multipleOf', 'by small number', () => { 467 | const schema = json.multipleOf(0.0001); 468 | return schema; 469 | }); 470 | 471 | }); 472 | 473 | describe('maximum and exclusiveMaximum', () => { 474 | 475 | test('maximum', 'maximum validation', () => { 476 | const schema = json.maximum(3.0); 477 | return schema; 478 | }); 479 | 480 | test('maximum', 'exclusiveMaximum validation', () => { 481 | const schema = json.maximum(3.0).exclusiveMaximum(true); 482 | return schema; 483 | }); 484 | 485 | }); 486 | 487 | describe('minimum and exclusiveMinimum', () => { 488 | 489 | test('minimum', 'minimum validation', () => { 490 | const schema = json.minimum(1.1); 491 | return schema; 492 | }); 493 | 494 | test('minimum', 'exclusiveMinimum validation', () => { 495 | const schema = json.minimum(1.1).exclusiveMinimum(true); 496 | return schema; 497 | }); 498 | 499 | }); 500 | 501 | }); 502 | 503 | describe('array keywords', () => { 504 | 505 | test('items', 'a schema given for items', () => { 506 | const schema = json.items(json.schema().integer()); 507 | return schema; 508 | }); 509 | 510 | test('items', 'an array of schemas for items', () => { 511 | const schema = json.items([json.integer(), json.string()]); 512 | return schema; 513 | }); 514 | 515 | // equivalent 516 | test('items', 'an array of schemas for items', () => { 517 | const schema = json.items(json.integer(), json.string()); 518 | return schema; 519 | }); 520 | 521 | test('additionalItems', 'additionalItems as schema', () => { 522 | const schema = json 523 | .items([json.schema()]) 524 | .additionalItems(json.integer()); 525 | 526 | return schema; 527 | }); 528 | 529 | test('additionalItems', 'items is schema, no additionalItems', () => { 530 | const schema = json 531 | .items(json.schema()) 532 | .additionalItems(false); 533 | 534 | return schema; 535 | }); 536 | 537 | test('additionalItems', 'array of items with no additionalItems', () => { 538 | const schema = json 539 | .items(json.schema(), json.schema(), json.schema()) 540 | .additionalItems(false); 541 | 542 | return schema; 543 | }); 544 | 545 | test('additionalItems', 'additionalItems as false without items', () => { 546 | const schema = json.additionalItems(false); 547 | return schema; 548 | }); 549 | 550 | test('additionalItems', 'additionalItems are allowed by default', () => { 551 | const schema = json.items([json.integer()]); 552 | return schema; 553 | }); 554 | 555 | test('maxItems', 'maxItems validation', () => { 556 | const schema = json.maxItems(2); 557 | return schema; 558 | }); 559 | 560 | test('minItems', 'minItems validation', () => { 561 | const schema = json.minItems(1); 562 | return schema; 563 | }); 564 | 565 | test('uniqueItems', 'uniqueItems validation', () => { 566 | const schema = json.uniqueItems(true); 567 | return schema; 568 | }); 569 | 570 | }); 571 | 572 | describe('string keywords', () => { 573 | 574 | test('maxLength', 'maxLength validation', () => { 575 | const schema = json.maxLength(2); 576 | return schema; 577 | }); 578 | 579 | test('minLength', 'minLength validation', () => { 580 | const schema = json.minLength(2); 581 | return schema; 582 | }); 583 | 584 | 585 | test('pattern', 'pattern validation', () => { 586 | const schema = json.pattern('^a*$'); 587 | return schema; 588 | }); 589 | 590 | test('pattern', 'pattern is not anchored', () => { 591 | const schema = json.pattern('a+'); 592 | return schema; 593 | }); 594 | 595 | }); 596 | 597 | describe('optional keywords', () => { 598 | 599 | describe('format', () => { 600 | 601 | test('format', 'validation of date-time strings', () => { 602 | const schema = json.format('date-time'); 603 | return schema; 604 | }); 605 | 606 | test('format', 'validation of URIs', () => { 607 | const schema = json.format('uri'); 608 | return schema; 609 | }); 610 | 611 | test('format', 'validation of e-mail addresses', () => { 612 | const schema = json.format('email'); 613 | return schema; 614 | }); 615 | 616 | test('format', 'validation of IP addresses', () => { 617 | const schema = json.format('ipv4'); 618 | return schema; 619 | }); 620 | 621 | test('format', 'validation of IPv6 addresses', () => { 622 | const schema = json.format('ipv6'); 623 | return schema; 624 | }); 625 | 626 | test('format', 'validation of host names', () => { 627 | const schema = json.format('hostname'); 628 | return schema; 629 | }); 630 | 631 | }); 632 | 633 | describe('default', () => { 634 | 635 | test('default', 'invalid type for default', () => { 636 | const schema = json.property('foo', json.integer().default([]) ); 637 | return schema; 638 | }); 639 | 640 | test('default', 'invalid string value for default', () => { 641 | const schema = json.property('bar', json.string().minLength(4).default('bad')); 642 | return schema; 643 | }); 644 | 645 | }); 646 | 647 | }); 648 | 649 | }); 650 | 651 | describe('Tests', () => { 652 | 653 | const expectedDir = join(__dirname, 'expected'); 654 | const actualDir = join(__dirname, 'actual'); 655 | 656 | function assertMatch(filename) { 657 | const expected = require(join(expectedDir, filename)); 658 | const actual = require(join(actualDir, filename)); 659 | 660 | if (verbose && !isEqual(actual, expected) || verbose) { 661 | print('\nFilename: %s', filename); 662 | print('Expected:'); 663 | print(expected); 664 | print('Actual:'); 665 | print(actual); 666 | } 667 | 668 | assert(isEqual(actual, expected)); 669 | } 670 | 671 | function rmdir(dir) { 672 | del.sync(dir, { force: true }); 673 | } 674 | 675 | function test(schema, sample) { 676 | schema.save(actualDir, sample); 677 | assertMatch(sample); 678 | } 679 | 680 | before(() => { 681 | 682 | rmdir(actualDir); 683 | mkdirSync(actualDir); 684 | 685 | }); 686 | 687 | after(() => { 688 | //rmdir(actualDir); 689 | }); 690 | 691 | describe ('save tests', () => { 692 | 693 | it('should write sample schema async', done => { 694 | const schema = json.schema().string(); 695 | const sample = 'sample1.json'; 696 | 697 | schema.save(actualDir, sample, (err) => { 698 | if (err) return done(err) 699 | assertMatch(sample); 700 | done(); 701 | }); 702 | }); 703 | 704 | it('should write sample schema sync', () => { 705 | const schema = json.schema().string(); 706 | const sample = 'sample1.json'; 707 | schema.save(actualDir, sample); 708 | assertMatch(sample); 709 | }); 710 | 711 | }); 712 | 713 | describe ('Simple tests', () => { 714 | 715 | it('should match empty schema', () => { 716 | const schema = json.schema(); 717 | test(schema, 'empty.json'); 718 | }); 719 | 720 | it('should match schema with property', () => { 721 | const schema = json.property('foo'); 722 | test(schema, 'single-property.json'); 723 | }); 724 | 725 | it('should also match schema with property', () => { 726 | const schema = json.schema().properties({ foo: {} }); 727 | test(schema, 'single-property.json'); 728 | }); 729 | 730 | it('should match object schema with property', () => { 731 | const schema = json.object().property('foo'); 732 | test(schema, 'explicit-object-single-property.json'); 733 | }); 734 | 735 | it('should match schema with additional properties allowed', () => { 736 | const schema = json.object().property('foo').additionalProperties(true); 737 | test(schema, 'additionalProperties-true.json'); 738 | }); 739 | 740 | it('should match schema with additional properties not allowed', () => { 741 | const schema = json.object().property('foo').additionalProperties(false); 742 | test(schema, 'additionalProperties-false.json'); 743 | }); 744 | 745 | it('should match schema with single required property', () => { 746 | const schema = json.property('foo', {}, true); 747 | test(schema, 'single-required-property.json'); 748 | }); 749 | 750 | it('should also match schema with single required property', () => { 751 | const schema = json.property('foo').required(true); 752 | test(schema, 'single-required-property.json'); 753 | }); 754 | 755 | it('should match schema with single required property and no others allowed', () => { 756 | const schema = json.property('foo').required('foo').additionalProperties(false); 757 | test(schema, 'single-required-property-additionalProperties-false.json'); 758 | }); 759 | 760 | it('should match schema with multiple properties', () => { 761 | const schema = json 762 | .property('foo', json.string(), true) 763 | .property('bar', json.integer()); 764 | 765 | test(schema, 'multiple-properties.json'); 766 | }); 767 | 768 | }); 769 | 770 | }); 771 | 772 | --------------------------------------------------------------------------------