diff --git a/lib/compile/_validate.dot.js b/lib/compile/_validate.dot.js index e60653f..49b3500 100644 --- a/lib/compile/_validate.dot.js +++ b/lib/compile/_validate.dot.js @@ -17,6 +17,13 @@ function (data, dataType, dataPath) { {{ var $schemaKeys = Object.keys(it.schema); }} {{ /* sort keys so that those that are easier to fail (e.g. type) are validated sooner */ }} + {{ + var $checkProperties = !it.schema.properties && + ( it.schema.patternProperties || + it.schema.hasOwnProperty('additionalProperties') ); + if ($checkProperties) $schemaKeys.push('properties'); + }} + {{~ $schemaKeys:$key }} {{ var $rule = it.RULES[$key]; }} {{? $rule }} @@ -29,6 +36,7 @@ function (data, dataType, dataPath) { $it.schema = it.schema[$key]; $it.schemaPath = it.schemaPath + '.' + $key; $it.parentSchema = it.schema; + $it.parentSchemaPath = it.schemaPath; }} var result = ({{= $rule.code($it) }})(data, dataType, dataPath); diff --git a/lib/compile/rules/enum.dot.js b/lib/compile/rules/enum.dot.js index 16e8fc1..698eed4 100644 --- a/lib/compile/rules/enum.dot.js +++ b/lib/compile/rules/enum.dot.js @@ -4,37 +4,14 @@ function (data, dataType, dataPath) { var errors = []; {{ - var $simpleTypes = [ 'boolean', 'null', 'number', 'string' ] - , $itemsHash = { 'boolean': {}, 'null': {}, 'number': {}, 'string': {} }; - - var $onlySimpleTypes = it.schema.every(function ($item) { - var $itemType = typeof $item; - var $isSimpleType = $simpleTypes.indexOf($itemType) >= 0; - if ($isSimpleType) $itemsHash[$itemType][$item] = true; - return $isSimpleType; + var $itemsHash = {}; + it.schema.forEach(function ($item) { + $itemsHash[it.stableStringify($item)] = true; }); - - if (!$onlySimpleTypes) { - var $itemsList = it.schema.map(function ($item) { - return { - type: it.getDataType($item), - valueStr: it.stableStringify($item) - } - }); - } }} - {{? $onlySimpleTypes }} - var itemsHash = {{= JSON.stringify($itemsHash) }}; - var valid = itemsHash[dataType] && itemsHash[dataType][data]; - {{??}} - var itemsList = {{= JSON.stringify($itemsList) }}; - for (var i = 0; i < itemsList.length; i++) { - var item = itemsList[i]; - var valid = dataType == item.type && stableStringify(data) == item.valueStr; - if (valid) break; - } - {{?}} + var itemsHash = {{= JSON.stringify($itemsHash) }}; + var valid = itemsHash[stableStringify(data)]; return { valid: valid || false, diff --git a/lib/compile/rules/index.js b/lib/compile/rules/index.js index 5812cc5..5526538 100644 --- a/lib/compile/rules/index.js +++ b/lib/compile/rules/index.js @@ -10,11 +10,11 @@ var RULES = module.exports = { anyOf: { code: fs.readFileSync(__dirname + '/anyOf.dot.js') }, oneOf: { code: fs.readFileSync(__dirname + '/oneOf.dot.js') }, not: { code: fs.readFileSync(__dirname + '/not.dot.js') }, - maximum: { + maximum: { // includes exclusiveMaximum code: fs.readFileSync(__dirname + '/maximum.dot.js'), type: 'number' }, - minimum: { + minimum: { // includes exclusiveMinimum code: fs.readFileSync(__dirname + '/minimum.dot.js'), type: 'number' }, @@ -34,14 +34,10 @@ var RULES = module.exports = { code: fs.readFileSync(__dirname + '/pattern.dot.js'), type: 'string' }, - // additionalItems: { - // code: '', - // type: 'array' - // }, - // items: { - // code: '', - // type: 'array' - // }, + items: { // includes additionalItems + code: fs.readFileSync(__dirname + '/items.dot.js'), + type: 'array' + }, maxItems: { code: fs.readFileSync(__dirname + '/maxItems.dot.js'), type: 'array' @@ -50,10 +46,10 @@ var RULES = module.exports = { code: fs.readFileSync(__dirname + '/minItems.dot.js'), type: 'array' }, - // uniqueItems: { - // code: '', - // type: 'array' - // }, + uniqueItems: { + code: fs.readFileSync(__dirname + '/uniqueItems.dot.js'), + type: 'array' + }, maxProperties: { code: fs.readFileSync(__dirname + '/maxProperties.dot.js'), type: 'object' @@ -66,18 +62,10 @@ var RULES = module.exports = { code: fs.readFileSync(__dirname + '/required.dot.js'), type: 'object' }, - // additionalProperties: { - // code: '', - // type: 'object' - // }, - properties: { + properties: { // includes patternProperties and additionalProperties code: fs.readFileSync(__dirname + '/properties.dot.js'), type: 'object' }, - // patternProperties: { - // code: '', - // type: 'object' - // }, // dependencies: { // code: '', // type: 'object' diff --git a/lib/compile/rules/items.dot.js b/lib/compile/rules/items.dot.js new file mode 100644 index 0000000..f4ee8ad --- /dev/null +++ b/lib/compile/rules/items.dot.js @@ -0,0 +1,88 @@ +{{## def.validateItems:startFrom: + for (var i = {{= startFrom }}; i < data.length; i++) { + var _data = data[i] + , _dataType = getDataType(_data) + , _dataPath = dataPath + '[' + i + ']' + , result = validateItems(_data, _dataType, _dataPath); + + if (!result.valid) { + {{? it.opts.allErrors }} + errors.push.apply(errors, result.errors); + {{??}} + return result; + {{?}} + } + } +#}} + +function (data, dataType, dataPath) { + 'use strict'; + + var errors = []; + + {{? Array.isArray(it.schema) }} + {{ var $additionalItems = it.parentSchema.additionalItems; }} + {{? $additionalItems === false }} + if (data.length > {{= it.schema.length }}) { + errors.push({ + keyword: 'additionalItems', + schema: false, + dataPath: dataPath, + message: 'data' + dataPath + ' is not valid, should NOT have more than {{= it.schema.length }} items' + {{? it.opts.verbose }}, data: data{{?}} + }); + + {{? !it.opts.allErrors }} + return { valid: false, errors: errors }; + {{?}} + } + {{?}} + + {{~ it.schema:$schema:$index }} + {{? Object.keys($schema).length }} + if (data.length > {{= $index }}) { + {{ + var $it = it.copy(it); + $it.schema = $schema; + $it.schemaPath = it.schemaPath + '[' + $index + ']'; + }} + + var _data = data[{{= $index }}] + , _dataType = getDataType(_data) + , _dataPath = dataPath + '[{{= $index }}]' + , result = ({{= it._validate($it) }})(_data, _dataType, _dataPath); + + if (!result.valid) { + {{? it.opts.allErrors }} + errors.push.apply(errors, result.errors); + {{??}} + return result; + {{?}} + } + } + {{?}} + {{~}} + + {{? typeof $additionalItems == 'object' && Object.keys($additionalItems).length }} + {{ + var $it = it.copy(it); + $it.schema = $additionalItems; + $it.schemaPath = it.parentSchemaPath + '.additionalItems'; + }} + + if (data.length > {{= it.schema.length }}) { + var validateItems = ({{= it._validate($it) }}); + {{# def.validateItems: it.schema.length }} + } + {{?}} + {{?? Object.keys(it.schema).length }} + var validateItems = ({{= it._validate(it) }}); + {{# def.validateItems: 0 }} + {{?}} + + {{? it.opts.allErrors }} + return { valid: !errors.length, errors: errors }; + {{??}} + return { valid: true, errors: [] }; + {{?}} +} diff --git a/lib/compile/rules/multipleOf.dot.js b/lib/compile/rules/multipleOf.dot.js index 2eec14b..48c4f7a 100644 --- a/lib/compile/rules/multipleOf.dot.js +++ b/lib/compile/rules/multipleOf.dot.js @@ -3,6 +3,7 @@ function (data, dataType, dataPath) { var division = data / {{= it.schema }}; var valid = division === parseInt(division); + return { valid: valid, errors: valid ? [] : [{ diff --git a/lib/compile/rules/properties.dot.js b/lib/compile/rules/properties.dot.js index c55d53d..7eac855 100644 --- a/lib/compile/rules/properties.dot.js +++ b/lib/compile/rules/properties.dot.js @@ -3,29 +3,177 @@ function (data, dataType, dataPath) { var errors = []; - {{ var $properties = Object.keys(it.schema); }} + {{ + var $propertyKeys = Object.keys(it.schema || {}) + , $pProperties = it.parentSchema.patternProperties || {} + , $pPropertyKeys = $pProperties && Object.keys($pProperties) + , $aProperties = it.parentSchema.additionalProperties; + }} - {{~ $properties:$property }} - {{ - var $it = it.copy(it); - $it.schema = it.schema[$property]; - $it.schemaPath = it.schemaPath + '.' + $property; - }} + {{ + var $noAdditional = $aProperties === false + , $additionalIsSchema = typeof $aProperties == 'object' + && Object.keys($aProperties).length + , $checkAdditional = $noAdditional || $additionalIsSchema; + }} - if (data.hasOwnProperty('{{= $property }}')) { - var _data = data['{{= $property }}'] - , _dataType = getDataType(_data) - , _dataPath = dataPath + '.{{= $property }}' - , result = ({{= it._validate($it) }})(_data, _dataType, _dataPath); + {{? $checkAdditional }} + var propertiesSchema = self.schema{{= it.schemaPath }} || {}; + {{?}} - if (!result.valid) { - {{? it.opts.allErrors }} - errors.push.apply(errors, result.errors); + {{? $noAdditional }} + var propertiesSchemaKeys = Object.keys(propertiesSchema); + + var dataKeys = Object.keys(data); + + var notValid = dataKeys.length > propertiesSchemaKeys.length; + if (notValid) { + var error = { + keyword: 'properties', + schema: propertiesSchema, + dataPath: dataPath, + message: 'data' + dataPath + ' is not valid, additional properties NOT allowed' + {{? it.opts.verbose }}, data: data{{?}} + }; + + {{? it.opts.allErrors }} + errors.push(error); + {{??}} + return { valid: false, errors: [error] }; + {{?}} + } + {{?}} + + {{? $pPropertyKeys.length }} + var pPropertiesSchema = self.schema{{= it.parentSchemaPath + '.patternProperties' }} + , pPropertiesRegexps = {} + , dataKeysPPs; /* map of arrays of applicable pattern properties */ + + for (var pProperty in pPropertiesSchema) + pPropertiesRegexps[pProperty] = new RegExp(pProperty); + {{?}} + + + {{? $checkAdditional }} + {{? $pPropertyKeys.length }} + dataKeysPPs = {}; + {{?}} + + for (var key in data) { + var isAdditional = !propertiesSchema.hasOwnProperty(key); + + {{? $pPropertyKeys.length }} + dataKeysPPs[key] = {}; + for (var pProperty in pPropertiesSchema) { + var keyMatches = pPropertiesRegexps[pProperty].test(key); + if (keyMatches) { + dataKeysPPs[key][pProperty] = true; + isAdditional = false; + } + } + {{?}} + + if (isAdditional) { + {{? $noAdditional }} + var error = { + keyword: 'properties', + schema: propertiesSchema, + dataPath: dataPath, + message: 'data' + dataPath + ' is not valid, property ' + key + ' NOT allowed' + {{? it.opts.verbose }}, data: data{{?}} + }; + + {{? it.opts.allErrors }} + errors.push(error); + {{??}} + return { valid: false, errors: [error] }; + {{?}} {{??}} - return result; + {{ + /* additionalProperties is schema */ + var $it = it.copy(it); + $it.schema = $aProperties; + $it.schemaPath = it.parentSchemaPath + '.additionalProperties'; + }} + + var _data = data[key] + , _dataType = getDataType(_data) + , _dataPath = dataPath + '.' + key + , result = ({{= it._validate($it) }})(_data, _dataType, _dataPath); + + if (!result.valid) { + {{? it.opts.allErrors }} + errors.push.apply(errors, result.errors); + {{??}} + return result; + {{?}} + } {{?}} } } + {{?}} + + {{~ $propertyKeys:$propertyKey }} + {{ var $schema = it.schema[$propertyKey]; }} + + {{? Object.keys($schema).length }} + {{ + var $it = it.copy(it); + $it.schema = $schema; + $it.schemaPath = it.schemaPath + '.' + $propertyKey; + }} + + if (data.hasOwnProperty('{{= $propertyKey }}')) { + var _data = data['{{= $propertyKey }}'] + , _dataType = getDataType(_data) + , _dataPath = dataPath + '.{{= $propertyKey }}' + , result = ({{= it._validate($it) }})(_data, _dataType, _dataPath); + + if (!result.valid) { + {{? it.opts.allErrors }} + errors.push.apply(errors, result.errors); + {{??}} + return result; + {{?}} + } + } + {{?}} + {{~}} + + + {{~ $pPropertyKeys:$propertyKey }} + {{ var $schema = $pProperties[$propertyKey]; }} + + {{? Object.keys($schema).length }} + {{ + var $it = it.copy(it); + $it.schema = $schema; + $it.schemaPath = it.parentSchemaPath + '.patternProperties.' + $propertyKey; + }} + + for (var key in data) { + var keyMatches = {{? $checkAdditional }} + dataKeysPPs[key]['{{= $propertyKey }}'] + {{??}} + pPropertiesRegexps['{{= $propertyKey }}'].test(key) + {{?}}; + + if (keyMatches) { + var _data = data[key] + , _dataType = getDataType(_data) + , _dataPath = dataPath + '.{{= $propertyKey }}' + , result = ({{= it._validate($it) }})(_data, _dataType, _dataPath); + + if (!result.valid) { + {{? it.opts.allErrors }} + errors.push.apply(errors, result.errors); + {{??}} + return result; + {{?}} + } + } + } + {{?}} {{~}} {{? it.opts.allErrors }} diff --git a/lib/compile/rules/uniqueItems.dot.js b/lib/compile/rules/uniqueItems.dot.js new file mode 100644 index 0000000..d621652 --- /dev/null +++ b/lib/compile/rules/uniqueItems.dot.js @@ -0,0 +1,38 @@ +function (data, dataType, dataPath) { + 'use strict'; + + {{? it.schema && it.opts.uniqueItems !== false }} + if (data.length <= 1) return { valid: true, errors: [] }; + var itemsHash = {}, errors = []; + + for (var i = 0; i < data.length; i++) { + var itemStr = stableStringify(data[i]); + var isDuplicate = itemsHash[itemStr]; + if (isDuplicate) { + var error = { + keyword: 'uniqueItems', + schema: {{= it.schema }}, + dataPath: dataPath, + message: 'data' + dataPath + ' is not valid, item #' + i + 'is duplicate' + {{? it.opts.verbose }}, data: data{{?}} + }; + + {{? it.opts.allErrors }} + errors.push(error); + {{??}} + return { valid: false, errors: [error] }; + {{?}} + } else { + itemsHash[itemStr] = true; + } + } + + {{? it.opts.allErrors }} + return { valid: !errors.length, errors: errors }; + {{??}} + return { valid: true, errors: [] }; + {{?}} + {{??}} + return { valid: true, errors: [] }; + {{?}} +} diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index 7510479..abd64cf 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -6,10 +6,15 @@ var fs = require('fs') , TESTS_PATH = 'JSON-Schema-Test-Suite/tests/draft4/'; var ONLY_RULES; -// ONLY_RULES = ['maxItems', 'minItems']; -// ONLY_RULES = ['type', 'not', 'maximum', 'minimum', 'allOf', 'anyOf', 'oneOf', 'enum', -// 'properties', 'required', 'multipleOf', 'maxProperties', 'minProperties', -// 'maxLength', 'minLength', 'pattern', 'maxItems', 'minItems']; +// ONLY_RULES = ['enum']; +ONLY_RULES = [ +'type', 'not', 'allOf', 'anyOf', 'oneOf', 'enum', +'maximum', 'minimum', 'multipleOf', +'maxLength', 'minLength', 'pattern', +'properties', 'patternProperties', 'additionalProperties', +'required', 'maxProperties', 'minProperties', +'maxItems', 'minItems', 'items', 'additionalItems', 'uniqueItems' +]; var Jv = require('../lib/jv') @@ -25,18 +30,18 @@ describe.only('JSON-Schema tests', function () { describe(file.name, function() { var testSets = require(file.path); testSets.forEach(function (testSet) { - // if (testSet.description != 'oneOf') return; - describe(testSet.description, function() { - // it(testSet.description, function() { + // if (testSet.description != 'additionalProperties can exist by itself') return; + // describe(testSet.description, function() { + it(testSet.description, function() { var validate, fullValidate; - before(function() { + // before(function() { validate = jv.compile(testSet.schema); fullValidate = fullJv.compile(testSet.schema); - }); + // }); testSet.tests.forEach(function (test) { - // if (test.description != 'neither oneOf valid') return; - it(test.description, function() { + // if (test.description != 'a single invalid match is invalid') return; + // it(test.description, function() { var result = validate(test.data); // console.log('result', result); assert.equal(result.valid, test.valid); @@ -48,7 +53,7 @@ describe.only('JSON-Schema tests', function () { assert.equal(result.valid, test.valid); if (result.valid) assert(result.errors.length == 0); else assert(result.errors.length > 0); - }); + // }); }); }); }); diff --git a/try.js b/try.js index 4d1050c..f553d2d 100644 --- a/try.js +++ b/try.js @@ -1,69 +1,215 @@ 'use strict'; -validate = function(data) { +validate = function(data, instance) { + var self = this; var dataType = getDataType(data); var result = (function(data, dataType, dataPath) { 'use strict'; - var rule = RULES.not; + var errors = []; + var rule = RULES.properties; if (!rule.type || rule.type == dataType) { var result = (function(data, dataType, dataPath) { - var result = (function(data, dataType, dataPath) { - 'use strict'; - var rule = RULES.type; - if (!rule.type || rule.type == dataType) { - var result = (function(data, dataType, dataPath) { + 'use strict'; + var errors = []; + var propertiesSchema = self.schema.properties; + var pPropertiesSchema = self.schema.patternProperties, + pPropertiesRegexps = {}, + dataKeysPPs; + for (var pProperty in pPropertiesSchema) pPropertiesRegexps[pProperty] = new RegExp(pProperty); + dataKeysPPs = {}; + for (var key in data) { + var isAdditional = !propertiesSchema.hasOwnProperty(key); + dataKeysPPs[key] = {}; + for (var pProperty in pPropertiesSchema) { + var keyMatches = pPropertiesRegexps[pProperty].test(key); + if (keyMatches) { + dataKeysPPs[key][pProperty] = true; + isAdditional = false; + } + } + if (isAdditional) { + var _data = data[key], + _dataType = getDataType(_data), + _dataPath = dataPath + '.' + key, + result = (function(data, dataType, dataPath) { + 'use strict'; + var errors = []; + var rule = RULES.type; + if (!rule.type || rule.type == dataType) { + var result = (function(data, dataType, dataPath) { + 'use strict'; + var valid; + valid = dataType == 'number' && data === parseInt(data); + return { + valid: valid, + errors: valid ? [] : [{ + keyword: 'type', + schema: 'integer', + dataPath: dataPath, + message: 'data' + dataPath + ' is not valid. Expected integer', + data: data + }] + }; + })(data, dataType, dataPath); + if (!result.valid) { + errors.push.apply(errors, result.errors); + } + } + return { + valid: !errors.length, + errors: errors + }; + })(_data, _dataType, _dataPath); + if (!result.valid) { + errors.push.apply(errors, result.errors); + } + } + } + if (data.hasOwnProperty('foo')) { + var _data = data['foo'], + _dataType = getDataType(_data), + _dataPath = dataPath + '.foo', + result = (function(data, dataType, dataPath) { 'use strict'; - var valid; - valid = dataType == 'object'; - return { - valid: valid, - errors: valid ? [] : [{ - keyword: 'type', - schema: "object", - dataPath: dataPath, - message: 'Type of data' + dataPath + ' is not valid. Expected object' - }] - }; - })(data, dataType); - if (!result.valid) { - return result; - } - } - var rule = RULES.properties; - if (!rule.type || rule.type == dataType) { - var result = ()(data, dataType); - if (!result.valid) { - return result; - } - } - return { - valid: true, - errors: [] - }; - })(data, dataType, dataPath); - result.valid = !result.valid; - result.errors = result.valid ? [] : [{ - keyword: 'not', - schema: { - "type": "object", - "properties": { - "foo": { - "type": "string" + var errors = []; + var rule = RULES.type; + if (!rule.type || rule.type == dataType) { + var result = (function(data, dataType, dataPath) { + 'use strict'; + var valid; + valid = dataType == 'array'; + return { + valid: valid, + errors: valid ? [] : [{ + keyword: 'type', + schema: 'array', + dataPath: dataPath, + message: 'data' + dataPath + ' is not valid. Expected array', + data: data + }] + }; + })(data, dataType, dataPath); + if (!result.valid) { + errors.push.apply(errors, result.errors); + } } + var rule = RULES.maxItems; + if (!rule.type || rule.type == dataType) { + var result = (function(data, dataType, dataPath) { + 'use strict'; + var valid = data.length <= 3; + return { + valid: valid, + errors: valid ? [] : [{ + keyword: 'maxItems', + schema: 3, + dataPath: dataPath, + message: 'data' + dataPath + ' is not valid, should NOT have more than 3 items', + data: data + }] + }; + })(data, dataType, dataPath); + if (!result.valid) { + errors.push.apply(errors, result.errors); + } + } + return { + valid: !errors.length, + errors: errors + }; + })(_data, _dataType, _dataPath); + if (!result.valid) { + errors.push.apply(errors, result.errors); + } + } + if (data.hasOwnProperty('bar')) { + var _data = data['bar'], + _dataType = getDataType(_data), + _dataPath = dataPath + '.bar', + result = (function(data, dataType, dataPath) { + 'use strict'; + var errors = []; + var rule = RULES.type; + if (!rule.type || rule.type == dataType) { + var result = (function(data, dataType, dataPath) { + 'use strict'; + var valid; + valid = dataType == 'array'; + return { + valid: valid, + errors: valid ? [] : [{ + keyword: 'type', + schema: 'array', + dataPath: dataPath, + message: 'data' + dataPath + ' is not valid. Expected array', + data: data + }] + }; + })(data, dataType, dataPath); + if (!result.valid) { + errors.push.apply(errors, result.errors); + } + } + return { + valid: !errors.length, + errors: errors + }; + })(_data, _dataType, _dataPath); + if (!result.valid) { + errors.push.apply(errors, result.errors); + } + } + for (var key in data) { + var keyMatches = dataKeysPPs[key]['f.o']; + if (keyMatches) { + var _data = data['f.o'], + _dataType = getDataType(_data), + _dataPath = dataPath + '.f.o', + result = (function(data, dataType, dataPath) { + 'use strict'; + var errors = []; + var rule = RULES.minItems; + if (!rule.type || rule.type == dataType) { + var result = (function(data, dataType, dataPath) { + 'use strict'; + var valid = data.length >= 2; + return { + valid: valid, + errors: valid ? [] : [{ + keyword: 'minItems', + schema: 2, + dataPath: dataPath, + message: 'data' + dataPath + ' is not valid, should NOT have less than 2 items', + data: data + }] + }; + })(data, dataType, dataPath); + if (!result.valid) { + errors.push.apply(errors, result.errors); + } + } + return { + valid: !errors.length, + errors: errors + }; + })(_data, _dataType, _dataPath); + if (!result.valid) { + errors.push.apply(errors, result.errors); } - }, - dataPath: dataPath, - message: 'data' + dataPath + ' is valid according to schema, should be NOT valid' - }]; - return result; - })(data, dataType); + } + } + return { + valid: !errors.length, + errors: errors + }; + })(data, dataType, dataPath); if (!result.valid) { - return result; + errors.push.apply(errors, result.errors); } } return { - valid: true, - errors: [] + valid: !errors.length, + errors: errors }; })(data, dataType, ''); return result; -} \ No newline at end of file +}