asynchronous custom keywords can define custom errors by returning the promise that rejects with Ajv.ValidationError, closes #118
parent
3aaeaf6ec0
commit
53a6c70138
16
CUSTOM.md
16
CUSTOM.md
|
@ -34,7 +34,7 @@ ajv.addKeyword('constant', { validate: function (schema, data) {
|
|||
return typeof schema == 'object && schema !== null'
|
||||
? deepEqual(schema, data)
|
||||
: schema === data;
|
||||
} });
|
||||
}, errors: false });
|
||||
|
||||
var schema = { "constant": 2 };
|
||||
var validate = ajv.compile(schema);
|
||||
|
@ -49,6 +49,10 @@ console.log(validate({foo: 'baz'})); // false
|
|||
|
||||
`constant` keyword is already available in Ajv with option `v5: true`.
|
||||
|
||||
__Please note:__ If the keyword does not define custom errors (see [Reporting errors in custom keywords](#reporting-errors-in-custom-keywords)) pass `errors: false` in its definition; it will make generated code more efficient.
|
||||
|
||||
To add asynchronous keyword pass `async: true` in its definition.
|
||||
|
||||
|
||||
### Define keyword with "compilation" function
|
||||
|
||||
|
@ -66,7 +70,7 @@ ajv.addKeyword('range', { type: 'number', compile: function (sch, parentSchema)
|
|||
return parentSchema.exclusiveRange === true
|
||||
? function (data) { return data > min && data < max; }
|
||||
: function (data) { return data >= min && data <= max; }
|
||||
} });
|
||||
}, errors: false });
|
||||
|
||||
var schema = { "range": [2, 4], "exclusiveRange": true };
|
||||
var validate = ajv.compile(schema);
|
||||
|
@ -76,6 +80,8 @@ console.log(validate(2)); // false
|
|||
console.log(validate(4)); // false
|
||||
```
|
||||
|
||||
See note on custom errors and asynchronous keywords in the previous section.
|
||||
|
||||
|
||||
### Define keyword with "macro" function
|
||||
|
||||
|
@ -310,9 +316,9 @@ Converts the JSON-Pointer fragment from URI to the property name.
|
|||
|
||||
## Reporting errors in custom keywords
|
||||
|
||||
All custom keywords but macro keywords can create custom error messages.
|
||||
All custom keywords but macro keywords can optionally create custom error messages.
|
||||
|
||||
Validating and compiled keywords should define errors by assigning them to `.errors` property of the validation function. It should not be done for asynchronous keywords (see #118).
|
||||
Synchronous validating and compiled keywords should define errors by assigning them to `.errors` property of the validation function. Asynchronous keywords can return promise that rejects with `new Ajv.ValidationError(errors)`, where `errors` is an array of custom validation errors (if you don't want to define custom errors in asynchronous keyword, its validation function can return the promise that resolves with `false`).
|
||||
|
||||
Inline custom keyword should increase error counter `errors` and add error to `vErrors` array (it can be null). This can be done for both synchronous and asynchronous keywords. See [example range keyword](https://github.com/epoberezkin/ajv/blob/master/spec/custom_rules/range_with_errors.jst).
|
||||
|
||||
|
@ -331,7 +337,7 @@ ajv.addKeyword('range', {
|
|||
|
||||
Each error object should at least have properties `keyword`, `message` and `params`, other properties will be added.
|
||||
|
||||
Inlined keywords can optionally define `dataPath` property in error objects, that will be added by ajv unless `errors` option of the keyword is `"full"`.
|
||||
Inlined keywords can optionally define `dataPath` and `schemaPath` properties in error objects, that will be assigned by Ajv unless `errors` option of the keyword is `"full"`.
|
||||
|
||||
If custom keyword doesn't create errors, the default error will be created in case the keyword fails validation (see [Validation errors](#validation-errors)).
|
||||
|
||||
|
|
|
@ -314,13 +314,13 @@ __Please note__: [Option](#options) `missingRefs` should NOT be set to `"ignore"
|
|||
|
||||
Example in node REPL: https://tonicdev.com/esp/ajv-asynchronous-validation
|
||||
|
||||
Starting from version 3.5.0 you can define custom formats and keywords that perform validation asyncronously by accessing database or some service. You should add `async: true` in the keyword or format defnition (see [addFormat](#api-addformat) and [addKeyword](#api-addkeyword)).
|
||||
Starting from version 3.5.0 you can define custom formats and keywords that perform validation asyncronously by accessing database or some service. You should add `async: true` in the keyword or format defnition (see [addFormat](#api-addformat), [addKeyword](#api-addkeyword) and [Defining custom keywords](#defining-custom-keywords)).
|
||||
|
||||
If your schema uses asynchronous formats/keywords or refers to some schema that contains them it should have `"$async": true` keyword so that Ajv can compile it correctly. If asynchronous format/keyword or reference to asynchronous schema is used in the schema without `$async` keyword Ajv will throw an exception during schema compilation.
|
||||
|
||||
__Please note__: all asynchronous subschemas that are referenced from the current or other schemas should have `"$async": true` keyword as well, otherwise the schema compilation will fail.
|
||||
|
||||
Validation function for an asynchronous custom format/keyword should return a promise that resolves to `true` or `false`. Ajv compiles asynchronous schemas to either [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) (default) that can be optionally transpiled with [regenerator](https://github.com/facebook/regenerator) or to [es7 async function](http://tc39.github.io/ecmascript-asyncawait/) that can be transpiled with [nodent](https://github.com/MatAtBread/nodent) or with regenerator as well. You can also supply any other transpiler as a function. See [Options](#options).
|
||||
Validation function for an asynchronous custom format/keyword should return a promise that resolves to `true` or `false` (or rejects with `new Ajv.ValidationError(errors)` if you want to return custom errors from the keyword function). Ajv compiles asynchronous schemas to either [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) (default) that can be optionally transpiled with [regenerator](https://github.com/facebook/regenerator) or to [es7 async function](http://tc39.github.io/ecmascript-asyncawait/) that can be transpiled with [nodent](https://github.com/MatAtBread/nodent) or with regenerator as well. You can also supply any other transpiler as a function. See [Options](#options).
|
||||
|
||||
The compiled validation function has `async: true` property (if the schema is asynchronous), so you can differentiate these functions if you are using both syncronous and asynchronous schemas.
|
||||
|
||||
|
|
|
@ -18,8 +18,25 @@
|
|||
|
||||
{{? !($inline || $macro) }}{{=$ruleErrs}} = null;{{?}}
|
||||
var {{=$errs}} = errors;
|
||||
var valid{{=$lvl}};
|
||||
|
||||
{{## def.callRuleValidate:
|
||||
{{=$ruleValidate.code}}.call(
|
||||
{{? it.opts.passContext }}this{{??}}self{{?}}
|
||||
{{ var $validateArgs = $ruleValidate.validate.length; }}
|
||||
{{? $rDef.compile || $rDef.schema === false }}
|
||||
, {{=$data}}
|
||||
{{??}}
|
||||
, validate.schema{{=$schemaPath}}
|
||||
, {{=$data}}
|
||||
, validate.schema{{=it.schemaPath}}
|
||||
{{?}}
|
||||
, {{# def.dataPath }}
|
||||
{{# def.passParentData }}
|
||||
)
|
||||
#}}
|
||||
|
||||
{{## def.ruleValidationResult:
|
||||
{{? $inline }}
|
||||
{{? $rDef.statements }}
|
||||
valid{{=$lvl}}
|
||||
|
@ -29,19 +46,15 @@ var {{=$errs}} = errors;
|
|||
{{?? $macro }}
|
||||
valid{{=$it.level}}
|
||||
{{??}}
|
||||
{{?$asyncKeyword}}{{=it.yieldAwait}} {{?}}{{=$ruleValidate.code}}.call(
|
||||
{{? it.opts.passContext }}this{{??}}self{{?}}
|
||||
{{ var $validateArgs = $ruleValidate.validate.length; }}
|
||||
{{? $rDef.compile || $rDef.schema === false }}
|
||||
, {{=$data}}
|
||||
{{? $asyncKeyword }}
|
||||
{{? $rDef.errors === false }}
|
||||
({{=it.yieldAwait}}{{= def_callRuleValidate }})
|
||||
{{??}}
|
||||
, validate.schema{{=$schemaPath}}
|
||||
, {{=$data}}
|
||||
, validate.schema{{=it.schemaPath}}
|
||||
valid{{=$lvl}}
|
||||
{{?}}
|
||||
, {{# def.dataPath }}
|
||||
{{# def.passParentData }}
|
||||
)
|
||||
{{??}}
|
||||
{{= def_callRuleValidate }}
|
||||
{{?}}
|
||||
{{?}}
|
||||
#}}
|
||||
|
||||
|
@ -51,6 +64,9 @@ var {{=$errs}} = errors;
|
|||
{{# _inline ? 'if (\{\{=$ruleErr\}\}.dataPath === undefined) {' : '' }}
|
||||
{{=$ruleErr}}.dataPath = (dataPath || '') + {{= it.errorPath }};
|
||||
{{# _inline ? '}' : '' }}
|
||||
{{# _inline ? 'if (\{\{=$ruleErr\}\}.schemaPath === undefined) {' : '' }}
|
||||
{{=$ruleErr}}.schemaPath = "{{=$errSchemaPath}}";
|
||||
{{# _inline ? '}' : '' }}
|
||||
{{? it.opts.verbose }}
|
||||
{{=$ruleErr}}.schema = validate.schema{{=$schemaPath}};
|
||||
{{=$ruleErr}}.data = {{=$data}};
|
||||
|
@ -70,9 +86,30 @@ var {{=$errs}} = errors;
|
|||
{{ var $code = it.validate($it).replace(/validate\.schema/g, $ruleValidate.code); }}
|
||||
{{# def.resetCompositeRule }}
|
||||
{{= $code }}
|
||||
{{?? $rDef.compile || $rDef.validate }}
|
||||
{{# def.beginDefOut}}
|
||||
{{# def.callRuleValidate }}
|
||||
{{# def.storeDefOut:def_callRuleValidate }}
|
||||
|
||||
{{? $rDef.errors !== false }}
|
||||
{{? $asyncKeyword }}
|
||||
{{ $ruleErrs = 'customErrors' + $lvl; }}
|
||||
var {{=$ruleErrs}} = null;
|
||||
try {
|
||||
valid{{=$lvl}} = {{=it.yieldAwait}}{{= def_callRuleValidate }};
|
||||
} catch (e) {
|
||||
valid{{=$lvl}} = false;
|
||||
if (e instanceof ValidationError) {{=$ruleErrs}} = e.errors;
|
||||
else throw e;
|
||||
}
|
||||
{{??}}
|
||||
{{=$ruleValidate.code}}.errors = null;
|
||||
{{?}}
|
||||
{{?}}
|
||||
{{?}}
|
||||
|
||||
if (!({{# def.callRuleValidate }})) {
|
||||
|
||||
if (!{{# def.ruleValidationResult }}) {
|
||||
{{ $errorKeyword = $rule.keyword; }}
|
||||
{{# def.beginDefOut}}
|
||||
{{# def.error:'custom' }}
|
||||
|
@ -97,14 +134,18 @@ if (!({{# def.callRuleValidate }})) {
|
|||
{{?? $macro}}
|
||||
{{# def.extraError:'custom' }}
|
||||
{{??}}
|
||||
if (Array.isArray({{=$ruleErrs}})) {
|
||||
if (vErrors === null) vErrors = {{=$ruleErrs}};
|
||||
else vErrors.concat({{=$ruleErrs}});
|
||||
errors = vErrors.length;
|
||||
{{# def.extendErrors:false }}
|
||||
} else {
|
||||
{{? $rDef.errors === false}}
|
||||
{{= def_customError }}
|
||||
}
|
||||
{{??}}
|
||||
if (Array.isArray({{=$ruleErrs}})) {
|
||||
if (vErrors === null) vErrors = {{=$ruleErrs}};
|
||||
else vErrors.concat({{=$ruleErrs}});
|
||||
errors = vErrors.length;
|
||||
{{# def.extendErrors:false }}
|
||||
} else {
|
||||
{{= def_customError }}
|
||||
}
|
||||
{{?}}
|
||||
{{?}}
|
||||
{{ $errorKeyword = undefined; }}
|
||||
|
||||
|
|
|
@ -47,6 +47,54 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "async custom keywords (validated with errors)",
|
||||
"schema": {
|
||||
"$async": true,
|
||||
"properties": {
|
||||
"userId": {
|
||||
"type": "integer",
|
||||
"idExistsWithError": { "table": "users" }
|
||||
},
|
||||
"postId": {
|
||||
"type": "integer",
|
||||
"idExistsWithError": { "table": "posts" }
|
||||
},
|
||||
"categoryId": {
|
||||
"description": "will throw if present, no such table",
|
||||
"type": "integer",
|
||||
"idExistsWithError": { "table": "categories" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid object",
|
||||
"data": { "userId": 1, "postId": 21 },
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "another valid object",
|
||||
"data": { "userId": 5, "postId": 25 },
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "invalid - no such post",
|
||||
"data": { "userId": 5, "postId": 10 },
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "invalid - no such user",
|
||||
"data": { "userId": 9, "postId": 25 },
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "should throw exception during validation - no such table",
|
||||
"data": { "postId": 25, "categoryId": 1 },
|
||||
"error": "no such table"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "async custom keywords (compiled)",
|
||||
"schema": {
|
||||
|
|
|
@ -3,11 +3,13 @@
|
|||
var jsonSchemaTest = require('json-schema-test')
|
||||
, Promise = require('./promise')
|
||||
, getAjvInstances = require('./ajv_async_instances')
|
||||
, assert = require('./chai').assert;
|
||||
, assert = require('./chai').assert
|
||||
, Ajv = require('./ajv');
|
||||
|
||||
|
||||
var instances = getAjvInstances({ v5: true });
|
||||
|
||||
|
||||
instances.forEach(addAsyncFormatsAndKeywords);
|
||||
|
||||
|
||||
|
@ -57,7 +59,15 @@ function addAsyncFormatsAndKeywords (ajv) {
|
|||
ajv.addKeyword('idExists', {
|
||||
async: true,
|
||||
type: 'number',
|
||||
validate: checkIdExists
|
||||
validate: checkIdExists,
|
||||
errors: false
|
||||
});
|
||||
|
||||
ajv.addKeyword('idExistsWithError', {
|
||||
async: true,
|
||||
type: 'number',
|
||||
validate: checkIdExistsWithError,
|
||||
errors: true
|
||||
});
|
||||
|
||||
ajv.addKeyword('idExistsCompiled', {
|
||||
|
@ -88,6 +98,28 @@ function checkIdExists(schema, data) {
|
|||
}
|
||||
|
||||
|
||||
function checkIdExistsWithError(schema, data) {
|
||||
var table = schema.table;
|
||||
switch (table) {
|
||||
case 'users': return check(table, [1, 5, 8]);
|
||||
case 'posts': return check(table, [21, 25, 28]);
|
||||
default: throw new Error('no such table');
|
||||
}
|
||||
|
||||
function check(table, IDs) {
|
||||
if (IDs.indexOf(data) >= 0) {
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
var error = {
|
||||
keyword: 'idExistsWithError',
|
||||
message: 'id not found in table ' + table
|
||||
};
|
||||
return Promise.reject(new Ajv.ValidationError([error]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function compileCheckIdExists(schema) {
|
||||
switch (schema.table) {
|
||||
case 'users': return compileCheck([1, 5, 8]);
|
||||
|
|
|
@ -94,7 +94,15 @@ describe('async schemas, formats and keywords', function() {
|
|||
ajv.addKeyword('idExists', {
|
||||
async: true,
|
||||
type: 'number',
|
||||
validate: checkIdExists
|
||||
validate: checkIdExists,
|
||||
errors: false
|
||||
});
|
||||
|
||||
ajv.addKeyword('idExistsWithError', {
|
||||
async: true,
|
||||
type: 'number',
|
||||
validate: checkIdExistsWithError,
|
||||
errors: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -122,6 +130,34 @@ describe('async schemas, formats and keywords', function() {
|
|||
});
|
||||
|
||||
|
||||
it('should return custom error', function() {
|
||||
return Promise.all(instances.map(function (ajv) {
|
||||
var schema = {
|
||||
$async: true,
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: {
|
||||
type: 'integer',
|
||||
idExistsWithError: { table: 'users' }
|
||||
},
|
||||
postId: {
|
||||
type: 'integer',
|
||||
idExistsWithError: { table: 'posts' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var validate = ajv.compile(schema);
|
||||
var _co = useCo(ajv);
|
||||
|
||||
return Promise.all([
|
||||
shouldBeInvalid(_co(validate({ userId: 5, postId: 10 })), [ 'id not found in table posts' ]),
|
||||
shouldBeInvalid(_co(validate({ userId: 9, postId: 25 })), [ 'id not found in table users' ])
|
||||
]);
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
function checkIdExists(schema, data) {
|
||||
switch (schema.table) {
|
||||
case 'users': return check([1, 5, 8]);
|
||||
|
@ -133,6 +169,27 @@ describe('async schemas, formats and keywords', function() {
|
|||
return Promise.resolve(IDs.indexOf(data) >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
function checkIdExistsWithError(schema, data) {
|
||||
var table = schema.table;
|
||||
switch (table) {
|
||||
case 'users': return check(table, [1, 5, 8]);
|
||||
case 'posts': return check(table, [21, 25, 28]);
|
||||
default: throw new Error('no such table');
|
||||
}
|
||||
|
||||
function check(table, IDs) {
|
||||
if (IDs.indexOf(data) >= 0) {
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
var error = {
|
||||
keyword: 'idExistsWithError',
|
||||
message: 'id not found in table ' + table
|
||||
};
|
||||
return Promise.reject(new Ajv.ValidationError([error]));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
@ -366,12 +423,18 @@ function shouldBeValid(p) {
|
|||
|
||||
|
||||
var SHOULD_BE_INVALID = 'test: should be invalid';
|
||||
function shouldBeInvalid(p) {
|
||||
function shouldBeInvalid(p, expectedMessages) {
|
||||
return checkNotValid(p)
|
||||
.then(function (err) {
|
||||
err .should.be.instanceof(Ajv.ValidationError);
|
||||
err.errors .should.be.an('array');
|
||||
err.validation .should.equal(true);
|
||||
if (expectedMessages) {
|
||||
var messages = err.errors.map(function (e) {
|
||||
return e.message;
|
||||
});
|
||||
messages .should.eql(expectedMessages);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -512,9 +512,9 @@ describe('Custom keywords', function () {
|
|||
shouldBeValid(validate, 'abc');
|
||||
|
||||
shouldBeInvalid(validate, 1.99, numErrors);
|
||||
if (customErrors) shouldBeRangeError(validate.errors[0], '', '>=', 2);
|
||||
if (customErrors) shouldBeRangeError(validate.errors[0], '', '#/range', '>=', 2);
|
||||
shouldBeInvalid(validate, 4.01, numErrors);
|
||||
if (customErrors) shouldBeRangeError(validate.errors[0], '', '<=', 4);
|
||||
if (customErrors) shouldBeRangeError(validate.errors[0], '', '#/range','<=', 4);
|
||||
|
||||
var schema = {
|
||||
"properties": {
|
||||
|
@ -531,9 +531,9 @@ describe('Custom keywords', function () {
|
|||
shouldBeValid(validate, { foo: 3.99 });
|
||||
|
||||
shouldBeInvalid(validate, { foo: 2 }, numErrors);
|
||||
if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '>', 2, true);
|
||||
if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '#/properties/foo/range', '>', 2, true);
|
||||
shouldBeInvalid(validate, { foo: 4 }, numErrors);
|
||||
if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '<', 4, true);
|
||||
if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '#/properties/foo/range', '<', 4, true);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -562,12 +562,13 @@ describe('Custom keywords', function () {
|
|||
});
|
||||
}
|
||||
|
||||
function shouldBeRangeError(error, dataPath, comparison, limit, exclusive) {
|
||||
function shouldBeRangeError(error, dataPath, schemaPath, comparison, limit, exclusive) {
|
||||
delete error.schema;
|
||||
delete error.data;
|
||||
error .should.eql({
|
||||
keyword: 'range',
|
||||
dataPath: dataPath,
|
||||
schemaPath: schemaPath,
|
||||
message: 'should be ' + comparison + ' ' + limit,
|
||||
params: {
|
||||
comparison: comparison,
|
||||
|
|
Loading…
Reference in New Issue