Skip to content

Commit

Permalink
feat: support meta-schema validation in custom keywords with $data su…
Browse files Browse the repository at this point in the history
…pport, #146
  • Loading branch information
epoberezkin committed Jul 22, 2016
1 parent 7f5fe70 commit 5a80c7c
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 28 deletions.
2 changes: 0 additions & 2 deletions lib/compile/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,7 @@ function vars(arr, statement) {
*/

var co = require('co');

var ucs2length = util.ucs2length;

var equal = require('./equal');

// this error is thrown by async schemas to return validation errors via exception
Expand Down
22 changes: 15 additions & 7 deletions lib/dot/custom.jst
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,29 @@

{{
var $rule = this
, $definition = 'definition' + $lvl
, $rDef = $rule.definition
, $validate = $rDef.validate
, $compile = $rDef.compile
, $compile
, $inline
, $macro
, $ruleValidate
, $validateCode;
}}

{{? $isData && $rDef.$data }}
{{ $validateCode = 'keywordValidate' + $lvl; }}
var {{=$validateCode}} = RULES.custom['{{=$keyword}}'].definition
{{? $validate }}.validate{{??}}.compile({{=$schemaValue}}, validate.schema{{=it.schemaPath}}){{?}};
{{
$validateCode = 'keywordValidate' + $lvl;
var $validateSchema = $rDef.validateSchema;
}}
var {{=$definition}} = RULES.custom['{{=$keyword}}'].definition;
var {{=$validateCode}} = {{=$definition}}.validate;
{{??}}
{{
$ruleValidate = it.useCustomRule($rule, $schema, it.schema, it);
$schemaValue = 'validate.schema' + $schemaPath;
$validateCode = $ruleValidate.code;
$compile = $rDef.compile;
$inline = $rDef.inline;
$macro = $rDef.macro;
}}
Expand Down Expand Up @@ -108,7 +113,7 @@ var valid{{=$lvl}};
{{ var $code = it.validate($it).replace(/validate\.schema/g, $validateCode); }}
{{# def.resetCompositeRule }}
{{= $code }}
{{?? $compile || $validate }}
{{?? !$inline }}
{{# def.beginDefOut}}
{{# def.callRuleValidate }}
{{# def.storeDefOut:def_callRuleValidate }}
Expand All @@ -131,7 +136,10 @@ var valid{{=$lvl}};
{{?}}


if (!{{# def.ruleValidationResult }}) {
if ({{? $validateSchema }}
!{{=$definition}}.validateSchema({{=$schemaValue}}) ||
{{?}}
!{{# def.ruleValidationResult }}) {
{{ $errorKeyword = $rule.keyword; }}
{{# def.beginDefOut}}
{{# def.error:'custom' }}
Expand All @@ -153,7 +161,7 @@ if (!{{# def.ruleValidationResult }}) {
}
{{?}}
{{?}}
{{?? $macro}}
{{?? $macro }}
{{# def.extraError:'custom' }}
{{??}}
{{? $rDef.errors === false}}
Expand Down
10 changes: 2 additions & 8 deletions lib/keyword.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,8 @@ module.exports = function addKeyword(keyword, definition) {
}

var $data = definition.$data === true && this._opts.v5;
if ($data) {
if (!definition.validate) {
if (definition.compile)
console.warn('$data support: it is recommended to define "validate" function');
else
throw new Error('$data support: neither "validate" nor "compile" functions are defined');
}
}
if ($data && !definition.validate)
throw new Error('$data support: neither "validate" nor "compile" functions are defined');

var metaSchema = definition.metaSchema;
if (metaSchema) {
Expand Down
141 changes: 130 additions & 11 deletions spec/custom.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -530,15 +530,23 @@ describe('Custom keywords', function () {
}
});

it('should validate "compiled" rule', function() {
it('should validate rule with "compile" and "validate" funcs', function() {
var compileCalled;
testEvenKeyword$data({
type: 'number',
$data: true,
compile: compileEven
compile: compileEven,
validate: validateEven
});
shouldBeInvalidSchema({ "even": "false" });
compileCalled .should.equal(true);

function validateEven(schema, data) {
if (typeof schema != 'boolean') return false;
return data % 2 ? !schema : schema;
}

function compileEven(schema) {
compileCalled = true;
if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean');
return schema ? isEven : isOdd;
}
Expand All @@ -547,21 +555,123 @@ describe('Custom keywords', function () {
function isOdd(data) { return data % 2 !== 0; }
});

it('should validate "interpreted" rule with meta-schema', function() {
it('should validate with "compile" and "validate" funcs with meta-schema', function() {
var compileCalled;
testEvenKeyword$data({
type: 'number',
$data: true,
compile: compileEven,
validate: validateEven,
metaSchema: { "type": "boolean" }
});
compileCalled .should.equal(true);
shouldBeInvalidSchema({ "even": "false" });

function validateEven(schema, data) {
return data % 2 ? !schema : schema;
}

function compileEven(schema) {
compileCalled = true;
return schema ? isEven : isOdd;
}

function isEven(data) { return data % 2 === 0; }
function isOdd(data) { return data % 2 !== 0; }
});

it('should validate rule with "macro" and "validate" funcs', function() {
var macroCalled;
testEvenKeyword$data({
type: 'number',
$data: true,
macro: macroEven,
validate: validateEven
}, 2);
macroCalled .should.equal(true);

function validateEven(schema, data) {
if (typeof schema != 'boolean') return false;
return data % 2 ? !schema : schema;
}

function macroEven(schema) {
macroCalled = true;
if (schema === true) return { "multipleOf": 2 };
if (schema === false) return { "not": { "multipleOf": 2 } };
throw new Error('Schema for "even" keyword should be boolean');
}
});

it('should validate with "macro" and "validate" funcs with meta-schema', function() {
var macroCalled;
testEvenKeyword$data({
type: 'number',
$data: true,
macro: macroEven,
validate: validateEven,
metaSchema: { "type": "boolean" }
}, 2);
macroCalled .should.equal(true);
shouldBeInvalidSchema({ "even": "false" });

function validateEven(schema, data) {
return data % 2 ? !schema : schema;
}

function macroEven(schema) {
macroCalled = true;
if (schema === true) return { "multipleOf": 2 };
if (schema === false) return { "not": { "multipleOf": 2 } };
}
});

it('should fail if keyword definition has "$data" but no "validate" or "compile"', function() {
it('should validate rule with "inline" and "validate" funcs', function() {
var inlineCalled;
testEvenKeyword$data({
type: 'number',
$data: true,
inline: inlineEven,
validate: validateEven
});
inlineCalled .should.equal(true);

function validateEven(schema, data) {
if (typeof schema != 'boolean') return false;
return data % 2 ? !schema : schema;
}

function inlineEven(it, keyword, schema) {
inlineCalled = true;
var op = schema ? '===' : '!==';
return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0';
}
});

it('should validate with "inline" and "validate" funcs with meta-schema', function() {
var inlineCalled;
testEvenKeyword$data({
type: 'number',
$data: true,
inline: inlineEven,
validate: validateEven,
metaSchema: { "type": "boolean" }
});
inlineCalled .should.equal(true);
shouldBeInvalidSchema({ "even": "false" });

function validateEven(schema, data) {
return data % 2 ? !schema : schema;
}

function inlineEven(it, keyword, schema) {
inlineCalled = true;
var op = schema ? '===' : '!==';
return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0';
}
});

it('should fail if keyword definition has "$data" but no "validate"', function() {
should.throw(function() {
ajv.addKeyword('even', {
type: 'number',
Expand All @@ -588,25 +698,34 @@ describe('Custom keywords', function () {

function testEvenKeyword$data(definition, numErrors) {
instances.forEach(function (ajv) {
var schema = {
ajv.addKeyword('even', definition);

var schema = { "even": true };
var validate = ajv.compile(schema);

shouldBeValid(validate, 2);
shouldBeValid(validate, 'abc');
shouldBeInvalid(validate, 2.5, numErrors);
shouldBeInvalid(validate, 3, numErrors);

schema = {
"properties": {
"data": { "even": { "$data": "1/evenValue" } },
"evenValue": {}
}
};
ajv.addKeyword('even', definition);
var validate = ajv.compile(schema);
validate = ajv.compile(schema);

shouldBeValid(validate, { data: 2, evenValue: true });
shouldBeInvalid(validate, { data: 2, evenValue: false });
shouldBeValid(validate, { data: 'abc', evenValue: true });
shouldBeValid(validate, { data: 'abc', evenValue: false });
shouldBeInvalid(validate, { data: 2.5, evenValue: true }, numErrors);
shouldBeInvalid(validate, { data: 2.5, evenValue: true });
shouldBeValid(validate, { data: 2.5, evenValue: false });
shouldBeInvalid(validate, { data: 3, evenValue: true }, numErrors);
shouldBeInvalid(validate, { data: 3, evenValue: true });
shouldBeValid(validate, { data: 3, evenValue: false });

// shouldBeInvalid(validate, { data: 2, evenValue: "true" });
shouldBeInvalid(validate, { data: 2, evenValue: "true" });
});
}

Expand Down

0 comments on commit 5a80c7c

Please sign in to comment.