check/extend errors in inline custom keywords; tests for custom keywords creating errors

master
Evgeny Poberezkin 2015-11-21 23:12:03 +00:00
parent 56a8b5b582
commit 65e534ee8b
11 changed files with 238 additions and 39 deletions

View File

@ -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.

View File

@ -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;

View File

@ -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}}<errors; {{=$i}}++) {
var {{=$ruleErr}} = vErrors[{{=$i}}];
{{# _inline ? 'if (\{\{=$ruleErr\}\}.dataPath === undefined) {' : '' }}
{{=$ruleErr}}.dataPath = (dataPath || '') + {{= it.errorPath }};
{{# _inline ? '}' : '' }}
{{? it.opts.verbose }}
{{=$ruleErr}}.schema = validate.schema{{=$schemaPath}};
{{=$ruleErr}}.data = {{=$data}};
@ -44,19 +48,27 @@
}
#}}
{{? $rule.definition.inline && $rule.definition.statements }}
{{? $inline && $rDef.statements }}
{{= $ruleValidate.validate }}
{{?}}
if (!{{# def.callRuleValidate }}) {
{{? $rule.definition.inline }}
{{# def.error:'custom' }}
{{? $inline }}
{{? $rDef.errors }}
{{# def.extendErrors:true }}
{{??}}
if ({{=$errs}} == errors) {
{{# def.error:'custom' }}
} else {
{{# def.extendErrors:true }}
}
{{?}}
{{??}}
if (Array.isArray({{=$ruleErrs}})) {
{{# def.extendErrors }}
if (vErrors === null) vErrors = {{=$ruleErrs}};
else vErrors.concat({{=$ruleErrs}});
errors = vErrors.length;
{{# def.extendErrors:false }}
} else {
{{# def.error:'custom' }}
}

View File

@ -39,6 +39,7 @@
"json-stable-stringify": "^1.0.0"
},
"devDependencies": {
"brfs": "^0.0.8",
"browserify": "^11.0.1",
"chai": "^3.0.0",
"dot": "^1.0.3",

View File

@ -8,4 +8,4 @@ browserify -r js-beautify -r ./lib/ajv.js:ajv -o .browser/ajv.beautify.js
find spec -type f -name '*.spec.js' | \
xargs -I {} sh -c \
'export f="{}"; browserify $f -t require-globify -x ajv -o $(echo $f | sed -e "s/spec/.browser/");'
'export f="{}"; browserify $f -t require-globify -t brfs -x ajv -o $(echo $f | sed -e "s/spec/.browser/");'

View File

@ -45,6 +45,24 @@ describe('Ajv', function () {
ajv.compile({ type: null });
});
});
it('should throw if compiled schema has an invalid JavaScript code', function() {
ajv.addKeyword('even', { inline: badEvenCode });
var schema = { even: true };
var validate = ajv.compile(schema);
validate(2) .should.equal(true);
validate(3) .should.equal(false);
var schema = { even: false };
should.throw(function() {
var validate = ajv.compile(schema);
});
function badEvenCode(it, schema) {
var op = schema ? '===' : '!==='; // invalid on purpose
return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0';
}
});
});

View File

@ -275,6 +275,20 @@ describe('compileAsync method', function() {
setTimeout(function() { callback(new Error('cant load')); });
}
});
it('if schema compilation throws some other exception', function (done) {
ajv.addKeyword('badkeyword', { compile: badCompile });
var schema = { badkeyword: true };
ajv.compileAsync(schema, function (err, validate) {
should.exist(err);
should.not.exist(validate);
done();
});
function badCompile(schema) {
throw new Error('cant compile keyword schema');
}
});
});

View File

@ -3,7 +3,7 @@
var getAjvInstances = require('./ajv_instances')
, should = require('chai').should()
, equal = require('../lib/compile/equal')
, doT = require('dot');
, customRules = require('./custom_rules');
describe('Custom keywords', function () {
@ -61,6 +61,42 @@ describe('Custom keywords', function () {
}
});
it('should allow defining custom errors for "interpreted" keyword', function() {
testRangeKeyword({ type: 'number', validate: validateRange }, true);
function validateRange(schema, data, parentSchema) {
validateRangeSchema(schema, parentSchema);
var min = schema[0]
, max = schema[1]
, exclusive = parentSchema.exclusiveRange === true;
var minOk = exclusive ? data > 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'

View File

@ -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'))
};

View File

@ -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}};

View File

@ -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}}];
}