Skip to content

Commit

Permalink
asynchronous schema compilation with loading missing remote schemas u…
Browse files Browse the repository at this point in the history
…sing supplied function, #43, mulesoft-labs/osprey-mock-service#11
  • Loading branch information
epoberezkin committed Sep 2, 2015
1 parent bec34c5 commit 4dc6cbe
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 6 deletions.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,31 @@ You can add additional formats and replace any of the formats above using [addFo
You can find patterns used for format validation and the sources that were used in [formats.js](https://github.com/epoberezkin/ajv/blob/master/lib/compile/formats.js).


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

Example:

```
var ajv = Ajv({ loadSchema: loadSchema });
ajv.compileAsync(schema, function (err, validate) {
if (err) return;
var valid = validate(data);
});
function loadSchema(uri, callback) {
request.json(uri, function(err, res, body) {
if (err || res.statusCode >= 400)
callback(err || new Error('Loading error: ' + res.statusCode));
else
callback(null, body);
});
}
```


## Filtering data

With [option `removeAdditional`](#options) (added by [andyscott](https://github.com/andyscott)) you can filter data during the validation.
Expand All @@ -140,6 +165,19 @@ Validating function returns boolean and has properties `errors` with the errors
Unless the option `validateSchema` is false, the schema will be validated against meta-schema and if schema is invalid the error will be thrown. See [options](#options).


##### .compileAsync(Object schema, Function callback)

Asyncronous version of `compile` method that loads missing remote schemas using asynchronous function in `options.loadSchema`. Callback will always be called with 2 parameters: error (or null) and validating function. Error will be not null in the following cases:

- missing schema can't be loaded (`loadSchema` calls callback with error).
- the schema containing missing reference is loaded, but the reference cannot be resolved.
- schema (or some referenced schema) is invalid.

The function compiles schema and loads the first missing schema multiple times, until all missing schemas are loaded.

See example in Asynchronous compilation.


##### .validate(Object schema|String key|String ref, data) -> Boolean

Validate data using passed schema (it will be compiled and cached).
Expand Down Expand Up @@ -227,6 +265,7 @@ Options can have properties `separator` (string used to separate errors, ", " by
- _validateSchema_: validate added/compiled schemas against meta-schema (true by default). `$schema` property in the schema can either be http://json-schema.org/schema or http://json-schema.org/draft-04/schema or absent (draft-4 meta-schema will be used) or can be a reference to the schema previously added with `addMetaSchema` method. If the validation fails, the exception is thrown. Pass "log" in this option to log error instead of throwing exception. Pass `false` to skip schema validation.
- _inlineRefs_: by default the referenced schemas that don't have refs in them are inlined, regardless of their size - that substantially improves performance at the cost of the bigger size of compiled schema functions. Pass `false` to not inline referenced schemas (they will be compiled as separate functions). Pass integer number to limit the maximum number of keywords of the schema that will be inlined.
- _missingRefs_: by default if the reference cannot be resolved during compilation the exception is thrown. The thrown error has properties `missingRef` (with hash fragment) and `missingSchema` (without it). Both properties are resolved relative to the current base id (usually schema id, unless it was substituted). Pass 'ignore' to log error during compilation and pass validation. Pass 'fail' to log error and successfully compile schema but fail validation if this rule is checked.
- _loadSchema_: asynchronous function that will be used to load remote schemas when the method `compileAsync` is used and some reference is missing (option `missingRefs` should not be 'fail' or 'ignore'). This function should accept 2 parameters: remote schema uri and node-style callback. See example in Asynchronous compilation.
- _uniqueItems_: validate `uniqueItems` keyword (true by default).
- _unicode_: calculate correct length of strings with unicode pairs (true by default). Pass `false` to use `.length` of strings that is faster, but gives "incorrect" lengths of strings with unicode pairs - each unicode pair is counted as two characters.
- _beautify_: format the generated function with [js-beautify](https://github.com/beautify-web/js-beautify) (the validating function is generated without line-breaks). `npm install js-beautify` to use this option. `true` or js-beautify options can be passed.
Expand Down
57 changes: 53 additions & 4 deletions lib/ajv.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function Ajv(opts) {
// (without using bind) so that they can be used without the instance
this.validate = validate;
this.compile = compile;
this.compileAsync = compileAsync;
this.addSchema = addSchema;
this.addMetaSchema = addMetaSchema;
this.validateSchema = validateSchema;
Expand Down Expand Up @@ -73,16 +74,61 @@ function Ajv(opts) {


/**
* Create validator for passed schema.
* Create validating function for passed schema.
* @param {String|Object} schema
* @return {Object} validation result { valid: true/false, errors: [...] }
* @return {Function} validating function
*/
function compile(schema) {
var schemaObj = _addSchema(schema);
return schemaObj.validate || _compile(schemaObj);
}


/**
* Create validating function for passed schema with asynchronous loading of missing schemas.
* `loadSchema` option should be a function that accepts schema uri and node-style callback.
* @param {String|Object} schema
* @param {Function} callback node-style callback, it is always called with 2 parameters: error (or null) and validating function.
*/
function compileAsync(schema, callback) {
var schemaObj = _addSchema(schema);
if (schemaObj.validate) callback(null, schemaObj);
else {
if (typeof self.opts.loadSchema != 'function')
throw new Error('options.loadSchema should be a function');
_compileAsync(schema, callback);
}
}


function _compileAsync(schema, callback) {

This comment has been minimized.

Copy link
@blakeembrey

blakeembrey Sep 2, 2015

Collaborator

Maybe remove console.log in production?

This comment has been minimized.

Copy link
@epoberezkin

epoberezkin Sep 2, 2015

Author Member

yes, pushed already :) There are plenty of them...

console.log('compiling', schema);
try { var validate = compile(schema); }
catch(e) {
if (e.missingSchema) loadMissingSchema(e);
else callback(e);
return;
}
callback(null, validate);

function loadMissingSchema(e) {
console.log('missing', e.missingRef, e.missingSchema);
var ref = e.missingSchema;
if (self._refs[ref] || self._schemas[ref])
return callback(new Error('Schema ' + ref + ' is loaded but' + e.missingRef + 'cannot be resolved'));
self.opts.loadSchema(ref, function(err, sch) {
console.log('loaded', ref);
console.log('schema', sch);
if (err) callback(err);
else {
addSchema(sch, ref);
_compileAsync(schema, callback);
}
});
}
}


/**
* Adds schema to the instance.
* @param {Object|Array} schema schema or array of schemas. If array is passed, `key` will be ignored.
Expand Down Expand Up @@ -219,8 +265,11 @@ function Ajv(opts) {

var currentRA = self.opts.removeAdditional;
if (currentRA && schemaObj.meta) self.opts.removeAdditional = false;
var v = compileSchema.call(self, schemaObj.schema, root, schemaObj.localRefs);
if (currentRA) self.opts.removeAdditional = currentRA;
try { var v = compileSchema.call(self, schemaObj.schema, root, schemaObj.localRefs); }
finally {
schemaObj.compiling = false;
if (currentRA) self.opts.removeAdditional = currentRA;
}

schemaObj.validate = v;
schemaObj.refs = v.refs;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ajv",
"version": "1.2.1",
"version": "1.3.0",
"description": "Another JSON Schema Validator",
"main": "lib/ajv.js",
"files": [
Expand Down
133 changes: 133 additions & 0 deletions spec/async.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
'use strict';


var Ajv = require(typeof window == 'object' ? 'ajv' : '../lib/ajv')
, should = require('chai').should()
, stableStringify = require('json-stable-stringify');


describe('compileAsync method', function() {
var ajv;

var SCHEMAS = {
"http://example.com/object.json": {
"id": "http://example.com/object.json",
"properties": {
"b": { "$ref": "int2plus.json" }
}
},
"http://example.com/int2plus.json": {
"id": "http://example.com/int2plus.json",
"type": "integer",
"minimum": 2
},
"http://example.com/tree.json": {
"id": "http://example.com/tree.json",
"type": "array",
"items": { "$ref": "leaf.json" }
},
"http://example.com/leaf.json": {
"id": "http://example.com/leaf.json",
"properties": {
"name": { "type": "string" },
"subtree": { "$ref": "tree.json" }
}
},
"http://example.com/recursive.json": {
"id": "http://example.com/recursive.json",
"properties": {
"b": { "$ref": "parent.json" }
},
"required": ["b"]
}
}

beforeEach(function() {
ajv = Ajv({ loadSchema: loadSchema });
});


it('should compile schemas loading missing schemas with options.loadSchema function', function (done) {
var schema = {
"id": "http://example.com/parent.json",
"properties": {
"a": { "$ref": "object.json" }
}
};
ajv.compileAsync(schema, function (err, validate) {
should.not.exist(err);
validate .should.be.a('function');
validate({ a: { b: 2 } }) .should.equal(true);
validate({ a: { b: 1 } }) .should.equal(false);
done();
});
});


it('should correctly load schemas when missing reference has JSON path', function (done) {
var schema = {
"id": "http://example.com/parent.json",
"properties": {
"a": { "$ref": "object.json#/properties/b" }
}
};
ajv.compileAsync(schema, function (err, validate) {
should.not.exist(err);
validate .should.be.a('function');
validate({ a: 2 }) .should.equal(true);
validate({ a: 1 }) .should.equal(false);
done();
});
});


it.skip('should correctly compile with remote schemas that have mutual references', function (done) {
var schema = {
"id": "http://example.com/root.json",
"properties": {
"tree": { "$ref": "tree.json" }
}
};
ajv.compileAsync(schema, function (err, validate) {
should.not.exist(err);
validate .should.be.a('function');
var validData = { tree: [
{ name: 'a', subtree: [ { name: 'a.a' } ] },
{ name: 'b' }
] };
var invalidData = { tree: [
{ name: 'a', subtree: [ { name: 1 } ] }
] };
validate(validData) .should.equal(true);
validate(invalidData) .should.equal(false);
done();
});
});


it('should correctly compile with remote schemas that reference the compiled schema', function (done) {
var schema = {
"id": "http://example.com/parent.json",
"properties": {
"a": { "$ref": "recursive.json" }
}
};
ajv.compileAsync(schema, function (err, validate) {
should.not.exist(err);
validate .should.be.a('function');
var validData = { a: { b: { a: { b: {} } } } };
var invalidData = { a: { b: { a: {} } } };
validate(validData) .should.equal(true);
validate(invalidData) .should.equal(false);
done();
});
});


function loadSchema(uri, callback) {
setTimeout(function() {
if (SCHEMAS[uri]) callback(null, SCHEMAS[uri]);
else callback(new Error('404'));
}, 10);
}
});
2 changes: 1 addition & 1 deletion spec/resolve.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ describe('resolve', function () {
}, done);
});

it('missingRef should and missingSchema should NOT include hash fragment', function (done) {
it('missingRef should and missingSchema should NOT include JSON path (hash fragment)', function (done) {
testMissingSchemaError({
baseId: 'http://example.com/1.json',
ref: 'int.json#/definitions/positive',
Expand Down

0 comments on commit 4dc6cbe

Please sign in to comment.