Revert "feat: meta schema for custom keywords schemas, closes #230"

This reverts commit 1a6e4b576c.
master
Evgeny Poberezkin 2016-07-18 18:51:57 +01:00
parent 1a6e4b576c
commit 09879d21ad
No known key found for this signature in database
GPG Key ID: 016D62451CED9D8E
6 changed files with 84 additions and 185 deletions

View File

@ -60,8 +60,6 @@ Compilation function will be called during schema compilation. It will be passed
In some cases it is the best approach to define keywords, but it has the performance cost of an extra function call during validation. If keyword logic can be expressed via some other JSON-schema then `macro` keyword definition is more efficient (see below).
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.
Example. `range` and `exclusiveRange` keywords using compiled schema:
```javascript
@ -72,11 +70,7 @@ ajv.addKeyword('range', { type: 'number', compile: function (sch, parentSchema)
return parentSchema.exclusiveRange === true
? function (data) { return data > min && data < max; }
: function (data) { return data >= min && data <= max; }
}, errors: false, metaSchema: {
type: 'array',
items: [ { type: 'number' }, { type: 'number' } ],
additionalItems: false
} });
}, errors: false });
var schema = { "range": [2, 4], "exclusiveRange": true };
var validate = ajv.compile(schema);
@ -108,10 +102,6 @@ ajv.addKeyword('range', { type: 'number', macro: function (schema, parentSchema)
exclusiveMinimum: !!parentSchema.exclusiveRange,
exclusiveMaximum: !!parentSchema.exclusiveRange
};
}, metaSchema: {
type: 'array',
items: [ { type: 'number' }, { type: 'number' } ],
additionalItems: false
} });
```
@ -159,7 +149,7 @@ Example `even` keyword:
ajv.addKeyword('even', { type: 'number', inline: function (it, keyword, schema) {
var op = schema ? '===' : '!==';
return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0';
}, metaSchema: { type: 'boolean' } });
} });
var schema = { "even": true };
@ -189,12 +179,7 @@ var valid{{=it.level}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{
ajv.addKeyword('range', {
type: 'number',
inline: inlineRangeTemplate,
statements: true,
metaSchema: {
type: 'array',
items: [ { type: 'number' }, { type: 'number' } ],
additionalItems: false
}
statements: true
});
```

View File

@ -813,8 +813,6 @@ Keyword definition is an object with the following properties:
- _compile_: compiling function
- _macro_: macro function
- _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
- _async_: an optional `true` value if the validation function is asynchronous (whether it is compiled or passed in _validate_ property); in this case it should return a promise that resolves with a value `true` or `false`. This option is ignored in case of "macro" and "inline" keywords.
- _errors_: an optional boolean indicating whether keyword returns errors. If this property is not set Ajv will determine if the errors were set in case of failed validation.

View File

@ -101,12 +101,10 @@ function Ajv(opts) {
/**
* Create validating function for passed schema.
* @param {Object} schema schema object
* @param {Boolean} _meta true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords.
* @return {Function} validating function
*/
function compile(schema, _meta) {
function compile(schema) {
var schemaObj = _addSchema(schema);
schemaObj.meta = _meta;
return schemaObj.validate || _compile(schemaObj);
}
@ -155,11 +153,10 @@ function Ajv(opts) {
self._formats.uri = typeof currentUriFormat == 'function'
? SCHEMA_URI_FORMAT_FUNC
: SCHEMA_URI_FORMAT;
var valid;
try { valid = validate($schema, schema); }
finally { self._formats.uri = currentUriFormat; }
var valid = validate($schema, schema);
self._formats.uri = currentUriFormat;
if (!valid && throwOrLogError) {
var message = 'schema is invalid: ' + errorsText();
var message = 'schema is invalid:' + errorsText();
if (self._opts.validateSchema == 'log') console.error(message);
else throw new Error(message);
}

View File

@ -214,16 +214,6 @@ 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') console.error(message);
else throw new Error(message);
}
}
var compile = rule.definition.compile
, inline = rule.definition.inline
, macro = rule.definition.macro;

View File

@ -27,9 +27,6 @@ module.exports = function addKeyword(keyword, definition) {
if (dataType) checkDataType(dataType);
_addRule(keyword, dataType, definition);
}
if (definition.metaSchema)
definition.validateSchema = self.compile(definition.metaSchema, true);
}
this.RULES.keywords[keyword] = true;

View File

@ -20,151 +20,90 @@ describe('Custom keywords', function () {
describe('custom rules', function() {
describe('rule with "interpreted" keyword validation', function() {
it('should add and validate rule', function() {
testEvenKeyword({ type: 'number', validate: validateEven });
it('should add and validate rule with "interpreted" keyword validation', function() {
testEvenKeyword({ type: 'number', validate: validateEven });
function validateEven(schema, data) {
if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean');
return data % 2 ? !schema : schema;
}
});
it('should add, validate keyword schema and validate rule', function() {
testEvenKeyword({
type: 'number',
validate: validateEven,
metaSchema: { "type": "boolean" }
});
shouldBeInvalidSchema({ "even": "not_boolean" });
function validateEven(schema, data) {
return data % 2 ? !schema : schema;
}
});
it('should pass parent schema to "interpreted" keyword validation', function() {
testRangeKeyword({
type: 'number',
validate: validateRange
});
function validateRange(schema, data, parentSchema) {
validateRangeSchema(schema, parentSchema);
return parentSchema.exclusiveRange === true
? data > schema[0] && data < schema[1]
: data >= schema[0] && data <= schema[1];
}
});
it('should validate meta schema and pass parent schema to "interpreted" keyword validation', function() {
testRangeKeyword({
type: 'number',
validate: validateRange,
metaSchema: {
"type": "array",
"items": [ { "type": "number" }, { "type": "number" } ],
"additionalItems": false
}
});
shouldBeInvalidSchema({ range: [ "1", 2 ] });
shouldBeInvalidSchema({ range: {} });
shouldBeInvalidSchema({ range: [ 1, 2, 3 ] });
function validateRange(schema, data, parentSchema) {
return parentSchema.exclusiveRange === true
? data > schema[0] && data < schema[1]
: data >= schema[0] && data <= schema[1];
}
});
it('should allow defining custom errors for "interpreted" keyword', function() {
testRangeKeyword({ type: 'number', validate: validateRange }, true);
function validateRange(schema, data, parentSchema) {
validateRangeSchema(schema, parentSchema);
var min = schema[0]
, max = schema[1]
, exclusive = parentSchema.exclusiveRange === true;
var minOk = exclusive ? data > min : data >= min;
var maxOk = exclusive ? data < max : data <= max;
var valid = minOk && maxOk;
if (!valid) {
var err = { keyword: 'range' };
validateRange.errors = [err];
var comparison, limit;
if (minOk) {
comparison = exclusive ? '<' : '<=';
limit = max;
} else {
comparison = exclusive ? '>' : '>=';
limit = min;
}
err.message = 'should be ' + comparison + ' ' + limit;
err.params = {
comparison: comparison,
limit: limit,
exclusive: exclusive
};
}
return valid;
}
});
function validateEven(schema, data) {
if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean');
return data % 2 ? !schema : schema;
}
});
it('should add and validate rule with "compiled" keyword validation', function() {
testEvenKeyword({ type: 'number', compile: compileEven });
describe('rule with "compiled" keyword validation', function() {
it('should add and validate rule', function() {
testEvenKeyword({ type: 'number', compile: compileEven });
shouldBeInvalidSchema({ "even": "not_boolean" });
function compileEven(schema) {
if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean');
return schema ? isEven : isOdd;
}
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 add, validate keyword schema and validate rule', function() {
testEvenKeyword({
type: 'number',
compile: compileEven,
metaSchema: { "type": "boolean" }
});
shouldBeInvalidSchema({ "even": "not_boolean" });
function compileEven(schema) {
return schema ? isEven : isOdd;
}
function isEven(data) { return data % 2 === 0; }
function isOdd(data) { return data % 2 !== 0; }
});
it('should compile keyword validating function only once per schema', function () {
testConstantKeyword({ compile: compileConstant });
});
it('should allow multiple schemas for the same keyword', function () {
testMultipleConstantKeyword({ compile: compileConstant });
});
it('should pass parent schema to "compiled" keyword validation', function() {
testRangeKeyword({ type: 'number', compile: compileRange });
});
it('should allow multiple parent schemas for the same keyword', function () {
testMultipleRangeKeyword({ type: 'number', compile: compileRange });
});
function isEven(data) { return data % 2 === 0; }
function isOdd(data) { return data % 2 !== 0; }
});
it('should compile keyword validating function only once per schema', function () {
testConstantKeyword({ compile: compileConstant });
});
it('should allow multiple schemas for the same keyword', function () {
testMultipleConstantKeyword({ compile: compileConstant });
});
it('should pass parent schema to "interpreted" keyword validation', function() {
testRangeKeyword({ type: 'number', validate: validateRange });
function validateRange(schema, data, parentSchema) {
validateRangeSchema(schema, parentSchema);
return parentSchema.exclusiveRange === true
? data > schema[0] && data < schema[1]
: data >= schema[0] && data <= schema[1];
}
});
it('should allow defining custom errors for "interpreted" keyword', function() {
testRangeKeyword({ type: 'number', validate: validateRange }, true);
function validateRange(schema, data, parentSchema) {
validateRangeSchema(schema, parentSchema);
var min = schema[0]
, max = schema[1]
, exclusive = parentSchema.exclusiveRange === true;
var minOk = exclusive ? data > min : data >= min;
var maxOk = exclusive ? data < max : data <= max;
var valid = minOk && maxOk;
if (!valid) {
var err = { keyword: 'range' };
validateRange.errors = [err];
var comparison, limit;
if (minOk) {
comparison = exclusive ? '<' : '<=';
limit = max;
} else {
comparison = exclusive ? '>' : '>=';
limit = min;
}
err.message = 'should be ' + comparison + ' ' + limit;
err.params = {
comparison: comparison,
limit: limit,
exclusive: exclusive
};
}
return valid;
}
});
it('should pass parent schema to "compiled" keyword validation', function() {
testRangeKeyword({ type: 'number', compile: compileRange });
});
it('should allow multiple parent schemas for the same keyword', function () {
testMultipleRangeKeyword({ type: 'number', compile: compileRange });
});
function compileConstant(schema) {
return typeof schema == 'object' && schema !== null
@ -660,13 +599,6 @@ describe('Custom keywords', function () {
validate.errors .should.have.length(numErrors || 1);
}
function shouldBeInvalidSchema(schema) {
instances.forEach(function (ajv) {
should.throw(function() {
ajv.compile(schema);
});
});
}
describe('addKeyword method', function() {
var TEST_TYPES = [ undefined, 'number', 'string', 'boolean', ['number', 'string']];