From 64b80cbfff4da5f062a75c8a58d0630541a8b20d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 6 Jun 2015 16:18:52 +0100 Subject: [PATCH] more tests, fixed/optimized refs --- lib/compile/index.js | 24 +++--- lib/compile/resolve.js | 35 ++++---- lib/compile/rules/$ref.dot.js | 16 ++-- lib/compile/rules/pattern.dot.js | 2 +- package.json | 2 +- spec/json-schema.spec.js | 66 +++++++------- spec/tests/advanced_schema.json | 143 +++++++++++++++++++++++++++++++ spec/tests/basic_schema.json | 136 +++++++++++++++++++++++++++++ 8 files changed, 355 insertions(+), 69 deletions(-) create mode 100644 spec/tests/advanced_schema.json create mode 100644 spec/tests/basic_schema.json diff --git a/lib/compile/index.js b/lib/compile/index.js index 3e5b149..0c3f214 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -16,8 +16,10 @@ var RULES = require('./rules') module.exports = compile; -function compile(schema) { - var self = this; +function compile(schema, _rootSchema) { + var self = this, refs = [], refIds = {}; + _rootSchema = _rootSchema || schema; + var validateCode = validateGenerator({ isRoot: true, schema: schema, @@ -32,7 +34,8 @@ function compile(schema) { }); if (this.opts.beautify) { - if (beautify) validateCode = beautify(validateCode, { indent_size: 2 }); + var opts = this.opts.beautify === true ? { indent_size: 2 } : this.opts.beautify; + if (beautify) validateCode = beautify(validateCode, opts); else console.error('"npm install js-beautify" to use beautify option'); } // console.log('\n\n\n *** \n', validateCode); @@ -45,13 +48,14 @@ function compile(schema) { return validate; function resolveRef(ref) { - return resolve.call(self, compile, schema, ref); - } - - function validateRef(ref, data) { - var v = ref == '#' ? validate : self._schemas[ref]; - var valid = v(data); - return { valid: valid, errors: v.errors }; + if (refIds[ref]) return refIds[ref]; + var v = resolve.call(self, compile, _rootSchema, ref); + if (v) { + var id = refs.length; + refs.push(v); + refIds[ref] = id; + return id; + } } } diff --git a/lib/compile/resolve.js b/lib/compile/resolve.js index c706a5c..4fd952b 100644 --- a/lib/compile/resolve.js +++ b/lib/compile/resolve.js @@ -2,26 +2,25 @@ module.exports = function resolve(compile, rootSchema, ref) { - var schema = rootSchema; - if (ref[0] != '#') - schema = undefined; - else if (ref != '#') { - if (this._schemas[ref]) - schema = this._schemas[ref]; - else { - var parts = ref.split('/'); - for (var i = 1; i < parts.length; i++) { - if (!schema) break; - var part = unescape(parts[i]); - schema = schema[part]; - if (schema.$ref) - schema = resolve.call(this, compile, rootSchema, schema.$ref); - } - if (schema) this._schemas[ref] = compile.call(this, schema); - } + if (ref[0] != '#') return; + if (this._schemas[ref]) return this._schemas[ref]; + var schema = _resolve(rootSchema, ref); + if (schema) return this._schemas[ref] = compile.call(this, schema, rootSchema); +}; + + +function _resolve(rootSchema, ref) { + var schema = rootSchema + , parts = ref.split('/'); + for (var i = 1; i < parts.length; i++) { + if (!schema) break; + var part = unescape(parts[i]); + schema = schema[part]; + if (schema.$ref) + schema = _resolve(rootSchema, schema.$ref); } return schema; -}; +} function unescape(str) { diff --git a/lib/compile/rules/$ref.dot.js b/lib/compile/rules/$ref.dot.js index fe08e6f..2eeedf3 100644 --- a/lib/compile/rules/$ref.dot.js +++ b/lib/compile/rules/$ref.dot.js @@ -1,11 +1,15 @@ {{# def.definitions }} {{# def.setup:'$ref' }} -{{? it.resolveRef($schema) }} - var result{{=$lvl}} = validateRef('{{=$schema}}', {{=$data}}, (dataPath || '') + {{= it.errorPath }}); - var {{=$valid}} = result{{=$lvl}}.valid; - if (!{{=$valid}}) validate.errors.push.apply(validate.errors, result{{=$lvl}}.errors); +{{? $schema == '#' }} + var {{=$valid}} = validate({{=$data}}, (dataPath || '') + {{= it.errorPath }}); {{??}} - {{# def.error:'$ref' }} - var {{=$valid}} = false; + {{ $refId = it.resolveRef($schema); }} + {{? $refId === undefined }} + {{# def.error:'$ref' }} + var {{=$valid}} = false; + {{??}} + var {{=$valid}} = refs[{{=$refId}}]({{=$data}}, (dataPath || '') + {{= it.errorPath }}); + if (!{{=$valid}}) validate.errors.push.apply(validate.errors, refs[{{=$refId}}].errors); + {{?}} {{?}} diff --git a/lib/compile/rules/pattern.dot.js b/lib/compile/rules/pattern.dot.js index 3b90f1c..9daeccc 100644 --- a/lib/compile/rules/pattern.dot.js +++ b/lib/compile/rules/pattern.dot.js @@ -2,6 +2,6 @@ {{# def.setup:'pattern' }} {{ new RegExp($schema); /* test if regexp is valid to fail at compile time rather than in eval */}} -var {{=$valid}} = /{{=$schema}}/.test({{=$data}}); +var {{=$valid}} = new RegExp("{{=$schema}}").test({{=$data}}); {{# def.checkError:'pattern' }} diff --git a/package.json b/package.json index 5cf7177..b2f7bfc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ajv", - "version": "0.2.7", + "version": "0.2.8", "description": "Another JSON schema Validator", "main": "lib/ajv.js", "scripts": { diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index f14e91a..cdd4f3f 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -38,50 +38,50 @@ for (var id in remoteRefs) { } -describe('JSON-Schema tests', function () { - addTests('draft4: ', './json-schema-test-suite/tests/draft4/{**/,}*.json'); +addTests('JSON-Schema tests draft4', './json-schema-test-suite/tests/draft4/{**/,}*.json'); +addTests('Advanced schema tests', './tests/{**/,}*.json'); - function addTests(description, testsPath) { - describe(description, function() { - var files = getTestFiles(testsPath); - files.forEach(function (file) { - if (ONLY_RULES && ONLY_RULES.indexOf(file.name) == -1) return; - if (SKIP_RULES && SKIP_RULES.indexOf(file.name) >= 0) return; +function addTests(description, testsPath) { + describe(description, function() { + var files = getTestFiles(testsPath); - describe(file.name, function() { - var testSets = require(file.path); - testSets.forEach(function (testSet) { - // if (testSet.description != 'allOf with base schema') return; - describe(testSet.description, function() { - // it(testSet.description, function() { - var validate = ajv.compile(testSet.schema); - var fullValidate = fullAjv.compile(testSet.schema); + files.forEach(function (file) { + if (ONLY_RULES && ONLY_RULES.indexOf(file.name) == -1) return; + if (SKIP_RULES && SKIP_RULES.indexOf(file.name) >= 0) return; - testSet.tests.forEach(function (test) { - // if (test.description != 'one supplementary Unicode code point is not long enough') return; - // console.log(testSet.schema, '\n\n***\n\n', validate.toString()); - it(test.description, function() { - var valid = validate(test.data); - // console.log('result', valid, validate.errors); - assert.equal(valid, test.valid); - if (valid) assert(validate.errors.length == 0); - else assert(validate.errors.length > 0); + describe(file.name, function() { + var testSets = require(file.path); + testSets.forEach(function (testSet) { + // if (testSet.description != 'allOf with base schema') return; + describe(testSet.description, function() { + // it(testSet.description, function() { + var validate = ajv.compile(testSet.schema); + var fullValidate = fullAjv.compile(testSet.schema); - var valid = fullValidate(test.data); - // console.log('full result', valid, fullValidate.errors); - assert.equal(valid, test.valid); - if (valid) assert(fullValidate.errors.length == 0); - else assert(fullValidate.errors.length > 0); - }); + testSet.tests.forEach(function (test) { + // if (test.description != 'one supplementary Unicode code point is not long enough') return; + // console.log(testSet.schema, '\n\n***\n\n', validate.toString()); + it(test.description, function() { + var valid = validate(test.data); + // console.log('result', valid, validate.errors); + assert.equal(valid, test.valid); + if (valid) assert(validate.errors.length == 0); + else assert(validate.errors.length > 0); + + var valid = fullValidate(test.data); + // console.log('full result', valid, fullValidate.errors); + assert.equal(valid, test.valid); + if (valid) assert(fullValidate.errors.length == 0); + else assert(fullValidate.errors.length > 0); }); }); }); }); }); }); - } -}); + }); +} function getTestFiles(testsPath) { diff --git a/spec/tests/advanced_schema.json b/spec/tests/advanced_schema.json new file mode 100644 index 0000000..3a04495 --- /dev/null +++ b/spec/tests/advanced_schema.json @@ -0,0 +1,143 @@ +[ + { + "description": "advanced schema from z-schema benchmark (https://github.com/zaggino/z-schema)", + "schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "/": { "$ref": "#/definitions/entry" } + }, + "patternProperties": { + "^(/[^/]+)+$": { "$ref": "#/definitions/entry" } + }, + "additionalProperties": false, + "required": [ "/" ], + "definitions": { + "entry": { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "schema for an fstab entry", + "type": "object", + "required": [ "storage" ], + "properties": { + "storage": { + "type": "object", + "oneOf": [ + { "$ref": "#/definitions/entry/definitions/diskDevice" }, + { "$ref": "#/definitions/entry/definitions/diskUUID" }, + { "$ref": "#/definitions/entry/definitions/nfs" }, + { "$ref": "#/definitions/entry/definitions/tmpfs" } + ] + }, + "fstype": { + "enum": [ "ext3", "ext4", "btrfs" ] + }, + "options": { + "type": "array", + "minItems": 1, + "items": { "type": "string" }, + "uniqueItems": true + }, + "readonly": { "type": "boolean" } + }, + "definitions": { + "diskDevice": { + "properties": { + "type": { "enum": [ "disk" ] }, + "device": { + "type": "string", + "pattern": "^/dev/[^/]+(/[^/]+)*$" + } + }, + "required": [ "type", "device" ], + "additionalProperties": false + }, + "diskUUID": { + "properties": { + "type": { "enum": [ "disk" ] }, + "label": { + "type": "string", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + } + }, + "required": [ "type", "label" ], + "additionalProperties": false + }, + "nfs": { + "properties": { + "type": { "enum": [ "nfs" ] }, + "remotePath": { + "type": "string", + "pattern": "^(/[^/]+)+$" + }, + "server": { + "type": "string", + "anyOf": [ + { "format": "host-name" }, + { "format": "ipv4" }, + { "format": "ipv6" } + ] + } + }, + "required": [ "type", "server", "remotePath" ], + "additionalProperties": false + }, + "tmpfs": { + "properties": { + "type": { "enum": [ "tmpfs" ] }, + "sizeInMB": { + "type": "integer", + "minimum": 16, + "maximum": 512 + } + }, + "required": [ "type", "sizeInMB" ], + "additionalProperties": false + } + } + } + } + }, + "tests": [ + { + "description": "valid object from z-schema benchmark", + "data": { + "/": { + "storage": { + "type": "disk", + "device": "/dev/sda1" + }, + "fstype": "btrfs", + "readonly": true + }, + "/var": { + "storage": { + "type": "disk", + "label": "8f3ba6f4-5c70-46ec-83af-0d5434953e5f" + }, + "fstype": "ext4", + "options": [ "nosuid" ] + }, + "/tmp": { + "storage": { + "type": "tmpfs", + "sizeInMB": 64 + } + }, + "/var/www": { + "storage": { + "type": "nfs", + "server": "my.nfs.server", + "remotePath": "/exports/mypath" + } + } + }, + "valid": true + }, + { + "description": "not array", + "data": 1, + "valid": false + } + ] + } +] diff --git a/spec/tests/basic_schema.json b/spec/tests/basic_schema.json new file mode 100644 index 0000000..1ab3c4a --- /dev/null +++ b/spec/tests/basic_schema.json @@ -0,0 +1,136 @@ +[ + { + "description": "basic schema from z-schema benchmark (https://github.com/zaggino/z-schema)", + "schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Product set", + "type": "array", + "items": { + "title": "Product", + "type": "object", + "properties": { + "id": { + "description": "The unique identifier for a product", + "type": "number" + }, + "name": { + "type": "string" + }, + "price": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "dimensions": { + "type": "object", + "properties": { + "length": {"type": "number"}, + "width": {"type": "number"}, + "height": {"type": "number"} + }, + "required": ["length", "width", "height"] + }, + "warehouseLocation": { + "description": "Coordinates of the warehouse with the product" + } + }, + "required": ["id", "name", "price"] + } + }, + "tests": [ + { + "description": "valid array from z-schema benchmark", + "data": [ + { + "id": 2, + "name": "An ice sculpture", + "price": 12.50, + "tags": ["cold", "ice"], + "dimensions": { + "length": 7.0, + "width": 12.0, + "height": 9.5 + }, + "warehouseLocation": { + "latitude": -78.75, + "longitude": 20.4 + } + }, + { + "id": 3, + "name": "A blue mouse", + "price": 25.50, + "dimensions": { + "length": 3.1, + "width": 1.0, + "height": 1.0 + }, + "warehouseLocation": { + "latitude": 54.4, + "longitude": -32.7 + } + } + ], + "valid": true + }, + { + "description": "not array", + "data": 1, + "valid": false + }, + { + "description": "array of not onjects", + "data": [1,2,3], + "valid": false + }, + { + "description": "missing required properties", + "data": [{}], + "valid": false + }, + { + "description": "required property of wrong type", + "data": [{"id": 1, "name": "product", "price": "not valid"}], + "valid": false + }, + { + "description": "smallest valid product", + "data": [{"id": 1, "name": "product", "price": 100}], + "valid": true + }, + { + "description": "tags should be array", + "data": [{"tags":{}, "id": 1, "name": "product", "price": 100}], + "valid": false + }, + { + "description": "dimensions should be object", + "data": [{"dimensions":[], "id": 1, "name": "product", "price": 100}], + "valid": false + }, + { + "description": "valid product with tag", + "data": [{"tags":["product"], "id": 1, "name": "product", "price": 100}], + "valid": true + }, + { + "description": "dimensions miss required properties", + "data": [{"dimensions":{}, "tags":["product"], "id": 1, "name": "product", "price": 100}], + "valid": false + }, + { + "description": "valid product with tag and dimensions", + "data": [{"dimensions":{"length": 7,"width": 12,"height": 9.5}, "tags":["product"], "id": 1, "name": "product", "price": 100}], + "valid": true + } + ] + } +]