async options, expose Ajv.ValidationError class

master
Evgeny Poberezkin 2016-01-30 22:13:00 +00:00
parent c19c02aa04
commit 86d97d4337
9 changed files with 119 additions and 65 deletions

View File

@ -272,9 +272,13 @@ If your schema uses asynchronous formats/keywords or refers to some schema that
__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). 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`. 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).
If you are using generators, the compiled validation function can be used with [co](https://github.com/tj/co) or directly, e.g. in [koa](http://koajs.com/) 1.0. Generator functions are currently supported in Chrome, Firefox and node.js (0.11+); if you are using Ajv in other browsers or in older versions of node.js you should use one of available transpiling options. All provided async modes use global Promise class. If your platform does not have Promise you should use a polyfill that defines it.
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.
If you are using generators, the compiled validation function can be either wrapped with [co](https://github.com/tj/co) (default) or returned as generator function, that can be used directly, e.g. in [koa](http://koajs.com/) 1.0. `co` is a very small library, it is included in Ajv (both as npm dependency and in the browser bundle).
Generator functions are currently supported in Chrome, Firefox and node.js (0.11+); if you are using Ajv in other browsers or in older versions of node.js you should use one of available transpiling options. All provided async modes use global Promise class. If your platform does not have Promise you should use a polyfill that defines it.
Validation result will be a promise that resolves to `true` or rejects with an exception `Ajv.ValidationError` that has the array of validation errors in `errors` property.
@ -283,9 +287,9 @@ Example:
```
// without "async" option Ajv will choose the first supported/installed option in this order:
// 1. native generators
// 1. native generator function wrapped with co
// 2. es7 async functions transpiled with nodent
// 3. generator functions transpiled with regenerator
// 3. es7 async functions transpiled with regenerator
var ajv = Ajv();
@ -321,8 +325,7 @@ var schema = {
var validate = ajv.compile(schema);
var co = require('co');
co(validate({ userId: 1, postId: 19 }))
validate({ userId: 1, postId: 19 }))
.then(function (valid) {
// "valid" is always true here
console.log('Data is valid');
@ -331,7 +334,7 @@ co(validate({ userId: 1, postId: 19 }))
if (!(err instanceof Ajv.ValidationError)) throw err;
// data is invalid
console.log('Validation errors:', err.errors);
});
};
```
@ -357,9 +360,9 @@ validate(data).then(successFunc).catch(errorFunc);
#### Using regenerator
```
var ajv = Ajv({ async: 'regenerator' });
var validate = ajv.compile(schema); // transpiled generator function
co(validate(data)).then(successFunc).catch(errorFunc);
var ajv = Ajv({ async: 'es7.regenerator' });
var validate = ajv.compile(schema); // transpiled es7 async function
validate(data).then(successFunc).catch(errorFunc);
```
- node.js: `npm install regenerator`
@ -386,6 +389,20 @@ co(validate(data)).then(successFunc).catch(errorFunc);
See [Options](#options).
#### Comparison of async modes
|mode|source code|returns|transpile<br>performance*|run-time<br>performance*|bundle size|
|---|:-:|:-:|:-:|:-:|:-:|
|generators (native)|generator<br>function|generator object,<br>promise if co.wrap'ped|-|1.0|-|
|es7.nodent|es7 async<br>function|promise|1.69|1.1|183Kb|
|es7.regenerator|es7 async<br>function|promise|1.0|2.7|322Kb|
|regenerator|generator<br>function|generator object|1.0|3.2|322Kb|
* Relative performance, smaller is better
[nodent](https://github.com/MatAtBread/nodent) is a substantially smaller library that generates the code with almost the same performance as native generators. [regenerator](https://github.com/facebook/regenerator) option is provided as a more widely known alternative that in some cases may work better for you. If you are using regenerator then transpiling from es7 async function generates faster code.
## Filtering data
With [option `removeAdditional`](#options) (added by [andyscott](https://github.com/andyscott)) you can filter data during the validation.
@ -731,18 +748,19 @@ Defaults:
- _jsonPointers_: set `dataPath` propery of errors using [JSON Pointers](https://tools.ietf.org/html/rfc6901) instead of JavaScript property access notation.
- _messages_: Include human-readable messages in errors. `true` by default. `false` can be passed when custom messages are used (e.g. with [ajv-i18n](https://github.com/epoberezkin/ajv-i18n)).
- _v5_: add keywords `switch`, `constant`, `contains`, `patternGroups`, `formatMaximum` / `formatMinimum` and `exclusiveFormatMaximum` / `exclusiveFormatMinimum` from [JSON-schema v5 proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals). With this option added schemas without `$schema` property are validated against [v5 meta-schema](https://raw.githubusercontent.com/epoberezkin/ajv/master/lib/refs/json-schema-v5.json#). `false` by default.
- _async_: determines how Ajv compiles asynchronous schemas (see [Asynchronous validation](#asynchronous-validation)) to functions. In all modes Option values:
- `"generators"` - compile to generators function. If generators are not supported, the exception will be thrown when Ajv instance is created.
- _async_: determines how Ajv compiles asynchronous schemas (see [Asynchronous validation](#asynchronous-validation)) to functions. Option values:
- `"generators"` / `"co.generators"` - compile to generators function (`"co.generators"` - wrapped with `co.wrap`). If generators are not supported and you don't sprovide `transpile` option, the exception will be thrown when Ajv instance is created.
- `"es7.nodent"` - compile to es7 async function and transpile with [nodent](https://github.com/MatAtBread/nodent). If nodent is not installed, the exception will be thrown.
- `"regenerator"` - compile to generators function and transpile with [regenerator](https://github.com/facebook/regenerator). If regenerator is not installed, the exception will be thrown.
- `true` - Ajv will choose the first supported/installed async mode (in the order of values above) during creation of the instance. If none of the options is available the exception will be thrown.
- `undefined`- Ajv will choose the first available async mode when the first asynchronous schema is compiled.
- _transpile_: an optional function to transpile the code of validation function. This option allows you to use any other transpiler you prefer. In case if `async` option is "es7" Ajv will compile asynchronous schemas to es7 async functions, otherwise to generator functions. This function should accept the code of validation function as a string and return transpiled code.
- `"es7.regenerator"` / `"regenerator"` - compile to es7 async or generator function and transpile with [regenerator](https://github.com/facebook/regenerator). If regenerator is not installed, the exception will be thrown.
- `"es7"` - compile to es7 async function. Unless your platform supports them (currently only MS Edge 13 with flag does according to [compatibility table](http://kangax.github.io/compat-table/es7/)) you need to provide `transpile` option.
- `true` - Ajv will choose the first supported/installed async mode in this order: "co.generators" (native with co.wrap), "es7.nodent", "es7.regenerator" during the creation of the Ajv instance. If none of the options is available the exception will be thrown.
- `undefined`- Ajv will choose the first available async mode in the same way as with `true` option but when the first asynchronous schema is compiled.
- _transpile_: an optional function to transpile the code of asynchronous validation function. This option allows you to use any other transpiler you prefer. In case if `async` option is "es7" Ajv will compile asynchronous schemas to es7 async functions, otherwise to generator functions. This function should accept the code of validation function as a string and return transpiled code.
## Validation errors
In case of validation failure Ajv assigns the array of errors to `.errors` property of validation function (or to `.errors` property of ajv instance in case `validate` or `validateSchema` methods were called).
In case of validation failure Ajv assigns the array of errors to `.errors` property of validation function (or to `.errors` property of ajv instance in case `validate` or `validateSchema` methods were called). In case of [asynchronous validation](#asynchronous-validation) the returned promise is rejected with the exception of the class `Ajv.ValidationError` that has `.errors` poperty.
### Error objects

View File

@ -15,10 +15,10 @@ module.exports = function(config) {
// list of files / patterns to load in the browser
files: [
'ajv.min.js',
'dist/ajv.min.js',
'node_modules/chai/chai.js',
'regenerator.min.js',
'nodent.min.js',
'dist/regenerator.min.js',
'dist/nodent.min.js',
'.browser/*.spec.js'
],

View File

@ -15,6 +15,7 @@ module.exports = Ajv;
Ajv.prototype.compileAsync = async.compile;
Ajv.prototype.addKeyword = require('./keyword');
Ajv.ValidationError = require('./compile/validation_error');
var META_SCHEMA_ID = 'http://json-schema.org/draft-04/schema';
var SCHEMA_URI_FORMAT = /^(?:(?:[a-z][a-z0-9+-.]*:)?\/\/)?[^\s]*$/i;

View File

@ -14,10 +14,13 @@ module.exports = {
var ASYNC = {
'generators': generatorsSupported,
'co.generators': generatorsSupported,
'es7.nodent': getNodent,
'regenerator': getRegenerator
'es7.regenerator': getRegenerator,
'regenerator': getRegenerator,
'co.regenerator': getRegenerator
};
var MODES = ['generators', 'es7.nodent', 'regenerator'];
var MODES = ['co.generators', 'es7.nodent', 'es7.regenerator'];
var MODES_STR = MODES.join('/');
var regenerator, nodent;
@ -27,30 +30,33 @@ function setTranspile(opts) {
var get = ASYNC[mode];
var transpile;
if (get) {
transpile = opts.transpile = get();
if (transpile) return transpile;
} else {
transpile = opts.transpile = opts.transpile || get(opts);
if (transpile) return;
} else if (mode === true) {
for (var i=0; i<MODES.length; i++) {
mode = MODES[i];
get = ASYNC[mode];
transpile = get();
transpile = get(opts);
if (transpile) {
opts.async = mode;
opts.transpile = transpile;
return transpile;
return;
}
}
mode = MODES_STR;
}
} else if (mode != 'es7')
throw new Error('unknown async mode:', mode);
throw new Error(mode + ' not available');
}
function generatorsSupported() {
function generatorsSupported(opts) {
/* jshint evil: true */
try { eval('(function*(){})()'); return true; }
catch(e) {}
try {
eval('(function*(){})()');
return true;
} catch(e) {}
}
@ -75,7 +81,7 @@ function getNodent() {
try {
// nodent declares functions not only on the top level, it won't work in node 0.10-0.12 in strict mode
eval('(function () { "use strict"; if (true) { b(); function b() {} } })()');
if (!nodent) nodent = require('' + 'nodent')({ log: noop });
if (!nodent) nodent = require('' + 'nodent')({ log: noop, dontInstallRequireHook: true });
return nodentTranspile;
} catch(e) {}
}

View File

@ -4,7 +4,8 @@ var resolve = require('./resolve')
, util = require('./util')
, equal = require('./equal')
, stableStringify = require('json-stable-stringify')
, async = require('../async');
, async = require('../async')
, co = require('co');
var beautify = (function() { try { return require('' + 'js-beautify').js_beautify; } catch(e) {} })();
@ -240,12 +241,4 @@ var ucs2length = util.ucs2length;
// this error is thrown by async schemas to return validation errors via exception
function ValidationError(errors) {
this.message = 'validation failed';
this.errors = errors;
this.ajv = this.validation = true;
}
ValidationError.prototype = Object.create(Error.prototype);
ValidationError.prototype.constructor = ValidationError;
var ValidationError = require('./validation_error');

View File

@ -0,0 +1,14 @@
'use strict';
module.exports = ValidationError;
function ValidationError(errors) {
this.message = 'validation failed';
this.errors = errors;
this.ajv = this.validation = true;
}
ValidationError.prototype = Object.create(Error.prototype);
ValidationError.prototype.constructor = ValidationError;

View File

@ -26,7 +26,9 @@
it.baseId = it.baseId || it.rootId;
if ($async) {
it.async = true;
var $es7 = it.opts.async.slice(0, 3) == 'es7';
var $asyncPrefix = typeof it.opts.async == 'string' && it.opts.async.slice(0, 3);
if ($asyncPrefix == 'co.') var $coWrap = true;
else if ($asyncPrefix == 'es7') var $es7 = true;
it.yieldAwait = $es7 ? 'await' : 'yield';
}
delete it.isTop;
@ -37,12 +39,12 @@
validate =
{{? $async }}
{{? $es7 }}
async function
(async function
{{??}}
function *
{{?$coWrap}}co.wrap{{?}}(function*
{{?}}
{{??}}
function
(function
{{?}}
(data, dataPath{{? it.opts.coerceTypes }}, parentData, parentDataProperty{{?}}) {
'use strict';
@ -157,7 +159,7 @@
validate.errors = vErrors; {{ /* don't edit, used in replace */ }}
return errors === 0; {{ /* don't edit, used in replace */ }}
{{?}}
};
});
{{??}}
var {{=$valid}} = errors === errs_{{=$lvl}};
{{?}}

View File

@ -15,14 +15,14 @@
"test-fast": "AJV_FAST_TEST=true npm run test-spec",
"test-debug": "mocha spec/*.spec.js --debug-brk -R spec",
"test-cov": "istanbul cover -x '**/spec/**' node_modules/mocha/bin/_mocha -- spec/*.spec.js -R spec",
"bundle": "browserify -r ./lib/ajv.js:ajv -o ajv.bundle.js -s Ajv && uglifyjs ajv.bundle.js -o ajv.min.js -c pure_getters -m --source-map ajv.min.js.map -r Ajv --preamble '/* Ajv JSON-schema validator */'",
"bundle-regenerator": "browserify -r ./node_modules/regenerator/main.js:regenerator -o regenerator.bundle.js && uglifyjs regenerator.bundle.js -o regenerator.min.js -c -m --source-map regenerator.min.js.map",
"bundle-nodent": "browserify -r ./node_modules/nodent/nodent.js:nodent -t brfs -o nodent.bundle.js && uglifyjs nodent.bundle.js -o nodent.min.js -c -m --source-map nodent.min.js.map",
"bundle": "mkdir -p dist && browserify -r ./lib/ajv.js:ajv -o dist/ajv.bundle.js -s Ajv && uglifyjs dist/ajv.bundle.js -o dist/ajv.min.js -c pure_getters -m --source-map dist/ajv.min.js.map -r Ajv --preamble '/* Ajv JSON-schema validator */'",
"bundle-regenerator": "mkdir -p dist && browserify -r ./node_modules/regenerator/main.js:regenerator -o dist/regenerator.bundle.js && uglifyjs dist/regenerator.bundle.js -o dist/regenerator.min.js -c -m --source-map dist/regenerator.min.js.map",
"bundle-nodent": "mkdir -p dist && browserify -r ./node_modules/nodent/nodent.js:nodent -t brfs -o dist/nodent.bundle.js && uglifyjs dist/nodent.bundle.js -o dist/nodent.min.js -c -m --source-map dist/nodent.min.js.map",
"bundle-all": "npm run bundle && npm run bundle-regenerator && npm run bundle-nodent",
"build": "node scripts/compile-dots.js",
"test-browser": "npm run bundle-all && scripts/prepare-tests && karma start --single-run --browsers PhantomJS",
"test": "npm run jshint && npm run build && npm run test-cov && npm run test-browser",
"prepublish": "npm run build && npm run bundle-all && mkdir -p dist && mv ajv.* dist && mv regenerator.* dist && mv nodent.* dist",
"prepublish": "npm run build && npm run bundle-all",
"watch": "watch 'npm run build' ./lib/dot"
},
"repository": {
@ -42,6 +42,7 @@
"homepage": "https://github.com/epoberezkin/ajv",
"tonicExampleFilename": ".tonic_example.js",
"dependencies": {
"co": "^4.6.0",
"json-stable-stringify": "^1.0.0"
},
"devDependencies": {
@ -49,7 +50,6 @@
"brfs": "^1.4.3",
"browserify": "^13.0.0",
"chai": "^3.0.0",
"co": "^4.6.0",
"coveralls": "^2.11.4",
"dot": "^1.0.3",
"glob": "^6.0.4",

View File

@ -15,7 +15,7 @@ g.Promise = g.Promise || Promise;
describe('async schemas, formats and keywords', function() {
this.timeout(5000);
this.timeout(10000);
var ajv, instances;
beforeEach(function () {
@ -32,10 +32,16 @@ describe('async schemas, formats and keywords', function() {
{ async: true, allErrors: true },
{ async: 'generators' },
{ async: 'generators', allErrors: true },
{ async: 'co.generators' },
{ async: 'co.generators', allErrors: true },
{ async: 'es7.nodent' },
{ async: 'es7.nodent', allErrors: true },
{ async: 'regenerator' },
{ async: 'regenerator', allErrors: true }
{ async: 'regenerator', allErrors: true },
{ async: 'co.regenerator' },
{ async: 'co.regenerator', allErrors: true },
{ async: 'es7.regenerator' },
{ async: 'es7.regenerator', allErrors: true }
];
options.forEach(function (_opts) {
@ -55,12 +61,17 @@ describe('async schemas, formats and keywords', function() {
}
}
function regeneratorTranspile(code) {
return regenerator.compile(code).code;
}
function getAjv(opts){
try { return Ajv(opts); } catch(e) {}
}
function useCo(ajv) {
return ajv._opts.async == 'es7.nodent' ? identity : co;
var str = ajv._opts.async.slice(0, 3);
return str == 'es7' || str == 'co.' ? identity : co;
}
function identity(x) { return x; }
@ -73,7 +84,7 @@ describe('async schemas, formats and keywords', function() {
maxLength: 3
};
return Promise.map(instances, test);
return repeat(function() { Promise.map(instances, test); });
function test(ajv) {
var validate = ajv.compile(schema);
@ -119,7 +130,7 @@ describe('async schemas, formats and keywords', function() {
format: 'english_word'
};
return Promise.map(instances, test);
return repeat(function() { Promise.map(instances, test); });
function test(ajv) {
var validate = ajv.compile(schema);
@ -167,7 +178,7 @@ describe('async schemas, formats and keywords', function() {
}
};
return Promise.map(instances, test);
return repeat(function() { Promise.map(instances, test); });
function test(ajv) {
var validate = ajv.compile(schema);
@ -240,10 +251,10 @@ describe('async schemas, formats and keywords', function() {
}
};
return Promise.all([
return repeat(function() { Promise.all([
test(instances, schema1, true),
test(instances, schema2)
]);
]); });
function test(instances, schema, checkThrow) {
return Promise.map(instances, function (ajv) {
@ -336,7 +347,7 @@ describe('async schemas, formats and keywords', function() {
}
};
return Promise.map(instances, function (ajv) {
return repeat(function() { Promise.map(instances, function (ajv) {
var validate = ajv.compile(schema);
var _co = useCo(ajv);
@ -346,7 +357,7 @@ describe('async schemas, formats and keywords', function() {
shouldBeInvalid( _co(validate({ word: 1 })) ),
shouldThrow( _co(validate({ word: 'today' })), 'unknown word' )
]);
});
}); });
});
it('should validate recursive async schema', function() {
@ -455,8 +466,8 @@ describe('async schemas, formats and keywords', function() {
});
function recursiveTest(schema, refSchema) {
return Promise.map(instances, function (ajv) {
if (refSchema) ajv.addSchema(refSchema);
return repeat(function() { Promise.map(instances, function (ajv) {
if (refSchema) try { ajv.addSchema(refSchema); } catch(e) {};
var validate = ajv.compile(schema);
var _co = useCo(ajv);
@ -474,7 +485,7 @@ describe('async schemas, formats and keywords', function() {
shouldBeInvalid( _co(validate({ foo: { foo: { foo: 1 }}})) ),
shouldThrow( _co(validate({ foo: { foo: { foo: 'today' }}})), 'unknown word' )
]);
});
}); });
}
});
@ -519,6 +530,7 @@ var SHOULD_BE_INVALID = 'test: should be invalid';
function shouldBeInvalid(p) {
return checkNotValid(p)
.then(function (err) {
err .should.be.instanceof(Ajv.ValidationError);
err.errors .should.be.an('array');
err.validation .should.equal(true);
});
@ -543,3 +555,11 @@ function checkNotValid(p) {
return err;
});
}
function repeat(func) {
return func();
// var promises = [];
// for (var i=0; i<1000; i++) promises.push(func());
// return Promise.all(promises);
}