Skip to content

Commit

Permalink
async options, expose Ajv.ValidationError class
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Jan 30, 2016
1 parent c19c02a commit 86d97d4
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 65 deletions.
52 changes: 35 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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();
Expand Down Expand Up @@ -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');
Expand All @@ -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);
});
};
```

Expand All @@ -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`
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
],

Expand Down
1 change: 1 addition & 0 deletions lib/ajv.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
30 changes: 18 additions & 12 deletions lib/async.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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) {}
}


Expand All @@ -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) {}
}
Expand Down
13 changes: 3 additions & 10 deletions lib/compile/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {} })();

Expand Down Expand Up @@ -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');
14 changes: 14 additions & 0 deletions lib/compile/validation_error.js
Original file line number Diff line number Diff line change
@@ -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;
12 changes: 7 additions & 5 deletions lib/dot/validate.jst
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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';
Expand Down Expand Up @@ -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}};
{{?}}
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -42,14 +42,14 @@
"homepage": "https://github.com/epoberezkin/ajv",
"tonicExampleFilename": ".tonic_example.js",
"dependencies": {
"co": "^4.6.0",
"json-stable-stringify": "^1.0.0"
},
"devDependencies": {
"bluebird": "^3.1.5",
"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",
Expand Down
Loading

0 comments on commit 86d97d4

Please sign in to comment.