From fcc776e79467e97ae75eaffe935d0c7a0bb0f4d2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 11 Nov 2015 07:01:27 +0000 Subject: [PATCH 01/20] basic implementation of custom keywords, #69 --- lib/ajv.js | 64 +++++++++++++++++++++++++++- lib/compile/index.js | 31 ++++++++++++-- lib/compile/rules.js | 51 ++++++++++++---------- lib/dot/custom.def | 22 ++++++++++ lib/dot/definitions.def | 9 ++-- lib/dot/validate.jst | 6 ++- scripts/compile-dots.js | 3 +- spec/custom.spec.js | 92 ++++++++++++++++++++++++++++++++++++++++ spec/json-schema.spec.js | 1 - 9 files changed, 246 insertions(+), 33 deletions(-) create mode 100644 lib/dot/custom.def create mode 100644 spec/custom.spec.js diff --git a/lib/ajv.js b/lib/ajv.js index b1cef99..7a6d858 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -5,7 +5,8 @@ var compileSchema = require('./compile') , Cache = require('./cache') , SchemaObject = require('./compile/schema_obj') , stableStringify = require('json-stable-stringify') - , formats = require('./compile/formats'); + , formats = require('./compile/formats') + , rules = require('./compile/rules'); module.exports = Ajv; @@ -31,6 +32,7 @@ function Ajv(opts) { this._formats = formats(this.opts.format); this._cache = this.opts.cache || new Cache; this._loadingSchemas = {}; + this.RULES = rules(); // this is done on purpose, so that methods are bound to the instance // (without using bind) so that they can be used without the instance @@ -43,6 +45,7 @@ function Ajv(opts) { this.getSchema = getSchema; this.removeSchema = removeSchema; this.addFormat = addFormat; + this.addKeyword = addKeyword; this.errorsText = errorsText; this._compile = _compile; @@ -324,6 +327,12 @@ function Ajv(opts) { } + /** + * Convert array of error message objects to string + * @param {Array} errors optional array of validation errors, if not passed errors from the instance are used. + * @param {Object} opts optional options with properties `separator` and `dataVar`. + * @return {String} + */ function errorsText(errors, opts) { errors = errors || self.errors; if (!errors) return 'No errors'; @@ -338,12 +347,65 @@ function Ajv(opts) { } + /** + * Add custom format + * @param {String} name format name + * @param {String|RegExp|Function} format string is converted to RegExp; function should return boolean (true when valid) + */ function addFormat(name, format) { if (typeof format == 'string') format = new RegExp(format); self._formats[name] = format; } + /** + * Add custom keyword + * @param {String} keyword custom keyword, should be different from all defined validation keywords, should be unique. + * @param {Object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`. + */ + function addKeyword(keyword, definition) { + if (self.RULES.keywords[keyword]) + throw new Error('Keyword ' + keyword + ' is already defined'); + + var dataType = definition.type; + if (Array.isArray(dataType)) { + var i, len = dataType.length; + for (i=0; i Date: Wed, 11 Nov 2015 23:00:50 +0000 Subject: [PATCH 02/20] support compiling schemas in custom keywords, #69 --- lib/ajv.js | 1 + lib/compile/index.js | 24 ++++++--- lib/dot/custom.def | 50 ++++++++++++------- spec/custom.spec.js | 115 +++++++++++++++++++++++++++++++++++++------ 4 files changed, 152 insertions(+), 38 deletions(-) diff --git a/lib/ajv.js b/lib/ajv.js index 7a6d858..eb75b96 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -378,6 +378,7 @@ function Ajv(opts) { } self.RULES.keywords[keyword] = true; + self.RULES.all[keyword] = true; } diff --git a/lib/compile/index.js b/lib/compile/index.js index 3bb583c..c94f127 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -2,7 +2,8 @@ var resolve = require('./resolve') , util = require('./util') - , equal = require('./equal'); + , equal = require('./equal') + , stableStringify = require('json-stable-stringify'); try { var beautify = require('' + 'js-beautify').js_beautify; } catch(e) {} @@ -142,13 +143,24 @@ function compile(schema, root, localRefs, baseId) { return 'pattern' + index; } - function useCustomRule(rule) { - var index = customRulesHash[rule.keyword]; + function useCustomRule(rule, schema) { + var compile = rule.definition.compile; + + var key = rule.keyword; + if (compile) key += ':' + stableStringify(schema); + + var index = customRulesHash[key]; if (index === undefined) { - index = customRulesHash[rule.keyword] = customRules.length; - customRules[index] = rule.definition.validate; + var validate = compile ? compile(schema) : rule.definition.validate; + index = customRulesHash[key] = customRules.length; + customRules[index] = validate; } - return 'customRule' + index; + + return { + code: 'customRule' + index, + compiled: !!compile, + validate: customRules[index] + }; } } diff --git a/lib/dot/custom.def b/lib/dot/custom.def index 07a3979..817b937 100644 --- a/lib/dot/custom.def +++ b/lib/dot/custom.def @@ -1,22 +1,36 @@ -{{? $rule.definition.validate }} - {{ - var $ruleValidate = it.useCustomRule($rule) - , $ruleErrs = $ruleValidate + '.errors' - , $schemaPath = it.schemaPath + '.' + $rule.keyword; - }} - {{=$ruleErrs}} = null; +{{ + var $schema = it.schema[$rule.keyword] + , $ruleValidate = it.useCustomRule($rule, $schema) + , $ruleErrs = $ruleValidate.code + '.errors' + , $schemaPath = it.schemaPath + '.' + $rule.keyword; +}} +{{=$ruleErrs}} = null; - if (!{{=$ruleValidate}}.call(self, validate.schema{{=$schemaPath}}, {{=$data}})) { - if (Array.isArray({{=$ruleErrs}})) { - if (vErrors === null) vErrors = []; - vErrors.concat({{=$ruleErrs}}); - } else { - {{# def.error:'custom' }} - } - {{? $breakOnError }} - return false; +{{## def.callRuleValidate: + {{ var argsLen; }} + {{=$ruleValidate.code}}.call(self + {{? !$ruleValidate.compiled }} + , validate.schema{{=$schemaPath}} + , {{=$data}} + {{ argsLen = 2; }} + {{??}} + , {{=$data}} + {{ argsLen = 1; }} + {{?}} + ) +#}} + + if (!{{# def.callRuleValidate }}) { + if (Array.isArray({{=$ruleErrs}})) { + if (vErrors === null) vErrors = {{=$ruleErrs}}; + else vErrors.concat({{=$ruleErrs}}); + errors = vErrors.length; } else { - {{??}} + {{# def.error:'custom' }} } - {{?}} +{{? $breakOnError }} + return false; + } else { +{{??}} + } {{?}} diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 3ec3d48..4eee971 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -3,7 +3,8 @@ var Ajv = require(typeof window == 'object' ? 'ajv' : '../lib/ajv') , should = require('chai').should() - , getAjvInstances = require('./ajv_instances'); + , getAjvInstances = require('./ajv_instances') + , equal = require('../lib/compile/equal'); describe('Custom keywords', function () { @@ -19,26 +20,112 @@ describe('Custom keywords', function () { }); }); - describe('interpreted custom rules', function() { - it('should add and validate rule', function() { - instances.forEach(testAddKeyword); + describe('custom rules', function() { + var compileCount; - function testAddKeyword(ajv) { - ajv.addKeyword('even', { type: 'number', validate: isEven }); - var validate = ajv.compile({ even: true }); - validate(2) .should.equal(true); - validate('abc') .should.equal(true); - validate(2.5) .should.equal(false); - validate(3) .should.equal(false); + it('should add and validate rule with "interpreted" keyword validation', function() { + instances.forEach(testAddEvenKeyword({ type: 'number', validate: validateEven })); + + function validateEven(schema, data) { + if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean'); + return data % 2 ? !schema : schema; } }); - function isEven(schema, data) { - if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean'); - return data % 2 ? !schema : schema; + it('should add and validate rule with "compiled" keyword validation', function() { + instances.forEach(testAddEvenKeyword({ type: 'number', compile: compileEven })); + + function compileEven(schema) { + if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean'); + return schema ? isEven : isOdd; + } + + function isEven(data) { return data % 2 === 0; } + function isOdd(data) { return data % 2 !== 0; } + }); + + it('should compile keyword validating function only once per schema', function () { + instances.forEach(test); + + function test(ajv) { + ajv.addKeyword('constant', { compile: compileConstant }); + + var schema = { "constant": "abc" }; + compileCount = 0; + var validate = ajv.compile(schema); + should.equal(compileCount, 1); + + shouldBeValid(validate, 'abc'); + shouldBeInvalid(validate, 2); + shouldBeInvalid(validate, {}); + } + }); + + it('should allow multiple schemas for the same keyword', function () { + instances.forEach(test); + + function test(ajv) { + ajv.addKeyword('constant', { compile: compileConstant }); + + var schema = { + "properties": { + "a": { "constant": 1 }, + "b": { "constant": 1 } + }, + "additionalProperties": { "constant": { "foo": "bar" } }, + "items": { "constant": { "foo": "bar" } } + }; + compileCount = 0; + var validate = ajv.compile(schema); + should.equal(compileCount, 2); + + shouldBeValid(validate, {a:1, b:1}); + shouldBeInvalid(validate, {a:2, b:1}); + + shouldBeValid(validate, {a:1, c: {foo: 'bar'}}); + shouldBeInvalid(validate, {a:1, c: {foo: 'baz'}}); + + shouldBeValid(validate, [{foo: 'bar'}]); + shouldBeValid(validate, [{foo: 'bar'}, {foo: 'bar'}]); + + shouldBeInvalid(validate, [1]); + } + }); + + function compileConstant(schema) { + compileCount++; + return typeof schema == 'object' && schema !== null + ? isDeepEqual + : isStrictEqual; + + function isDeepEqual(data) { return equal(data, schema); } + function isStrictEqual(data) { return data === schema; } + } + + function testAddEvenKeyword(definition) { + return function (ajv) { + ajv.addKeyword('even', definition); + var schema = { "even": true }; + var validate = ajv.compile(schema); + + shouldBeValid(validate, 2); + shouldBeValid(validate, 'abc'); + shouldBeInvalid(validate, 2.5); + shouldBeInvalid(validate, 3); + }; } }); + function shouldBeValid(validate, data) { + validate(data) .should.equal(true); + should.not.exist(validate.errors); + } + + function shouldBeInvalid(validate, data, numErrors) { + validate(data) .should.equal(false); + validate.errors .should.have.length(numErrors || 1); + } + describe('addKeyword method', function() { var TEST_TYPES = [ undefined, 'number', 'string', 'boolean', ['number', 'string']]; From 285850ce2097a6ebbb4c3994473b89fd4bc59fd0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 12 Nov 2015 23:52:53 +0000 Subject: [PATCH 03/20] pass parent schema to compile/validate functions of custom keywords if they use it, #69 --- lib/compile/index.js | 11 ++-- lib/dot/custom.def | 7 +-- spec/custom.spec.js | 126 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 123 insertions(+), 21 deletions(-) diff --git a/lib/compile/index.js b/lib/compile/index.js index c94f127..4f27047 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -143,15 +143,20 @@ function compile(schema, root, localRefs, baseId) { return 'pattern' + index; } - function useCustomRule(rule, schema) { + function useCustomRule(rule, schema, parentSchema) { var compile = rule.definition.compile; var key = rule.keyword; - if (compile) key += ':' + stableStringify(schema); + if (compile) { + key += '::' + stableStringify(schema); + if (compile.length > 1) key += '::' + stableStringify(parentSchema); + } var index = customRulesHash[key]; if (index === undefined) { - var validate = compile ? compile(schema) : rule.definition.validate; + var validate = compile + ? compile.call(self, schema, parentSchema) + : rule.definition.validate; index = customRulesHash[key] = customRules.length; customRules[index] = validate; } diff --git a/lib/dot/custom.def b/lib/dot/custom.def index 817b937..1d0060f 100644 --- a/lib/dot/custom.def +++ b/lib/dot/custom.def @@ -1,6 +1,6 @@ {{ var $schema = it.schema[$rule.keyword] - , $ruleValidate = it.useCustomRule($rule, $schema) + , $ruleValidate = it.useCustomRule($rule, $schema, it.schema) , $ruleErrs = $ruleValidate.code + '.errors' , $schemaPath = it.schemaPath + '.' + $rule.keyword; }} @@ -12,10 +12,11 @@ {{? !$ruleValidate.compiled }} , validate.schema{{=$schemaPath}} , {{=$data}} - {{ argsLen = 2; }} + {{? $ruleValidate.validate.length > 2 }} + , validate.schema{{=it.schemaPath}} + {{?}} {{??}} , {{=$data}} - {{ argsLen = 1; }} {{?}} ) #}} diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 4eee971..f7872ef 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -11,20 +11,20 @@ describe('Custom keywords', function () { var ajv, instances; beforeEach(function() { - ajv = Ajv(); instances = getAjvInstances({ allErrors: true, verbose: true, inlineRefs: false, i18n: true }); + ajv = instances[0]; }); describe('custom rules', function() { - var compileCount; + var compileCount = 0; it('should add and validate rule with "interpreted" keyword validation', function() { - instances.forEach(testAddEvenKeyword({ type: 'number', validate: validateEven })); + testAddEvenKeyword({ type: 'number', validate: validateEven }); function validateEven(schema, data) { if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean'); @@ -33,7 +33,7 @@ describe('Custom keywords', function () { }); it('should add and validate rule with "compiled" keyword validation', function() { - instances.forEach(testAddEvenKeyword({ type: 'number', compile: compileEven })); + testAddEvenKeyword({ type: 'number', compile: compileEven }); function compileEven(schema) { if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean'); @@ -45,9 +45,7 @@ describe('Custom keywords', function () { }); it('should compile keyword validating function only once per schema', function () { - instances.forEach(test); - - function test(ajv) { + instances.forEach(function (ajv) { ajv.addKeyword('constant', { compile: compileConstant }); var schema = { "constant": "abc" }; @@ -58,13 +56,11 @@ describe('Custom keywords', function () { shouldBeValid(validate, 'abc'); shouldBeInvalid(validate, 2); shouldBeInvalid(validate, {}); - } + }); }); it('should allow multiple schemas for the same keyword', function () { - instances.forEach(test); - - function test(ajv) { + instances.forEach(function (ajv) { ajv.addKeyword('constant', { compile: compileConstant }); var schema = { @@ -89,9 +85,52 @@ describe('Custom keywords', function () { shouldBeValid(validate, [{foo: 'bar'}, {foo: 'bar'}]); shouldBeInvalid(validate, [1]); + }); + }); + + it('should pass parent schema to "interpreted" keyword validation', function() { + testRangeKeyword({ type: 'number', validate: validateRange }); + + function validateRange(schema, data, parentSchema) { + validateRangeSchema(schema, parentSchema); + + return parentSchema.exclusiveRange === true + ? data > schema[0] && data < schema[1] + : data >= schema[0] && data <= schema[1]; } }); + it('should pass parent schema to "compiled" keyword validation', function() { + testRangeKeyword({ type: 'number', compile: compileRange }); + }); + + it('should allow multiple parent schemas for the same keyword', function () { + instances.forEach(function (ajv) { + ajv.addKeyword('range', { type: 'number', compile: compileRange }); + + var schema = { + "properties": { + "a": { "range": [2, 4], "exclusiveRange": true }, + "b": { "range": [2, 4], "exclusiveRange": false } + }, + "additionalProperties": { "range": [5, 7] }, + "items": { "range": [5, 7] } + }; + compileCount = 0; + var validate = ajv.compile(schema); + should.equal(compileCount, 3); + + shouldBeValid(validate, {a:3.99, b:4}); + shouldBeInvalid(validate, {a:4, b:4}); + + shouldBeValid(validate, {a:2.01, c: 7}); + shouldBeInvalid(validate, {a:2.01, c: 7.01}); + + shouldBeValid(validate, [5, 6, 7]); + shouldBeInvalid(validate, [7.01]); + }); + }); + function compileConstant(schema) { compileCount++; return typeof schema == 'object' && schema !== null @@ -102,8 +141,20 @@ describe('Custom keywords', function () { function isStrictEqual(data) { return data === schema; } } + function compileRange(schema, parentSchema) { + compileCount++; + validateRangeSchema(schema, parentSchema); + + var min = schema[0]; + var max = schema[1]; + + return parentSchema.exclusiveRange === true + ? function (data) { return data > min && data < max; } + : function (data) { return data >= min && data <= max; } + } + function testAddEvenKeyword(definition) { - return function (ajv) { + instances.forEach(function (ajv) { ajv.addKeyword('even', definition); var schema = { "even": true }; var validate = ajv.compile(schema); @@ -112,7 +163,52 @@ describe('Custom keywords', function () { shouldBeValid(validate, 'abc'); shouldBeInvalid(validate, 2.5); shouldBeInvalid(validate, 3); - }; + }); + } + + function testRangeKeyword(definition) { + instances.forEach(function (ajv) { + ajv.addKeyword('range', definition); + + var schema = { "range": [2, 4] }; + var validate = ajv.compile(schema); + + shouldBeValid(validate, 2); + shouldBeValid(validate, 3); + shouldBeValid(validate, 4); + shouldBeValid(validate, 'abc'); + + shouldBeInvalid(validate, 1.99); + shouldBeInvalid(validate, 4.01); + + var schema = { + "properties": { + "foo": { + "range": [2, 4], + "exclusiveRange": true + } + } + }; + var validate = ajv.compile(schema); + + shouldBeValid(validate, { foo: 2.01 }); + shouldBeValid(validate, { foo: 3 }); + shouldBeValid(validate, { foo: 3.99 }); + + shouldBeInvalid(validate, { foo: 2 }); + shouldBeInvalid(validate, { foo: 4 }); + }); + } + + function validateRangeSchema(schema, parentSchema) { + var schemaValid = Array.isArray(schema) && schema.length == 2 + && typeof schema[0] == 'number' + && typeof schema[1] == 'number'; + if (!schemaValid) throw new Error('Invalid schema for range keyword, should be array of 2 numbers'); + + var exclusiveRangeSchemaValid = parentSchema.exclusiveRange === undefined + || typeof parentSchema.exclusiveRange == 'boolean'; + if (!exclusiveRangeSchemaValid) throw new Error('Invalid schema for exclusiveRange keyword, should be bolean'); } }); @@ -161,11 +257,11 @@ describe('Custom keywords', function () { }); should.throw(function() { - addKeyword('custom3', ['number', 'wrongtype']); + addKeyword('custom2', ['number', 'wrongtype']); }); should.throw(function() { - addKeyword('custom4', ['number', undefined]); + addKeyword('custom3', ['number', undefined]); }); }); From c1b208816c01b0f482dbfb769b049e30ecc40e6a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 13 Nov 2015 12:16:31 +0000 Subject: [PATCH 04/20] readme for custom keywords --- README.md | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 438455d..8e3a25e 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,13 @@ Currently the fastest JSON Schema validator for node.js and browser. It uses [doT templates](https://github.com/olado/doT) to generate super-fast validating functions. -[![Build Status](https://travis-ci.org/epoberezkin/ajv.svg?branch=master)](https://travis-ci.org/epoberezkin/ajv) +[![Build Status](https://travis-ci.org/epoberezkin/ajv.svg?branch=v2.0)](https://travis-ci.org/epoberezkin/ajv) [![npm version](https://badge.fury.io/js/ajv.svg)](http://badge.fury.io/js/ajv) [![Code Climate](https://codeclimate.com/github/epoberezkin/ajv/badges/gpa.svg)](https://codeclimate.com/github/epoberezkin/ajv) [![Test Coverage](https://codeclimate.com/github/epoberezkin/ajv/badges/coverage.svg)](https://codeclimate.com/github/epoberezkin/ajv/coverage) + ## JSON Schema standard ajv implements full [JSON Schema draft 4](http://json-schema.org/) standard: @@ -123,6 +124,95 @@ You can add additional formats and replace any of the formats above using [addFo You can find patterns used for format validation and the sources that were used in [formats.js](https://github.com/epoberezkin/ajv/blob/master/lib/compile/formats.js). +## Defining custom keywords + +Starting from version 2.0 (2.0.0-beta.0) ajv supports custom keyword definitions. + +Disclaimer: The main drawback of extending JSON-schema standard with custom keywords is the loss of portability of your schemas - it may not be possible to support these custom keywords on some other platforms. Also your schemas may be more challenging to read for other people. If portability is important you may prefer using additional validation logic outside of JSON-schema rather than putting it inside your JSON-schema. + +The advantages of using custom keywords are: +- they allow you keeping a larger portion of your validation logic in the schema +- they may make schema more expressive and less verbose +- they are fun to use and help keeping more people off the street + +You can define custom keywords with `addKeyword` method. Keywords are defined on the `ajv` instance level - new instances will not have previously defined keywords automatically. + +Ajv allows defining keywords in several ways: +- with validation function that will be called with keyword schema, data and, optionally, parent schema during validation (generally not recommended for performance reason, but can be useful to test your custom keyword ideas) +- with compilation function that will be called only once during schema compilation with keyword schema and, optionally, parent schema and should return schema validation function (in some cases it's the best approach with the performance cost of an extra function call during validation). +- NOT IMPLEMENTED: with schema macro function that will be called once before schema compilation with keyword schema and parent schema and should return another schema that will be applied to the data in addition to the original schema (`allOf` keyword is used to achieve that). In many cases it is the most efficient approach as it is relatively easy to implement and no extra function call happens during validation. +- NOT IMPLEMETED: with compilation function that should return code that will be inlined in the currently compiled schema. It can be more performance-efficient approach than macro expansion for some cases but it is the most difficult to use as it requires good understanding of Ajv schema compilation process. + + +### Define keyword using validation function + +Keyword can be defined with validation function that will be passed schema, data and parentSchema (if it has 3 arguments) at validation time and that should return validation result as boolean. + +Example. draft5 `constant` keyword (that is equivalent to `enum` keyword with one item): + +``` +ajv.addKeyword('constant', { validate: validateConstant }); + +var schema = { "constant": 2 }; +var validate = ajv.compile(schema); +console.log(validate(2)); // true +console.log(validate(3)); // false + +var schema = { "constant": { "foo": "bar" } }; +var validate = ajv.compile(schema); +console.log(validate({foo: 'bar'})); // true +console.log(validate({foo: 'baz'})); // false + +function validateConstant(schema, data) { + return typeof schema == 'object && schema !== null' + ? deepEqual(schema, data) + : schema === data; +} +``` + +This approach exists as a way to quickly test your keyword and is not recommended because of worse performance than compiling schemas. + + +### Define keyword using "compilation" function + +Keyword can also be defined using schema compilation function that is passed schema (and optionally parentSchema) and that should return validation function that will only be passed data. + +Example. `range` and `exclusiveRange` keywords using compiled schema: + +``` +ajv.addKeyword('range', { type: 'number', compile: compileRange }); + +var schema = { "range": [2, 4], "exclusiveRange": true }; +var validate = ajv.compile(schema); +console.log(validate(2.01)); // true +console.log(validate(3.99)); // true +console.log(validate(2)); // false +console.log(validate(4)); // false + +function compileRange(schema, parentSchema) { + validateRangeSchema(schema, parentSchema); + + var min = schema[0]; + var max = schema[1]; + + return parentSchema.exclusiveRange === true + ? function (data) { return data > min && data < max; } + : function (data) { return data >= min && data <= max; } +} + +function validateRangeSchema(schema, parentSchema) { + var schemaValid = Array.isArray(schema) && schema.length == 2 + && typeof schema[0] == 'number' + && typeof schema[1] == 'number'; + if (!schemaValid) throw new Error('Invalid schema for range keyword, should be array of 2 numbers'); + + var exclusiveRangeSchemaValid = parentSchema.exclusiveRange === undefined + || typeof parentSchema.exclusiveRange == 'boolean'; + if (!exclusiveRangeSchemaValid) throw new Error('Invalid schema for exclusiveRange keyword, should be bolean'); +} +``` + + ## Asynchronous compilation Starting from version 1.3 ajv supports asynchronous compilation when remote references are loaded using supplied function. See `compileAsync` method and `loadSchema` option. @@ -256,6 +346,25 @@ Function should return validation result as `true` or `false`. Custom formats can be also added via `formats` option. +##### .addKeyword(String keyword, Object definition) + +Add custom validation keyword to ajv instance. + +Keyword should be a valid JavaScript identifier. + +Keyword should be different from all standard JSON schema keywords and different from previously defined keywords. There is no way to redefine keywords or remove keyword definition from the instance. + +Keyword definition is an object with the following properties: + +- _type_: optional string or array of strings with data type(s) that the keyword will apply to. If keyword is validating another type the validation function will not be called, so there is no need to check for data type inside validation function if `type` property is used. +- _validate_: validating function that will be called at validation time with keyword schema, data and parent schema (it will be passed only if the function has 3 arguments) and should return validation result as boolean. Validation function can return custom errors via `errors` property of itself. If validation result is false and errors are not returned, ajv will add default error with `keyword` property set to 'custom'. This aproach has the worst performance and is only recommending only for trying your custom keywords. +- _compile_: compiling function that will be called at schema compilation time with keyword schema and parent schema (it will be passed only if the function has 2 arguments) and should return validating function. +- _macro_ (NOT IMPLEMENTED): macro function that that will be called before schema compilation with keyword schema and parent schema (it will be passed only if the function has 2 arguments) and should return another schema that will be applied on the save data level in addition to the original schema (`allOf` keyword us used to do it). The returned schema may contain the same or other custom keywords that will be recusively expanded until the final schema has no custom keywords defined with macros. +- _inline_ (NOT IMPLEMENTED): compiling function that will be called at schema compilation time with keyword schema and parent schema (it will be passed only if the function has 2 arguments) and should return code (as string) that will be inlined in the currently compiled validation function. + +_validate_, _compile_, _macro_ and _inline_ are mutually exclusive, only one should be used at a time. + + ##### .errorsText([Array<Object> errors [, Object options]]) -> String Returns the text with all errors in a String. diff --git a/package.json b/package.json index c653bdc..d8027cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ajv", - "version": "1.4.10", + "version": "2.0.0-beta.2", "description": "Another JSON Schema Validator", "main": "lib/ajv.js", "files": [ From 18c1ef858c615ce6420436693dcca9b526563b43 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 14 Nov 2015 22:05:37 +0000 Subject: [PATCH 05/20] support for "macro" custom keywords, #69 --- README.md | 135 ++++++++------ lib/ajv.js | 33 +++- lib/compile/index.js | 5 + lib/compile/macro.js | 90 ++++++++++ lib/compile/resolve.js | 2 +- lib/compile/util.js | 17 ++ lib/dot/validate.jst | 4 + spec/custom.spec.js | 387 ++++++++++++++++++++++++++++++++++++----- 8 files changed, 571 insertions(+), 102 deletions(-) create mode 100644 lib/compile/macro.js diff --git a/README.md b/README.md index 8e3a25e..425d025 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ It uses [doT templates](https://github.com/olado/doT) to generate super-fast val [![Test Coverage](https://codeclimate.com/github/epoberezkin/ajv/badges/coverage.svg)](https://codeclimate.com/github/epoberezkin/ajv/coverage) - ## JSON Schema standard ajv implements full [JSON Schema draft 4](http://json-schema.org/) standard: @@ -126,32 +125,39 @@ You can find patterns used for format validation and the sources that were used ## Defining custom keywords -Starting from version 2.0 (2.0.0-beta.0) ajv supports custom keyword definitions. +Starting from version 2.0 (ajv@^2.0.0-beta) ajv supports custom keyword definitions. -Disclaimer: The main drawback of extending JSON-schema standard with custom keywords is the loss of portability of your schemas - it may not be possible to support these custom keywords on some other platforms. Also your schemas may be more challenging to read for other people. If portability is important you may prefer using additional validation logic outside of JSON-schema rather than putting it inside your JSON-schema. +WARNING: The main drawback of extending JSON-schema standard with custom keywords is the loss of portability of your schemas - it may not be possible to support these custom keywords on some other platforms. Also your schemas may be more challenging to read for other people. If portability is important you may prefer using additional validation logic outside of JSON-schema rather than putting it inside your JSON-schema. The advantages of using custom keywords are: - they allow you keeping a larger portion of your validation logic in the schema -- they may make schema more expressive and less verbose -- they are fun to use and help keeping more people off the street +- they make your schemas more expressive and less verbose +- they are fun to use -You can define custom keywords with `addKeyword` method. Keywords are defined on the `ajv` instance level - new instances will not have previously defined keywords automatically. +You can define custom keywords with [addKeyword](https://github.com/epoberezkin/ajv/tree/v2.0#api-addkeyword) method. Keywords are defined on the `ajv` instance level - new instances will not have previously defined keywords automatically. -Ajv allows defining keywords in several ways: -- with validation function that will be called with keyword schema, data and, optionally, parent schema during validation (generally not recommended for performance reason, but can be useful to test your custom keyword ideas) -- with compilation function that will be called only once during schema compilation with keyword schema and, optionally, parent schema and should return schema validation function (in some cases it's the best approach with the performance cost of an extra function call during validation). -- NOT IMPLEMENTED: with schema macro function that will be called once before schema compilation with keyword schema and parent schema and should return another schema that will be applied to the data in addition to the original schema (`allOf` keyword is used to achieve that). In many cases it is the most efficient approach as it is relatively easy to implement and no extra function call happens during validation. -- NOT IMPLEMETED: with compilation function that should return code that will be inlined in the currently compiled schema. It can be more performance-efficient approach than macro expansion for some cases but it is the most difficult to use as it requires good understanding of Ajv schema compilation process. +Ajv allows defining keywords with: +- validation function +- compilation function +- macro function +- NOT IMPLEMETED: compilation function that should return code (as string) that will be inlined in the currently compiled schema. -### Define keyword using validation function +### Define keyword using validation function (NOT RECOMMENDED) + +Validation function will be called during data validation. It will be passed schema, data and parentSchema (if it has 3 arguments) at validation time and it should return validation result as boolean. It can return an array of validation errors via `.errors` property of itself (otherwise a standard error will be used). + +This way to define keywords is added as a way to quickly test your keyword and is not recommended because of worse performance than compiling schemas. -Keyword can be defined with validation function that will be passed schema, data and parentSchema (if it has 3 arguments) at validation time and that should return validation result as boolean. Example. draft5 `constant` keyword (that is equivalent to `enum` keyword with one item): ``` -ajv.addKeyword('constant', { validate: validateConstant }); +ajv.addKeyword('constant', { validate: function (schema, data) { + return typeof schema == 'object && schema !== null' + ? deepEqual(schema, data) + : schema === data; +} }); var schema = { "constant": 2 }; var validate = ajv.compile(schema); @@ -162,25 +168,26 @@ var schema = { "constant": { "foo": "bar" } }; var validate = ajv.compile(schema); console.log(validate({foo: 'bar'})); // true console.log(validate({foo: 'baz'})); // false - -function validateConstant(schema, data) { - return typeof schema == 'object && schema !== null' - ? deepEqual(schema, data) - : schema === data; -} ``` -This approach exists as a way to quickly test your keyword and is not recommended because of worse performance than compiling schemas. - ### Define keyword using "compilation" function -Keyword can also be defined using schema compilation function that is passed schema (and optionally parentSchema) and that should return validation function that will only be passed data. +Compilation function will be called during schema compilation. It will be passed schema and parent schema and it should return a validation function. This validation function will be passed data during validation; it should return validation result as boolean and it can return an array of validation errors via `.errors` property of itself (otherwise a standard error will be used). + +In some cases it is the best approach to define keywords, but it has the performance cost of an extra function call during validation. If keyword logic can be expressed via some other JSON-schema then `macro` keyword definition is more efficient (see below). Example. `range` and `exclusiveRange` keywords using compiled schema: ``` -ajv.addKeyword('range', { type: 'number', compile: compileRange }); +ajv.addKeyword('range', { type: 'number', compile: function (sch, parentSchema) { + var min = sch[0]; + var max = sch[1]; + + return parentSchema.exclusiveRange === true + ? function (data) { return data > min && data < max; } + : function (data) { return data >= min && data <= max; } +} }); var schema = { "range": [2, 4], "exclusiveRange": true }; var validate = ajv.compile(schema); @@ -188,31 +195,53 @@ console.log(validate(2.01)); // true console.log(validate(3.99)); // true console.log(validate(2)); // false console.log(validate(4)); // false - -function compileRange(schema, parentSchema) { - validateRangeSchema(schema, parentSchema); - - var min = schema[0]; - var max = schema[1]; - - return parentSchema.exclusiveRange === true - ? function (data) { return data > min && data < max; } - : function (data) { return data >= min && data <= max; } -} - -function validateRangeSchema(schema, parentSchema) { - var schemaValid = Array.isArray(schema) && schema.length == 2 - && typeof schema[0] == 'number' - && typeof schema[1] == 'number'; - if (!schemaValid) throw new Error('Invalid schema for range keyword, should be array of 2 numbers'); - - var exclusiveRangeSchemaValid = parentSchema.exclusiveRange === undefined - || typeof parentSchema.exclusiveRange == 'boolean'; - if (!exclusiveRangeSchemaValid) throw new Error('Invalid schema for exclusiveRange keyword, should be bolean'); -} ``` +### Define keyword using "macro" function + +"Macro" function is called duting schema compilation. It is passed schema and parent schema and it should return another schema that will be applied to the data in addition to the original schema (if possible, schemas are merged, otherwise `allOf` keyword is used). + +It is the most efficient approach (in cases when the keyword logic can be expressed with another JSON-schema) because it is usually easy to implement and there is no extra function call during validation. + + +`range` and `exclusiveRange` keywords from the previous example defined with macro: + +``` +ajv.addKeyword('range', { macro: function (schema, parentSchema) { + return { + minimum: schema[0], + maximum: schema[1], + exclusiveMinimum: !!parentSchema.exclusiveRange, + exclusiveMaximum: !!parentSchema.exclusiveRange + }; +} }); +``` + +Example draft5 `contains` keyword that requires that the array has at least one item matching schema (see https://github.com/json-schema/json-schema/wiki/contains-(v5-proposal)): + +``` +ajv.addKeyword('contains', { macro: function (schema) { + return { "not": { "items": { "not": schema } } }; +} }); + +var schema = { + "contains": { + "type": "number", + "minimum": 4, + "exclusiveMinimum": true + } +}; + +var validate = ajv.compile(schema); +console.log(validate([1,2,3])); // false +console.log(validate([2,3,4])); // false +console.log(validate([3,4,5])); // true, number 5 matches schema inside "contains" +``` + +See the example of defining recursive macro keyword `deepProperties` in the [test](https://github.com/epoberezkin/ajv/blob/v2.0/spec/custom.spec.js#L240). + + ## Asynchronous compilation Starting from version 1.3 ajv supports asynchronous compilation when remote references are loaded using supplied function. See `compileAsync` method and `loadSchema` option. @@ -346,7 +375,7 @@ Function should return validation result as `true` or `false`. Custom formats can be also added via `formats` option. -##### .addKeyword(String keyword, Object definition) +##### .addKeyword(String keyword, Object definition) Add custom validation keyword to ajv instance. @@ -357,13 +386,17 @@ Keyword should be different from all standard JSON schema keywords and different Keyword definition is an object with the following properties: - _type_: optional string or array of strings with data type(s) that the keyword will apply to. If keyword is validating another type the validation function will not be called, so there is no need to check for data type inside validation function if `type` property is used. -- _validate_: validating function that will be called at validation time with keyword schema, data and parent schema (it will be passed only if the function has 3 arguments) and should return validation result as boolean. Validation function can return custom errors via `errors` property of itself. If validation result is false and errors are not returned, ajv will add default error with `keyword` property set to 'custom'. This aproach has the worst performance and is only recommending only for trying your custom keywords. -- _compile_: compiling function that will be called at schema compilation time with keyword schema and parent schema (it will be passed only if the function has 2 arguments) and should return validating function. -- _macro_ (NOT IMPLEMENTED): macro function that that will be called before schema compilation with keyword schema and parent schema (it will be passed only if the function has 2 arguments) and should return another schema that will be applied on the save data level in addition to the original schema (`allOf` keyword us used to do it). The returned schema may contain the same or other custom keywords that will be recusively expanded until the final schema has no custom keywords defined with macros. -- _inline_ (NOT IMPLEMENTED): compiling function that will be called at schema compilation time with keyword schema and parent schema (it will be passed only if the function has 2 arguments) and should return code (as string) that will be inlined in the currently compiled validation function. +- _validate_: validating function +- _compile_: compiling function +- _macro_: macro function +- _inline_ (NOT IMPLEMENTED): compiling function that returns code (as string) _validate_, _compile_, _macro_ and _inline_ are mutually exclusive, only one should be used at a time. +With _macro_ function _type_ must not be specified, the types that the keyword will be applied for will be determined by the final schema. + +See [Defining custom keywords](https://github.com/epoberezkin/ajv/tree/v2.0#defining-custom-keywords) for more details. + ##### .errorsText([Array<Object> errors [, Object options]]) -> String diff --git a/lib/ajv.js b/lib/ajv.js index eb75b96..122a42d 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -359,22 +359,27 @@ function Ajv(opts) { /** - * Add custom keyword - * @param {String} keyword custom keyword, should be different from all defined validation keywords, should be unique. + * Define custom keyword + * @param {String} keyword custom keyword, should be a valid identifier, should be different from all standard, custom and macro keywords. * @param {Object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`. */ function addKeyword(keyword, definition) { if (self.RULES.keywords[keyword]) throw new Error('Keyword ' + keyword + ' is already defined'); - var dataType = definition.type; - if (Array.isArray(dataType)) { - var i, len = dataType.length; - for (i=0; i 0) + schemaCopy = util.copy(schema); + + var success = true; + out: // try to merge schemas + for (i=0; i min && data < max; } : function (data) { return data >= min && data <= max; } } + }); - function testAddEvenKeyword(definition) { + describe('macro rules', function() { + it('should add and validate rule with "macro" keyword', function() { + testAddEvenKeyword({ macro: macroEven }); + }); + + it('should add and expand macro rule', function() { instances.forEach(function (ajv) { - ajv.addKeyword('even', definition); - var schema = { "even": true }; + try { + ajv.addKeyword('constant', { macro: macroConstant }); + + var schema = { "constant": "abc" }; var validate = ajv.compile(schema); - shouldBeValid(validate, 2); shouldBeValid(validate, 'abc'); - shouldBeInvalid(validate, 2.5); - shouldBeInvalid(validate, 3); + shouldBeInvalid(validate, 2); + shouldBeInvalid(validate, {}); + } catch(e) { + console.log(e.stack); + console.log(validate.toString()); + throw e; + } }); - } + }); - function testRangeKeyword(definition) { + it('should allow multiple schemas for the same macro keyword', function () { instances.forEach(function (ajv) { - ajv.addKeyword('range', definition); - - var schema = { "range": [2, 4] }; - var validate = ajv.compile(schema); - - shouldBeValid(validate, 2); - shouldBeValid(validate, 3); - shouldBeValid(validate, 4); - shouldBeValid(validate, 'abc'); - - shouldBeInvalid(validate, 1.99); - shouldBeInvalid(validate, 4.01); + ajv.addKeyword('constant', { macro: macroConstant }); var schema = { "properties": { - "foo": { - "range": [2, 4], - "exclusiveRange": true - } - } + "a": { "constant": 1 }, + "b": { "constant": 1 } + }, + "additionalProperties": { "constant": { "foo": "bar" } }, + "items": { "constant": { "foo": "bar" } } }; var validate = ajv.compile(schema); - shouldBeValid(validate, { foo: 2.01 }); - shouldBeValid(validate, { foo: 3 }); - shouldBeValid(validate, { foo: 3.99 }); + shouldBeValid(validate, {a:1, b:1}); + shouldBeInvalid(validate, {a:2, b:1}); - shouldBeInvalid(validate, { foo: 2 }); - shouldBeInvalid(validate, { foo: 4 }); + shouldBeValid(validate, {a:1, c: {foo: 'bar'}}); + shouldBeInvalid(validate, {a:1, c: {foo: 'baz'}}); + + shouldBeValid(validate, [{foo: 'bar'}]); + shouldBeValid(validate, [{foo: 'bar'}, {foo: 'bar'}]); + + shouldBeInvalid(validate, [1]); }); + }); + + it('should pass parent schema to "macro" keyword', function() { + testRangeKeyword({ macro: macroRange }); + }); + + it('should allow multiple parent schemas for the same macro keyword', function () { + instances.forEach(function (ajv) { + ajv.addKeyword('range', { macro: macroRange }); + + var schema = { + "properties": { + "a": { "range": [2, 4], "exclusiveRange": true }, + "b": { "range": [2, 4], "exclusiveRange": false } + }, + "additionalProperties": { "range": [5, 7] }, + "items": { "range": [5, 7] } + }; + var validate = ajv.compile(schema); + + shouldBeValid(validate, {a:3.99, b:4}); + shouldBeInvalid(validate, {a:4, b:4}); + + shouldBeValid(validate, {a:2.01, c: 7}); + shouldBeInvalid(validate, {a:2.01, c: 7.01}); + + shouldBeValid(validate, [5, 6, 7]); + shouldBeInvalid(validate, [7.01]); + }); + }); + + it('should recursively expand macro keywords', function() { + instances.forEach(function (ajv) { + ajv.addKeyword('deepProperties', { macro: macroDeepProperties }); + ajv.addKeyword('range', { macro: macroRange }); + + var schema = { + "deepProperties": { + "a.b.c": { "type": "number", "range": [2,4] }, + "d.e.f.g": { "type": "string" } + } + }; + + /* This schema recursively expands to: + { + "allOf": [ + { + "properties": { + "a": { + "properties": { + "b": { + "properties": { + "c": { + "type": "number", + "minimum": 2, + "exclusiveMinimum": false, + "maximum": 4, + "exclusiveMaximum": false + } + } + } + } + } + } + }, + { + "properties": { + "d": { + "properties": { + "e": { + "properties": { + "f": { + "properties": { + "g": { + "type": "string" + } + } + } + } + } + } + } + } + } + ] + } + */ + + var validate = ajv.compile(schema); + + shouldBeValid(validate, { + a: {b: {c: 3}}, + d: {e: {f: {g: 'foo'}}} + }); + + shouldBeInvalid(validate, { + a: {b: {c: 5}}, // out of range + d: {e: {f: {g: 'foo'}}} + }); + + shouldBeInvalid(validate, { + a: {b: {c: 'bar'}}, // not number + d: {e: {f: {g: 'foo'}}} + }); + + shouldBeInvalid(validate, { + a: {b: {c: 3}}, + d: {e: {f: {g: 2}}} // not string + }); + + function macroDeepProperties(schema) { + if (typeof schema != 'object') + throw new Error('schema of deepProperty should be an object'); + + var expanded = []; + + for (var prop in schema) { + var path = prop.split('.'); + var properties = {}; + if (path.length == 1) { + properties[prop] = schema[prop]; + } else { + var deepProperties = {}; + deepProperties[path.slice(1).join('.')] = schema[prop]; + properties[path[0]] = { "deepProperties": deepProperties }; + } + expanded.push({ "properties": properties }); + } + + return expanded.length == 1 ? expanded[0] : { "allOf": expanded }; + } + }); + }); + + it('should correctly expand multiple macros on the same level', function() { + instances.forEach(function (ajv) { + ajv.addKeyword('range', { macro: macroRange }); + ajv.addKeyword('even', { macro: macroEven }); + + var schema = { + "range": [4,6], + "even": true + }; + + var validate = ajv.compile(schema); + var numErrors = ajv.opts.allErrors ? 2 : 1; + + shouldBeInvalid(validate, 2); + shouldBeInvalid(validate, 3, numErrors); + shouldBeValid(validate, 4); + shouldBeInvalid(validate, 5); + shouldBeValid(validate, 6); + shouldBeInvalid(validate, 7, numErrors); + shouldBeInvalid(validate, 8); + }); + }); + + it('should use "allOf" keyword if macro schemas cannot be merged', function() { + instances.forEach(function (ajv) { + ajv.addKeyword('range', { macro: macroRange }); + + var schema = { + "range": [1,4], + "minimum": 2.5 + }; + + var validate = ajv.compile(schema); + validate.schema.allOf .should.be.an('array'); + validate.schema.allOf .should.have.length(2); + + shouldBeValid(validate, 3); + shouldBeInvalid(validate, 2); + }); + }); + + it('should correctly expand macros in subschemas', function() { + instances.forEach(function (ajv) { + ajv.addKeyword('range', { macro: macroRange }); + + var schema = { + "allOf": [ + { "range": [4,8] }, + { "range": [2,6] } + ] + } + + var validate = ajv.compile(schema); + + shouldBeInvalid(validate, 2); + shouldBeInvalid(validate, 3); + shouldBeValid(validate, 4); + shouldBeValid(validate, 5); + shouldBeValid(validate, 6); + shouldBeInvalid(validate, 7); + shouldBeInvalid(validate, 8); + }); + }); + + it('should correctly expand macros in macro expansions', function() { + instances.forEach(function (ajv) { + ajv.addKeyword('range', { macro: macroRange }); + ajv.addKeyword('contains', { macro: macroContains }); + + var schema = { + "contains": { + "type": "number", + "range": [4,7], + "exclusiveRange": true + } + }; + + var validate = ajv.compile(schema); + + shouldBeInvalid(validate, [1,2,3]); + shouldBeInvalid(validate, [2,3,4]); + shouldBeValid(validate, [3,4,5]); // only 5 is in range + shouldBeValid(validate, [6,7,8]); // only 6 is in range + shouldBeInvalid(validate, [7,8,9]); + shouldBeInvalid(validate, [8,9,10]); + + function macroContains(schema) { + return { "not": { "items": { "not": schema } } }; + } + }); + }); + + function macroEven(schema) { + if (schema === true) return { "multipleOf": 2 }; + if (schema === false) return { "not": { "multipleOf": 2 } }; + throw new Error('Schema for "even" keyword should be boolean'); } - function validateRangeSchema(schema, parentSchema) { - var schemaValid = Array.isArray(schema) && schema.length == 2 - && typeof schema[0] == 'number' - && typeof schema[1] == 'number'; - if (!schemaValid) throw new Error('Invalid schema for range keyword, should be array of 2 numbers'); + function macroConstant(schema, parentSchema) { + return { "enum": [schema] }; + } - var exclusiveRangeSchemaValid = parentSchema.exclusiveRange === undefined - || typeof parentSchema.exclusiveRange == 'boolean'; - if (!exclusiveRangeSchemaValid) throw new Error('Invalid schema for exclusiveRange keyword, should be bolean'); + function macroRange(schema, parentSchema) { + validateRangeSchema(schema, parentSchema); + var exclusive = !!parentSchema.exclusiveRange; + + return { + minimum: schema[0], + exclusiveMinimum: exclusive, + maximum: schema[1], + exclusiveMaximum: exclusive + }; } }); + function testAddEvenKeyword(definition) { + instances.forEach(function (ajv) { + ajv.addKeyword('even', definition); + var schema = { "even": true }; + var validate = ajv.compile(schema); + + shouldBeValid(validate, 2); + shouldBeValid(validate, 'abc'); + shouldBeInvalid(validate, 2.5); + shouldBeInvalid(validate, 3); + }); + } + + function testRangeKeyword(definition) { + instances.forEach(function (ajv) { + ajv.addKeyword('range', definition); + + var schema = { "range": [2, 4] }; + var validate = ajv.compile(schema); + + shouldBeValid(validate, 2); + shouldBeValid(validate, 3); + shouldBeValid(validate, 4); + shouldBeValid(validate, 'abc'); + + shouldBeInvalid(validate, 1.99); + shouldBeInvalid(validate, 4.01); + + var schema = { + "properties": { + "foo": { + "range": [2, 4], + "exclusiveRange": true + } + } + }; + var validate = ajv.compile(schema); + + shouldBeValid(validate, { foo: 2.01 }); + shouldBeValid(validate, { foo: 3 }); + shouldBeValid(validate, { foo: 3.99 }); + + shouldBeInvalid(validate, { foo: 2 }); + shouldBeInvalid(validate, { foo: 4 }); + }); + } + + function validateRangeSchema(schema, parentSchema) { + var schemaValid = Array.isArray(schema) && schema.length == 2 + && typeof schema[0] == 'number' + && typeof schema[1] == 'number'; + if (!schemaValid) throw new Error('Invalid schema for range keyword, should be array of 2 numbers'); + + var exclusiveRangeSchemaValid = parentSchema.exclusiveRange === undefined + || typeof parentSchema.exclusiveRange == 'boolean'; + if (!exclusiveRangeSchemaValid) throw new Error('Invalid schema for exclusiveRange keyword, should be bolean'); + } + function shouldBeValid(validate, data) { validate(data) .should.equal(true); should.not.exist(validate.errors); @@ -265,6 +561,15 @@ describe('Custom keywords', function () { }); }); + it('should throw if type is passed to macro keyword', function() { + should.throw(function() { + ajv.addKeyword(keyword, { + type: 'number', + macro: function() {} + }); + }); + }); + function addKeyword(keyword, dataType) { ajv.addKeyword(keyword, { type: dataType, From 87292f80ead600f5fdfce285c9739ac12237978b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 15 Nov 2015 17:38:50 +0000 Subject: [PATCH 06/20] validate schemas generated by macro keywords --- lib/ajv.js | 19 ++++++++++--------- lib/compile/index.js | 3 ++- lib/compile/macro.js | 5 +++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/ajv.js b/lib/ajv.js index 122a42d..66c18ed 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -199,10 +199,11 @@ function Ajv(opts) { /** * Validate schema - * @param {Object} schema schema to validate + * @param {Object} schema schema to validate + * @param {Boolean} throwOrLogError pass true to throw on error * @return {Boolean} */ - function validateSchema(schema) { + function validateSchema(schema, throwOrLogError) { var $schema = schema.$schema || META_SCHEMA_ID; var currentUriFormat = self._formats.uri; self._formats.uri = typeof currentUriFormat == 'function' @@ -210,6 +211,11 @@ function Ajv(opts) { : SCHEMA_URI_FORMAT; var valid = validate($schema, schema); self._formats.uri = currentUriFormat; + if (!valid && throwOrLogError) { + var message = 'schema is invalid:' + errorsText(); + if (self.opts.validateSchema == 'log') console.error(message); + else throw new Error(message); + } return valid; } @@ -268,13 +274,8 @@ function Ajv(opts) { var id = resolve.normalizeId(schema.id); if (id) checkUnique(id); - var ok = skipValidation || self.opts.validateSchema === false - || validateSchema(schema); - if (!ok) { - var message = 'schema is invalid:' + errorsText(); - if (self.opts.validateSchema == 'log') console.error(message); - else throw new Error(message); - } + if (self.opts.validateSchema !== false && !skipValidation) + validateSchema(schema, true); var localRefs = resolve.ids.call(self, schema); diff --git a/lib/compile/index.js b/lib/compile/index.js index 21449c3..37952dc 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -56,7 +56,8 @@ function compile(schema, root, localRefs, baseId) { useCustomRule: useCustomRule, expandMacros: macro.expand, opts: self.opts, - formats: formats + formats: formats, + self: self }); validateCode = refsCode(refVal) + patternsCode(patterns) diff --git a/lib/compile/macro.js b/lib/compile/macro.js index 4b77495..100eebb 100644 --- a/lib/compile/macro.js +++ b/lib/compile/macro.js @@ -25,9 +25,10 @@ function expandMacros() { } } - // TODO validate expSchemas - if (expSchemas) { + if (this.self.opts.validateSchema !== false) + this.self.validateSchema({ "allOf": expSchemas }, true); + var schemaCopy; if (Object.keys(schema).length > 0) schemaCopy = util.copy(schema); From ae5b4c0f45b37252b15dac53acaba48e6dedcbbd Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 16 Nov 2015 22:22:33 +0000 Subject: [PATCH 07/20] support for "inline" custom keywords, #69 --- lib/compile/index.js | 17 +++++++---- lib/compile/macro.js | 2 +- lib/dot/custom.def | 67 +++++++++++++++++++++++++++++++------------- spec/custom.spec.js | 65 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 124 insertions(+), 27 deletions(-) diff --git a/lib/compile/index.js b/lib/compile/index.js index 37952dc..d097ed9 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -149,27 +149,32 @@ function compile(schema, root, localRefs, baseId) { return 'pattern' + index; } - function useCustomRule(rule, schema, parentSchema) { - var compile = rule.definition.compile; + function useCustomRule(rule, schema, parentSchema, it) { + var compile = rule.definition.compile + , inline = rule.definition.inline; var key = rule.keyword; - if (compile) { + if (compile || inline) { key += '::' + stableStringify(schema); - if (compile.length > 1) key += '::' + stableStringify(parentSchema); + var usesParentSchema = (compile && compile.length > 1) + || (inline && inline.length > 2) + || rule.definition.parentSchema; + if (usesParentSchema) key += '::' + stableStringify(parentSchema); } var index = customRulesHash[key]; if (index === undefined) { var validate = compile ? compile.call(self, schema, parentSchema) - : rule.definition.validate; + : inline + ? inline.call(self, it, schema, parentSchema) + : rule.definition.validate; index = customRulesHash[key] = customRules.length; customRules[index] = validate; } return { code: 'customRule' + index, - compiled: !!compile, validate: customRules[index] }; } diff --git a/lib/compile/macro.js b/lib/compile/macro.js index 100eebb..d94abe9 100644 --- a/lib/compile/macro.js +++ b/lib/compile/macro.js @@ -34,7 +34,7 @@ function expandMacros() { schemaCopy = util.copy(schema); var success = true; - out: // try to merge schemas + out: // try to merge schemas without merging keywords for (i=0; i 2 }} - , validate.schema{{=it.schemaPath}} - {{?}} + {{? $rule.definition.inline }} + {{? $rule.definition.statements }} + valid{{=it.lvl}} {{??}} - , {{=$data}} + ({{= $ruleValidate.validate }}) {{?}} - ) + {{??}} + {{=$ruleValidate.code}}.call(self + {{? $rule.definition.compile }} + , {{=$data}} + {{??}} + , validate.schema{{=$schemaPath}} + , {{=$data}} + {{? $ruleValidate.validate.length > 2 }} + , validate.schema{{=it.schemaPath}} + {{?}} + {{?}} + ) + {{?}} #}} - if (!{{# def.callRuleValidate }}) { +{{## def.extendErrors: + {{ var $i = 'i' + $lvl; }} + for (var {{=$i}}=0; {{=$i}}<{{=$ruleErrs}}.length; {{=$i}}++) { + {{ var $ruleErr = 'ruleErr' + $lvl; }} + var {{=$ruleErr}} = {{=$ruleErrs}}[{{=$i}}]; + {{=$ruleErr}}.dataPath = (dataPath || '') + {{= it.errorPath }}; + {{? it.opts.verbose || it.opts.i18n }} + {{=$ruleErr}}.schema = validate.schema{{=$schemaPath}}; + {{?}} + {{? it.opts.verbose }} + {{=$ruleErr}}.data = {{=$data}}; + {{?}} + } +#}} + +{{? $rule.definition.inline && $rule.definition.statements }} + {{= $ruleValidate.validate }} +{{?}} + +if (!{{# def.callRuleValidate }}) { + {{? $rule.definition.inline }} + {{# def.error:'custom' }} + {{??}} if (Array.isArray({{=$ruleErrs}})) { + {{# def.extendErrors }} if (vErrors === null) vErrors = {{=$ruleErrs}}; else vErrors.concat({{=$ruleErrs}}); errors = vErrors.length; } else { {{# def.error:'custom' }} } -{{? $breakOnError }} - return false; - } else { -{{??}} - } -{{?}} + {{?}} +} {{? $breakOnError }} else { {{?}} diff --git a/spec/custom.spec.js b/spec/custom.spec.js index dfbfe0e..30b3906 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -2,7 +2,8 @@ var getAjvInstances = require('./ajv_instances') , should = require('chai').should() - , equal = require('../lib/compile/equal'); + , equal = require('../lib/compile/equal') + , doT = require('dot'); describe('Custom keywords', function () { @@ -18,6 +19,7 @@ describe('Custom keywords', function () { ajv = instances[0]; }); + describe('custom rules', function() { var compileCount = 0; @@ -152,6 +154,7 @@ describe('Custom keywords', function () { } }); + describe('macro rules', function() { it('should add and validate rule with "macro" keyword', function() { testAddEvenKeyword({ macro: macroEven }); @@ -427,6 +430,19 @@ describe('Custom keywords', function () { }); }); + it('should throw exception is macro expansion is an invalid schema', function() { + ajv.addKeyword('invalid', { macro: macroInvalid }); + var schema = { "invalid": true }; + + should.throw(function() { + var validate = ajv.compile(schema); + }); + + function macroInvalid(schema) { + return { "type": "invalid" }; + } + }); + function macroEven(schema) { if (schema === true) return { "multipleOf": 2 }; if (schema === false) return { "not": { "multipleOf": 2 } }; @@ -450,6 +466,52 @@ describe('Custom keywords', function () { } }); + + describe('inline rules', function() { + it('should add and validate rule with "inline" code keyword', function() { + testAddEvenKeyword({ type: 'number', inline: inlineEven }); + }); + + it('should pass parent schema to "inline" keyword', function() { + testRangeKeyword({ type: 'number', inline: inlineRange, statements: true }); + }); + + it('should define "inline" keyword as template', function() { + var inlineRangeTemplate = doT.compile("\ +{{ \ + var $data = 'data' + (it.dataLevel || '') \ + , $min = it.schema.range[0] \ + , $max = it.schema.range[1] \ + , $gt = it.schema.exclusiveRange ? '>' : '>=' \ + , $lt = it.schema.exclusiveRange ? '<' : '<='; \ +}} \ +var valid{{=it.lvl}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$max}}; \ +"); + + testRangeKeyword({ + type: 'number', + inline: inlineRangeTemplate, + parentSchema: true, + statements: true + }); + }); + + function inlineEven(it, schema) { + var op = schema ? '===' : '!=='; + return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0'; + } + + function inlineRange(it, schema, parentSchema) { + var min = schema[0] + , max = schema[1] + , data = 'data' + (it.dataLevel || '') + , gt = parentSchema.exclusiveRange ? ' > ' : ' >= ' + , lt = parentSchema.exclusiveRange ? ' < ' : ' <= '; + return 'var valid' + it.lvl + ' = ' + data + gt + min + ' && ' + data + lt + max + ';'; + } + }); + + function testAddEvenKeyword(definition) { instances.forEach(function (ajv) { ajv.addKeyword('even', definition); @@ -518,6 +580,7 @@ describe('Custom keywords', function () { validate.errors .should.have.length(numErrors || 1); } + describe('addKeyword method', function() { var TEST_TYPES = [ undefined, 'number', 'string', 'boolean', ['number', 'string']]; From ee450ec8d0108990df16d5bdbd4a862f7f1dd39f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 16 Nov 2015 23:12:54 +0000 Subject: [PATCH 08/20] removed caching of compiled subschemas of custom keywords (it made performance worse in most cases) --- lib/compile/index.js | 29 ++++++++++------------------- spec/custom.spec.js | 10 ---------- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/lib/compile/index.js b/lib/compile/index.js index d097ed9..6a75a15 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -153,29 +153,20 @@ function compile(schema, root, localRefs, baseId) { var compile = rule.definition.compile , inline = rule.definition.inline; - var key = rule.keyword; - if (compile || inline) { - key += '::' + stableStringify(schema); - var usesParentSchema = (compile && compile.length > 1) - || (inline && inline.length > 2) - || rule.definition.parentSchema; - if (usesParentSchema) key += '::' + stableStringify(parentSchema); - } + var validate; + if (compile) + validate = compile.call(self, schema, parentSchema); + else if (inline) + validate = inline.call(self, it, schema, parentSchema); + else + validate = rule.definition.validate; - var index = customRulesHash[key]; - if (index === undefined) { - var validate = compile - ? compile.call(self, schema, parentSchema) - : inline - ? inline.call(self, it, schema, parentSchema) - : rule.definition.validate; - index = customRulesHash[key] = customRules.length; - customRules[index] = validate; - } + var index = customRules.length; + customRules[index] = validate; return { code: 'customRule' + index, - validate: customRules[index] + validate: validate }; } } diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 30b3906..c41889c 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -21,8 +21,6 @@ describe('Custom keywords', function () { describe('custom rules', function() { - var compileCount = 0; - it('should add and validate rule with "interpreted" keyword validation', function() { testAddEvenKeyword({ type: 'number', validate: validateEven }); @@ -49,9 +47,7 @@ describe('Custom keywords', function () { ajv.addKeyword('constant', { compile: compileConstant }); var schema = { "constant": "abc" }; - compileCount = 0; var validate = ajv.compile(schema); - should.equal(compileCount, 1); shouldBeValid(validate, 'abc'); shouldBeInvalid(validate, 2); @@ -71,9 +67,7 @@ describe('Custom keywords', function () { "additionalProperties": { "constant": { "foo": "bar" } }, "items": { "constant": { "foo": "bar" } } }; - compileCount = 0; var validate = ajv.compile(schema); - should.equal(compileCount, 2); shouldBeValid(validate, {a:1, b:1}); shouldBeInvalid(validate, {a:2, b:1}); @@ -116,9 +110,7 @@ describe('Custom keywords', function () { "additionalProperties": { "range": [5, 7] }, "items": { "range": [5, 7] } }; - compileCount = 0; var validate = ajv.compile(schema); - should.equal(compileCount, 3); shouldBeValid(validate, {a:3.99, b:4}); shouldBeInvalid(validate, {a:4, b:4}); @@ -132,7 +124,6 @@ describe('Custom keywords', function () { }); function compileConstant(schema) { - compileCount++; return typeof schema == 'object' && schema !== null ? isDeepEqual : isStrictEqual; @@ -142,7 +133,6 @@ describe('Custom keywords', function () { } function compileRange(schema, parentSchema) { - compileCount++; validateRangeSchema(schema, parentSchema); var min = schema[0]; From 41f313a79804fc28cb04f79ff8d83a3a2725b34b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 16 Nov 2015 23:39:27 +0000 Subject: [PATCH 09/20] refactor custom keywords tests --- spec/custom.spec.js | 199 +++++++++++++++++--------------------------- 1 file changed, 76 insertions(+), 123 deletions(-) diff --git a/spec/custom.spec.js b/spec/custom.spec.js index c41889c..fa4fc0d 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -22,7 +22,7 @@ describe('Custom keywords', function () { describe('custom rules', function() { it('should add and validate rule with "interpreted" keyword validation', function() { - testAddEvenKeyword({ type: 'number', validate: validateEven }); + testEvenKeyword({ type: 'number', validate: validateEven }); function validateEven(schema, data) { if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean'); @@ -31,7 +31,7 @@ describe('Custom keywords', function () { }); it('should add and validate rule with "compiled" keyword validation', function() { - testAddEvenKeyword({ type: 'number', compile: compileEven }); + testEvenKeyword({ type: 'number', compile: compileEven }); function compileEven(schema) { if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean'); @@ -43,43 +43,11 @@ describe('Custom keywords', function () { }); it('should compile keyword validating function only once per schema', function () { - instances.forEach(function (ajv) { - ajv.addKeyword('constant', { compile: compileConstant }); - - var schema = { "constant": "abc" }; - var validate = ajv.compile(schema); - - shouldBeValid(validate, 'abc'); - shouldBeInvalid(validate, 2); - shouldBeInvalid(validate, {}); - }); + testConstantKeyword({ compile: compileConstant }); }); it('should allow multiple schemas for the same keyword', function () { - instances.forEach(function (ajv) { - ajv.addKeyword('constant', { compile: compileConstant }); - - var schema = { - "properties": { - "a": { "constant": 1 }, - "b": { "constant": 1 } - }, - "additionalProperties": { "constant": { "foo": "bar" } }, - "items": { "constant": { "foo": "bar" } } - }; - var validate = ajv.compile(schema); - - shouldBeValid(validate, {a:1, b:1}); - shouldBeInvalid(validate, {a:2, b:1}); - - shouldBeValid(validate, {a:1, c: {foo: 'bar'}}); - shouldBeInvalid(validate, {a:1, c: {foo: 'baz'}}); - - shouldBeValid(validate, [{foo: 'bar'}]); - shouldBeValid(validate, [{foo: 'bar'}, {foo: 'bar'}]); - - shouldBeInvalid(validate, [1]); - }); + testMultipleConstantKeyword({ compile: compileConstant }); }); it('should pass parent schema to "interpreted" keyword validation', function() { @@ -99,28 +67,7 @@ describe('Custom keywords', function () { }); it('should allow multiple parent schemas for the same keyword', function () { - instances.forEach(function (ajv) { - ajv.addKeyword('range', { type: 'number', compile: compileRange }); - - var schema = { - "properties": { - "a": { "range": [2, 4], "exclusiveRange": true }, - "b": { "range": [2, 4], "exclusiveRange": false } - }, - "additionalProperties": { "range": [5, 7] }, - "items": { "range": [5, 7] } - }; - var validate = ajv.compile(schema); - - shouldBeValid(validate, {a:3.99, b:4}); - shouldBeInvalid(validate, {a:4, b:4}); - - shouldBeValid(validate, {a:2.01, c: 7}); - shouldBeInvalid(validate, {a:2.01, c: 7.01}); - - shouldBeValid(validate, [5, 6, 7]); - shouldBeInvalid(validate, [7.01]); - }); + testMultipleRangeKeyword({ type: 'number', compile: compileRange }); }); function compileConstant(schema) { @@ -147,53 +94,15 @@ describe('Custom keywords', function () { describe('macro rules', function() { it('should add and validate rule with "macro" keyword', function() { - testAddEvenKeyword({ macro: macroEven }); + testEvenKeyword({ macro: macroEven }); }); it('should add and expand macro rule', function() { - instances.forEach(function (ajv) { - try { - ajv.addKeyword('constant', { macro: macroConstant }); - - var schema = { "constant": "abc" }; - var validate = ajv.compile(schema); - - shouldBeValid(validate, 'abc'); - shouldBeInvalid(validate, 2); - shouldBeInvalid(validate, {}); - } catch(e) { - console.log(e.stack); - console.log(validate.toString()); - throw e; - } - }); + testConstantKeyword({ macro: macroConstant }); }); it('should allow multiple schemas for the same macro keyword', function () { - instances.forEach(function (ajv) { - ajv.addKeyword('constant', { macro: macroConstant }); - - var schema = { - "properties": { - "a": { "constant": 1 }, - "b": { "constant": 1 } - }, - "additionalProperties": { "constant": { "foo": "bar" } }, - "items": { "constant": { "foo": "bar" } } - }; - var validate = ajv.compile(schema); - - shouldBeValid(validate, {a:1, b:1}); - shouldBeInvalid(validate, {a:2, b:1}); - - shouldBeValid(validate, {a:1, c: {foo: 'bar'}}); - shouldBeInvalid(validate, {a:1, c: {foo: 'baz'}}); - - shouldBeValid(validate, [{foo: 'bar'}]); - shouldBeValid(validate, [{foo: 'bar'}, {foo: 'bar'}]); - - shouldBeInvalid(validate, [1]); - }); + testMultipleConstantKeyword({ macro: macroConstant }); }); it('should pass parent schema to "macro" keyword', function() { @@ -201,28 +110,7 @@ describe('Custom keywords', function () { }); it('should allow multiple parent schemas for the same macro keyword', function () { - instances.forEach(function (ajv) { - ajv.addKeyword('range', { macro: macroRange }); - - var schema = { - "properties": { - "a": { "range": [2, 4], "exclusiveRange": true }, - "b": { "range": [2, 4], "exclusiveRange": false } - }, - "additionalProperties": { "range": [5, 7] }, - "items": { "range": [5, 7] } - }; - var validate = ajv.compile(schema); - - shouldBeValid(validate, {a:3.99, b:4}); - shouldBeInvalid(validate, {a:4, b:4}); - - shouldBeValid(validate, {a:2.01, c: 7}); - shouldBeInvalid(validate, {a:2.01, c: 7.01}); - - shouldBeValid(validate, [5, 6, 7]); - shouldBeInvalid(validate, [7.01]); - }); + testMultipleRangeKeyword({ macro: macroRange }); }); it('should recursively expand macro keywords', function() { @@ -459,7 +347,7 @@ describe('Custom keywords', function () { describe('inline rules', function() { it('should add and validate rule with "inline" code keyword', function() { - testAddEvenKeyword({ type: 'number', inline: inlineEven }); + testEvenKeyword({ type: 'number', inline: inlineEven }); }); it('should pass parent schema to "inline" keyword', function() { @@ -502,7 +390,7 @@ var valid{{=it.lvl}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$ }); - function testAddEvenKeyword(definition) { + function testEvenKeyword(definition) { instances.forEach(function (ajv) { ajv.addKeyword('even', definition); var schema = { "even": true }; @@ -515,6 +403,46 @@ var valid{{=it.lvl}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$ }); } + function testConstantKeyword(definition) { + instances.forEach(function (ajv) { + ajv.addKeyword('constant', definition); + + var schema = { "constant": "abc" }; + var validate = ajv.compile(schema); + + shouldBeValid(validate, 'abc'); + shouldBeInvalid(validate, 2); + shouldBeInvalid(validate, {}); + }); + } + + function testMultipleConstantKeyword(definition) { + instances.forEach(function (ajv) { + ajv.addKeyword('constant', definition); + + var schema = { + "properties": { + "a": { "constant": 1 }, + "b": { "constant": 1 } + }, + "additionalProperties": { "constant": { "foo": "bar" } }, + "items": { "constant": { "foo": "bar" } } + }; + var validate = ajv.compile(schema); + + shouldBeValid(validate, {a:1, b:1}); + shouldBeInvalid(validate, {a:2, b:1}); + + shouldBeValid(validate, {a:1, c: {foo: 'bar'}}); + shouldBeInvalid(validate, {a:1, c: {foo: 'baz'}}); + + shouldBeValid(validate, [{foo: 'bar'}]); + shouldBeValid(validate, [{foo: 'bar'}, {foo: 'bar'}]); + + shouldBeInvalid(validate, [1]); + }); + } + function testRangeKeyword(definition) { instances.forEach(function (ajv) { ajv.addKeyword('range', definition); @@ -549,6 +477,31 @@ var valid{{=it.lvl}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$ }); } + function testMultipleRangeKeyword(definition) { + instances.forEach(function (ajv) { + ajv.addKeyword('range', definition); + + var schema = { + "properties": { + "a": { "range": [2, 4], "exclusiveRange": true }, + "b": { "range": [2, 4], "exclusiveRange": false } + }, + "additionalProperties": { "range": [5, 7] }, + "items": { "range": [5, 7] } + }; + var validate = ajv.compile(schema); + + shouldBeValid(validate, {a:3.99, b:4}); + shouldBeInvalid(validate, {a:4, b:4}); + + shouldBeValid(validate, {a:2.01, c: 7}); + shouldBeInvalid(validate, {a:2.01, c: 7.01}); + + shouldBeValid(validate, [5, 6, 7]); + shouldBeInvalid(validate, [7.01]); + }); + } + function validateRangeSchema(schema, parentSchema) { var schemaValid = Array.isArray(schema) && schema.length == 2 && typeof schema[0] == 'number' From 6a2961bcf36889f41212837d52c780d149345ae2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 17 Nov 2015 22:23:21 +0000 Subject: [PATCH 10/20] updated error parameters so that ajv-i18n does not depend on schema, compatible with ajv-i18n >= 1.0.0 --- lib/dot/custom.def | 4 +--- lib/dot/definitions.def | 28 +++++++++++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/dot/custom.def b/lib/dot/custom.def index c6cd52e..f320276 100644 --- a/lib/dot/custom.def +++ b/lib/dot/custom.def @@ -37,10 +37,8 @@ {{ var $ruleErr = 'ruleErr' + $lvl; }} var {{=$ruleErr}} = {{=$ruleErrs}}[{{=$i}}]; {{=$ruleErr}}.dataPath = (dataPath || '') + {{= it.errorPath }}; - {{? it.opts.verbose || it.opts.i18n }} - {{=$ruleErr}}.schema = validate.schema{{=$schemaPath}}; - {{?}} {{? it.opts.verbose }} + {{=$ruleErr}}.schema = validate.schema{{=$schemaPath}}; {{=$ruleErr}}.data = {{=$data}}; {{?}} } diff --git a/lib/dot/definitions.def b/lib/dot/definitions.def index e611279..ac48e6a 100644 --- a/lib/dot/definitions.def +++ b/lib/dot/definitions.def @@ -107,10 +107,8 @@ {{? it.opts.messages !== false }} , message: {{# def._errorMessages[_rule] }} {{?}} - {{? it.opts.verbose || it.opts.i18n }} - , schema: {{# def._errorSchemas[_rule] }} - {{?}} {{? it.opts.verbose }} + , schema: {{# def._errorSchemas[_rule] }} , data: {{=$data}} {{?}} {{# def._errorParams[_rule] || '' }} @@ -200,12 +198,24 @@ {{## def._params = "{{? it.opts.i18n }}, params: " #}} {{## def._errorParams = { - $ref: "{{# def._params }}{ escaped: '{{=it.util.escapeQuotes($schema)}}' }{{?}}", - dependencies: "{{# def._params }}{ n: {{=$deps.length}}, deps: '{{? $deps.length==1 }}{{= it.util.escapeQuotes($deps[0]) }}{{??}}{{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}}', property: '{{= it.util.escapeQuotes($property) }}' }{{?}}", - format: "{{# def._params }}{ escaped: '{{=it.util.escapeQuotes($schema)}}' }{{?}}", - maximum: "{{# def._params }}{ condition: '{{=$op}} {{=$schema}}' }{{?}}", - minimum: "{{# def._params }}{ condition: '{{=$op}} {{=$schema}}' }{{?}}", - pattern: "{{# def._params }}{ escaped: '{{=it.util.escapeQuotes($schema)}}' }{{?}}", + $ref: "{{# def._params }}{ ref: '{{=it.util.escapeQuotes($schema)}}' }{{?}}", + additionalItems: "{{# def._params }}{ limit: {{=$schema.length}}, length: {{=$data}}.length }{{?}}", + additionalProperties: "", + anyOf: "", + dependencies: "{{# def._params }}{ depsCount: {{=$deps.length}}, deps: '{{? $deps.length==1 }}{{= it.util.escapeQuotes($deps[0]) }}{{??}}{{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}}', property: '{{= it.util.escapeQuotes($property) }}' }{{?}}", + format: "{{# def._params }}{ format: '{{=it.util.escapeQuotes($schema)}}' }{{?}}", + maximum: "{{# def._params }}{ comparison: '{{=$op}}', limit: {{=$schema}}, exclusive: {{=$exclusive}} }{{?}}", + minimum: "{{# def._params }}{ comparison: '{{=$op}}', limit: {{=$schema}}, exclusive: {{=$exclusive}} }{{?}}", + maxItems: "{{# def._params }}{ limit: {{=$schema}} }{{?}}", + minItems: "{{# def._params }}{ limit: {{=$schema}} }{{?}}", + maxLength: "{{# def._params }}{ limit: {{=$schema}} }{{?}}", + minLength: "{{# def._params }}{ limit: {{=$schema}} }{{?}}", + maxProperties:"{{# def._params }}{ limit: {{=$schema}} }{{?}}", + minProperties:"{{# def._params }}{ limit: {{=$schema}} }{{?}}", + multipleOf: "{{# def._params }}{ multipleOf: {{=$schema}} }{{?}}", + not: "", + oneOf: "", + pattern: "{{# def._params }}{ pattern: '{{=it.util.escapeQuotes($schema)}}' }{{?}}", required: "{{# def._params }}{ missingProperty: '{{=$missingProperty}}' }{{?}}", type: "{{# def._params }}{ type: '{{? $isArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}' }{{?}}", uniqueItems: "{{# def._params }}{ i: i, j: j }{{?}}", From a3688f73c1f0cc5d1f1e0fd32b1d0c3cc447286a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 17 Nov 2015 23:22:15 +0000 Subject: [PATCH 11/20] ajv-i18n peerDependency --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d8027cc..314585f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ajv", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "description": "Another JSON Schema Validator", "main": "lib/ajv.js", "files": [ @@ -59,6 +59,6 @@ "watch": "^0.16.0" }, "peerDependencies": { - "ajv-i18n": "0.1.x" + "ajv-i18n": "^1.0.0-beta.0" } } From 982264f19294a8d7cf035dc3675e3c61ea1929b3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 17 Nov 2015 23:45:19 +0000 Subject: [PATCH 12/20] removed i18n option (params always added to error objects) --- README.md | 4 ++-- lib/dot/definitions.def | 52 +++++++++++++++++++--------------------- spec/custom.spec.js | 3 +-- spec/json-schema.spec.js | 3 +-- spec/options.spec.js | 4 ++-- spec/resolve.spec.js | 3 +-- 6 files changed, 32 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 425d025..4ccc05b 100644 --- a/README.md +++ b/README.md @@ -427,7 +427,7 @@ Defaults: beautify: false, cache: new Cache, jsonPointers: false, - i18n: false, + i18n: false, // deprecated messages: true } ``` @@ -448,7 +448,7 @@ Defaults: - _beautify_: format the generated function with [js-beautify](https://github.com/beautify-web/js-beautify) (the validating function is generated without line-breaks). `npm install js-beautify` to use this option. `true` or js-beautify options can be passed. - _cache_: an optional instance of cache to store compiled schemas using stable-stringified schema as a key. For example, set-associative cache [sacjs](https://github.com/epoberezkin/sacjs) can be used. If not passed then a simple hash is used which is good enough for the common use case (a limited number of statically defined schemas). Cache should have methods `put(key, value)`, `get(key)` and `del(key)`. - _jsonPointers_: Output `dataPath` using JSON Pointers instead of JS path notation. -- _i18n_: Support internationalization of error messages using [ajv-i18n](https://github.com/epoberezkin/ajv-i18n). See its repo for details. +- _i18n_: DEPRECATED. Support internationalization of error messages using [ajv-i18n](https://github.com/epoberezkin/ajv-i18n). See its repo for details. - _messages_: Include human-readable messages in errors. `true` by default. `messages: false` can be added when internationalization (options `i18n`) is used. diff --git a/lib/dot/definitions.def b/lib/dot/definitions.def index ac48e6a..a915d2a 100644 --- a/lib/dot/definitions.def +++ b/lib/dot/definitions.def @@ -102,8 +102,9 @@ {{## def._error:_rule: { - keyword: '{{=_rule}}', - dataPath: (dataPath || '') + {{= it.errorPath }} + keyword: '{{=_rule}}' + , dataPath: (dataPath || '') + {{= it.errorPath }} + , params: {{# def._errorParams[_rule] }} {{? it.opts.messages !== false }} , message: {{# def._errorMessages[_rule] }} {{?}} @@ -111,7 +112,6 @@ , schema: {{# def._errorSchemas[_rule] }} , data: {{=$data}} {{?}} - {{# def._errorParams[_rule] || '' }} } #}} @@ -195,29 +195,27 @@ } #}} -{{## def._params = "{{? it.opts.i18n }}, params: " #}} - {{## def._errorParams = { - $ref: "{{# def._params }}{ ref: '{{=it.util.escapeQuotes($schema)}}' }{{?}}", - additionalItems: "{{# def._params }}{ limit: {{=$schema.length}}, length: {{=$data}}.length }{{?}}", - additionalProperties: "", - anyOf: "", - dependencies: "{{# def._params }}{ depsCount: {{=$deps.length}}, deps: '{{? $deps.length==1 }}{{= it.util.escapeQuotes($deps[0]) }}{{??}}{{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}}', property: '{{= it.util.escapeQuotes($property) }}' }{{?}}", - format: "{{# def._params }}{ format: '{{=it.util.escapeQuotes($schema)}}' }{{?}}", - maximum: "{{# def._params }}{ comparison: '{{=$op}}', limit: {{=$schema}}, exclusive: {{=$exclusive}} }{{?}}", - minimum: "{{# def._params }}{ comparison: '{{=$op}}', limit: {{=$schema}}, exclusive: {{=$exclusive}} }{{?}}", - maxItems: "{{# def._params }}{ limit: {{=$schema}} }{{?}}", - minItems: "{{# def._params }}{ limit: {{=$schema}} }{{?}}", - maxLength: "{{# def._params }}{ limit: {{=$schema}} }{{?}}", - minLength: "{{# def._params }}{ limit: {{=$schema}} }{{?}}", - maxProperties:"{{# def._params }}{ limit: {{=$schema}} }{{?}}", - minProperties:"{{# def._params }}{ limit: {{=$schema}} }{{?}}", - multipleOf: "{{# def._params }}{ multipleOf: {{=$schema}} }{{?}}", - not: "", - oneOf: "", - pattern: "{{# def._params }}{ pattern: '{{=it.util.escapeQuotes($schema)}}' }{{?}}", - required: "{{# def._params }}{ missingProperty: '{{=$missingProperty}}' }{{?}}", - type: "{{# def._params }}{ type: '{{? $isArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}' }{{?}}", - uniqueItems: "{{# def._params }}{ i: i, j: j }{{?}}", - custom: "{{# def._params }}{ keyword: '$rule.keyword' }{{?}}" + $ref: "{ ref: '{{=it.util.escapeQuotes($schema)}}' }", + additionalItems: "{ limit: {{=$schema.length}} }", + additionalProperties: "{}", + anyOf: "{}", + dependencies: "{ depsCount: {{=$deps.length}}, deps: '{{? $deps.length==1 }}{{= it.util.escapeQuotes($deps[0]) }}{{??}}{{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}}', property: '{{= it.util.escapeQuotes($property) }}' }", + format: "{ format: '{{=it.util.escapeQuotes($schema)}}' }", + maximum: "{ comparison: '{{=$op}}', limit: {{=$schema}}, exclusive: {{=$exclusive}} }", + minimum: "{ comparison: '{{=$op}}', limit: {{=$schema}}, exclusive: {{=$exclusive}} }", + maxItems: "{ limit: {{=$schema}} }", + minItems: "{ limit: {{=$schema}} }", + maxLength: "{ limit: {{=$schema}} }", + minLength: "{ limit: {{=$schema}} }", + maxProperties:"{ limit: {{=$schema}} }", + minProperties:"{ limit: {{=$schema}} }", + multipleOf: "{ multipleOf: {{=$schema}} }", + not: "{}", + oneOf: "{}", + pattern: "{ pattern: '{{=it.util.escapeQuotes($schema)}}' }", + required: "{ missingProperty: '{{=$missingProperty}}' }", + type: "{ type: '{{? $isArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}' }", + uniqueItems: "{ i: i, j: j }", + custom: "{ keyword: '$rule.keyword' }" } #}} diff --git a/spec/custom.spec.js b/spec/custom.spec.js index fa4fc0d..b2c9e8a 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -13,8 +13,7 @@ describe('Custom keywords', function () { instances = getAjvInstances({ allErrors: true, verbose: true, - inlineRefs: false, - i18n: true + inlineRefs: false }); ajv = instances[0]; }); diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index b8ea111..53d6c27 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -12,8 +12,7 @@ var instances = getAjvInstances(fullTest ? { verbose: true, format: 'full', inlineRefs: false, - jsonPointers: true, - i18n: true + jsonPointers: true } : { allErrors: true }); var remoteRefs = { diff --git a/spec/options.spec.js b/spec/options.spec.js index 1588a23..aa08102 100644 --- a/spec/options.spec.js +++ b/spec/options.spec.js @@ -193,9 +193,9 @@ describe('Ajv Options', function () { it('should not throw and fail validation with missingRef == "fail" if the ref is used', function() { testMissingRefsFail(Ajv({ missingRefs: 'fail' })); - testMissingRefsFail(Ajv({ missingRefs: 'fail', verbose: true, i18n: true })); + testMissingRefsFail(Ajv({ missingRefs: 'fail', verbose: true })); testMissingRefsFail(Ajv({ missingRefs: 'fail', allErrors: true })); - testMissingRefsFail(Ajv({ missingRefs: 'fail', allErrors: true, verbose: true, i18n: true })); + testMissingRefsFail(Ajv({ missingRefs: 'fail', allErrors: true, verbose: true })); function testMissingRefsFail(ajv) { var validate = ajv.compile({ diff --git a/spec/resolve.spec.js b/spec/resolve.spec.js index c6fb1dd..6247dfb 100644 --- a/spec/resolve.spec.js +++ b/spec/resolve.spec.js @@ -39,8 +39,7 @@ describe('resolve', function () { instances = getAjvInstances({ allErrors: true, verbose: true, - inlineRefs: false, - i18n: true + inlineRefs: false }); }); From 771e79f36b947d9881ab9cb7515b2e44c48e0a8b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 18 Nov 2015 21:27:13 +0000 Subject: [PATCH 13/20] readme: validation errors, inline custom keywords --- README.md | 138 +++++++++++++++++++++++++++++++++++++------- spec/custom.spec.js | 1 - 2 files changed, 118 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 4ccc05b..643a39e 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,20 @@ It uses [doT templates](https://github.com/olado/doT) to generate super-fast val [![Test Coverage](https://codeclimate.com/github/epoberezkin/ajv/badges/coverage.svg)](https://codeclimate.com/github/epoberezkin/ajv/coverage) -## JSON Schema standard +## Features -ajv implements full [JSON Schema draft 4](http://json-schema.org/) standard: - -- all validation keywords (see [JSON-Schema validation keywords](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md)) -- full support of remote refs (remote schemas have to be added with `addSchema` or compiled to be available) -- [asynchronous loading](#asynchronous-compilation) of referenced schemas during compilation. -- support of circular dependencies between schemas -- correct string lengths for strings with unicode pairs (can be turned off) -- formats defined by JSON Schema draft 4 standard and custom formats (can be turned off) +- ajv implements full [JSON Schema draft 4](http://json-schema.org/) standard: + - all validation keywords (see [JSON-Schema validation keywords](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md)) + - full support of remote refs (remote schemas have to be added with `addSchema` or compiled to be available) + - support of circular dependencies between schemas + - correct string lengths for strings with unicode pairs (can be turned off) + - [formats](#formats) defined by JSON Schema draft 4 standard and custom formats (can be turned off) + - [validates schemas against meta-schema](#api-validateschema) +- supports [browsers](#using-in-browser) and nodejs 0.10-5.0 +- [asynchronous loading](#asynchronous-compilation) of referenced schemas during compilation +- [error messages with parameters](#validation-errors) describing error reasons to allow creating custom error messages +- i18n error messages support with [ajv-i18n](https://github.com/epoberezkin/ajv-i18n) package +- [filtering data](#filtering-data) from additional properties - BETA: [custom keywords](https://github.com/epoberezkin/ajv/tree/v2.0#defining-custom-keywords) supported starting from version [2.0.0](https://github.com/epoberezkin/ajv/tree/v2.0), `npm install ajv@^2.0.0-beta` to use it Currently ajv is the only validator that passes all the tests from [JSON Schema Test Suite](https://github.com/json-schema/JSON-Schema-Test-Suite) (according to [json-schema-benchmark](https://github.com/ebdrup/json-schema-benchmark), apart from the test that requires that `1.0` is not an integer that is impossible to satisfy in JavaScript). @@ -53,7 +57,7 @@ The fastest validation call: ``` var Ajv = require('ajv'); -var ajv = Ajv(); // options can be passed +var ajv = Ajv(); // options can be passed, e.g. {allErrors: true} var validate = ajv.compile(schema); var valid = validate(data); if (!valid) console.log(validate.errors); @@ -82,7 +86,7 @@ ajv compiles schemas to functions and caches them in all cases (using stringifie The best performance is achieved when using compiled functions returned by `compile` or `getSchema` methods (there is no additional function call). -__Please note__: every time validation function or `ajv.validate` are called `errors` property is overwritten. You need to copy `errors` array reference to another variable if you want to use it later (e.g., in the callback). +__Please note__: every time validation function or `ajv.validate` are called `errors` property is overwritten. You need to copy `errors` array reference to another variable if you want to use it later (e.g., in the callback). See [Validation errors](#validation-errors) ## Using in browser @@ -134,16 +138,16 @@ The advantages of using custom keywords are: - they make your schemas more expressive and less verbose - they are fun to use -You can define custom keywords with [addKeyword](https://github.com/epoberezkin/ajv/tree/v2.0#api-addkeyword) method. Keywords are defined on the `ajv` instance level - new instances will not have previously defined keywords automatically. +You can define custom keywords with [addKeyword](#api-addkeyword) method. Keywords are defined on the `ajv` instance level - new instances will not have previously defined keywords. Ajv allows defining keywords with: - validation function - compilation function - macro function -- NOT IMPLEMETED: compilation function that should return code (as string) that will be inlined in the currently compiled schema. +- inline compilation function that should return code (as string) that will be inlined in the currently compiled schema. -### Define keyword using validation function (NOT RECOMMENDED) +### Define keyword with validation function (NOT RECOMMENDED) Validation function will be called during data validation. It will be passed schema, data and parentSchema (if it has 3 arguments) at validation time and it should return validation result as boolean. It can return an array of validation errors via `.errors` property of itself (otherwise a standard error will be used). @@ -171,7 +175,7 @@ console.log(validate({foo: 'baz'})); // false ``` -### Define keyword using "compilation" function +### Define keyword with "compilation" function Compilation function will be called during schema compilation. It will be passed schema and parent schema and it should return a validation function. This validation function will be passed data during validation; it should return validation result as boolean and it can return an array of validation errors via `.errors` property of itself (otherwise a standard error will be used). @@ -198,9 +202,9 @@ console.log(validate(4)); // false ``` -### Define keyword using "macro" function +### Define keyword with "macro" function -"Macro" function is called duting schema compilation. It is passed schema and parent schema and it should return another schema that will be applied to the data in addition to the original schema (if possible, schemas are merged, otherwise `allOf` keyword is used). +"Macro" function is called during schema compilation. It is passed schema and parent schema and it should return another schema that will be applied to the data in addition to the original schema (if schemas have different keys they are merged, otherwise `allOf` keyword is used). It is the most efficient approach (in cases when the keyword logic can be expressed with another JSON-schema) because it is usually easy to implement and there is no extra function call during validation. @@ -242,6 +246,57 @@ console.log(validate([3,4,5])); // true, number 5 matches schema inside "contain See the example of defining recursive macro keyword `deepProperties` in the [test](https://github.com/epoberezkin/ajv/blob/v2.0/spec/custom.spec.js#L240). +### Define keyword with "inline" compilation function + +Inline compilation function is called during schema compilation. It is passed three parameters: `it` (the current schema compilation context), `schema` and `parentSchema` and it should return the code (as a string) that will be inlined in the code of compiled schema. This code can be either an expression that evaluates to the validation result (boolean) or a set of statements that assign the validation result to a variable. + +While it can be more difficult to define keywords with "inline" functions, it can have the best performance. + +Example `even` keyword: + +``` +ajv.addKeyword('even', { type: 'number', inline: function (it, schema) { + var op = schema ? '===' : '!=='; + return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0'; +} }); + +var schema = { "even": true }; + +var validate = ajv.compile(schema); +console.log(validate(2)); // true +console.log(validate(3)); // false +``` + +`'data' + (it.dataLevel || '')` in the example above is the reference to the currently validated data. Also note that `schema` (keyword schema) is the same as `it.schema.even`, so schema is not strictly necessary here - it is passed for convenience. + + +Example `range` keyword defined using [doT template](https://github.com/olado/doT): + +``` +var doT = require('dot'); +var inlineRangeTemplate = doT.compile("\ +{{ \ + var $data = 'data' + (it.dataLevel || '') \ + , $min = it.schema.range[0] \ + , $max = it.schema.range[1] \ + , $gt = it.schema.exclusiveRange ? '>' : '>=' \ + , $lt = it.schema.exclusiveRange ? '<' : '<='; \ +}} \ +var valid{{=it.lvl}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$max}}; \ +"); + +ajv.addKeyword('range', { + type: 'number', + inline: inlineRangeTemplate, + statements: true +}); +``` + +`'valid' + it.lvl` in the example above is the expected name of the variable that should be set to the validation result. + +Property `statements` in the keyword definition should be set to `true` if the validation code sets the variable instead of evaluating to the validation result. + + ## Asynchronous compilation Starting from version 1.3 ajv supports asynchronous compilation when remote references are loaded using supplied function. See `compileAsync` method and `loadSchema` option. @@ -273,6 +328,8 @@ With [option `removeAdditional`](#options) (added by [andyscott](https://github. This option modifies original object. +TODO: example + ## API @@ -339,7 +396,7 @@ Adds meta schema that can be used to validate other schemas. That function shoul There is no need to explicitly add draft 4 meta schema (http://json-schema.org/draft-04/schema and http://json-schema.org/schema) - it is added by default, unless option `meta` is set to `false`. You only need to use it if you have a changed meta-schema that you want to use to validate your schemas. See `validateSchema`. -##### .validateSchema(Object schema) -> Boolean +##### .validateSchema(Object schema) -> Boolean Validates schema. This method should be used to validate schemas rather than `validate` due to the inconsistency of `uri` format in JSON-Schema standart. @@ -389,7 +446,7 @@ Keyword definition is an object with the following properties: - _validate_: validating function - _compile_: compiling function - _macro_: macro function -- _inline_ (NOT IMPLEMENTED): compiling function that returns code (as string) +- _inline_: compiling function that returns code (as string) _validate_, _compile_, _macro_ and _inline_ are mutually exclusive, only one should be used at a time. @@ -447,11 +504,52 @@ Defaults: - _unicode_: calculate correct length of strings with unicode pairs (true by default). Pass `false` to use `.length` of strings that is faster, but gives "incorrect" lengths of strings with unicode pairs - each unicode pair is counted as two characters. - _beautify_: format the generated function with [js-beautify](https://github.com/beautify-web/js-beautify) (the validating function is generated without line-breaks). `npm install js-beautify` to use this option. `true` or js-beautify options can be passed. - _cache_: an optional instance of cache to store compiled schemas using stable-stringified schema as a key. For example, set-associative cache [sacjs](https://github.com/epoberezkin/sacjs) can be used. If not passed then a simple hash is used which is good enough for the common use case (a limited number of statically defined schemas). Cache should have methods `put(key, value)`, `get(key)` and `del(key)`. -- _jsonPointers_: Output `dataPath` using JSON Pointers instead of JS path notation. +- _jsonPointers_: set `dataPath` propery of errors using [JSON Pointers](https://tools.ietf.org/html/rfc6901) instead of JavaScript property access notation. - _i18n_: DEPRECATED. Support internationalization of error messages using [ajv-i18n](https://github.com/epoberezkin/ajv-i18n). See its repo for details. - _messages_: Include human-readable messages in errors. `true` by default. `messages: false` can be added when internationalization (options `i18n`) is used. +## Validation errors + +In case of validation failure Ajv assigns the array of errors to `.errors` property of validation function (or to `.errors` property of ajv instance in case `validate` or `validateSchema` methods were called). + + +### Error objects + +Each error is an object with the following properties: + +- _keyword_: validation keyword. For user defined validation keywords it is set to `"custom"` (with the exception of macro keywords and unless keyword definition defines its own errors). +- _dataPath_: the path to the part of the data that was validated. By default `dataPath` uses JavaScript property access notation (e.g., `".prop[1].subProp"`). When the option `jsonPointers` is true (see [Options](#options)) `dataPath` will be set using JSON pointer standard (e.g., `"/prop/1/subProp"`). +- _params_: the object with the additional information about error that can be used to create custom error messages (e.g., using [ajv-i18n](https://github.com/epoberezkin/ajv-i18n) package). See below for parameters set by all keywords. +- _message_: the standard error message (can be excluded with option `messages` set to false). +- _schema_: the schema of the keyword (added with `verbose` option). +- _data_: the data validated by the keyword (added with `verbose` option). + + +### Error parameters + +Properties of `params` object in errors depend on the keyword that failed validation. + +- `maxItems`, `minItems`, `maxLength`, `minLength`, `maxProperties`, `minProperties` - property `limit` (number, the schema of the keyword). +- `additionalItems` - property `limit` (the maximum number of allowed items in case when `items` keyword is an array of schemas and `additionalItems` is false). +- `dependencies` - properties: + - `property` (dependent property), + - `deps` (required dependencies, comma separated list as a string), + - `depsCount` (the number of required dependedncies). +- `format` - property `format` (the schema of the keyword). +- `maximum`, `minimum` - properties: + - `limit` (number, the schema of the keyword), + - `exclusive` (boolean, the schema of `exclusiveMaximum` or `exclusiveMinimum`), + - `comparison` (string, comparison operation to compare the data to the limit, with the data on the left and the limit on the right; can be "<", "<=", ">", ">=") +- `multipleOf` - property `multipleOf` (the schema of the keyword) +- `pattern` - property `pattern` (the schema of the keyword) +- `required` - property `missingProperty` (required property that is missing). +- `type` - property `type` (required type(s), a string, can be a comma-separated list) +- `uniqueItems` - properties `i` and `j` (indices of duplicate items). +- `$ref` - property `ref` with the referenced schema URI. +- custom keywords (in case keyword definition doesn't create errors) - property `keyword` (the keyword name). + + ## Command line interface Simple JSON-schema validation can be done from command line using [ajv-cli](https://github.com/jessedc/ajv-cli) package. At the moment it does not support referenced schemas. diff --git a/spec/custom.spec.js b/spec/custom.spec.js index b2c9e8a..01b1bb5 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -368,7 +368,6 @@ var valid{{=it.lvl}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$ testRangeKeyword({ type: 'number', inline: inlineRangeTemplate, - parentSchema: true, statements: true }); }); From a46600e14a5ee11de501915e0ae8789b849c3250 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 19 Nov 2015 23:52:08 +0000 Subject: [PATCH 14/20] default behaviour in >=2.0.0 (#69) reverts #18 and #55 - now dataPath points to the object that is validated and not to the missing property; old (<=1.4.10 ) error reporting of dataPath for "required" keyword is available with option errorDataPath == "property" --- lib/ajv.js | 3 ++ lib/dot/definitions.def | 2 +- lib/dot/required.jst | 16 +++++--- spec/errors.spec.js | 86 ++++++++++++++++++++++++++++++---------- spec/json-schema.spec.js | 3 +- 5 files changed, 83 insertions(+), 27 deletions(-) diff --git a/lib/ajv.js b/lib/ajv.js index 66c18ed..dab6ccc 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -53,6 +53,9 @@ function Ajv(opts) { addInitialSchemas(); if (this.opts.formats) addInitialFormats(); + if (this.opts.errorDataPath == 'property') + this.opts._errorDataPathProperty = true; + /** * Validate data using schema diff --git a/lib/dot/definitions.def b/lib/dot/definitions.def index a915d2a..e230bc0 100644 --- a/lib/dot/definitions.def +++ b/lib/dot/definitions.def @@ -161,7 +161,7 @@ not: "'should NOT be valid'", oneOf: "'should match exactly one schema in oneOf'", pattern: "'should match pattern \"{{=it.util.escapeQuotes($schema)}}\"'", - required: "'is a required property'", + required: "'{{? it.opts._errorDataPathProperty }}is a required property{{??}}should have required property {{=$missingProperty}}{{?}}'", type: "'should be {{? $isArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}'", uniqueItems: "'should NOT have duplicate items (items ## ' + j + ' and ' + i + ' are identical)'", custom: "'should pass \"{{=$rule.keyword}}\" keyword validation'" diff --git a/lib/dot/required.jst b/lib/dot/required.jst index f5e0ae5..49b30e7 100644 --- a/lib/dot/required.jst +++ b/lib/dot/required.jst @@ -15,7 +15,9 @@ var $i = 'i' + $lvl , $propertyPath = 'schema' + $lvl + '[' + $i + ']' , $missingProperty = '\' + "\'" + ' + $propertyPath + ' + "\'" + \''; - it.errorPath = it.util.getPathExpr($currentErrorPath, $propertyPath, it.opts.jsonPointers); + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPathExpr($currentErrorPath, $propertyPath, it.opts.jsonPointers); + } }} #}} @@ -43,9 +45,11 @@ {{ var $propertyPath = 'missing' + $lvl , $missingProperty = '\' + ' + $propertyPath + ' + \''; - it.errorPath = it.opts.jsonPointers - ? it.util.getPathExpr($currentErrorPath, $propertyPath, true) - : $currentErrorPath + ' + ' + $propertyPath; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.opts.jsonPointers + ? it.util.getPathExpr($currentErrorPath, $propertyPath, true) + : $currentErrorPath + ' + ' + $propertyPath; + } }} {{# def.error:'required' }} } else { @@ -66,7 +70,9 @@ {{ var $prop = it.util.getProperty($property) , $missingProperty = it.util.escapeQuotes($prop); - it.errorPath = it.util.getPath($currentErrorPath, $property, it.opts.jsonPointers); + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPath($currentErrorPath, $property, it.opts.jsonPointers); + } }} if ({{=$data}}{{=$prop}} === undefined) { {{# def.addError:'required' }} diff --git a/spec/errors.spec.js b/spec/errors.spec.js index 2060d2f..6027e4c 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -9,11 +9,15 @@ describe('Validation errors', function () { var ajv, ajvJP, fullAjv; beforeEach(function() { - ajv = Ajv(); - ajvJP = Ajv({ jsonPointers: true }); - fullAjv = Ajv({ allErrors: true, jsonPointers: true }); + createInstances(); }); + function createInstances(errorDataPath) { + ajv = Ajv({ errorDataPath: errorDataPath }); + ajvJP = Ajv({ errorDataPath: errorDataPath, jsonPointers: true }); + fullAjv = Ajv({ errorDataPath: errorDataPath, allErrors: true, jsonPointers: true }); + } + it('error should include dataPath', function() { testSchema1({ properties: { @@ -71,7 +75,18 @@ describe('Validation errors', function () { }); - it('errors for required should include missing property in dataPath', function() { + it('with option errorDataPath="property" errors for required should include missing property in dataPath', function() { + createInstances('property'); + testRequired('property'); + }); + + + it('without option errorDataPath errors for required should NOT include missing property in dataPath', function() { + testRequired(); + }); + + + function testRequired(errorDataPath) { var schema = { required: ['foo', 'bar', 'baz'] }; @@ -83,28 +98,49 @@ describe('Validation errors', function () { var validate = ajv.compile(schema); shouldBeValid(validate, data); shouldBeInvalid(validate, invalidData1); - shouldBeError(validate.errors[0], 'required', '.bar', 'is a required property'); + shouldBeError(validate.errors[0], 'required', path('.bar'), msg('.bar')); shouldBeInvalid(validate, invalidData2); - shouldBeError(validate.errors[0], 'required', '.foo', 'is a required property'); + shouldBeError(validate.errors[0], 'required', path('.foo'), msg('.foo')); var validateJP = ajvJP.compile(schema); shouldBeValid(validateJP, data); shouldBeInvalid(validateJP, invalidData1); - shouldBeError(validateJP.errors[0], 'required', '/bar', 'is a required property'); + shouldBeError(validateJP.errors[0], 'required', path('/bar'), msg('bar')); shouldBeInvalid(validateJP, invalidData2); - shouldBeError(validateJP.errors[0], 'required', '/foo', 'is a required property'); + shouldBeError(validateJP.errors[0], 'required', path('/foo'), msg('foo')); var fullValidate = fullAjv.compile(schema); shouldBeValid(fullValidate, data); shouldBeInvalid(fullValidate, invalidData1); - shouldBeError(fullValidate.errors[0], 'required', '/bar', 'is a required property'); + shouldBeError(fullValidate.errors[0], 'required', path('/bar'), msg('.bar')); shouldBeInvalid(fullValidate, invalidData2, 2); - shouldBeError(fullValidate.errors[0], 'required', '/foo', 'is a required property'); - shouldBeError(fullValidate.errors[1], 'required', '/baz', 'is a required property'); + shouldBeError(fullValidate.errors[0], 'required', path('/foo'), msg('.foo')); + shouldBeError(fullValidate.errors[1], 'required', path('/baz'), msg('.baz')); + + function path(dataPath) { + return errorDataPath == 'property' ? dataPath : ''; + } + + function msg(prop) { + return errorDataPath == 'property' + ? 'is a required property' + : 'should have required property ' + prop; + } + } + + + it('required validation and errors for large data/schemas with option errorDataPath="property"', function() { + createInstances('property'); + testRequiredLargeSchema('property'); }); - it('required validation and errors for large data/schemas', function() { + it('required validation and errors for large data/schemas WITHOUT option errorDataPath="property"', function() { + testRequiredLargeSchema(); + }); + + + function testRequiredLargeSchema(errorDataPath) { var schema = { required: [] } , data = {} , invalidData1 = {} @@ -121,25 +157,35 @@ describe('Validation errors', function () { var validate = ajv.compile(schema); shouldBeValid(validate, data); shouldBeInvalid(validate, invalidData1); - shouldBeError(validate.errors[0], 'required', "['1']", "is a required property"); + shouldBeError(validate.errors[0], 'required', path("['1']"), msg("'1'")); shouldBeInvalid(validate, invalidData2); - shouldBeError(validate.errors[0], 'required', "['2']", "is a required property"); + shouldBeError(validate.errors[0], 'required', path("['2']"), msg("'2'")); var validateJP = ajvJP.compile(schema); shouldBeValid(validateJP, data); shouldBeInvalid(validateJP, invalidData1); - shouldBeError(validateJP.errors[0], 'required', "/1", "is a required property"); + shouldBeError(validateJP.errors[0], 'required', path("/1"), msg("'1'")); shouldBeInvalid(validateJP, invalidData2); - shouldBeError(validateJP.errors[0], 'required', "/2", "is a required property"); + shouldBeError(validateJP.errors[0], 'required', path("/2"), msg("'2'")); var fullValidate = fullAjv.compile(schema); shouldBeValid(fullValidate, data); shouldBeInvalid(fullValidate, invalidData1); - shouldBeError(fullValidate.errors[0], 'required', '/1', "is a required property"); + shouldBeError(fullValidate.errors[0], 'required', path('/1'), msg("'1'")); shouldBeInvalid(fullValidate, invalidData2, 2); - shouldBeError(fullValidate.errors[0], 'required', '/2', "is a required property"); - shouldBeError(fullValidate.errors[1], 'required', '/98', "is a required property"); - }); + shouldBeError(fullValidate.errors[0], 'required', path('/2'), msg("'2'")); + shouldBeError(fullValidate.errors[1], 'required', path('/98'), msg("'98'")); + + function path(dataPath) { + return errorDataPath == 'property' ? dataPath : ''; + } + + function msg(prop) { + return errorDataPath == 'property' + ? 'is a required property' + : 'should have required property ' + prop; + } + } it('errors for items should include item index without quotes in dataPath (#48)', function() { diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index 53d6c27..9494cd9 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -12,7 +12,8 @@ var instances = getAjvInstances(fullTest ? { verbose: true, format: 'full', inlineRefs: false, - jsonPointers: true + jsonPointers: true, + errorDataPath: 'property' } : { allErrors: true }); var remoteRefs = { From 79ab4add573fdc585eafab8d6a41773e744bcfce Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 20 Nov 2015 22:52:54 +0000 Subject: [PATCH 15/20] fixed errors for "required", tests --- lib/dot/properties.jst | 4 +- lib/dot/required.jst | 2 +- spec/errors.spec.js | 167 ++++++++++++++++++++++++++------------- spec/json-schema.spec.js | 3 +- 4 files changed, 116 insertions(+), 60 deletions(-) diff --git a/lib/dot/properties.jst b/lib/dot/properties.jst index ac80e6f..86dedef 100644 --- a/lib/dot/properties.jst +++ b/lib/dot/properties.jst @@ -137,7 +137,9 @@ var valid{{=$it.level}} = true; {{ var $currentErrorPath = it.errorPath , $missingProperty = it.util.escapeQuotes($propertyKey); - it.errorPath = it.util.getPath($currentErrorPath, $propertyKey, it.opts.jsonPointers); + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPath($currentErrorPath, $propertyKey, it.opts.jsonPointers); + } }} {{# def.error:'required' }} {{ it.errorPath = $currentErrorPath; }} diff --git a/lib/dot/required.jst b/lib/dot/required.jst index 49b30e7..9722f46 100644 --- a/lib/dot/required.jst +++ b/lib/dot/required.jst @@ -69,7 +69,7 @@ {{~ $required:$property:$i }} {{ var $prop = it.util.getProperty($property) - , $missingProperty = it.util.escapeQuotes($prop); + , $missingProperty = it.util.escapeQuotes($property); if (it.opts._errorDataPathProperty) { it.errorPath = it.util.getPath($currentErrorPath, $property, it.opts.jsonPointers); } diff --git a/spec/errors.spec.js b/spec/errors.spec.js index 6027e4c..0587291 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -80,52 +80,16 @@ describe('Validation errors', function () { testRequired('property'); }); - it('without option errorDataPath errors for required should NOT include missing property in dataPath', function() { testRequired(); }); - function testRequired(errorDataPath) { var schema = { required: ['foo', 'bar', 'baz'] }; - var data = { foo: 1, bar: 2, baz: 3 } - , invalidData1 = { foo: 1, baz: 3 } - , invalidData2 = { bar: 2 }; - - var validate = ajv.compile(schema); - shouldBeValid(validate, data); - shouldBeInvalid(validate, invalidData1); - shouldBeError(validate.errors[0], 'required', path('.bar'), msg('.bar')); - shouldBeInvalid(validate, invalidData2); - shouldBeError(validate.errors[0], 'required', path('.foo'), msg('.foo')); - - var validateJP = ajvJP.compile(schema); - shouldBeValid(validateJP, data); - shouldBeInvalid(validateJP, invalidData1); - shouldBeError(validateJP.errors[0], 'required', path('/bar'), msg('bar')); - shouldBeInvalid(validateJP, invalidData2); - shouldBeError(validateJP.errors[0], 'required', path('/foo'), msg('foo')); - - var fullValidate = fullAjv.compile(schema); - shouldBeValid(fullValidate, data); - shouldBeInvalid(fullValidate, invalidData1); - shouldBeError(fullValidate.errors[0], 'required', path('/bar'), msg('.bar')); - shouldBeInvalid(fullValidate, invalidData2, 2); - shouldBeError(fullValidate.errors[0], 'required', path('/foo'), msg('.foo')); - shouldBeError(fullValidate.errors[1], 'required', path('/baz'), msg('.baz')); - - function path(dataPath) { - return errorDataPath == 'property' ? dataPath : ''; - } - - function msg(prop) { - return errorDataPath == 'property' - ? 'is a required property' - : 'should have required property ' + prop; - } + _testRequired(errorDataPath, schema, '.'); } @@ -134,12 +98,10 @@ describe('Validation errors', function () { testRequiredLargeSchema('property'); }); - it('required validation and errors for large data/schemas WITHOUT option errorDataPath="property"', function() { testRequiredLargeSchema(); }); - function testRequiredLargeSchema(errorDataPath) { var schema = { required: [] } , data = {} @@ -154,37 +116,130 @@ describe('Validation errors', function () { delete invalidData2[2]; // properties '2' and '198' will be missing delete invalidData2[98]; + var path = pathFunc(errorDataPath); + var msg = msgFunc(errorDataPath); + + test(); + + var schema = { anyOf: [ schema ] }; + test(1); + + function test(extraErrors) { + extraErrors = extraErrors || 0; + var validate = ajv.compile(schema); + shouldBeValid(validate, data); + shouldBeInvalid(validate, invalidData1, 1 + extraErrors); + shouldBeError(validate.errors[0], 'required', path("['1']"), msg("'1'")); + shouldBeInvalid(validate, invalidData2, 1 + extraErrors); + shouldBeError(validate.errors[0], 'required', path("['2']"), msg("'2'")); + + var validateJP = ajvJP.compile(schema); + shouldBeValid(validateJP, data); + shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors); + shouldBeError(validateJP.errors[0], 'required', path("/1"), msg("'1'")); + shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors); + shouldBeError(validateJP.errors[0], 'required', path("/2"), msg("'2'")); + + var fullValidate = fullAjv.compile(schema); + shouldBeValid(fullValidate, data); + shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors); + shouldBeError(fullValidate.errors[0], 'required', path('/1'), msg("'1'")); + shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors); + shouldBeError(fullValidate.errors[0], 'required', path('/2'), msg("'2'")); + shouldBeError(fullValidate.errors[1], 'required', path('/98'), msg("'98'")); + } + } + + + it('required validation with "properties" with option errorDataPath="property"', function() { + createInstances('property'); + testRequiredAndProperties('property'); + }); + + it('required validation with "properties" WITHOUT option errorDataPath="property"', function() { + testRequiredAndProperties(); + }); + + function testRequiredAndProperties(errorDataPath) { + var schema = { + properties: { + 'foo': { type: 'number' }, + 'bar': { type: 'number' }, + 'baz': { type: 'number' }, + }, + required: ['foo', 'bar', 'baz'] + }; + + _testRequired(errorDataPath, schema); + } + + + it('required validation in "anyOf" with option errorDataPath="property"', function() { + createInstances('property'); + testRequiredInAnyOf('property'); + }); + + it('required validation with "anyOf" WITHOUT option errorDataPath="property"', function() { + testRequiredInAnyOf(); + }); + + function testRequiredInAnyOf(errorDataPath) { + var schema = { + anyOf: [ + { required: ['foo', 'bar', 'baz'] } + ] + }; + + _testRequired(errorDataPath, schema, '.', 1); + } + + + function _testRequired(errorDataPath, schema, propPrefix, extraErrors) { + propPrefix = propPrefix || ''; + extraErrors = extraErrors || 0; + + var data = { foo: 1, bar: 2, baz: 3 } + , invalidData1 = { foo: 1, baz: 3 } + , invalidData2 = { bar: 2 }; + + var path = pathFunc(errorDataPath); + var msg = msgFunc(errorDataPath); + var validate = ajv.compile(schema); shouldBeValid(validate, data); - shouldBeInvalid(validate, invalidData1); - shouldBeError(validate.errors[0], 'required', path("['1']"), msg("'1'")); - shouldBeInvalid(validate, invalidData2); - shouldBeError(validate.errors[0], 'required', path("['2']"), msg("'2'")); + shouldBeInvalid(validate, invalidData1, 1 + extraErrors); + shouldBeError(validate.errors[0], 'required', path('.bar'), msg(propPrefix + 'bar')); + shouldBeInvalid(validate, invalidData2, 1 + extraErrors); + shouldBeError(validate.errors[0], 'required', path('.foo'), msg(propPrefix + 'foo')); var validateJP = ajvJP.compile(schema); shouldBeValid(validateJP, data); - shouldBeInvalid(validateJP, invalidData1); - shouldBeError(validateJP.errors[0], 'required', path("/1"), msg("'1'")); - shouldBeInvalid(validateJP, invalidData2); - shouldBeError(validateJP.errors[0], 'required', path("/2"), msg("'2'")); + shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors); + shouldBeError(validateJP.errors[0], 'required', path('/bar'), msg('bar')); + shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors); + shouldBeError(validateJP.errors[0], 'required', path('/foo'), msg('foo')); var fullValidate = fullAjv.compile(schema); shouldBeValid(fullValidate, data); - shouldBeInvalid(fullValidate, invalidData1); - shouldBeError(fullValidate.errors[0], 'required', path('/1'), msg("'1'")); - shouldBeInvalid(fullValidate, invalidData2, 2); - shouldBeError(fullValidate.errors[0], 'required', path('/2'), msg("'2'")); - shouldBeError(fullValidate.errors[1], 'required', path('/98'), msg("'98'")); + shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors); + shouldBeError(fullValidate.errors[0], 'required', path('/bar'), msg('bar')); + shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors); + shouldBeError(fullValidate.errors[0], 'required', path('/foo'), msg('foo')); + shouldBeError(fullValidate.errors[1], 'required', path('/baz'), msg('baz')); + } - function path(dataPath) { + function pathFunc(errorDataPath) { + return function (dataPath) { return errorDataPath == 'property' ? dataPath : ''; - } + }; + } - function msg(prop) { + function msgFunc(errorDataPath) { + return function (prop) { return errorDataPath == 'property' ? 'is a required property' : 'should have required property ' + prop; - } + }; } diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index 9494cd9..53d6c27 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -12,8 +12,7 @@ var instances = getAjvInstances(fullTest ? { verbose: true, format: 'full', inlineRefs: false, - jsonPointers: true, - errorDataPath: 'property' + jsonPointers: true } : { allErrors: true }); var remoteRefs = { From 5c0c8b3b1c3f3df9e7ce46f703883156a09e61dc Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 21 Nov 2015 01:45:52 +0000 Subject: [PATCH 16/20] default behaviour of "additionalProperties" in >=2.0.0 (#69) reverts #11 - now dataPath points to the object that is validated and not to the additional property; old (<=1.4.10 ) error reporting of dataPath for "additionalProperties" keyword is available with option errorDataPath == "property" --- README.md | 1 + lib/dot/definitions.def | 4 +-- lib/dot/properties.jst | 9 +++-- lib/dot/required.jst | 2 +- spec/errors.spec.js | 76 ++++++++++++++++++++++++----------------- 5 files changed, 56 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 643a39e..39940cf 100644 --- a/README.md +++ b/README.md @@ -532,6 +532,7 @@ Properties of `params` object in errors depend on the keyword that failed valida - `maxItems`, `minItems`, `maxLength`, `minLength`, `maxProperties`, `minProperties` - property `limit` (number, the schema of the keyword). - `additionalItems` - property `limit` (the maximum number of allowed items in case when `items` keyword is an array of schemas and `additionalItems` is false). +- `additionalProperties` - property `additionalProperty` (the property not used in `properties` and `patternProperties` keywords). - `dependencies` - properties: - `property` (dependent property), - `deps` (required dependencies, comma separated list as a string), diff --git a/lib/dot/definitions.def b/lib/dot/definitions.def index e230bc0..b037574 100644 --- a/lib/dot/definitions.def +++ b/lib/dot/definitions.def @@ -161,7 +161,7 @@ not: "'should NOT be valid'", oneOf: "'should match exactly one schema in oneOf'", pattern: "'should match pattern \"{{=it.util.escapeQuotes($schema)}}\"'", - required: "'{{? it.opts._errorDataPathProperty }}is a required property{{??}}should have required property {{=$missingProperty}}{{?}}'", + required: "'{{? it.opts._errorDataPathProperty }}is a required property{{??}}should have required property \\'{{=$missingProperty}}\\'{{?}}'", type: "'should be {{? $isArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}'", uniqueItems: "'should NOT have duplicate items (items ## ' + j + ' and ' + i + ' are identical)'", custom: "'should pass \"{{=$rule.keyword}}\" keyword validation'" @@ -198,7 +198,7 @@ {{## def._errorParams = { $ref: "{ ref: '{{=it.util.escapeQuotes($schema)}}' }", additionalItems: "{ limit: {{=$schema.length}} }", - additionalProperties: "{}", + additionalProperties: "{ additionalProperty: '{{=$additionalProperty}}' }", anyOf: "{}", dependencies: "{ depsCount: {{=$deps.length}}, deps: '{{? $deps.length==1 }}{{= it.util.escapeQuotes($deps[0]) }}{{??}}{{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}}', property: '{{= it.util.escapeQuotes($property) }}' }", format: "{ format: '{{=it.util.escapeQuotes($schema)}}' }", diff --git a/lib/dot/properties.jst b/lib/dot/properties.jst index 86dedef..e2f3a69 100644 --- a/lib/dot/properties.jst +++ b/lib/dot/properties.jst @@ -7,7 +7,9 @@ {{ /* additionalProperties is schema */ $it.schema = $aProperties; $it.schemaPath = it.schemaPath + '.additionalProperties'; - $it.errorPath = it.errorPath; + $it.errorPath = it.opts._errorDataPathProperty + ? it.errorPath + : it.util.getPathExpr(it.errorPath, 'key' + $lvl, it.opts.jsonPointers); var $passData = $data + '[key' + $lvl + ']'; }} @@ -64,7 +66,10 @@ var valid{{=$it.level}} = true; {{??}} {{ var $currentErrorPath = it.errorPath; - it.errorPath = it.util.getPathExpr(it.errorPath, 'key' + $lvl, it.opts.jsonPointers); + var $additionalProperty = '\' + key' + $lvl + ' + \''; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPathExpr(it.errorPath, 'key' + $lvl, it.opts.jsonPointers); + } }} {{? $noAdditional }} {{? $removeAdditional }} diff --git a/lib/dot/required.jst b/lib/dot/required.jst index 9722f46..f8277de 100644 --- a/lib/dot/required.jst +++ b/lib/dot/required.jst @@ -14,7 +14,7 @@ {{ var $i = 'i' + $lvl , $propertyPath = 'schema' + $lvl + '[' + $i + ']' - , $missingProperty = '\' + "\'" + ' + $propertyPath + ' + "\'" + \''; + , $missingProperty = '\' + ' + $propertyPath + ' + \''; if (it.opts._errorDataPathProperty) { it.errorPath = it.util.getPathExpr($currentErrorPath, $propertyPath, it.opts.jsonPointers); } diff --git a/spec/errors.spec.js b/spec/errors.spec.js index 0587291..3b75914 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -38,7 +38,16 @@ describe('Validation errors', function () { }); - it('errors for additionalProperties should include property in dataPath', function() { + it('with option errorDataPath="property" errors for additionalProperties should include property in dataPath ', function() { + createInstances('property'); + testAdditional('property'); + }); + + it('WITHOUT option errorDataPath errors for additionalProperties should NOT include property in dataPath', function() { + testAdditional(); + }); + + function testAdditional(errorDataPath) { var schema = { properties: { foo: {}, @@ -50,29 +59,34 @@ describe('Validation errors', function () { var data = { foo: 1, bar: 2 } , invalidData = { foo: 1, bar: 2, baz: 3, quux: 4 }; + var path = pathFunc(errorDataPath); + var msg = msgFunc(errorDataPath); + var validate = ajv.compile(schema); shouldBeValid(validate, data); shouldBeInvalid(validate, invalidData); - shouldBeError(validate.errors[0], 'additionalProperties', "['baz']"); + shouldBeError(validate.errors[0], 'additionalProperties', path("['baz']"), undefined, { additionalProperty: 'baz' }); var validateJP = ajvJP.compile(schema); shouldBeValid(validateJP, data); shouldBeInvalid(validateJP, invalidData); - shouldBeError(validateJP.errors[0], 'additionalProperties', "/baz"); + shouldBeError(validateJP.errors[0], 'additionalProperties', path("/baz"), undefined, { additionalProperty: 'baz' }); var fullValidate = fullAjv.compile(schema); shouldBeValid(fullValidate, data); shouldBeInvalid(fullValidate, invalidData, 2); - shouldBeError(fullValidate.errors[0], 'additionalProperties', '/baz'); - shouldBeError(fullValidate.errors[1], 'additionalProperties', '/quux'); + shouldBeError(fullValidate.errors[0], 'additionalProperties', path('/baz'), undefined, { additionalProperty: 'baz' }); + shouldBeError(fullValidate.errors[1], 'additionalProperties', path('/quux'), undefined, { additionalProperty: 'quux' }); - fullValidate.errors - .filter(function(err) { return err.keyword == 'additionalProperties'; }) - .map(function(err) { return fullAjv.opts.jsonPointers ? err.dataPath.substr(1) : err.dataPath.slice(2,-2); }) - .forEach(function(p) { delete invalidData[p]; }); + if (errorDataPath == 'property') { + fullValidate.errors + .filter(function(err) { return err.keyword == 'additionalProperties'; }) + .map(function(err) { return fullAjv.opts.jsonPointers ? err.dataPath.substr(1) : err.dataPath.slice(2,-2); }) + .forEach(function(p) { delete invalidData[p]; }); - invalidData .should.eql({ foo: 1, bar: 2 }); - }); + invalidData .should.eql({ foo: 1, bar: 2 }); + } + } it('with option errorDataPath="property" errors for required should include missing property in dataPath', function() { @@ -129,24 +143,24 @@ describe('Validation errors', function () { var validate = ajv.compile(schema); shouldBeValid(validate, data); shouldBeInvalid(validate, invalidData1, 1 + extraErrors); - shouldBeError(validate.errors[0], 'required', path("['1']"), msg("'1'")); + shouldBeError(validate.errors[0], 'required', path("['1']"), msg('1'), { missingProperty: '1' }); shouldBeInvalid(validate, invalidData2, 1 + extraErrors); - shouldBeError(validate.errors[0], 'required', path("['2']"), msg("'2'")); + shouldBeError(validate.errors[0], 'required', path("['2']"), msg('2'), { missingProperty: '2' }); var validateJP = ajvJP.compile(schema); shouldBeValid(validateJP, data); shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors); - shouldBeError(validateJP.errors[0], 'required', path("/1"), msg("'1'")); + shouldBeError(validateJP.errors[0], 'required', path("/1"), msg('1'), { missingProperty: '1' }); shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors); - shouldBeError(validateJP.errors[0], 'required', path("/2"), msg("'2'")); + shouldBeError(validateJP.errors[0], 'required', path("/2"), msg('2'), { missingProperty: '2' }); var fullValidate = fullAjv.compile(schema); shouldBeValid(fullValidate, data); shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors); - shouldBeError(fullValidate.errors[0], 'required', path('/1'), msg("'1'")); + shouldBeError(fullValidate.errors[0], 'required', path('/1'), msg('1'), { missingProperty: '1' }); shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors); - shouldBeError(fullValidate.errors[0], 'required', path('/2'), msg("'2'")); - shouldBeError(fullValidate.errors[1], 'required', path('/98'), msg("'98'")); + shouldBeError(fullValidate.errors[0], 'required', path('/2'), msg('2'), { missingProperty: '2' }); + shouldBeError(fullValidate.errors[1], 'required', path('/98'), msg('98'), { missingProperty: '98' }); } } @@ -194,8 +208,8 @@ describe('Validation errors', function () { } - function _testRequired(errorDataPath, schema, propPrefix, extraErrors) { - propPrefix = propPrefix || ''; + function _testRequired(errorDataPath, schema, prefix, extraErrors) { + prefix = prefix || ''; extraErrors = extraErrors || 0; var data = { foo: 1, bar: 2, baz: 3 } @@ -208,24 +222,24 @@ describe('Validation errors', function () { var validate = ajv.compile(schema); shouldBeValid(validate, data); shouldBeInvalid(validate, invalidData1, 1 + extraErrors); - shouldBeError(validate.errors[0], 'required', path('.bar'), msg(propPrefix + 'bar')); + shouldBeError(validate.errors[0], 'required', path('.bar'), msg(prefix + 'bar'), { missingProperty: prefix + 'bar' }); shouldBeInvalid(validate, invalidData2, 1 + extraErrors); - shouldBeError(validate.errors[0], 'required', path('.foo'), msg(propPrefix + 'foo')); + shouldBeError(validate.errors[0], 'required', path('.foo'), msg(prefix + 'foo'), { missingProperty: prefix + 'foo' }); var validateJP = ajvJP.compile(schema); shouldBeValid(validateJP, data); shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors); - shouldBeError(validateJP.errors[0], 'required', path('/bar'), msg('bar')); + shouldBeError(validateJP.errors[0], 'required', path('/bar'), msg('bar'), { missingProperty: 'bar' }); shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors); - shouldBeError(validateJP.errors[0], 'required', path('/foo'), msg('foo')); + shouldBeError(validateJP.errors[0], 'required', path('/foo'), msg('foo'), { missingProperty: 'foo' }); var fullValidate = fullAjv.compile(schema); shouldBeValid(fullValidate, data); shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors); - shouldBeError(fullValidate.errors[0], 'required', path('/bar'), msg('bar')); + shouldBeError(fullValidate.errors[0], 'required', path('/bar'), msg('bar'), { missingProperty: 'bar' }); shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors); - shouldBeError(fullValidate.errors[0], 'required', path('/foo'), msg('foo')); - shouldBeError(fullValidate.errors[1], 'required', path('/baz'), msg('baz')); + shouldBeError(fullValidate.errors[0], 'required', path('/foo'), msg('foo'), { missingProperty: 'foo' }); + shouldBeError(fullValidate.errors[1], 'required', path('/baz'), msg('baz'), { missingProperty: 'baz' }); } function pathFunc(errorDataPath) { @@ -238,7 +252,7 @@ describe('Validation errors', function () { return function (prop) { return errorDataPath == 'property' ? 'is a required property' - : 'should have required property ' + prop; + : 'should have required property \'' + prop + '\''; }; } @@ -324,11 +338,11 @@ describe('Validation errors', function () { } - function shouldBeError(error, keyword, dataPath, message) { + function shouldBeError(error, keyword, dataPath, message, params) { error.keyword .should.equal(keyword); error.dataPath .should.equal(dataPath); error.message .should.be.a('string'); - if (message !== undefined) - error.message .should.equal(message); + if (message !== undefined) error.message .should.equal(message); + if (params !== undefined) error.params .should.eql(params); } }); From 56a8b5b582941e849c09848477c21d043962d79f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 21 Nov 2015 12:43:44 +0000 Subject: [PATCH 17/20] "dependencies" with option errorDataPath = "property" sets dataPath to missing property, error params has missingProperty, #68, #69 --- README.md | 1 + lib/dot/definitions.def | 2 +- lib/dot/dependencies.jst | 14 +- lib/dot/missing.def | 20 +++ lib/dot/required.jst | 21 +-- lib/dot/validate.jst | 2 +- scripts/compile-dots.js | 8 +- spec/errors.spec.js | 359 +++++++++++++++++++++++---------------- 8 files changed, 249 insertions(+), 178 deletions(-) create mode 100644 lib/dot/missing.def diff --git a/README.md b/README.md index 39940cf..401d27c 100644 --- a/README.md +++ b/README.md @@ -535,6 +535,7 @@ Properties of `params` object in errors depend on the keyword that failed valida - `additionalProperties` - property `additionalProperty` (the property not used in `properties` and `patternProperties` keywords). - `dependencies` - properties: - `property` (dependent property), + - `missingProperty` (required missing dependency - only the first one is reported currently) - `deps` (required dependencies, comma separated list as a string), - `depsCount` (the number of required dependedncies). - `format` - property `format` (the schema of the keyword). diff --git a/lib/dot/definitions.def b/lib/dot/definitions.def index b037574..85f6951 100644 --- a/lib/dot/definitions.def +++ b/lib/dot/definitions.def @@ -200,7 +200,7 @@ additionalItems: "{ limit: {{=$schema.length}} }", additionalProperties: "{ additionalProperty: '{{=$additionalProperty}}' }", anyOf: "{}", - dependencies: "{ depsCount: {{=$deps.length}}, deps: '{{? $deps.length==1 }}{{= it.util.escapeQuotes($deps[0]) }}{{??}}{{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}}', property: '{{= it.util.escapeQuotes($property) }}' }", + dependencies: "{ property: '{{= it.util.escapeQuotes($property) }}', missingProperty: '{{=$missingProperty}}', depsCount: {{=$deps.length}}, deps: '{{? $deps.length==1 }}{{= it.util.escapeQuotes($deps[0]) }}{{??}}{{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}}' }", format: "{ format: '{{=it.util.escapeQuotes($schema)}}' }", maximum: "{ comparison: '{{=$op}}', limit: {{=$schema}}, exclusive: {{=$exclusive}} }", minimum: "{ comparison: '{{=$op}}', limit: {{=$schema}}, exclusive: {{=$exclusive}} }", diff --git a/lib/dot/dependencies.jst b/lib/dot/dependencies.jst index fff1e3c..9f1cc0e 100644 --- a/lib/dot/dependencies.jst +++ b/lib/dot/dependencies.jst @@ -1,4 +1,5 @@ {{# def.definitions }} +{{# def.missing }} {{# def.setup:'dependencies' }} {{# def.setupNextLevel }} @@ -16,22 +17,21 @@ var {{=$errs}} = errors; -{{## def.checkPropertyDeps: - {{~ $deps:$dep:$i }} - {{?$i}} || {{?}} - {{=$data}}{{= it.util.getProperty($dep) }} === undefined - {{~}} -#}} +{{ var $currentErrorPath = it.errorPath; }} +var missing{{=$lvl}}; {{ for (var $property in $propertyDeps) { }} if ({{=$data}}{{= it.util.getProperty($property) }} !== undefined) { {{ $deps = $propertyDeps[$property]; }} - if ({{# def.checkPropertyDeps }}) { + if ({{# def.checkMissingProperty:$deps }}) { + {{# def.errorMissingProperty }} {{# def.error:'dependencies' }} } {{# def.elseIfValid }} } {{ } }} +{{ it.errorPath = $currentErrorPath; }} + {{ for (var $property in $schemaDeps) { }} {{ var $sch = $schemaDeps[$property]; }} diff --git a/lib/dot/missing.def b/lib/dot/missing.def new file mode 100644 index 0000000..9d3d74c --- /dev/null +++ b/lib/dot/missing.def @@ -0,0 +1,20 @@ +{{## def.checkMissingProperty:_properties: + {{~ _properties:_$property:$i }} + {{?$i}} || {{?}} + {{ var $prop = it.util.getProperty(_$property); }} + ( {{=$data}}{{=$prop}} === undefined && (missing{{=$lvl}} = {{= it.util.toQuotedString(it.opts.jsonPointers ? _$property : $prop) }}) ) + {{~}} +#}} + + +{{## def.errorMissingProperty: + {{ + var $propertyPath = 'missing' + $lvl + , $missingProperty = '\' + ' + $propertyPath + ' + \''; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.opts.jsonPointers + ? it.util.getPathExpr($currentErrorPath, $propertyPath, true) + : $currentErrorPath + ' + ' + $propertyPath; + } + }} +#}} diff --git a/lib/dot/required.jst b/lib/dot/required.jst index f8277de..af1537c 100644 --- a/lib/dot/required.jst +++ b/lib/dot/required.jst @@ -1,14 +1,7 @@ {{# def.definitions }} +{{# def.missing }} {{# def.setup:'required' }} -{{## def.checkRequired: - {{~ $required:$property:$i }} - {{? $i}} || {{?}} - {{ var $prop = it.util.getProperty($property); }} - ( {{=$data}}{{=$prop}} === undefined && (missing{{=$lvl}} = {{= it.util.toQuotedString(it.opts.jsonPointers ? $property : $prop) }}) ) - {{~}} -#}} - {{## def.setupLoop: var schema{{=$lvl}} = validate.schema{{=$schemaPath}}; {{ @@ -41,16 +34,8 @@ {{? $breakOnError }} var missing{{=$lvl}}; {{? $required.length <= 20 }} - if ({{# def.checkRequired }}) { - {{ - var $propertyPath = 'missing' + $lvl - , $missingProperty = '\' + ' + $propertyPath + ' + \''; - if (it.opts._errorDataPathProperty) { - it.errorPath = it.opts.jsonPointers - ? it.util.getPathExpr($currentErrorPath, $propertyPath, true) - : $currentErrorPath + ' + ' + $propertyPath; - } - }} + if ({{# def.checkMissingProperty:$required }}) { + {{# def.errorMissingProperty }} {{# def.error:'required' }} } else { {{??}} diff --git a/lib/dot/validate.jst b/lib/dot/validate.jst index feb4cde..465ff5c 100644 --- a/lib/dot/validate.jst +++ b/lib/dot/validate.jst @@ -59,7 +59,7 @@ {{~ $rulesGroup.rules:$rule }} {{? $shouldUseRule($rule) }} {{? $rule.custom }} - {{# def.customRule }} + {{# def.custom }} {{??}} {{= $rule.code(it) }} {{?}} diff --git a/scripts/compile-dots.js b/scripts/compile-dots.js index abc2a72..adac8dd 100644 --- a/scripts/compile-dots.js +++ b/scripts/compile-dots.js @@ -6,8 +6,10 @@ var glob = require('glob') , doT = require('dot') , beautify = require('js-beautify').js_beautify; -var defs = fs.readFileSync(path.join(__dirname, '../lib/dot/definitions.def')); -var customRule = fs.readFileSync(path.join(__dirname, '../lib/dot/custom.def')); +var defs = {}; +['definitions', 'custom', 'missing'].forEach(function (name) { + defs[name] = fs.readFileSync(path.join(__dirname, '../lib/dot/' + name + '.def')); +}); var files = glob.sync('../lib/dot/*.jst', { cwd: __dirname }); var dotjsPath = path.join(__dirname, '../lib/dotjs'); @@ -22,7 +24,7 @@ files.forEach(function (f) { var keyword = path.basename(f, '.jst'); var targetPath = path.join(dotjsPath, keyword + '.js'); var template = fs.readFileSync(path.join(__dirname, f)); - var code = doT.compile(template, { definitions: defs, customRule: customRule }); + var code = doT.compile(template, defs); code = code.toString() .replace(OUT_EMPTY_STRING, '') .replace(FUNCTION_NAME, 'function generate_' + keyword + '(it) {'); diff --git a/spec/errors.spec.js b/spec/errors.spec.js index 3b75914..9b42ebf 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -26,7 +26,7 @@ describe('Validation errors', function () { }); }); - it('error should include dataPath in refs', function() { + it('"refs" error should include dataPath', function() { testSchema1({ definitions: { num: { type: 'number' } @@ -38,175 +38,238 @@ describe('Validation errors', function () { }); - it('with option errorDataPath="property" errors for additionalProperties should include property in dataPath ', function() { - createInstances('property'); - testAdditional('property'); - }); + describe('"additionalProperties" errors', function() { + it('should include property in dataPath with option errorDataPath="property"', function() { + createInstances('property'); + testAdditional('property'); + }); - it('WITHOUT option errorDataPath errors for additionalProperties should NOT include property in dataPath', function() { - testAdditional(); - }); + it('should NOT include property in dataPath WITHOUT option errorDataPath', function() { + testAdditional(); + }); - function testAdditional(errorDataPath) { - var schema = { - properties: { - foo: {}, - bar: {} - }, - additionalProperties: false - }; + function testAdditional(errorDataPath) { + var schema = { + properties: { + foo: {}, + bar: {} + }, + additionalProperties: false + }; - var data = { foo: 1, bar: 2 } - , invalidData = { foo: 1, bar: 2, baz: 3, quux: 4 }; + var data = { foo: 1, bar: 2 } + , invalidData = { foo: 1, bar: 2, baz: 3, quux: 4 }; - var path = pathFunc(errorDataPath); - var msg = msgFunc(errorDataPath); + var path = pathFunc(errorDataPath); + var msg = msgFunc(errorDataPath); - var validate = ajv.compile(schema); - shouldBeValid(validate, data); - shouldBeInvalid(validate, invalidData); - shouldBeError(validate.errors[0], 'additionalProperties', path("['baz']"), undefined, { additionalProperty: 'baz' }); - - var validateJP = ajvJP.compile(schema); - shouldBeValid(validateJP, data); - shouldBeInvalid(validateJP, invalidData); - shouldBeError(validateJP.errors[0], 'additionalProperties', path("/baz"), undefined, { additionalProperty: 'baz' }); - - var fullValidate = fullAjv.compile(schema); - shouldBeValid(fullValidate, data); - shouldBeInvalid(fullValidate, invalidData, 2); - shouldBeError(fullValidate.errors[0], 'additionalProperties', path('/baz'), undefined, { additionalProperty: 'baz' }); - shouldBeError(fullValidate.errors[1], 'additionalProperties', path('/quux'), undefined, { additionalProperty: 'quux' }); - - if (errorDataPath == 'property') { - fullValidate.errors - .filter(function(err) { return err.keyword == 'additionalProperties'; }) - .map(function(err) { return fullAjv.opts.jsonPointers ? err.dataPath.substr(1) : err.dataPath.slice(2,-2); }) - .forEach(function(p) { delete invalidData[p]; }); - - invalidData .should.eql({ foo: 1, bar: 2 }); - } - } - - - it('with option errorDataPath="property" errors for required should include missing property in dataPath', function() { - createInstances('property'); - testRequired('property'); - }); - - it('without option errorDataPath errors for required should NOT include missing property in dataPath', function() { - testRequired(); - }); - - function testRequired(errorDataPath) { - var schema = { - required: ['foo', 'bar', 'baz'] - }; - - _testRequired(errorDataPath, schema, '.'); - } - - - it('required validation and errors for large data/schemas with option errorDataPath="property"', function() { - createInstances('property'); - testRequiredLargeSchema('property'); - }); - - it('required validation and errors for large data/schemas WITHOUT option errorDataPath="property"', function() { - testRequiredLargeSchema(); - }); - - function testRequiredLargeSchema(errorDataPath) { - var schema = { required: [] } - , data = {} - , invalidData1 = {} - , invalidData2 = {}; - for (var i=0; i<100; i++) { - schema.required.push(''+i); // properties from '0' to '99' are required - data[i] = invalidData1[i] = invalidData2[i] = i; - } - - delete invalidData1[1]; // property '1' will be missing - delete invalidData2[2]; // properties '2' and '198' will be missing - delete invalidData2[98]; - - var path = pathFunc(errorDataPath); - var msg = msgFunc(errorDataPath); - - test(); - - var schema = { anyOf: [ schema ] }; - test(1); - - function test(extraErrors) { - extraErrors = extraErrors || 0; var validate = ajv.compile(schema); shouldBeValid(validate, data); - shouldBeInvalid(validate, invalidData1, 1 + extraErrors); - shouldBeError(validate.errors[0], 'required', path("['1']"), msg('1'), { missingProperty: '1' }); - shouldBeInvalid(validate, invalidData2, 1 + extraErrors); - shouldBeError(validate.errors[0], 'required', path("['2']"), msg('2'), { missingProperty: '2' }); + shouldBeInvalid(validate, invalidData); + shouldBeError(validate.errors[0], 'additionalProperties', path("['baz']"), undefined, { additionalProperty: 'baz' }); var validateJP = ajvJP.compile(schema); shouldBeValid(validateJP, data); - shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors); - shouldBeError(validateJP.errors[0], 'required', path("/1"), msg('1'), { missingProperty: '1' }); - shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors); - shouldBeError(validateJP.errors[0], 'required', path("/2"), msg('2'), { missingProperty: '2' }); + shouldBeInvalid(validateJP, invalidData); + shouldBeError(validateJP.errors[0], 'additionalProperties', path("/baz"), undefined, { additionalProperty: 'baz' }); var fullValidate = fullAjv.compile(schema); shouldBeValid(fullValidate, data); - shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors); - shouldBeError(fullValidate.errors[0], 'required', path('/1'), msg('1'), { missingProperty: '1' }); - shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors); - shouldBeError(fullValidate.errors[0], 'required', path('/2'), msg('2'), { missingProperty: '2' }); - shouldBeError(fullValidate.errors[1], 'required', path('/98'), msg('98'), { missingProperty: '98' }); + shouldBeInvalid(fullValidate, invalidData, 2); + shouldBeError(fullValidate.errors[0], 'additionalProperties', path('/baz'), undefined, { additionalProperty: 'baz' }); + shouldBeError(fullValidate.errors[1], 'additionalProperties', path('/quux'), undefined, { additionalProperty: 'quux' }); + + if (errorDataPath == 'property') { + fullValidate.errors + .filter(function(err) { return err.keyword == 'additionalProperties'; }) + .map(function(err) { return fullAjv.opts.jsonPointers ? err.dataPath.substr(1) : err.dataPath.slice(2,-2); }) + .forEach(function(p) { delete invalidData[p]; }); + + invalidData .should.eql({ foo: 1, bar: 2 }); + } } - } - - - it('required validation with "properties" with option errorDataPath="property"', function() { - createInstances('property'); - testRequiredAndProperties('property'); }); - it('required validation with "properties" WITHOUT option errorDataPath="property"', function() { - testRequiredAndProperties(); + + describe('"required" errors', function() { + it('should include missing property in dataPath with option errorDataPath="property"', function() { + createInstances('property'); + testRequired('property'); + }); + + it('should NOT include missing property in dataPath WITHOUT option errorDataPath', function() { + testRequired(); + }); + + function testRequired(errorDataPath) { + var schema = { + required: ['foo', 'bar', 'baz'] + }; + + _testRequired(errorDataPath, schema, '.'); + } + + + it('large data/schemas with option errorDataPath="property"', function() { + createInstances('property'); + testRequiredLargeSchema('property'); + }); + + it('large data/schemas WITHOUT option errorDataPath', function() { + testRequiredLargeSchema(); + }); + + function testRequiredLargeSchema(errorDataPath) { + var schema = { required: [] } + , data = {} + , invalidData1 = {} + , invalidData2 = {}; + for (var i=0; i<100; i++) { + schema.required.push(''+i); // properties from '0' to '99' are required + data[i] = invalidData1[i] = invalidData2[i] = i; + } + + delete invalidData1[1]; // property '1' will be missing + delete invalidData2[2]; // properties '2' and '198' will be missing + delete invalidData2[98]; + + var path = pathFunc(errorDataPath); + var msg = msgFunc(errorDataPath); + + test(); + + var schema = { anyOf: [ schema ] }; + test(1); + + function test(extraErrors) { + extraErrors = extraErrors || 0; + var validate = ajv.compile(schema); + shouldBeValid(validate, data); + shouldBeInvalid(validate, invalidData1, 1 + extraErrors); + shouldBeError(validate.errors[0], 'required', path("['1']"), msg('1'), { missingProperty: '1' }); + shouldBeInvalid(validate, invalidData2, 1 + extraErrors); + shouldBeError(validate.errors[0], 'required', path("['2']"), msg('2'), { missingProperty: '2' }); + + var validateJP = ajvJP.compile(schema); + shouldBeValid(validateJP, data); + shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors); + shouldBeError(validateJP.errors[0], 'required', path("/1"), msg('1'), { missingProperty: '1' }); + shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors); + shouldBeError(validateJP.errors[0], 'required', path("/2"), msg('2'), { missingProperty: '2' }); + + var fullValidate = fullAjv.compile(schema); + shouldBeValid(fullValidate, data); + shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors); + shouldBeError(fullValidate.errors[0], 'required', path('/1'), msg('1'), { missingProperty: '1' }); + shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors); + shouldBeError(fullValidate.errors[0], 'required', path('/2'), msg('2'), { missingProperty: '2' }); + shouldBeError(fullValidate.errors[1], 'required', path('/98'), msg('98'), { missingProperty: '98' }); + } + } + + + it('with "properties" with option errorDataPath="property"', function() { + createInstances('property'); + testRequiredAndProperties('property'); + }); + + it('with "properties" WITHOUT option errorDataPath', function() { + testRequiredAndProperties(); + }); + + function testRequiredAndProperties(errorDataPath) { + var schema = { + properties: { + 'foo': { type: 'number' }, + 'bar': { type: 'number' }, + 'baz': { type: 'number' }, + }, + required: ['foo', 'bar', 'baz'] + }; + + _testRequired(errorDataPath, schema); + } + + + it('in "anyOf" with option errorDataPath="property"', function() { + createInstances('property'); + testRequiredInAnyOf('property'); + }); + + it('in "anyOf" WITHOUT option errorDataPath', function() { + testRequiredInAnyOf(); + }); + + function testRequiredInAnyOf(errorDataPath) { + var schema = { + anyOf: [ + { required: ['foo', 'bar', 'baz'] } + ] + }; + + _testRequired(errorDataPath, schema, '.', 1); + } }); - function testRequiredAndProperties(errorDataPath) { - var schema = { - properties: { - 'foo': { type: 'number' }, - 'bar': { type: 'number' }, - 'baz': { type: 'number' }, - }, - required: ['foo', 'bar', 'baz'] - }; - _testRequired(errorDataPath, schema); - } + describe('"dependencies" errors', function() { + it('should include missing property in dataPath with option errorDataPath="property"', function() { + createInstances('property'); + testDependencies('property'); + }); + it('should NOT include missing property in dataPath WITHOUT option errorDataPath', function() { + testDependencies(); + }); - it('required validation in "anyOf" with option errorDataPath="property"', function() { - createInstances('property'); - testRequiredInAnyOf('property'); + function testDependencies(errorDataPath) { + var schema = { + dependencies: { + a: ['foo', 'bar', 'baz'] + } + }; + + var data = { a: 0, foo: 1, bar: 2, baz: 3 } + , invalidData1 = { a: 0, foo: 1, baz: 3 } + , invalidData2 = { a: 0, bar: 2 }; + + var path = pathFunc(errorDataPath); + var msg = 'should have properties foo, bar, baz when property a is present'; + + var validate = ajv.compile(schema); + shouldBeValid(validate, data); + shouldBeInvalid(validate, invalidData1); + shouldBeError(validate.errors[0], 'dependencies', path('.bar'), msg, params('.bar')); + shouldBeInvalid(validate, invalidData2); + shouldBeError(validate.errors[0], 'dependencies', path('.foo'), msg, params('.foo')); + + var validateJP = ajvJP.compile(schema); + shouldBeValid(validateJP, data); + shouldBeInvalid(validateJP, invalidData1); + shouldBeError(validateJP.errors[0], 'dependencies', path('/bar'), msg, params('bar')); + shouldBeInvalid(validateJP, invalidData2); + shouldBeError(validateJP.errors[0], 'dependencies', path('/foo'), msg, params('foo')); + + var fullValidate = fullAjv.compile(schema); + shouldBeValid(fullValidate, data); + shouldBeInvalid(fullValidate, invalidData1); + shouldBeError(fullValidate.errors[0], 'dependencies', path('/bar'), msg, params('bar')); + shouldBeInvalid(fullValidate, invalidData2/*, 2*/); + shouldBeError(fullValidate.errors[0], 'dependencies', path('/foo'), msg, params('foo')); + // shouldBeError(fullValidate.errors[1], 'dependencies', path('/baz'), msg, params('baz')); + + function params(missing) { + var p = { + property: 'a', + deps: 'foo, bar, baz', + depsCount: 3 + }; + p.missingProperty = missing; + return p; + } + } }); - it('required validation with "anyOf" WITHOUT option errorDataPath="property"', function() { - testRequiredInAnyOf(); - }); - - function testRequiredInAnyOf(errorDataPath) { - var schema = { - anyOf: [ - { required: ['foo', 'bar', 'baz'] } - ] - }; - - _testRequired(errorDataPath, schema, '.', 1); - } - function _testRequired(errorDataPath, schema, prefix, extraErrors) { prefix = prefix || ''; @@ -257,7 +320,7 @@ describe('Validation errors', function () { } - it('errors for items should include item index without quotes in dataPath (#48)', function() { + it('"items" errors should include item index without quotes in dataPath (#48)', function() { var schema1 = { id: 'schema1', type: 'array', From 65e534ee8beed6af332a2be095ca3a1d9f885632 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 21 Nov 2015 23:12:03 +0000 Subject: [PATCH 18/20] check/extend errors in inline custom keywords; tests for custom keywords creating errors --- README.md | 30 +++++++- lib/compile/index.js | 11 +-- lib/dot/custom.def | 48 ++++++++----- package.json | 1 + scripts/prepare-tests | 2 +- spec/ajv.spec.js | 18 +++++ spec/async.spec.js | 14 ++++ spec/custom.spec.js | 94 +++++++++++++++++++++---- spec/custom_rules/index.js | 9 +++ spec/custom_rules/range.jst | 8 +++ spec/custom_rules/range_with_errors.jst | 42 +++++++++++ 11 files changed, 238 insertions(+), 39 deletions(-) create mode 100644 spec/custom_rules/index.js create mode 100644 spec/custom_rules/range.jst create mode 100644 spec/custom_rules/range_with_errors.jst diff --git a/README.md b/README.md index 401d27c..b7b195f 100644 --- a/README.md +++ b/README.md @@ -282,7 +282,7 @@ var inlineRangeTemplate = doT.compile("\ , $gt = it.schema.exclusiveRange ? '>' : '>=' \ , $lt = it.schema.exclusiveRange ? '<' : '<='; \ }} \ -var valid{{=it.lvl}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$max}}; \ +var valid{{=it.level}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$max}}; \ "); ajv.addKeyword('range', { @@ -292,11 +292,37 @@ ajv.addKeyword('range', { }); ``` -`'valid' + it.lvl` in the example above is the expected name of the variable that should be set to the validation result. +`'valid' + it.level` in the example above is the expected name of the variable that should be set to the validation result. Property `statements` in the keyword definition should be set to `true` if the validation code sets the variable instead of evaluating to the validation result. +### Defining errors in custom keywords + +All custom keywords but macro keywords can create custom error messages. + +Validating and compiled keywords should define errors by assigning them to `.errors` property of validation function. + +Inline custom keyword should increase error counter `errors` and add error to `vErrors` array (it can be null). See [example range keyword](https://github.com/epoberezkin/ajv/blob/v2.0/spec/custom_rules/range_with_errors.jst). + +When inline keyword performes validation Ajv checks whether it created errors by comparing errors count before and after validation. To skip this check add option `errors` to keyword definition: + +``` +ajv.addKeyword('range', { + type: 'number', + inline: inlineRangeTemplate, + statements: true, + errors: true // keyword should create custom errors when validation fails +}); +``` + +Each error object should have properties `keyword`, `message` and `params`, other properties will be added. + +Inlined keywords can optionally define `dataPath` property in error objects. + +If custom keyword doesn't create errors, the default error will be created in case the keyword fails validation (see [Validation errors](#validation-errors)). + + ## Asynchronous compilation Starting from version 1.3 ajv supports asynchronous compilation when remote references are loaded using supplied function. See `compileAsync` method and `loadSchema` option. diff --git a/lib/compile/index.js b/lib/compile/index.js index 6a75a15..eeebc37 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -65,18 +65,19 @@ function compile(schema, root, localRefs, baseId) { if (self.opts.beautify) { var opts = self.opts.beautify === true ? { indent_size: 2 } : self.opts.beautify; + /* istanbul ignore else */ if (beautify) validateCode = beautify(validateCode, opts); else console.error('"npm install js-beautify" to use beautify option'); } // console.log('\n\n\n *** \n', validateCode); var validate; - // try { + try { eval(validateCode); refVal[0] = validate; - // } catch(e) { - // console.log('Error compiling schema, function code:', validateCode); - // throw e; - // } + } catch(e) { + console.log('Error compiling schema, function code:', validateCode); + throw e; + } validate.schema = _schema; validate.errors = null; diff --git a/lib/dot/custom.def b/lib/dot/custom.def index f320276..7fffec9 100644 --- a/lib/dot/custom.def +++ b/lib/dot/custom.def @@ -2,23 +2,27 @@ var $schema = it.schema[$rule.keyword] , $ruleValidate = it.useCustomRule($rule, $schema, it.schema, it) , $ruleErrs = $ruleValidate.code + '.errors' - , $schemaPath = it.schemaPath + '.' + $rule.keyword; + , $schemaPath = it.schemaPath + '.' + $rule.keyword + , $errs = 'errs' + $lvl + , $i = 'i' + $lvl + , $ruleErr = 'ruleErr' + $lvl + , $rDef = $rule.definition + , $inline = $rDef.inline; }} -{{? !$rule.definition.inline }} - {{=$ruleErrs}} = null; -{{?}} +{{? !$inline }}{{=$ruleErrs}} = null;{{?}} +var {{=$errs}} = errors; {{## def.callRuleValidate: - {{? $rule.definition.inline }} - {{? $rule.definition.statements }} - valid{{=it.lvl}} + {{? $inline }} + {{? $rDef.statements }} + valid{{=$lvl}} {{??}} ({{= $ruleValidate.validate }}) {{?}} {{??}} {{=$ruleValidate.code}}.call(self - {{? $rule.definition.compile }} + {{? $rDef.compile }} , {{=$data}} {{??}} , validate.schema{{=$schemaPath}} @@ -31,12 +35,12 @@ {{?}} #}} -{{## def.extendErrors: - {{ var $i = 'i' + $lvl; }} - for (var {{=$i}}=0; {{=$i}}<{{=$ruleErrs}}.length; {{=$i}}++) { - {{ var $ruleErr = 'ruleErr' + $lvl; }} - var {{=$ruleErr}} = {{=$ruleErrs}}[{{=$i}}]; - {{=$ruleErr}}.dataPath = (dataPath || '') + {{= it.errorPath }}; +{{## def.extendErrors:_inline: + for (var {{=$i}}={{=$errs}}; {{=$i}} min : data >= min; + var maxOk = exclusive ? data < max : data <= max; + var valid = minOk && maxOk; + + if (!valid) { + var err = { keyword: 'range' }; + validateRange.errors = [err]; + var comparison, limit; + if (minOk) { + comparison = exclusive ? '<' : '<='; + limit = max; + } else { + comparison = exclusive ? '>' : '>='; + limit = min; + } + err.message = 'should be ' + comparison + ' ' + limit; + err.params = { + comparison: comparison, + limit: limit, + exclusive: exclusive + }; + } + + return valid; + } + }); + it('should pass parent schema to "compiled" keyword validation', function() { testRangeKeyword({ type: 'number', compile: compileRange }); }); @@ -354,16 +390,7 @@ describe('Custom keywords', function () { }); it('should define "inline" keyword as template', function() { - var inlineRangeTemplate = doT.compile("\ -{{ \ - var $data = 'data' + (it.dataLevel || '') \ - , $min = it.schema.range[0] \ - , $max = it.schema.range[1] \ - , $gt = it.schema.exclusiveRange ? '>' : '>=' \ - , $lt = it.schema.exclusiveRange ? '<' : '<='; \ -}} \ -var valid{{=it.lvl}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$max}}; \ -"); + var inlineRangeTemplate = customRules.range; testRangeKeyword({ type: 'number', @@ -372,6 +399,28 @@ var valid{{=it.lvl}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$ }); }); + it('should allow defining optional errors', function() { + var inlineRangeTemplate = customRules.rangeWithErrors; + + testRangeKeyword({ + type: 'number', + inline: inlineRangeTemplate, + statements: true + }, true); + }); + + it('should allow defining required errors', function() { + var inlineRangeTemplate = customRules.rangeWithErrors; + + testRangeKeyword({ + type: 'number', + inline: inlineRangeTemplate, + statements: true, + errors: true + }, true); + }); + + function inlineEven(it, schema) { var op = schema ? '===' : '!=='; return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0'; @@ -383,7 +432,7 @@ var valid{{=it.lvl}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$ , data = 'data' + (it.dataLevel || '') , gt = parentSchema.exclusiveRange ? ' > ' : ' >= ' , lt = parentSchema.exclusiveRange ? ' < ' : ' <= '; - return 'var valid' + it.lvl + ' = ' + data + gt + min + ' && ' + data + lt + max + ';'; + return 'var valid' + it.level + ' = ' + data + gt + min + ' && ' + data + lt + max + ';'; } }); @@ -441,7 +490,7 @@ var valid{{=it.lvl}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$ }); } - function testRangeKeyword(definition) { + function testRangeKeyword(definition, customErrors) { instances.forEach(function (ajv) { ajv.addKeyword('range', definition); @@ -454,7 +503,9 @@ var valid{{=it.lvl}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$ shouldBeValid(validate, 'abc'); shouldBeInvalid(validate, 1.99); + if (customErrors) shouldBeRangeError(validate.errors[0], '', '>=', 2); shouldBeInvalid(validate, 4.01); + if (customErrors) shouldBeRangeError(validate.errors[0], '', '<=', 4); var schema = { "properties": { @@ -471,7 +522,9 @@ var valid{{=it.lvl}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$ shouldBeValid(validate, { foo: 3.99 }); shouldBeInvalid(validate, { foo: 2 }); + if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '>', 2, true); shouldBeInvalid(validate, { foo: 4 }); + if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '<', 4, true); }); } @@ -500,6 +553,21 @@ var valid{{=it.lvl}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$ }); } + function shouldBeRangeError(error, dataPath, comparison, limit, exclusive) { + delete error.schema; + delete error.data; + error .should.eql({ + keyword: 'range', + dataPath: dataPath, + message: 'should be ' + comparison + ' ' + limit, + params: { + comparison: comparison, + limit: limit, + exclusive: !!exclusive + } + }); + } + function validateRangeSchema(schema, parentSchema) { var schemaValid = Array.isArray(schema) && schema.length == 2 && typeof schema[0] == 'number' diff --git a/spec/custom_rules/index.js b/spec/custom_rules/index.js new file mode 100644 index 0000000..8f9c450 --- /dev/null +++ b/spec/custom_rules/index.js @@ -0,0 +1,9 @@ +'use strict'; + +var fs = require('fs') + , doT = require('dot'); + +module.exports = { + range: doT.compile(fs.readFileSync(__dirname + '/range.jst')), + rangeWithErrors: doT.compile(fs.readFileSync(__dirname + '/range_with_errors.jst')) +}; diff --git a/spec/custom_rules/range.jst b/spec/custom_rules/range.jst new file mode 100644 index 0000000..74f5686 --- /dev/null +++ b/spec/custom_rules/range.jst @@ -0,0 +1,8 @@ +{{ + var $data = 'data' + (it.dataLevel || '') + , $min = it.schema.range[0] + , $max = it.schema.range[1] + , $gt = it.schema.exclusiveRange ? '>' : '>=' + , $lt = it.schema.exclusiveRange ? '<' : '<='; +}} +var valid{{=it.level}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$max}}; diff --git a/spec/custom_rules/range_with_errors.jst b/spec/custom_rules/range_with_errors.jst new file mode 100644 index 0000000..a9bb7da --- /dev/null +++ b/spec/custom_rules/range_with_errors.jst @@ -0,0 +1,42 @@ +{{ + var $data = 'data' + (it.dataLevel || '') + , $min = it.schema.range[0] + , $max = it.schema.range[1] + , $exclusive = !!it.schema.exclusiveRange + , $gt = $exclusive ? '>' : '>=' + , $lt = $exclusive ? '<' : '<=' + , $lvl = it.level + , $err = 'err' + $lvl; +}} + +var minOk{{=$lvl}} = {{=$data}} {{=$gt}} {{=$min}}; +var valid{{=$lvl}} = minOk{{=$lvl}} && {{=$data}} {{=$lt}} {{=$max}}; + +if (!valid{{=$lvl}}) { + var {{=$err}}; + if (minOk{{=$lvl}}) { + {{=$err}} = { + keyword: 'range', + message: 'should be {{=$lt}} {{=$max}}', + params: { + comparison: '{{=$lt}}', + limit: {{=$max}}, + exclusive: {{=$exclusive}} + } + }; + } else { + {{=$err}} = { + keyword: 'range', + message: 'should be {{=$gt}} {{=$min}}', + params: { + comparison: '{{=$gt}}', + limit: {{=$min}}, + exclusive: {{=$exclusive}} + } + }; + } + + errors++; + if (vErrors) vErrors[vErrors.length] = {{=$err}}; + else vErrors = [{{=$err}}]; +} From 0fe807b68a5872972030b090d4b136b7c2bbe5fe Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 22 Nov 2015 00:31:01 +0000 Subject: [PATCH 19/20] v5 keywords constant and contains --- README.md | 23 +++++++++++++++-------- lib/ajv.js | 5 ++++- lib/v5.js | 20 ++++++++++++++++++++ spec/json-schema.spec.js | 2 +- spec/options.spec.js | 25 +++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 lib/v5.js diff --git a/README.md b/README.md index b7b195f..66f70aa 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,15 @@ Currently the fastest JSON Schema validator for node.js and browser. It uses [doT templates](https://github.com/olado/doT) to generate super-fast validating functions. -[![Build Status](https://travis-ci.org/epoberezkin/ajv.svg?branch=v2.0)](https://travis-ci.org/epoberezkin/ajv) +[![Build Status](https://travis-ci.org/epoberezkin/ajv.svg)](https://travis-ci.org/epoberezkin/ajv) [![npm version](https://badge.fury.io/js/ajv.svg)](http://badge.fury.io/js/ajv) [![Code Climate](https://codeclimate.com/github/epoberezkin/ajv/badges/gpa.svg)](https://codeclimate.com/github/epoberezkin/ajv) [![Test Coverage](https://codeclimate.com/github/epoberezkin/ajv/badges/coverage.svg)](https://codeclimate.com/github/epoberezkin/ajv/coverage) +NB: [Upgrading to version 2.0.0](https://github.com/epoberezkin/ajv/releases/tag/2.0.0). + + ## Features - ajv implements full [JSON Schema draft 4](http://json-schema.org/) standard: @@ -21,10 +24,12 @@ It uses [doT templates](https://github.com/olado/doT) to generate super-fast val - [validates schemas against meta-schema](#api-validateschema) - supports [browsers](#using-in-browser) and nodejs 0.10-5.0 - [asynchronous loading](#asynchronous-compilation) of referenced schemas during compilation +- "All errors" validation mode with [option allErrors](#options) - [error messages with parameters](#validation-errors) describing error reasons to allow creating custom error messages - i18n error messages support with [ajv-i18n](https://github.com/epoberezkin/ajv-i18n) package - [filtering data](#filtering-data) from additional properties -- BETA: [custom keywords](https://github.com/epoberezkin/ajv/tree/v2.0#defining-custom-keywords) supported starting from version [2.0.0](https://github.com/epoberezkin/ajv/tree/v2.0), `npm install ajv@^2.0.0-beta` to use it +- NEW: [custom keywords](#defining-custom-keywords) +- NEW: keywords `constant` and `contains` from [JSON-schema v5 proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals) with [option v5](#options) Currently ajv is the only validator that passes all the tests from [JSON Schema Test Suite](https://github.com/json-schema/JSON-Schema-Test-Suite) (according to [json-schema-benchmark](https://github.com/ebdrup/json-schema-benchmark), apart from the test that requires that `1.0` is not an integer that is impossible to satisfy in JavaScript). @@ -243,7 +248,7 @@ console.log(validate([2,3,4])); // false console.log(validate([3,4,5])); // true, number 5 matches schema inside "contains" ``` -See the example of defining recursive macro keyword `deepProperties` in the [test](https://github.com/epoberezkin/ajv/blob/v2.0/spec/custom.spec.js#L240). +See the example of defining recursive macro keyword `deepProperties` in the [test](https://github.com/epoberezkin/ajv/blob/master/spec/custom.spec.js#L151). ### Define keyword with "inline" compilation function @@ -303,7 +308,7 @@ All custom keywords but macro keywords can create custom error messages. Validating and compiled keywords should define errors by assigning them to `.errors` property of validation function. -Inline custom keyword should increase error counter `errors` and add error to `vErrors` array (it can be null). See [example range keyword](https://github.com/epoberezkin/ajv/blob/v2.0/spec/custom_rules/range_with_errors.jst). +Inline custom keyword should increase error counter `errors` and add error to `vErrors` array (it can be null). See [example range keyword](https://github.com/epoberezkin/ajv/blob/master/spec/custom_rules/range_with_errors.jst). When inline keyword performes validation Ajv checks whether it created errors by comparing errors count before and after validation. To skip this check add option `errors` to keyword definition: @@ -478,7 +483,7 @@ _validate_, _compile_, _macro_ and _inline_ are mutually exclusive, only one sho With _macro_ function _type_ must not be specified, the types that the keyword will be applied for will be determined by the final schema. -See [Defining custom keywords](https://github.com/epoberezkin/ajv/tree/v2.0#defining-custom-keywords) for more details. +See [Defining custom keywords](#defining-custom-keywords) for more details. ##### .errorsText([Array<Object> errors [, Object options]]) -> String @@ -509,9 +514,10 @@ Defaults: unicode: true, beautify: false, cache: new Cache, + errorDataPath: 'object', jsonPointers: false, - i18n: false, // deprecated messages: true + v5: true } ``` @@ -530,9 +536,10 @@ Defaults: - _unicode_: calculate correct length of strings with unicode pairs (true by default). Pass `false` to use `.length` of strings that is faster, but gives "incorrect" lengths of strings with unicode pairs - each unicode pair is counted as two characters. - _beautify_: format the generated function with [js-beautify](https://github.com/beautify-web/js-beautify) (the validating function is generated without line-breaks). `npm install js-beautify` to use this option. `true` or js-beautify options can be passed. - _cache_: an optional instance of cache to store compiled schemas using stable-stringified schema as a key. For example, set-associative cache [sacjs](https://github.com/epoberezkin/sacjs) can be used. If not passed then a simple hash is used which is good enough for the common use case (a limited number of statically defined schemas). Cache should have methods `put(key, value)`, `get(key)` and `del(key)`. +- _errorDataPath_: set `dataPath` to point to 'object' (default) or to 'property' (default behavior in versions before 2.0) when validating keywords `required`, `additionalProperties` and `dependencies`. - _jsonPointers_: set `dataPath` propery of errors using [JSON Pointers](https://tools.ietf.org/html/rfc6901) instead of JavaScript property access notation. -- _i18n_: DEPRECATED. Support internationalization of error messages using [ajv-i18n](https://github.com/epoberezkin/ajv-i18n). See its repo for details. -- _messages_: Include human-readable messages in errors. `true` by default. `messages: false` can be added when internationalization (options `i18n`) is used. +- _messages_: Include human-readable messages in errors. `true` by default. `messages: false` can be added when custom messages are used (e.g. with [ajv-i18n](https://github.com/epoberezkin/ajv-i18n)). +- _v5_: add keywords `constant` and `contains` from [JSON-schema v5 proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals) ## Validation errors diff --git a/lib/ajv.js b/lib/ajv.js index dab6ccc..63f3c74 100644 --- a/lib/ajv.js +++ b/lib/ajv.js @@ -6,7 +6,8 @@ var compileSchema = require('./compile') , SchemaObject = require('./compile/schema_obj') , stableStringify = require('json-stable-stringify') , formats = require('./compile/formats') - , rules = require('./compile/rules'); + , rules = require('./compile/rules') + , v5 = require('./v5'); module.exports = Ajv; @@ -56,6 +57,8 @@ function Ajv(opts) { if (this.opts.errorDataPath == 'property') this.opts._errorDataPathProperty = true; + if (this.opts.v5) v5.enable(this); + /** * Validate data using schema diff --git a/lib/v5.js b/lib/v5.js new file mode 100644 index 0000000..bc12fc2 --- /dev/null +++ b/lib/v5.js @@ -0,0 +1,20 @@ +'use strict'; + + +module.exports = { + enable: enableV5 +}; + + +function enableV5(ajv) { + ajv.addKeyword('constant', { macro: constantMacro }); + ajv.addKeyword('contains', { macro: containsMacro }); +} + +function constantMacro(schema) { + return { enum: [schema] }; +} + +function containsMacro(schema) { + return { not: { items: { not: schema } } }; +} diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index 53d6c27..57eb4f1 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -12,7 +12,7 @@ var instances = getAjvInstances(fullTest ? { verbose: true, format: 'full', inlineRefs: false, - jsonPointers: true + jsonPointers: true, } : { allErrors: true }); var remoteRefs = { diff --git a/spec/options.spec.js b/spec/options.spec.js index aa08102..95dfd2b 100644 --- a/spec/options.spec.js +++ b/spec/options.spec.js @@ -249,4 +249,29 @@ describe('Ajv Options', function () { } }); }); + + + describe('v5', function() { + it('should define keywords "constant" and "contains"', function() { + testV5(Ajv({ v5: true })); + testV5(Ajv({ v5: true, allErrors: true })); + + function testV5(ajv) { + var validate = ajv.compile({ constant: 2 }); + validate(2) .should.equal(true); + validate(5) .should.equal(false); + validate('a') .should.equal(false); + + var validate = ajv.compile({ contains: { minimum: 5 }}); + validate([1,2,3,4]) .should.equal(false); + validate([3,4,5]) .should.equal(true); + validate([3,4,6]) .should.equal(true); + + var validate = ajv.compile({ contains: { constant: 5 }}); + validate([1,2,3,4]) .should.equal(false); + validate([3,4,6]) .should.equal(false); + validate([3,4,5]) .should.equal(true); + } + }); + }); }); From 31b1e9ceff3a843b0c4e999fd8e55043301527e3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 22 Nov 2015 00:45:29 +0000 Subject: [PATCH 20/20] update versions, node 4 & 5 in .travis.yml --- .travis.yml | 2 ++ package.json | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6ada95b..c69a671 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,5 +5,7 @@ before_script: node_js: - "0.10" - "0.12" + - "4" + - "5" after_script: - codeclimate-test-reporter < coverage/lcov.info diff --git a/package.json b/package.json index bd60777..de151ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ajv", - "version": "2.0.0-beta.3", + "version": "2.0.0", "description": "Another JSON Schema Validator", "main": "lib/ajv.js", "files": [ @@ -60,6 +60,6 @@ "watch": "^0.16.0" }, "peerDependencies": { - "ajv-i18n": "^1.0.0-beta.0" + "ajv-i18n": "^1.0.0" } }