Merge pull request #245 from epoberezkin/4.2

4.2
master
Evgeny Poberezkin 2016-07-22 15:28:49 +01:00 committed by GitHub
commit bbf6b37a3b
10 changed files with 294 additions and 37 deletions

View File

@ -23,6 +23,7 @@ This way to define keywords is useful for:
- testing your keywords before converting them to compiled/inlined keywords
- defining keywords that do not depend on the schema value (e.g., when the value is always `true`). In this case you can add option `schema: false` to the keyword definition and the schemas won't be passed to the validation function, it will only receive the same 4 parameters as compiled validation function (see the next section).
- defining keywords where the schema is a value used in some expression.
- defining keywords that support [$data reference](https://github.com/epoberezkin/ajv#data-reference) - in this case validation function is required, either as the only option or in addition to compile, macro or inline function (see below).
__Please note__: In cases when validation flow is different depending on the schema and you have to use `if`s, this way to define keywords will have worse performance than compiled keyword returning different validation functions depending on the schema.

View File

@ -817,10 +817,11 @@ Keyword definition is an object with the following properties:
- _inline_: compiling function that returns code (as string)
- _schema_: an optional `false` value used with "validate" keyword to not pass schema
- _metaSchema_: an optional meta-schema for keyword schema
- _$data_: an optional `true` value to support [$data reference](#data-reference) as the value of custom keyword. The reference will be resolved at validation time. If the keyword has meta-schema it would be extended to allow $data and it will be used to validate the resolved value. Supporting $data reference requires that keyword has validating function (as the only option or in addition to compile, macro or inline function).
- _async_: an optional `true` value if the validation function is asynchronous (whether it is compiled or passed in _validate_ property); in this case it should return a promise that resolves with a value `true` or `false`. This option is ignored in case of "macro" and "inline" keywords.
- _errors_: an optional boolean indicating whether keyword returns errors. If this property is not set Ajv will determine if the errors were set in case of failed validation.
_validate_, _compile_, _macro_ and _inline_ are mutually exclusive, only one should be used at a time.
_compile_, _macro_ and _inline_ are mutually exclusive, only one should be used at a time. _validate_ can be used separately or in addition to them to support $data reference.
__Please note__: If the keyword is validating data type that is different from the type(s) in its definition, the validation function will not be called (and expanded macro will not be used), so there is no need to check for data type inside validation function or inside schema returned by macro function (unless you want to enforce a specific type and for some reason do not want to use a separate `type` keyword for that). In the same way as standard keywords work, if the keyword does not apply to the data type being validated, the validation of this keyword will succeed.

View File

@ -341,9 +341,7 @@ function vars(arr, statement) {
*/
var co = require('co');
var ucs2length = util.ucs2length;
var equal = require('./equal');
// this error is thrown by async schemas to return validation errors via exception

View File

@ -33,6 +33,7 @@ module.exports = function rules() {
RULES.keywords = util.toHash(RULES.all.concat(RULES.keywords));
RULES.all = util.toHash(RULES.all);
RULES.types = util.toHash(RULES.types);
RULES.custom = {};
return RULES;
};

View File

@ -1,32 +1,60 @@
{{# def.definitions }}
{{# def.errors }}
{{# def.setupKeyword }}
{{# def.$data }}
{{
var $schema = it.schema[$rule.keyword]
, $ruleValidate = it.useCustomRule($rule, $schema, it.schema, it)
, $ruleErrs = $ruleValidate.code + '.errors'
, $schemaPath = it.schemaPath + '.' + $rule.keyword
, $errSchemaPath = it.errSchemaPath + '/' + $rule.keyword
, $errs = 'errs' + $lvl
var $rule = this
, $definition = 'definition' + $lvl
, $rDef = $rule.definition
, $validate = $rDef.validate
, $compile
, $inline
, $macro
, $ruleValidate
, $validateCode;
}}
{{? $isData && $rDef.$data }}
{{
$validateCode = 'keywordValidate' + $lvl;
var $validateSchema = $rDef.validateSchema;
}}
var {{=$definition}} = RULES.custom['{{=$keyword}}'].definition;
var {{=$validateCode}} = {{=$definition}}.validate;
{{??}}
{{
$ruleValidate = it.useCustomRule($rule, $schema, it.schema, it);
$schemaValue = 'validate.schema' + $schemaPath;
$validateCode = $ruleValidate.code;
$compile = $rDef.compile;
$inline = $rDef.inline;
$macro = $rDef.macro;
}}
{{?}}
{{
var $ruleErrs = $validateCode + '.errors'
, $i = 'i' + $lvl
, $ruleErr = 'ruleErr' + $lvl
, $rDef = $rule.definition
, $asyncKeyword = $rDef.async
, $inline = $rDef.inline
, $macro = $rDef.macro;
, $asyncKeyword = $rDef.async;
if ($asyncKeyword && !it.async)
throw new Error('async keyword in sync schema');
}}
{{? !($inline || $macro) }}{{=$ruleErrs}} = null;{{?}}
var {{=$errs}} = errors;
var valid{{=$lvl}};
{{## def.callRuleValidate:
{{=$ruleValidate.code}}.call(
{{=$validateCode}}.call(
{{? it.opts.passContext }}this{{??}}self{{?}}
{{? $rDef.compile || $rDef.schema === false }}
{{? $compile || $rDef.schema === false }}
, {{=$data}}
{{??}}
, validate.schema{{=$schemaPath}}
, {{=$schemaValue}}
, {{=$data}}
, validate.schema{{=it.schemaPath}}
{{?}}
@ -67,7 +95,7 @@ var valid{{=$lvl}};
{{=$ruleErr}}.schemaPath = "{{=$errSchemaPath}}";
{{# _inline ? '}' : '' }}
{{? it.opts.verbose }}
{{=$ruleErr}}.schema = validate.schema{{=$schemaPath}};
{{=$ruleErr}}.schema = {{=$schemaValue}};
{{=$ruleErr}}.data = {{=$data}};
{{?}}
}
@ -82,10 +110,10 @@ var valid{{=$lvl}};
$it.schemaPath = '';
}}
{{# def.setCompositeRule }}
{{ var $code = it.validate($it).replace(/validate\.schema/g, $ruleValidate.code); }}
{{ var $code = it.validate($it).replace(/validate\.schema/g, $validateCode); }}
{{# def.resetCompositeRule }}
{{= $code }}
{{?? $rDef.compile || $rDef.validate }}
{{?? !$inline }}
{{# def.beginDefOut}}
{{# def.callRuleValidate }}
{{# def.storeDefOut:def_callRuleValidate }}
@ -102,13 +130,16 @@ var valid{{=$lvl}};
else throw e;
}
{{??}}
{{=$ruleValidate.code}}.errors = null;
{{=$validateCode}}.errors = null;
{{?}}
{{?}}
{{?}}
if (!{{# def.ruleValidationResult }}) {
if ({{? $validateSchema }}
!{{=$definition}}.validateSchema({{=$schemaValue}}) ||
{{?}}
!{{# def.ruleValidationResult }}) {
{{ $errorKeyword = $rule.keyword; }}
{{# def.beginDefOut}}
{{# def.error:'custom' }}
@ -130,7 +161,7 @@ if (!{{# def.ruleValidationResult }}) {
}
{{?}}
{{?}}
{{?? $macro}}
{{?? $macro }}
{{# def.extraError:'custom' }}
{{??}}
{{? $rDef.errors === false}}
@ -146,6 +177,5 @@ if (!{{# def.ruleValidationResult }}) {
}
{{?}}
{{?}}
{{ $errorKeyword = undefined; }}
} {{? $breakOnError }} else { {{?}}

View File

@ -117,14 +117,14 @@
{{## def.$data:
{{
var $isData = it.opts.v5 && $schema.$data;
var $schemaValue = $isData
? it.util.getData($schema.$data, $dataLvl, it.dataPathArr)
: $schema;
var $isData = it.opts.v5 && $schema.$data
, $schemaValue;
}}
{{? $isData }}
var schema{{=$lvl}} = {{=$schemaValue}};
var schema{{=$lvl}} = {{= it.util.getData($schema.$data, $dataLvl, it.dataPathArr) }};
{{ $schemaValue = 'schema' + $lvl; }}
{{??}}
{{ $schemaValue = $schema; }}
{{?}}
#}}

View File

@ -106,11 +106,7 @@
{{?}}
{{~ $rulesGroup.rules:$rule }}
{{? $shouldUseRule($rule) }}
{{? $rule.custom }}
{{# def.custom }}
{{??}}
{{= $rule.code(it, $rule.keyword) }}
{{?}}
{{= $rule.code(it, $rule.keyword) }}
{{? $breakOnError }}
{{ $closingBraces1 += '}'; }}
{{?}}

View File

@ -1,6 +1,7 @@
'use strict';
var IDENTIFIER = /^[a-z_$][a-z0-9_$]*$/i;
var customRuleCode = require('./dotjs/custom');
/**
* Define custom keyword
@ -28,8 +29,22 @@ module.exports = function addKeyword(keyword, definition) {
_addRule(keyword, dataType, definition);
}
if (definition.metaSchema)
definition.validateSchema = self.compile(definition.metaSchema, true);
var $data = definition.$data === true && this._opts.v5;
if ($data && !definition.validate)
throw new Error('$data support: neither "validate" nor "compile" functions are defined');
var metaSchema = definition.metaSchema;
if (metaSchema) {
if ($data) {
metaSchema = {
anyOf: [
metaSchema,
{ '$ref': 'https://raw.githubusercontent.com/epoberezkin/ajv/master/lib/refs/json-schema-v5.json#/definitions/$data' }
]
};
}
definition.validateSchema = self.compile(metaSchema, true);
}
}
this.RULES.keywords[keyword] = true;
@ -51,8 +66,14 @@ module.exports = function addKeyword(keyword, definition) {
self.RULES.push(ruleGroup);
}
var rule = { keyword: keyword, definition: definition, custom: true };
var rule = {
keyword: keyword,
definition: definition,
custom: true,
code: customRuleCode
};
ruleGroup.rules.push(rule);
self.RULES.custom[keyword] = rule;
}

View File

@ -1,6 +1,6 @@
{
"name": "ajv",
"version": "4.1.8",
"version": "4.2.0",
"description": "Another JSON Schema Validator",
"main": "lib/ajv.js",
"typings": "lib/ajv.d.ts",

View File

@ -507,6 +507,182 @@ describe('Custom keywords', function () {
});
describe('$data reference support with custom keywords (v5 only)', function() {
beforeEach(function() {
instances = getAjvInstances({
allErrors: true,
verbose: true,
inlineRefs: false
}, { v5: true });
ajv = instances[0];
});
it('should validate "interpreted" rule', function() {
testEvenKeyword$data({
type: 'number',
$data: true,
validate: validateEven
});
function validateEven(schema, data) {
if (typeof schema != 'boolean') return false;
return data % 2 ? !schema : schema;
}
});
it('should validate rule with "compile" and "validate" funcs', function() {
var compileCalled;
testEvenKeyword$data({
type: 'number',
$data: true,
compile: compileEven,
validate: validateEven
});
compileCalled .should.equal(true);
function validateEven(schema, data) {
if (typeof schema != 'boolean') return false;
return data % 2 ? !schema : schema;
}
function compileEven(schema) {
compileCalled = true;
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 validate with "compile" and "validate" funcs with meta-schema', function() {
var compileCalled;
testEvenKeyword$data({
type: 'number',
$data: true,
compile: compileEven,
validate: validateEven,
metaSchema: { "type": "boolean" }
});
compileCalled .should.equal(true);
shouldBeInvalidSchema({ "even": "false" });
function validateEven(schema, data) {
return data % 2 ? !schema : schema;
}
function compileEven(schema) {
compileCalled = true;
return schema ? isEven : isOdd;
}
function isEven(data) { return data % 2 === 0; }
function isOdd(data) { return data % 2 !== 0; }
});
it('should validate rule with "macro" and "validate" funcs', function() {
var macroCalled;
testEvenKeyword$data({
type: 'number',
$data: true,
macro: macroEven,
validate: validateEven
}, 2);
macroCalled .should.equal(true);
function validateEven(schema, data) {
if (typeof schema != 'boolean') return false;
return data % 2 ? !schema : schema;
}
function macroEven(schema) {
macroCalled = true;
if (schema === true) return { "multipleOf": 2 };
if (schema === false) return { "not": { "multipleOf": 2 } };
throw new Error('Schema for "even" keyword should be boolean');
}
});
it('should validate with "macro" and "validate" funcs with meta-schema', function() {
var macroCalled;
testEvenKeyword$data({
type: 'number',
$data: true,
macro: macroEven,
validate: validateEven,
metaSchema: { "type": "boolean" }
}, 2);
macroCalled .should.equal(true);
shouldBeInvalidSchema({ "even": "false" });
function validateEven(schema, data) {
return data % 2 ? !schema : schema;
}
function macroEven(schema) {
macroCalled = true;
if (schema === true) return { "multipleOf": 2 };
if (schema === false) return { "not": { "multipleOf": 2 } };
}
});
it('should validate rule with "inline" and "validate" funcs', function() {
var inlineCalled;
testEvenKeyword$data({
type: 'number',
$data: true,
inline: inlineEven,
validate: validateEven
});
inlineCalled .should.equal(true);
function validateEven(schema, data) {
if (typeof schema != 'boolean') return false;
return data % 2 ? !schema : schema;
}
function inlineEven(it, keyword, schema) {
inlineCalled = true;
var op = schema ? '===' : '!==';
return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0';
}
});
it('should validate with "inline" and "validate" funcs with meta-schema', function() {
var inlineCalled;
testEvenKeyword$data({
type: 'number',
$data: true,
inline: inlineEven,
validate: validateEven,
metaSchema: { "type": "boolean" }
});
inlineCalled .should.equal(true);
shouldBeInvalidSchema({ "even": "false" });
function validateEven(schema, data) {
return data % 2 ? !schema : schema;
}
function inlineEven(it, keyword, schema) {
inlineCalled = true;
var op = schema ? '===' : '!==';
return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0';
}
});
it('should fail if keyword definition has "$data" but no "validate"', function() {
should.throw(function() {
ajv.addKeyword('even', {
type: 'number',
$data: true,
macro: function() { return {}; }
});
});
});
});
function testEvenKeyword(definition, numErrors) {
instances.forEach(function (ajv) {
ajv.addKeyword('even', definition);
@ -520,6 +696,39 @@ describe('Custom keywords', function () {
});
}
function testEvenKeyword$data(definition, numErrors) {
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, numErrors);
shouldBeInvalid(validate, 3, numErrors);
schema = {
"properties": {
"data": { "even": { "$data": "1/evenValue" } },
"evenValue": {}
}
};
validate = ajv.compile(schema);
shouldBeValid(validate, { data: 2, evenValue: true });
shouldBeInvalid(validate, { data: 2, evenValue: false });
shouldBeValid(validate, { data: 'abc', evenValue: true });
shouldBeValid(validate, { data: 'abc', evenValue: false });
shouldBeInvalid(validate, { data: 2.5, evenValue: true });
shouldBeValid(validate, { data: 2.5, evenValue: false });
shouldBeInvalid(validate, { data: 3, evenValue: true });
shouldBeValid(validate, { data: 3, evenValue: false });
shouldBeInvalid(validate, { data: 2, evenValue: "true" });
});
}
function testConstantKeyword(definition, numErrors) {
instances.forEach(function (ajv) {
ajv.addKeyword('constant', definition);