diff --git a/CUSTOM.md b/CUSTOM.md index 6a5cbe8..4d3065e 100644 --- a/CUSTOM.md +++ b/CUSTOM.md @@ -92,6 +92,8 @@ In some cases it is the best approach to define keywords, but it has the perform All custom keywords types can have an optional `metaSchema` property in their definitions. It is a schema against which the value of keyword will be validated during schema compilation. +Custom keyword can also have an optional `dependencies` property in their definitions - it is a list of required keywords in a containing (parent) schema. + Example. `range` and `exclusiveRange` keywords using compiled schema: ```javascript diff --git a/README.md b/README.md index ee12e36..547c374 100644 --- a/README.md +++ b/README.md @@ -1005,6 +1005,7 @@ Keyword definition is an object with the following properties: - _inline_: compiling function that returns code (as string) - _schema_: an optional `false` value used with "validate" keyword to not pass schema - _metaSchema_: an optional meta-schema for keyword schema +- _dependencies_: an optional list of properties that must be present in the parent schema - it will be checked during schema compilation - _modifying_: `true` MUST be passed if keyword modifies data - _valid_: pass `true`/`false` to pre-define validation result, the result returned from validation function will be ignored. This option cannot be used with macro keywords. - _$data_: an optional `true` value to support [$data reference](#data-reference) as the value of custom keyword. The reference will be resolved at validation time. If the keyword has meta-schema it would be extended to allow $data and it will be used to validate the resolved value. Supporting $data reference requires that keyword has validating function (as the only option or in addition to compile, macro or inline function). diff --git a/lib/compile/index.js b/lib/compile/index.js index 8e369db..bac5627 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -255,13 +255,22 @@ function compile(schema, root, localRefs, baseId) { } function useCustomRule(rule, schema, parentSchema, it) { - var validateSchema = rule.definition.validateSchema; - if (validateSchema && self._opts.validateSchema !== false) { - var valid = validateSchema(schema); - if (!valid) { - var message = 'keyword schema is invalid: ' + self.errorsText(validateSchema.errors); - if (self._opts.validateSchema == 'log') self.logger.error(message); - else throw new Error(message); + if (self._opts.validateSchema !== false) { + var deps = rule.definition.dependencies; + if (deps && !deps.every(function(keyword) { + return Object.prototype.hasOwnProperty.call(parentSchema, keyword); + })) { + throw new Error('parent schema must have all required keywords: ' + deps.join(',')); + } + + var validateSchema = rule.definition.validateSchema; + if (validateSchema) { + var valid = validateSchema(schema); + if (!valid) { + var message = 'keyword schema is invalid: ' + self.errorsText(validateSchema.errors); + if (self._opts.validateSchema == 'log') self.logger.error(message); + else throw new Error(message); + } } } diff --git a/lib/keyword.js b/lib/keyword.js index 6ed84c1..950dceb 100644 --- a/lib/keyword.js +++ b/lib/keyword.js @@ -24,7 +24,7 @@ function addKeyword(keyword, definition) { if (RULES.keywords[keyword]) throw new Error('Keyword ' + keyword + ' is already defined'); - if (!IDENTIFIER.test(keyword)) + if (!isIdentifier(keyword)) throw new Error('Keyword ' + keyword + ' is not a valid identifier'); if (definition) { @@ -45,6 +45,10 @@ function addKeyword(keyword, definition) { if ($data && !definition.validate) throw new Error('$data support: "validate" function is not defined'); + var deps = definition.dependencies; + if (deps && !(Array.isArray(deps) && deps.every(isIdentifier))) + throw new Error('"dependencies" option should be a list of valid identifiers'); + var metaSchema = definition.metaSchema; if (metaSchema) { if ($data) { @@ -93,6 +97,10 @@ function addKeyword(keyword, definition) { if (!RULES.types[dataType]) throw new Error('Unknown type ' + dataType); } + function isIdentifier(name) { + return typeof name == 'string' && IDENTIFIER.test(name); + } + return this; } diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 21635bc..d2891da 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -1182,4 +1182,50 @@ describe('Custom keywords', function () { }); }); }); + + + describe('"dependencies" in keyword definition', function() { + it("should require properties in the parent schema", function() { + ajv.addKeyword('allRequired', { + macro: function(schema, parentSchema) { + return schema ? {required: Object.keys(parentSchema.properties)} : true; + }, + metaSchema: {type: 'boolean'}, + dependencies: ['properties'] + }); + + var invalidSchema = { + allRequired: true + }; + + should.throw(function () { + ajv.compile(invalidSchema); + }); + + var schema = { + properties: { + foo: true + }, + allRequired: true + }; + + var v = ajv.compile(schema); + v({foo: 1}) .should.equal(true); + v({}) .should.equal(false); + }); + + it("'dependencies'should be array of valid strings", function() { + ajv.addKeyword('newKeyword1', { + metaSchema: {type: 'boolean'}, + dependencies: ['dep1', 'dep-2'] + }); + + should.throw(function () { + ajv.addKeyword('newKeyword2', { + metaSchema: {type: 'boolean'}, + dependencies: ['dep.1'] + }); + }); + }); + }); });