support compiling schemas in custom keywords, #69
parent
fcc776e794
commit
5e45d4eadf
|
@ -378,6 +378,7 @@ function Ajv(opts) {
|
|||
}
|
||||
|
||||
self.RULES.keywords[keyword] = true;
|
||||
self.RULES.all[keyword] = true;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
{{??}}
|
||||
}
|
||||
{{?}}
|
||||
|
|
|
@ -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']];
|
||||
|
||||
|
|
Loading…
Reference in New Issue