Merge pull request #391 from farrago/non-identifier-keywords

Allow custom keywords that are not valid JS identifiers (fixes issue #389)
master
Evgeny Poberezkin 2017-01-05 18:43:26 +00:00 committed by GitHub
commit 41aeb848f2
6 changed files with 70 additions and 46 deletions

View File

@ -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.

View File

@ -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;

View File

@ -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) {

View File

@ -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; }
});
});

View File

@ -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 ? '<' : '<=';
}}

View File

@ -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}}',