check/extend errors in inline custom keywords; tests for custom keywords creating errors
parent
56a8b5b582
commit
65e534ee8b
30
README.md
30
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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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' }}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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/");'
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'))
|
||||
};
|
|
@ -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}};
|
|
@ -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}}];
|
||||
}
|
Loading…
Reference in New Issue