diff --git a/lib/compile/rules.js b/lib/compile/rules.js index 4ab254494..922c21aa7 100644 --- a/lib/compile/rules.js +++ b/lib/compile/rules.js @@ -33,6 +33,7 @@ module.exports = function rules() { RULES.keywords = util.toHash(RULES.all.concat(RULES.keywords)); RULES.all = util.toHash(RULES.all); RULES.types = util.toHash(RULES.types); + RULES.custom = {}; return RULES; }; diff --git a/lib/dot/custom.jst b/lib/dot/custom.jst index 68e7d0f77..e92985a49 100644 --- a/lib/dot/custom.jst +++ b/lib/dot/custom.jst @@ -1,17 +1,38 @@ {{# def.definitions }} {{# def.errors }} {{# def.setupKeyword }} +{{# def.$data }} {{ var $rule = this - , $ruleValidate = it.useCustomRule($rule, $schema, it.schema, it) - , $ruleErrs = $ruleValidate.code + '.errors' + , $rDef = $rule.definition + , $validate = $rDef.validate + , $compile = $rDef.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}}){{?}}; +{{??}} + {{ + $ruleValidate = it.useCustomRule($rule, $schema, it.schema, it); + $schemaValue = 'validate.schema' + $schemaPath; + $validateCode = $ruleValidate.code; + $inline = $rDef.inline; + $macro = $rDef.macro; + }} +{{?}} + +{{ + var $ruleErrs = $validateCode + '.errors' , $i = 'i' + $lvl , $ruleErr = 'ruleErr' + $lvl - , $rDef = $rule.definition - , $asyncKeyword = $rDef.async - , $inline = $rDef.inline - , $macro = $rDef.macro; + , $asyncKeyword = $rDef.async; if ($asyncKeyword && !it.async) throw new Error('async keyword in sync schema'); @@ -23,12 +44,12 @@ var {{=$errs}} = errors; var valid{{=$lvl}}; {{## def.callRuleValidate: - {{=$ruleValidate.code}}.call( + {{=$validateCode}}.call( {{? it.opts.passContext }}this{{??}}self{{?}} - {{? $rDef.compile || $rDef.schema === false }} + {{? $compile || $rDef.schema === false }} , {{=$data}} {{??}} - , validate.schema{{=$schemaPath}} + , {{=$schemaValue}} , {{=$data}} , validate.schema{{=it.schemaPath}} {{?}} @@ -69,7 +90,7 @@ var valid{{=$lvl}}; {{=$ruleErr}}.schemaPath = "{{=$errSchemaPath}}"; {{# _inline ? '}' : '' }} {{? it.opts.verbose }} - {{=$ruleErr}}.schema = validate.schema{{=$schemaPath}}; + {{=$ruleErr}}.schema = {{=$schemaValue}}; {{=$ruleErr}}.data = {{=$data}}; {{?}} } @@ -84,10 +105,10 @@ var valid{{=$lvl}}; $it.schemaPath = ''; }} {{# def.setCompositeRule }} - {{ var $code = it.validate($it).replace(/validate\.schema/g, $ruleValidate.code); }} + {{ var $code = it.validate($it).replace(/validate\.schema/g, $validateCode); }} {{# def.resetCompositeRule }} {{= $code }} -{{?? $rDef.compile || $rDef.validate }} +{{?? $compile || $validate }} {{# def.beginDefOut}} {{# def.callRuleValidate }} {{# def.storeDefOut:def_callRuleValidate }} @@ -104,7 +125,7 @@ var valid{{=$lvl}}; else throw e; } {{??}} - {{=$ruleValidate.code}}.errors = null; + {{=$validateCode}}.errors = null; {{?}} {{?}} {{?}} diff --git a/lib/keyword.js b/lib/keyword.js index 38209f7ab..d55a50b00 100644 --- a/lib/keyword.js +++ b/lib/keyword.js @@ -29,8 +29,28 @@ module.exports = function addKeyword(keyword, definition) { _addRule(keyword, dataType, definition); } - if (definition.metaSchema) - definition.validateSchema = self.compile(definition.metaSchema, true); + 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'); + } + } + + var metaSchema = definition.metaSchema; + if (metaSchema) { + if ($data) { + metaSchema = { + anyOf: [ + metaSchema, + { '$ref': 'https://raw.githubusercontent.com/epoberezkin/ajv/master/lib/refs/json-schema-v5.json#/definitions/$data' } + ] + }; + } + definition.validateSchema = self.compile(metaSchema, true); + } } this.RULES.keywords[keyword] = true; @@ -59,6 +79,7 @@ module.exports = function addKeyword(keyword, definition) { code: customRuleCode }; ruleGroup.rules.push(rule); + self.RULES.custom[keyword] = rule; } diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 2c97daa51..7dcb77b0d 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -507,6 +507,72 @@ describe('Custom keywords', function () { }); + describe('$data reference support with custom keywords (v5 only)', function() { + beforeEach(function() { + instances = getAjvInstances({ + allErrors: true, + verbose: true, + inlineRefs: false + }, { v5: true }); + ajv = instances[0]; + }); + + it('should validate "interpreted" rule', function() { + testEvenKeyword$data({ + type: 'number', + $data: true, + validate: validateEven + }); + + function validateEven(schema, data) { + if (typeof schema != 'boolean') return false; + return data % 2 ? !schema : schema; + } + }); + + it('should validate "compiled" rule', function() { + testEvenKeyword$data({ + type: 'number', + $data: true, + compile: compileEven + }); + shouldBeInvalidSchema({ "even": "false" }); + + function compileEven(schema) { + if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean'); + return schema ? isEven : isOdd; + } + + function isEven(data) { return data % 2 === 0; } + function isOdd(data) { return data % 2 !== 0; } + }); + + it('should validate "interpreted" rule with meta-schema', function() { + testEvenKeyword$data({ + type: 'number', + $data: true, + validate: validateEven, + metaSchema: { "type": "boolean" } + }); + shouldBeInvalidSchema({ "even": "false" }); + + function validateEven(schema, data) { + return data % 2 ? !schema : schema; + } + }); + + it('should fail if keyword definition has "$data" but no "validate" or "compile"', function() { + should.throw(function() { + ajv.addKeyword('even', { + type: 'number', + $data: true, + macro: function() { return {}; } + }); + }); + }); + }); + + function testEvenKeyword(definition, numErrors) { instances.forEach(function (ajv) { ajv.addKeyword('even', definition); @@ -520,6 +586,30 @@ describe('Custom keywords', function () { }); } + function testEvenKeyword$data(definition, numErrors) { + instances.forEach(function (ajv) { + var schema = { + "properties": { + "data": { "even": { "$data": "1/evenValue" } }, + "evenValue": {} + } + }; + ajv.addKeyword('even', definition); + var 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); + shouldBeValid(validate, { data: 2.5, evenValue: false }); + shouldBeInvalid(validate, { data: 3, evenValue: true }, numErrors); + shouldBeValid(validate, { data: 3, evenValue: false }); + + // shouldBeInvalid(validate, { data: 2, evenValue: "true" }); + }); + } + function testConstantKeyword(definition, numErrors) { instances.forEach(function (ajv) { ajv.addKeyword('constant', definition);