diff --git a/README.md b/README.md index d09f2f3..2d08625 100644 --- a/README.md +++ b/README.md @@ -906,10 +906,16 @@ Custom formats can be also added via `formats` option. Add custom validation keyword to Ajv instance. -Keyword should be a valid JavaScript identifier. - Keyword should be different from all standard JSON schema keywords and different from previously defined keywords. There is no way to redefine keywords or to remove keyword definition from the instance. +Keyword must start with a letter, `_` or `$`, and may continue with letters, numbers, `_`, `$`, or `-`. +It is recommended to use an application-specific prefix for keywords to avoid current and future name collisions. + +Example Keywords: +- `"xyz-example"`: valid, and uses prefix for the xyz project to avoid name collisions. +- `"example"`: valid, but not recommended as it could collide with future versions of JSON schema etc. +- `"3-example"`: invalid as numbers are not allowed to be the first character in a keyword + Keyword definition is an object with the following properties: - _type_: optional string or array of strings with data type(s) that the keyword applies to. If not present, the keyword will apply to all types. diff --git a/lib/dot/definitions.def b/lib/dot/definitions.def index 76150ce..8f2719f 100644 --- a/lib/dot/definitions.def +++ b/lib/dot/definitions.def @@ -3,7 +3,7 @@ var $lvl = it.level; var $dataLvl = it.dataLevel; var $schema = it.schema[$keyword]; - var $schemaPath = it.schemaPath + '.' + $keyword; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); var $errSchemaPath = it.errSchemaPath + '/' + $keyword; var $breakOnError = !it.opts.allErrors; var $errorKeyword; diff --git a/lib/keyword.js b/lib/keyword.js index 954baca..784a9a4 100644 --- a/lib/keyword.js +++ b/lib/keyword.js @@ -1,6 +1,6 @@ 'use strict'; -var IDENTIFIER = /^[a-z_$][a-z0-9_$]*$/i; +var IDENTIFIER = /^[a-z_$][a-z0-9_$\-]*$/i; var customRuleCode = require('./dotjs/custom'); module.exports = { @@ -12,7 +12,7 @@ module.exports = { /** * Define custom keyword * @this Ajv - * @param {String} keyword custom keyword, should be a valid identifier, should be different from all standard, custom and macro keywords. + * @param {String} keyword custom keyword, should be unique (including different from all standard, custom and macro keywords). * @param {Object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`. */ function addKeyword(keyword, definition) { diff --git a/spec/custom.spec.js b/spec/custom.spec.js index 6150222..5e5770f 100644 --- a/spec/custom.spec.js +++ b/spec/custom.spec.js @@ -37,7 +37,7 @@ describe('Custom keywords', function () { metaSchema: { "type": "boolean" } }); - shouldBeInvalidSchema({ "even": "not_boolean" }); + shouldBeInvalidSchema({ "x-even": "not_boolean" }); function validateEven(schema, data) { return data % 2 ? !schema : schema; @@ -69,9 +69,9 @@ describe('Custom keywords', function () { "additionalItems": false } }); - shouldBeInvalidSchema({ range: [ "1", 2 ] }); - shouldBeInvalidSchema({ range: {} }); - shouldBeInvalidSchema({ range: [ 1, 2, 3 ] }); + shouldBeInvalidSchema({ 'x-range': [ "1", 2 ] }); + shouldBeInvalidSchema({ 'x-range': {} }); + shouldBeInvalidSchema({ 'x-range': [ 1, 2, 3 ] }); function validateRange(schema, data, parentSchema) { return parentSchema.exclusiveRange === true @@ -94,7 +94,7 @@ describe('Custom keywords', function () { var valid = minOk && maxOk; if (!valid) { - var err = { keyword: 'range' }; + var err = { keyword: 'x-range' }; validateRange.errors = [err]; var comparison, limit; if (minOk) { @@ -121,7 +121,7 @@ describe('Custom keywords', function () { describe('rule with "compiled" keyword validation', function() { it('should add and validate rule', function() { testEvenKeyword({ type: 'number', compile: compileEven }); - shouldBeInvalidSchema({ "even": "not_boolean" }); + shouldBeInvalidSchema({ "x-even": "not_boolean" }); function compileEven(schema) { if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean'); @@ -138,7 +138,7 @@ describe('Custom keywords', function () { compile: compileEven, metaSchema: { "type": "boolean" } }); - shouldBeInvalidSchema({ "even": "not_boolean" }); + shouldBeInvalidSchema({ "x-even": "not_boolean" }); function compileEven(schema) { return schema ? isEven : isOdd; @@ -565,7 +565,7 @@ describe('Custom keywords', function () { metaSchema: { "type": "boolean" } }); compileCalled .should.equal(true); - shouldBeInvalidSchema({ "even": "false" }); + shouldBeInvalidSchema({ "x-even-$data": "false" }); function validateEven(schema, data) { return data % 2 ? !schema : schema; @@ -613,7 +613,7 @@ describe('Custom keywords', function () { metaSchema: { "type": "boolean" } }, 2); macroCalled .should.equal(true); - shouldBeInvalidSchema({ "even": "false" }); + shouldBeInvalidSchema({ "x-even-$data": "false" }); function validateEven(schema, data) { return data % 2 ? !schema : schema; @@ -658,7 +658,7 @@ describe('Custom keywords', function () { metaSchema: { "type": "boolean" } }); inlineCalled .should.equal(true); - shouldBeInvalidSchema({ "even": "false" }); + shouldBeInvalidSchema({ "x-even-$data": "false" }); function validateEven(schema, data) { return data % 2 ? !schema : schema; @@ -685,8 +685,8 @@ describe('Custom keywords', function () { function testEvenKeyword(definition, numErrors) { instances.forEach(function (_ajv) { - _ajv.addKeyword('even', definition); - var schema = { "even": true }; + _ajv.addKeyword('x-even', definition); + var schema = { "x-even": true }; var validate = _ajv.compile(schema); shouldBeValid(validate, 2); @@ -698,9 +698,9 @@ describe('Custom keywords', function () { function testEvenKeyword$data(definition, numErrors) { instances.forEach(function (_ajv) { - _ajv.addKeyword('even', definition); + _ajv.addKeyword('x-even-$data', definition); - var schema = { "even": true }; + var schema = { "x-even-$data": true }; var validate = _ajv.compile(schema); shouldBeValid(validate, 2); @@ -710,7 +710,7 @@ describe('Custom keywords', function () { schema = { "properties": { - "data": { "even": { "$data": "1/evenValue" } }, + "data": { "x-even-$data": { "$data": "1/evenValue" } }, "evenValue": {} } }; @@ -744,15 +744,15 @@ describe('Custom keywords', function () { function testMultipleConstantKeyword(definition, numErrors) { instances.forEach(function (_ajv) { - _ajv.addKeyword('constant', definition); + _ajv.addKeyword('x-constant', definition); var schema = { "properties": { - "a": { "constant": 1 }, - "b": { "constant": 1 } + "a": { "x-constant": 1 }, + "b": { "x-constant": 1 } }, - "additionalProperties": { "constant": { "foo": "bar" } }, - "items": { "constant": { "foo": "bar" } } + "additionalProperties": { "x-constant": { "foo": "bar" } }, + "items": { "x-constant": { "foo": "bar" } } }; var validate = _ajv.compile(schema); @@ -771,9 +771,9 @@ describe('Custom keywords', function () { function testRangeKeyword(definition, customErrors, numErrors) { instances.forEach(function (_ajv) { - _ajv.addKeyword('range', definition); + _ajv.addKeyword('x-range', definition); - var schema = { "range": [2, 4] }; + var schema = { "x-range": [2, 4] }; var validate = _ajv.compile(schema); shouldBeValid(validate, 2); @@ -782,14 +782,14 @@ describe('Custom keywords', function () { shouldBeValid(validate, 'abc'); shouldBeInvalid(validate, 1.99, numErrors); - if (customErrors) shouldBeRangeError(validate.errors[0], '', '#/range', '>=', 2); + if (customErrors) shouldBeRangeError(validate.errors[0], '', '#/x-range', '>=', 2); shouldBeInvalid(validate, 4.01, numErrors); - if (customErrors) shouldBeRangeError(validate.errors[0], '', '#/range','<=', 4); + if (customErrors) shouldBeRangeError(validate.errors[0], '', '#/x-range','<=', 4); schema = { "properties": { "foo": { - "range": [2, 4], + "x-range": [2, 4], "exclusiveRange": true } } @@ -801,23 +801,23 @@ describe('Custom keywords', function () { shouldBeValid(validate, { foo: 3.99 }); shouldBeInvalid(validate, { foo: 2 }, numErrors); - if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '#/properties/foo/range', '>', 2, true); + if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '#/properties/foo/x-range', '>', 2, true); shouldBeInvalid(validate, { foo: 4 }, numErrors); - if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '#/properties/foo/range', '<', 4, true); + if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '#/properties/foo/x-range', '<', 4, true); }); } function testMultipleRangeKeyword(definition, numErrors) { instances.forEach(function (_ajv) { - _ajv.addKeyword('range', definition); + _ajv.addKeyword('x-range', definition); var schema = { "properties": { - "a": { "range": [2, 4], "exclusiveRange": true }, - "b": { "range": [2, 4], "exclusiveRange": false } + "a": { "x-range": [2, 4], "exclusiveRange": true }, + "b": { "x-range": [2, 4], "exclusiveRange": false } }, - "additionalProperties": { "range": [5, 7] }, - "items": { "range": [5, 7] } + "additionalProperties": { "x-range": [5, 7] }, + "items": { "x-range": [5, 7] } }; var validate = _ajv.compile(schema); @@ -836,7 +836,7 @@ describe('Custom keywords', function () { delete error.schema; delete error.data; error .should.eql({ - keyword: 'range', + keyword: 'x-range', dataPath: dataPath, schemaPath: schemaPath, message: 'should be ' + comparison + ' ' + limit, @@ -906,15 +906,33 @@ describe('Custom keywords', function () { } }); - it('should throw if keyword is not a valid identifier', function() { + it('should throw if keyword is not a valid name', function() { should.not.throw(function() { ajv.addKeyword('mykeyword', { validate: function() { return true; } }); }); + should.not.throw(function() { + ajv.addKeyword('hyphens-are-valid', { + validate: function() { return true; } + }); + }); + should.throw(function() { - ajv.addKeyword('my-keyword', { + ajv.addKeyword('3-start-with-number-not-valid`', { + validate: function() { return true; } + }); + }); + + should.throw(function() { + ajv.addKeyword('-start-with-hyphen-not-valid`', { + validate: function() { return true; } + }); + }); + + should.throw(function() { + ajv.addKeyword('spaces not valid`', { validate: function() { return true; } }); }); diff --git a/spec/custom_rules/range.jst b/spec/custom_rules/range.jst index 74f5686..adb481e 100644 --- a/spec/custom_rules/range.jst +++ b/spec/custom_rules/range.jst @@ -1,7 +1,7 @@ {{ var $data = 'data' + (it.dataLevel || '') - , $min = it.schema.range[0] - , $max = it.schema.range[1] + , $min = it.schema['x-range'][0] + , $max = it.schema['x-range'][1] , $gt = it.schema.exclusiveRange ? '>' : '>=' , $lt = it.schema.exclusiveRange ? '<' : '<='; }} diff --git a/spec/custom_rules/range_with_errors.jst b/spec/custom_rules/range_with_errors.jst index a9bb7da..6bfb6e5 100644 --- a/spec/custom_rules/range_with_errors.jst +++ b/spec/custom_rules/range_with_errors.jst @@ -1,7 +1,7 @@ {{ var $data = 'data' + (it.dataLevel || '') - , $min = it.schema.range[0] - , $max = it.schema.range[1] + , $min = it.schema['x-range'][0] + , $max = it.schema['x-range'][1] , $exclusive = !!it.schema.exclusiveRange , $gt = $exclusive ? '>' : '>=' , $lt = $exclusive ? '<' : '<=' @@ -16,7 +16,7 @@ if (!valid{{=$lvl}}) { var {{=$err}}; if (minOk{{=$lvl}}) { {{=$err}} = { - keyword: 'range', + keyword: 'x-range', message: 'should be {{=$lt}} {{=$max}}', params: { comparison: '{{=$lt}}', @@ -26,7 +26,7 @@ if (!valid{{=$lvl}}) { }; } else { {{=$err}} = { - keyword: 'range', + keyword: 'x-range', message: 'should be {{=$gt}} {{=$min}}', params: { comparison: '{{=$gt}}',