Skip to content

Commit

Permalink
async schemas and async formats using generators, #40
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Jan 24, 2016
1 parent a8a7b2d commit 7a0e4fb
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 22 deletions.
9 changes: 9 additions & 0 deletions lib/compile/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,12 @@ function vars(arr, statement) {
*/

var ucs2length = util.ucs2length;


// this function is used by async schemas to return validation errors via exception
function throwErrors(errors) {
var err = new Error('validation failed');
err.errors = errors;
err.ajv = err.validation = true;
throw err;
}
18 changes: 12 additions & 6 deletions lib/compile/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,21 @@ function cleanUpCode(out) {

var ERRORS_REGEXP = /[^v\.]errors/g
, REMOVE_ERRORS = /var errors = 0;|var vErrors = null;|validate.errors = vErrors;/g
, REMOVE_ERRORS_ASYNC = /var errors = 0;|var vErrors = null;/g
, RETURN_VALID = 'return errors === 0;'
, RETURN_TRUE = 'validate.errors = null; return true;';
, RETURN_TRUE = 'validate.errors = null; return true;'
, RETURN_ASYNC = /if \(errors === 0\) return true;\s*else throwErrors\(vErrors\);/
, RETURN_TRUE_ASYNC = 'return true;';

function cleanUpVarErrors(out) {
function cleanUpVarErrors(out, async) {
var matches = out.match(ERRORS_REGEXP);
if (matches && matches.length === 2)
return out.replace(REMOVE_ERRORS, '')
.replace(RETURN_VALID, RETURN_TRUE);
else
if (matches && matches.length === 2) {
return async
? out.replace(REMOVE_ERRORS_ASYNC, '')
.replace(RETURN_ASYNC, RETURN_TRUE_ASYNC)
: out.replace(REMOVE_ERRORS, '')
.replace(RETURN_VALID, RETURN_TRUE);
} else
return out;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/dot/definitions.def
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
{{## def.cleanUp: {{ out = it.util.cleanUpCode(out); }} #}}


{{## def.cleanUpVarErrors: {{ out = it.util.cleanUpVarErrors(out); }} #}}
{{## def.cleanUpVarErrors: {{ out = it.util.cleanUpVarErrors(out, $async); }} #}}


{{## def.$data:
Expand Down
16 changes: 12 additions & 4 deletions lib/dot/errors.def
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,12 @@
{{# def.storeDefOut:__err }}

{{? !it.compositeRule && $breakOnError }}
validate.errors = [{{=__err}}];
return false;
{{? it.async }}
throwErrors([{{=__err}}]);
{{??}}
validate.errors = [{{=__err}}];
return false;
{{?}}
{{??}}
var err = {{=__err}};
{{# def._addError:_rule }}
Expand All @@ -54,8 +58,12 @@
{{## def.extraError:_rule:
{{# def.addError:_rule}}
{{? !it.compositeRule && $breakOnError }}
validate.errors = vErrors;
return false
{{? it.async }}
throwErrors(vErrors);
{{??}}
validate.errors = vErrors;
return false
{{?}}
{{?}}
#}}

Expand Down
12 changes: 10 additions & 2 deletions lib/dot/format.jst
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,17 @@
var $isObject = typeof $format == 'object'
&& !($format instanceof RegExp)
&& $format.validate;
if ($isObject) $format = $format.validate;
if ($isObject) {
var $async = $format.async === true;
$format = $format.validate;
}
}}
if (!{{# def.checkFormat }}) {
{{? $async }}
{{ var $formatRef = 'formats' + it.util.getProperty($schema) + '.validate'; }}
if (!(yield {{=$formatRef}}({{=$data}}))) {
{{??}}
if (!{{# def.checkFormat }}) {
{{?}}
{{?}}
{{# def.error:'format' }}
} {{? $breakOnError }} else { {{?}}
21 changes: 16 additions & 5 deletions lib/dot/validate.jst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* validateRef etc. are defined in the parent scope in index.js
*/ }}

{{ var $async = it.schema.$async === true; }}

{{? it.isTop}}
{{
var $top = it.isTop
Expand All @@ -22,12 +24,13 @@
, $data = 'data';
it.rootId = it.resolve.fullPath(it.root.schema.id);
it.baseId = it.baseId || it.rootId;
if ($async) it.async = { current: true };
delete it.isTop;

it.dataPathArr = [undefined];
}}

validate = function (data, dataPath{{? it.opts.coerceTypes }}, parentData, parentDataProperty{{?}}) {
validate = function{{?$async}} *{{?}}(data, dataPath{{? it.opts.coerceTypes }}, parentData, parentDataProperty{{?}}) {
'use strict';
var vErrors = null; {{ /* don't edit, used in replace */ }}
var errors = 0; {{ /* don't edit, used in replace */ }}
Expand All @@ -38,6 +41,9 @@
, $data = 'data' + ($dataLvl || '');

if (it.schema.id) it.baseId = it.resolve.url(it.baseId, it.schema.id);

if (it.async) it.async.current = $async;
else if ($async) throw new Error('async schema in sync schema');
}}

var errs_{{=$lvl}} = errors;
Expand Down Expand Up @@ -131,11 +137,16 @@
{{? $breakOnError }} {{= $closingBraces2 }} {{?}}

{{? $top }}
validate.errors = vErrors; {{ /* don't edit, used in replace */ }}
return errors === 0; {{ /* don't edit, used in replace */ }}
}
{{? $async }}
if (errors === 0) return true; {{ /* don't edit, used in replace */ }}
else throwErrors(vErrors); {{ /* don't edit, used in replace */ }}
{{??}}
validate.errors = vErrors; {{ /* don't edit, used in replace */ }}
return errors === 0; {{ /* don't edit, used in replace */ }}
{{?}}
};
{{??}}
var {{=$valid}} = errors === errs_{{=$lvl}};
var {{=$valid}} = errors === errs_{{=$lvl}};
{{?}}

{{# def.cleanUp }}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"brfs": "^0.0.8",
"browserify": "^11.0.1",
"chai": "^3.0.0",
"co": "^4.6.0",
"coveralls": "^2.11.4",
"dot": "^1.0.3",
"glob": "^5.0.10",
Expand Down
125 changes: 125 additions & 0 deletions spec/async_validate.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
'use strict';

var isBrowser = typeof window == 'object';
try { eval("(function*(){})()"); var hasGenerators = true; } catch(e){}
var skipTest = isBrowser || !hasGenerators;

var Ajv = require('./ajv')
, should = require('./chai').should();

if (!skipTest) var co = require('' + 'co');


(skipTest ? describe.skip : describe)
('async schemas, formats and keywords', function() {
var ajv, fullAjv;

beforeEach(function () {
ajv = Ajv();
fullAjv = Ajv({ allErrors: true });
});

describe('async schemas without async elements', function() {
it('should pass result via callback in setTimeout', function() {
var schema = {
$async: true,
type: 'string',
maxLength: 3
};

return Promise.all([
test(ajv),
test(fullAjv)
]);

function test(ajv) {
var validate = ajv.compile(schema);

return Promise.all([
shouldBeValid( co(validate('abc')) ),
shouldBeInvalid( co(validate('abcd')) ),
shouldBeInvalid( co(validate(1)) )
]);
}
});

it('should fail compilation if async schema is inside sync schema', function() {
var schema = {
properties: {
foo: {
$async: true,
type: 'string',
maxLength: 3
}
}
};

should.throw(function() {
ajv.compile(schema);
});

schema.$async = true;

ajv.compile(schema);
});
});


describe('async formats', function() {
it('should return promise that resolves as true or rejects with array of errors', function() {
var schema = {
$async: true,
type: 'string',
format: 'english_word',
minimum: 5
};

return Promise.all([
test(ajv),
test(fullAjv)
]);

function test(ajv) {
ajv.addFormat('english_word', {
async: true,
validate: checkWordOnServer
});

var validate = ajv.compile(schema);

return Promise.all([
shouldBeValid( co(validate('tomorrow')) ),
shouldBeInvalid( co(validate('manana')) ),
shouldBeInvalid( co(validate(1)) )
]);
}

function checkWordOnServer(str) {
return str == 'tomorrow' ? Promise.resolve(true)
: str == 'manana' ? Promise.resolve(false)
: Promise.reject(new Error('unknown word'));
}
});
});
});


function shouldBeValid(p) {
return p.then(function (valid) {
valid .should.equal(true);
});
}


var SHOULD_BE_INVALID = 'test: should be invalid';
function shouldBeInvalid(p) {
return p.then(function (valid) {
throw new Error(SHOULD_BE_INVALID);
})
.catch(function (err) {
if (err.message == SHOULD_BE_INVALID) throw err;
err. should.be.instanceof(Error);
err.errors .should.be.an('array');
err.validation .should.equal(true);
});
}
16 changes: 12 additions & 4 deletions spec/json-schema.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

var jsonSchemaTest = require('json-schema-test')
, getAjvInstances = require('./ajv_instances')
, options = require('./ajv_options');
, options = require('./ajv_options')
, should = require('./chai').should();

var instances = getAjvInstances(options);

Expand Down Expand Up @@ -50,9 +51,16 @@ jsonSchemaTest(instances, {
afterError: function (res) {
console.log('ajv options:', res.validator._opts);
},
// afterEach: function (res) {
// console.log(res.errors);
// },
afterEach: function (res) {
// console.log(res.errors);
res.valid .should.be.a('boolean');
if (res.valid === true ) should.equal(res.errors, null);
else {
res.errors .should.be.an('array');
for (var i=0; i<res.errors.length; i++)
res.errors[i] .should.be.an('object');
}
},
cwd: __dirname,
hideFolder: 'draft4/',
timeout: 90000
Expand Down

0 comments on commit 7a0e4fb

Please sign in to comment.