Merge pull request #74 from epoberezkin/v2.0

V2.0
master
Evgeny Poberezkin 2015-11-22 01:03:56 +00:00
commit f3d053615f
29 changed files with 1814 additions and 214 deletions

View File

@ -5,5 +5,7 @@ before_script:
node_js:
- "0.10"
- "0.12"
- "4"
- "5"
after_script:
- codeclimate-test-reporter < coverage/lcov.info

309
README.md
View File

@ -4,23 +4,32 @@ Currently the fastest JSON Schema validator for node.js and browser.
It uses [doT templates](https://github.com/olado/doT) to generate super-fast validating functions.
[![Build Status](https://travis-ci.org/epoberezkin/ajv.svg?branch=master)](https://travis-ci.org/epoberezkin/ajv)
[![Build Status](https://travis-ci.org/epoberezkin/ajv.svg)](https://travis-ci.org/epoberezkin/ajv)
[![npm version](https://badge.fury.io/js/ajv.svg)](http://badge.fury.io/js/ajv)
[![Code Climate](https://codeclimate.com/github/epoberezkin/ajv/badges/gpa.svg)](https://codeclimate.com/github/epoberezkin/ajv)
[![Test Coverage](https://codeclimate.com/github/epoberezkin/ajv/badges/coverage.svg)](https://codeclimate.com/github/epoberezkin/ajv/coverage)
## JSON Schema standard
NB: [Upgrading to version 2.0.0](https://github.com/epoberezkin/ajv/releases/tag/2.0.0).
ajv implements full [JSON Schema draft 4](http://json-schema.org/) standard:
- all validation keywords (see [JSON-Schema validation keywords](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md))
- full support of remote refs (remote schemas have to be added with `addSchema` or compiled to be available)
- [asynchronous loading](#asynchronous-compilation) of referenced schemas during compilation.
- support of circular dependencies between schemas
- correct string lengths for strings with unicode pairs (can be turned off)
- formats defined by JSON Schema draft 4 standard and custom formats (can be turned off)
- BETA: [custom keywords](https://github.com/epoberezkin/ajv/tree/v2.0#defining-custom-keywords) supported starting from version [2.0.0](https://github.com/epoberezkin/ajv/tree/v2.0), `npm install ajv@^2.0.0-beta` to use it
## Features
- ajv implements full [JSON Schema draft 4](http://json-schema.org/) standard:
- all validation keywords (see [JSON-Schema validation keywords](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md))
- full support of remote refs (remote schemas have to be added with `addSchema` or compiled to be available)
- support of circular dependencies between schemas
- correct string lengths for strings with unicode pairs (can be turned off)
- [formats](#formats) defined by JSON Schema draft 4 standard and custom formats (can be turned off)
- [validates schemas against meta-schema](#api-validateschema)
- supports [browsers](#using-in-browser) and nodejs 0.10-5.0
- [asynchronous loading](#asynchronous-compilation) of referenced schemas during compilation
- "All errors" validation mode with [option allErrors](#options)
- [error messages with parameters](#validation-errors) describing error reasons to allow creating custom error messages
- i18n error messages support with [ajv-i18n](https://github.com/epoberezkin/ajv-i18n) package
- [filtering data](#filtering-data) from additional properties
- NEW: [custom keywords](#defining-custom-keywords)
- NEW: keywords `constant` and `contains` from [JSON-schema v5 proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals) with [option v5](#options)
Currently ajv is the only validator that passes all the tests from [JSON Schema Test Suite](https://github.com/json-schema/JSON-Schema-Test-Suite) (according to [json-schema-benchmark](https://github.com/ebdrup/json-schema-benchmark), apart from the test that requires that `1.0` is not an integer that is impossible to satisfy in JavaScript).
@ -53,7 +62,7 @@ The fastest validation call:
```
var Ajv = require('ajv');
var ajv = Ajv(); // options can be passed
var ajv = Ajv(); // options can be passed, e.g. {allErrors: true}
var validate = ajv.compile(schema);
var valid = validate(data);
if (!valid) console.log(validate.errors);
@ -82,7 +91,7 @@ ajv compiles schemas to functions and caches them in all cases (using stringifie
The best performance is achieved when using compiled functions returned by `compile` or `getSchema` methods (there is no additional function call).
__Please note__: every time validation function or `ajv.validate` are called `errors` property is overwritten. You need to copy `errors` array reference to another variable if you want to use it later (e.g., in the callback).
__Please note__: every time validation function or `ajv.validate` are called `errors` property is overwritten. You need to copy `errors` array reference to another variable if you want to use it later (e.g., in the callback). See [Validation errors](#validation-errors)
## Using in browser
@ -123,6 +132,202 @@ You can add additional formats and replace any of the formats above using [addFo
You can find patterns used for format validation and the sources that were used in [formats.js](https://github.com/epoberezkin/ajv/blob/master/lib/compile/formats.js).
## Defining custom keywords
Starting from version 2.0 (ajv@^2.0.0-beta) ajv supports custom keyword definitions.
WARNING: The main drawback of extending JSON-schema standard with custom keywords is the loss of portability of your schemas - it may not be possible to support these custom keywords on some other platforms. Also your schemas may be more challenging to read for other people. If portability is important you may prefer using additional validation logic outside of JSON-schema rather than putting it inside your JSON-schema.
The advantages of using custom keywords are:
- they allow you keeping a larger portion of your validation logic in the schema
- they make your schemas more expressive and less verbose
- they are fun to use
You can define custom keywords with [addKeyword](#api-addkeyword) method. Keywords are defined on the `ajv` instance level - new instances will not have previously defined keywords.
Ajv allows defining keywords with:
- validation function
- compilation function
- macro function
- inline compilation function that should return code (as string) that will be inlined in the currently compiled schema.
### Define keyword with validation function (NOT RECOMMENDED)
Validation function will be called during data validation. It will be passed schema, data and parentSchema (if it has 3 arguments) at validation time and it should return validation result as boolean. It can return an array of validation errors via `.errors` property of itself (otherwise a standard error will be used).
This way to define keywords is added as a way to quickly test your keyword and is not recommended because of worse performance than compiling schemas.
Example. draft5 `constant` keyword (that is equivalent to `enum` keyword with one item):
```
ajv.addKeyword('constant', { validate: function (schema, data) {
return typeof schema == 'object && schema !== null'
? deepEqual(schema, data)
: schema === data;
} });
var schema = { "constant": 2 };
var validate = ajv.compile(schema);
console.log(validate(2)); // true
console.log(validate(3)); // false
var schema = { "constant": { "foo": "bar" } };
var validate = ajv.compile(schema);
console.log(validate({foo: 'bar'})); // true
console.log(validate({foo: 'baz'})); // false
```
### Define keyword with "compilation" function
Compilation function will be called during schema compilation. It will be passed schema and parent schema and it should return a validation function. This validation function will be passed data during validation; it should return validation result as boolean and it can return an array of validation errors via `.errors` property of itself (otherwise a standard error will be used).
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).
Example. `range` and `exclusiveRange` keywords using compiled schema:
```
ajv.addKeyword('range', { type: 'number', compile: function (sch, parentSchema) {
var min = sch[0];
var max = sch[1];
return parentSchema.exclusiveRange === true
? function (data) { return data > min && data < max; }
: function (data) { return data >= min && data <= max; }
} });
var schema = { "range": [2, 4], "exclusiveRange": true };
var validate = ajv.compile(schema);
console.log(validate(2.01)); // true
console.log(validate(3.99)); // true
console.log(validate(2)); // false
console.log(validate(4)); // false
```
### Define keyword with "macro" function
"Macro" function is called during schema compilation. It is passed schema and parent schema and it should return another schema that will be applied to the data in addition to the original schema (if schemas have different keys they are merged, otherwise `allOf` keyword is used).
It is the most efficient approach (in cases when the keyword logic can be expressed with another JSON-schema) because it is usually easy to implement and there is no extra function call during validation.
`range` and `exclusiveRange` keywords from the previous example defined with macro:
```
ajv.addKeyword('range', { macro: function (schema, parentSchema) {
return {
minimum: schema[0],
maximum: schema[1],
exclusiveMinimum: !!parentSchema.exclusiveRange,
exclusiveMaximum: !!parentSchema.exclusiveRange
};
} });
```
Example draft5 `contains` keyword that requires that the array has at least one item matching schema (see https://github.com/json-schema/json-schema/wiki/contains-(v5-proposal)):
```
ajv.addKeyword('contains', { macro: function (schema) {
return { "not": { "items": { "not": schema } } };
} });
var schema = {
"contains": {
"type": "number",
"minimum": 4,
"exclusiveMinimum": true
}
};
var validate = ajv.compile(schema);
console.log(validate([1,2,3])); // false
console.log(validate([2,3,4])); // false
console.log(validate([3,4,5])); // true, number 5 matches schema inside "contains"
```
See the example of defining recursive macro keyword `deepProperties` in the [test](https://github.com/epoberezkin/ajv/blob/master/spec/custom.spec.js#L151).
### Define keyword with "inline" compilation function
Inline compilation function is called during schema compilation. It is passed three parameters: `it` (the current schema compilation context), `schema` and `parentSchema` and it should return the code (as a string) that will be inlined in the code of compiled schema. This code can be either an expression that evaluates to the validation result (boolean) or a set of statements that assign the validation result to a variable.
While it can be more difficult to define keywords with "inline" functions, it can have the best performance.
Example `even` keyword:
```
ajv.addKeyword('even', { type: 'number', inline: function (it, schema) {
var op = schema ? '===' : '!==';
return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0';
} });
var schema = { "even": true };
var validate = ajv.compile(schema);
console.log(validate(2)); // true
console.log(validate(3)); // false
```
`'data' + (it.dataLevel || '')` in the example above is the reference to the currently validated data. Also note that `schema` (keyword schema) is the same as `it.schema.even`, so schema is not strictly necessary here - it is passed for convenience.
Example `range` keyword defined using [doT template](https://github.com/olado/doT):
```
var doT = require('dot');
var inlineRangeTemplate = doT.compile("\
{{ \
var $data = 'data' + (it.dataLevel || '') \
, $min = it.schema.range[0] \
, $max = it.schema.range[1] \
, $gt = it.schema.exclusiveRange ? '>' : '>=' \
, $lt = it.schema.exclusiveRange ? '<' : '<='; \
}} \
var valid{{=it.level}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$max}}; \
");
ajv.addKeyword('range', {
type: 'number',
inline: inlineRangeTemplate,
statements: true
});
```
`'valid' + it.level` in the example above is the expected name of the variable that should be set to the validation result.
Property `statements` in the keyword definition should be set to `true` if the validation code sets the variable instead of evaluating to the validation result.
### Defining errors in custom keywords
All custom keywords but macro keywords can create custom error messages.
Validating and compiled keywords should define errors by assigning them to `.errors` property of validation function.
Inline custom keyword should increase error counter `errors` and add error to `vErrors` array (it can be null). See [example range keyword](https://github.com/epoberezkin/ajv/blob/master/spec/custom_rules/range_with_errors.jst).
When inline keyword performes validation Ajv checks whether it created errors by comparing errors count before and after validation. To skip this check add option `errors` to keyword definition:
```
ajv.addKeyword('range', {
type: 'number',
inline: inlineRangeTemplate,
statements: true,
errors: true // keyword should create custom errors when validation fails
});
```
Each error object should have properties `keyword`, `message` and `params`, other properties will be added.
Inlined keywords can optionally define `dataPath` property in error objects.
If custom keyword doesn't create errors, the default error will be created in case the keyword fails validation (see [Validation errors](#validation-errors)).
## Asynchronous compilation
Starting from version 1.3 ajv supports asynchronous compilation when remote references are loaded using supplied function. See `compileAsync` method and `loadSchema` option.
@ -154,6 +359,8 @@ With [option `removeAdditional`](#options) (added by [andyscott](https://github.
This option modifies original object.
TODO: example
## API
@ -220,7 +427,7 @@ Adds meta schema that can be used to validate other schemas. That function shoul
There is no need to explicitly add draft 4 meta schema (http://json-schema.org/draft-04/schema and http://json-schema.org/schema) - it is added by default, unless option `meta` is set to `false`. You only need to use it if you have a changed meta-schema that you want to use to validate your schemas. See `validateSchema`.
##### .validateSchema(Object schema) -&gt; Boolean
##### <a name="api-validateschema"></a>.validateSchema(Object schema) -&gt; Boolean
Validates schema. This method should be used to validate schemas rather than `validate` due to the inconsistency of `uri` format in JSON-Schema standart.
@ -256,6 +463,29 @@ Function should return validation result as `true` or `false`.
Custom formats can be also added via `formats` option.
##### <a name="api-addkeyword"></a>.addKeyword(String keyword, Object definition)
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 remove keyword definition from the instance.
Keyword definition is an object with the following properties:
- _type_: optional string or array of strings with data type(s) that the keyword will apply to. If keyword is validating another type the validation function will not be called, so there is no need to check for data type inside validation function if `type` property is used.
- _validate_: validating function
- _compile_: compiling function
- _macro_: macro function
- _inline_: compiling function that returns code (as string)
_validate_, _compile_, _macro_ and _inline_ are mutually exclusive, only one should be used at a time.
With _macro_ function _type_ must not be specified, the types that the keyword will be applied for will be determined by the final schema.
See [Defining custom keywords](#defining-custom-keywords) for more details.
##### .errorsText([Array&lt;Object&gt; errors [, Object options]]) -&gt; String
Returns the text with all errors in a String.
@ -284,9 +514,10 @@ Defaults:
unicode: true,
beautify: false,
cache: new Cache,
errorDataPath: 'object',
jsonPointers: false,
i18n: false,
messages: true
v5: true
}
```
@ -305,9 +536,53 @@ Defaults:
- _unicode_: calculate correct length of strings with unicode pairs (true by default). Pass `false` to use `.length` of strings that is faster, but gives "incorrect" lengths of strings with unicode pairs - each unicode pair is counted as two characters.
- _beautify_: format the generated function with [js-beautify](https://github.com/beautify-web/js-beautify) (the validating function is generated without line-breaks). `npm install js-beautify` to use this option. `true` or js-beautify options can be passed.
- _cache_: an optional instance of cache to store compiled schemas using stable-stringified schema as a key. For example, set-associative cache [sacjs](https://github.com/epoberezkin/sacjs) can be used. If not passed then a simple hash is used which is good enough for the common use case (a limited number of statically defined schemas). Cache should have methods `put(key, value)`, `get(key)` and `del(key)`.
- _jsonPointers_: Output `dataPath` using JSON Pointers instead of JS path notation.
- _i18n_: Support internationalization of error messages using [ajv-i18n](https://github.com/epoberezkin/ajv-i18n). See its repo for details.
- _messages_: Include human-readable messages in errors. `true` by default. `messages: false` can be added when internationalization (options `i18n`) is used.
- _errorDataPath_: set `dataPath` to point to 'object' (default) or to 'property' (default behavior in versions before 2.0) when validating keywords `required`, `additionalProperties` and `dependencies`.
- _jsonPointers_: set `dataPath` propery of errors using [JSON Pointers](https://tools.ietf.org/html/rfc6901) instead of JavaScript property access notation.
- _messages_: Include human-readable messages in errors. `true` by default. `messages: false` can be added when custom messages are used (e.g. with [ajv-i18n](https://github.com/epoberezkin/ajv-i18n)).
- _v5_: add keywords `constant` and `contains` from [JSON-schema v5 proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals)
## Validation errors
In case of validation failure Ajv assigns the array of errors to `.errors` property of validation function (or to `.errors` property of ajv instance in case `validate` or `validateSchema` methods were called).
### Error objects
Each error is an object with the following properties:
- _keyword_: validation keyword. For user defined validation keywords it is set to `"custom"` (with the exception of macro keywords and unless keyword definition defines its own errors).
- _dataPath_: the path to the part of the data that was validated. By default `dataPath` uses JavaScript property access notation (e.g., `".prop[1].subProp"`). When the option `jsonPointers` is true (see [Options](#options)) `dataPath` will be set using JSON pointer standard (e.g., `"/prop/1/subProp"`).
- _params_: the object with the additional information about error that can be used to create custom error messages (e.g., using [ajv-i18n](https://github.com/epoberezkin/ajv-i18n) package). See below for parameters set by all keywords.
- _message_: the standard error message (can be excluded with option `messages` set to false).
- _schema_: the schema of the keyword (added with `verbose` option).
- _data_: the data validated by the keyword (added with `verbose` option).
### Error parameters
Properties of `params` object in errors depend on the keyword that failed validation.
- `maxItems`, `minItems`, `maxLength`, `minLength`, `maxProperties`, `minProperties` - property `limit` (number, the schema of the keyword).
- `additionalItems` - property `limit` (the maximum number of allowed items in case when `items` keyword is an array of schemas and `additionalItems` is false).
- `additionalProperties` - property `additionalProperty` (the property not used in `properties` and `patternProperties` keywords).
- `dependencies` - properties:
- `property` (dependent property),
- `missingProperty` (required missing dependency - only the first one is reported currently)
- `deps` (required dependencies, comma separated list as a string),
- `depsCount` (the number of required dependedncies).
- `format` - property `format` (the schema of the keyword).
- `maximum`, `minimum` - properties:
- `limit` (number, the schema of the keyword),
- `exclusive` (boolean, the schema of `exclusiveMaximum` or `exclusiveMinimum`),
- `comparison` (string, comparison operation to compare the data to the limit, with the data on the left and the limit on the right; can be "<", "<=", ">", ">=")
- `multipleOf` - property `multipleOf` (the schema of the keyword)
- `pattern` - property `pattern` (the schema of the keyword)
- `required` - property `missingProperty` (required property that is missing).
- `type` - property `type` (required type(s), a string, can be a comma-separated list)
- `uniqueItems` - properties `i` and `j` (indices of duplicate items).
- `$ref` - property `ref` with the referenced schema URI.
- custom keywords (in case keyword definition doesn't create errors) - property `keyword` (the keyword name).
## Command line interface

View File

@ -5,7 +5,9 @@ var compileSchema = require('./compile')
, Cache = require('./cache')
, SchemaObject = require('./compile/schema_obj')
, stableStringify = require('json-stable-stringify')
, formats = require('./compile/formats');
, formats = require('./compile/formats')
, rules = require('./compile/rules')
, v5 = require('./v5');
module.exports = Ajv;
@ -31,6 +33,7 @@ function Ajv(opts) {
this._formats = formats(this.opts.format);
this._cache = this.opts.cache || new Cache;
this._loadingSchemas = {};
this.RULES = rules();
// this is done on purpose, so that methods are bound to the instance
// (without using bind) so that they can be used without the instance
@ -43,6 +46,7 @@ function Ajv(opts) {
this.getSchema = getSchema;
this.removeSchema = removeSchema;
this.addFormat = addFormat;
this.addKeyword = addKeyword;
this.errorsText = errorsText;
this._compile = _compile;
@ -50,6 +54,11 @@ function Ajv(opts) {
addInitialSchemas();
if (this.opts.formats) addInitialFormats();
if (this.opts.errorDataPath == 'property')
this.opts._errorDataPathProperty = true;
if (this.opts.v5) v5.enable(this);
/**
* Validate data using schema
@ -196,10 +205,11 @@ function Ajv(opts) {
/**
* Validate schema
* @param {Object} schema schema to validate
* @param {Object} schema schema to validate
* @param {Boolean} throwOrLogError pass true to throw on error
* @return {Boolean}
*/
function validateSchema(schema) {
function validateSchema(schema, throwOrLogError) {
var $schema = schema.$schema || META_SCHEMA_ID;
var currentUriFormat = self._formats.uri;
self._formats.uri = typeof currentUriFormat == 'function'
@ -207,6 +217,11 @@ function Ajv(opts) {
: SCHEMA_URI_FORMAT;
var valid = validate($schema, schema);
self._formats.uri = currentUriFormat;
if (!valid && throwOrLogError) {
var message = 'schema is invalid:' + errorsText();
if (self.opts.validateSchema == 'log') console.error(message);
else throw new Error(message);
}
return valid;
}
@ -265,13 +280,8 @@ function Ajv(opts) {
var id = resolve.normalizeId(schema.id);
if (id) checkUnique(id);
var ok = skipValidation || self.opts.validateSchema === false
|| validateSchema(schema);
if (!ok) {
var message = 'schema is invalid:' + errorsText();
if (self.opts.validateSchema == 'log') console.error(message);
else throw new Error(message);
}
if (self.opts.validateSchema !== false && !skipValidation)
validateSchema(schema, true);
var localRefs = resolve.ids.call(self, schema);
@ -324,6 +334,12 @@ function Ajv(opts) {
}
/**
* Convert array of error message objects to string
* @param {Array<Object>} errors optional array of validation errors, if not passed errors from the instance are used.
* @param {Object} opts optional options with properties `separator` and `dataVar`.
* @return {String}
*/
function errorsText(errors, opts) {
errors = errors || self.errors;
if (!errors) return 'No errors';
@ -338,12 +354,81 @@ function Ajv(opts) {
}
/**
* Add custom format
* @param {String} name format name
* @param {String|RegExp|Function} format string is converted to RegExp; function should return boolean (true when valid)
*/
function addFormat(name, format) {
if (typeof format == 'string') format = new RegExp(format);
self._formats[name] = format;
}
/**
* Define custom keyword
* @param {String} keyword custom keyword, should be a valid identifier, should be 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) {
if (self.RULES.keywords[keyword])
throw new Error('Keyword ' + keyword + ' is already defined');
if (definition.macro) {
if (definition.type) throw new Error('type cannot be defined for macro keywords');
_addMacro(keyword, definition.macro);
} else {
var dataType = definition.type;
if (Array.isArray(dataType)) {
var i, len = dataType.length;
for (i=0; i<len; i++) checkDataType(dataType[i]);
for (i=0; i<len; i++) _addRule(keyword, dataType[i], definition);
} else {
if (dataType) checkDataType(dataType);
_addRule(keyword, dataType, definition);
}
}
self.RULES.keywords[keyword] = true;
self.RULES.all[keyword] = true;
}
function _addRule(keyword, dataType, definition) {
var ruleGroup;
for (var i=0; i<self.RULES.length; i++) {
var rg = self.RULES[i];
if (rg.type == dataType) {
ruleGroup = rg;
break;
}
}
if (!ruleGroup) {
ruleGroup = { type: dataType, rules: [] };
self.RULES.push(ruleGroup);
}
var rule = { keyword: keyword, definition: definition, custom: true };
ruleGroup.rules.push(rule);
}
function _addMacro(keyword, macro) {
var macros = self.RULES.macros;
var rule = { keyword: keyword, macro: macro };
if (macros) macros[macros.length] = rule;
else self.RULES.macros = [rule];
self.RULES.allMacros = self.RULES.allMacros || {};
self.RULES.allMacros[keyword] = true;
}
function checkDataType(dataType) {
if (!self.RULES.types[dataType]) throw new Error('Unknown type ' + dataType);
}
function addInitialSchemas() {
if (self.opts.meta !== false) {
var metaSchema = require('./refs/json-schema-draft-04.json');

View File

@ -2,12 +2,13 @@
var resolve = require('./resolve')
, util = require('./util')
, equal = require('./equal');
, equal = require('./equal')
, macro = require('./macro')
, stableStringify = require('json-stable-stringify');
try { var beautify = require('' + 'js-beautify').js_beautify; } catch(e) {}
var RULES = require('./rules')
, validateGenerator = require('../dotjs/validate');
var validateGenerator = require('../dotjs/validate');
module.exports = compile;
@ -18,11 +19,14 @@ function compile(schema, root, localRefs, baseId) {
, refVal = [ undefined ]
, refs = {}
, patterns = []
, patternsHash = {};
, patternsHash = {}
, customRules = []
, customRulesHash = {};
root = root || { schema: schema, refVal: refVal, refs: refs };
var formats = this._formats;
var RULES = this.RULES;
return localCompile(schema, root, localRefs, baseId);
@ -32,6 +36,9 @@ function compile(schema, root, localRefs, baseId) {
if (_root.schema != root.schema)
return compile.call(self, _schema, _root, localRefs, baseId);
if (self.RULES.macros && macro.hasMacro(_schema, RULES))
_schema = util.deepClone(_schema);
var validateCode = validateGenerator({
isTop: true,
schema: _schema,
@ -46,26 +53,31 @@ function compile(schema, root, localRefs, baseId) {
resolve: resolve,
resolveRef: resolveRef,
usePattern: usePattern,
useCustomRule: useCustomRule,
expandMacros: macro.expand,
opts: self.opts,
formats: formats
formats: formats,
self: self
});
validateCode = refsCode(refVal) + patternsCode(patterns) + validateCode;
validateCode = refsCode(refVal) + patternsCode(patterns)
+ customRulesCode(customRules) + validateCode;
if (self.opts.beautify) {
var opts = self.opts.beautify === true ? { indent_size: 2 } : self.opts.beautify;
/* istanbul ignore else */
if (beautify) validateCode = beautify(validateCode, opts);
else console.error('"npm install js-beautify" to use beautify option');
}
// console.log('\n\n\n *** \n', validateCode);
var validate;
// try {
try {
eval(validateCode);
refVal[0] = validate;
// } catch(e) {
// console.log('Error compiling schema, function code:', validateCode);
// throw e;
// }
} catch(e) {
console.log('Error compiling schema, function code:', validateCode);
throw e;
}
validate.schema = _schema;
validate.errors = null;
@ -137,6 +149,27 @@ function compile(schema, root, localRefs, baseId) {
}
return 'pattern' + index;
}
function useCustomRule(rule, schema, parentSchema, it) {
var compile = rule.definition.compile
, inline = rule.definition.inline;
var validate;
if (compile)
validate = compile.call(self, schema, parentSchema);
else if (inline)
validate = inline.call(self, it, schema, parentSchema);
else
validate = rule.definition.validate;
var index = customRules.length;
customRules[index] = validate;
return {
code: 'customRule' + index,
validate: validate
};
}
}
@ -160,6 +193,16 @@ function refCode(i, refVal) {
}
function customRulesCode(customRules) {
return _arrCode(customRules, customRuleCode);
}
function customRuleCode(i, rule) {
return 'var customRule' + i + ' = customRules[' + i + '];';
}
function _arrCode(arr, statement) {
if (!arr.length) return '';
var code = '';

91
lib/compile/macro.js Normal file
View File

@ -0,0 +1,91 @@
'use strict';
var util = require('./util');
module.exports = {
expand: expandMacros,
hasMacro: hasMacro
};
function expandMacros() {
/* jshint validthis: true */
var macros = this.RULES.macros
, schema = this.schema
, expSchemas, i, key;
for (i=0; i<macros.length; i++) {
var rule = macros[i]
, keywordSchema = schema[rule.keyword];
if (keywordSchema !== undefined) {
var expanded = rule.macro(keywordSchema, schema);
delete schema[rule.keyword];
expandMacros.call({ RULES: this.RULES, schema: expanded });
if (expSchemas) expSchemas[expSchemas.length] = expanded;
else expSchemas = [expanded];
}
}
if (expSchemas) {
if (this.self.opts.validateSchema !== false)
this.self.validateSchema({ "allOf": expSchemas }, true);
var schemaCopy;
if (Object.keys(schema).length > 0)
schemaCopy = util.copy(schema);
var success = true;
out: // try to merge schemas without merging keywords
for (i=0; i<expSchemas.length; i++) {
var sch = expSchemas[i];
for (key in sch) {
if (schema[key] === undefined)
schema[key] = sch[key];
else {
success = false;
break out;
}
}
}
if (!success) {
for (key in schema) delete schema[key];
if (schemaCopy) expSchemas[expSchemas.length] = schemaCopy;
schema.allOf = expSchemas;
}
}
}
function hasMacro(schema, RULES) {
for (var key in schema) {
if (RULES.allMacros[key]) return true;
var sch = schema[key];
switch (key) {
case 'properties':
case 'patternProperties':
case 'dependencies':
for (var prop in sch)
if (typeof sch[prop] == 'object' && hasMacro(sch[prop], RULES))
return true;
break;
case 'additionalProperties':
if (typeof sch != 'object') break;
/* falls through */
case 'not':
if (hasMacro(sch, RULES)) return true;
break;
case 'items':
if (!Array.isArray(sch)) {
if (hasMacro(sch, RULES)) return true;
break;
}
/* falls through */
case 'anyOf':
case 'oneOf':
case 'allOf':
for (var i=0; i<sch.length; i++)
if (hasMacro(sch[i], RULES))
return true;
}
}
}

View File

@ -88,7 +88,7 @@ function resolveRecursive(root, ref, parsedRef) {
}
var PREVENT_SCOPE_CHANGE = util.toHash(['properties', 'patternProperties', 'enum']);
var PREVENT_SCOPE_CHANGE = util.toHash(['properties', 'patternProperties', 'enum', 'dependencies']);
function getJsonPointer(parsedRef, baseId, schema, root) {
/* jshint validthis: true */
parsedRef.hash = parsedRef.hash || '';

View File

@ -3,29 +3,36 @@
var ruleModules = require('./_rules')
, util = require('./util');
var RULES = module.exports = [
{ type: 'number',
rules: [ 'maximum', 'minimum', 'multipleOf'] },
{ type: 'string',
rules: [ 'maxLength', 'minLength', 'pattern', 'format' ] },
{ type: 'array',
rules: [ 'maxItems', 'minItems', 'uniqueItems', 'items' ] },
{ type: 'object',
rules: [ 'maxProperties', 'minProperties', 'required', 'dependencies', 'properties' ] },
{ rules: [ '$ref', 'enum', 'not', 'anyOf', 'oneOf', 'allOf' ] }
];
module.exports = function rules() {
var RULES = [
{ type: 'number',
rules: [ 'maximum', 'minimum', 'multipleOf'] },
{ type: 'string',
rules: [ 'maxLength', 'minLength', 'pattern', 'format' ] },
{ type: 'array',
rules: [ 'maxItems', 'minItems', 'uniqueItems', 'items' ] },
{ type: 'object',
rules: [ 'maxProperties', 'minProperties', 'required', 'dependencies', 'properties' ] },
{ rules: [ '$ref', 'enum', 'not', 'anyOf', 'oneOf', 'allOf' ] }
];
RULES.all = [ 'type', 'additionalProperties', 'patternProperties' ];
RULES.all = [ 'type', 'additionalProperties', 'patternProperties' ];
RULES.keywords = [ '$schema', 'id', 'title', 'description', 'default' ];
RULES.types = [ 'number', 'integer', 'string', 'array', 'object', 'boolean', 'null' ];
RULES.forEach(function (group) {
group.rules = group.rules.map(function (keyword) {
RULES.all.push(keyword);
return {
keyword: keyword,
code: ruleModules[keyword]
};
RULES.forEach(function (group) {
group.rules = group.rules.map(function (keyword) {
RULES.all.push(keyword);
return {
keyword: keyword,
code: ruleModules[keyword]
};
});
});
});
RULES.all = util.toHash(RULES.all);
RULES.keywords = util.toHash(RULES.all.concat(RULES.keywords));
RULES.all = util.toHash(RULES.all);
RULES.types = util.toHash(RULES.types);
return RULES;
};

View File

@ -3,6 +3,7 @@
module.exports = {
copy: copy,
deepClone: deepClone,
checkDataType: checkDataType,
checkDataTypes: checkDataTypes,
toHash: toHash,
@ -28,6 +29,22 @@ function copy(o, to) {
}
function deepClone(o) {
if (typeof o != 'object' || o === null) return o;
var res;
if (Array.isArray(o)) {
res = [];
for (var i=0; i<o.length; i++)
res[i] = deepClone(o[i]);
} else {
res = {};
for (var key in o)
res[key] = deepClone(o[key]);
}
return res;
}
function checkDataType(dataType, data, negate) {
var EQUAL = negate ? ' !== ' : ' === '
, AND = negate ? ' || ' : ' && '

76
lib/dot/custom.def Normal file
View File

@ -0,0 +1,76 @@
{{
var $schema = it.schema[$rule.keyword]
, $ruleValidate = it.useCustomRule($rule, $schema, it.schema, it)
, $ruleErrs = $ruleValidate.code + '.errors'
, $schemaPath = it.schemaPath + '.' + $rule.keyword
, $errs = 'errs' + $lvl
, $i = 'i' + $lvl
, $ruleErr = 'ruleErr' + $lvl
, $rDef = $rule.definition
, $inline = $rDef.inline;
}}
{{? !$inline }}{{=$ruleErrs}} = null;{{?}}
var {{=$errs}} = errors;
{{## def.callRuleValidate:
{{? $inline }}
{{? $rDef.statements }}
valid{{=$lvl}}
{{??}}
({{= $ruleValidate.validate }})
{{?}}
{{??}}
{{=$ruleValidate.code}}.call(self
{{? $rDef.compile }}
, {{=$data}}
{{??}}
, validate.schema{{=$schemaPath}}
, {{=$data}}
{{? $ruleValidate.validate.length > 2 }}
, validate.schema{{=it.schemaPath}}
{{?}}
{{?}}
)
{{?}}
#}}
{{## def.extendErrors:_inline:
for (var {{=$i}}={{=$errs}}; {{=$i}}<errors; {{=$i}}++) {
var {{=$ruleErr}} = vErrors[{{=$i}}];
{{# _inline ? 'if (\{\{=$ruleErr\}\}.dataPath === undefined) {' : '' }}
{{=$ruleErr}}.dataPath = (dataPath || '') + {{= it.errorPath }};
{{# _inline ? '}' : '' }}
{{? it.opts.verbose }}
{{=$ruleErr}}.schema = validate.schema{{=$schemaPath}};
{{=$ruleErr}}.data = {{=$data}};
{{?}}
}
#}}
{{? $inline && $rDef.statements }}
{{= $ruleValidate.validate }}
{{?}}
if (!{{# def.callRuleValidate }}) {
{{? $inline }}
{{? $rDef.errors }}
{{# def.extendErrors:true }}
{{??}}
if ({{=$errs}} == errors) {
{{# def.error:'custom' }}
} else {
{{# def.extendErrors:true }}
}
{{?}}
{{??}}
if (Array.isArray({{=$ruleErrs}})) {
if (vErrors === null) vErrors = {{=$ruleErrs}};
else vErrors.concat({{=$ruleErrs}});
errors = vErrors.length;
{{# def.extendErrors:false }}
} else {
{{# def.error:'custom' }}
}
{{?}}
} {{? $breakOnError }} else { {{?}}

View File

@ -102,18 +102,16 @@
{{## def._error:_rule:
{
keyword: '{{=_rule}}',
dataPath: (dataPath || '') + {{= it.errorPath }}
keyword: '{{=_rule}}'
, dataPath: (dataPath || '') + {{= it.errorPath }}
, params: {{# def._errorParams[_rule] }}
{{? it.opts.messages !== false }}
, message: {{# def._errorMessages[_rule] }}
{{?}}
{{? it.opts.verbose || it.opts.i18n }}
, schema: {{# def._errorSchemas[_rule] }}
{{?}}
{{? it.opts.verbose }}
, schema: {{# def._errorSchemas[_rule] }}
, data: {{=$data}}
{{?}}
{{# def._errorParams[_rule] || '' }}
}
#}}
@ -163,9 +161,10 @@
not: "'should NOT be valid'",
oneOf: "'should match exactly one schema in oneOf'",
pattern: "'should match pattern \"{{=it.util.escapeQuotes($schema)}}\"'",
required: "'is a required property'",
required: "'{{? it.opts._errorDataPathProperty }}is a required property{{??}}should have required property \\'{{=$missingProperty}}\\'{{?}}'",
type: "'should be {{? $isArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}'",
uniqueItems: "'should NOT have duplicate items (items ## ' + j + ' and ' + i + ' are identical)'"
uniqueItems: "'should NOT have duplicate items (items ## ' + j + ' and ' + i + ' are identical)'",
custom: "'should pass \"{{=$rule.keyword}}\" keyword validation'"
} #}}
@ -191,20 +190,32 @@
pattern: "{{=it.util.toQuotedString($schema)}}",
required: "validate.schema{{=$schemaPath}}",
type: "{{? $isArray }}['{{= $typeSchema.join(\"','\") }}']{{??}}'{{=$typeSchema}}'{{?}}",
uniqueItems: "{{=$schema}}"
uniqueItems: "{{=$schema}}",
custom: "validate.schema{{=$schemaPath}}"
} #}}
{{## def._params = "{{? it.opts.i18n }}, params: " #}}
{{## def._errorParams = {
$ref: "{{# def._params }}{ escaped: '{{=it.util.escapeQuotes($schema)}}' }{{?}}",
dependencies: "{{# def._params }}{ n: {{=$deps.length}}, deps: '{{? $deps.length==1 }}{{= it.util.escapeQuotes($deps[0]) }}{{??}}{{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}}', property: '{{= it.util.escapeQuotes($property) }}' }{{?}}",
format: "{{# def._params }}{ escaped: '{{=it.util.escapeQuotes($schema)}}' }{{?}}",
maximum: "{{# def._params }}{ condition: '{{=$op}} {{=$schema}}' }{{?}}",
minimum: "{{# def._params }}{ condition: '{{=$op}} {{=$schema}}' }{{?}}",
pattern: "{{# def._params }}{ escaped: '{{=it.util.escapeQuotes($schema)}}' }{{?}}",
required: "{{# def._params }}{ missingProperty: '{{=$missingProperty}}' }{{?}}",
type: "{{# def._params }}{ type: '{{? $isArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}' }{{?}}",
uniqueItems: "{{# def._params }}{ i: i, j: j }{{?}}"
$ref: "{ ref: '{{=it.util.escapeQuotes($schema)}}' }",
additionalItems: "{ limit: {{=$schema.length}} }",
additionalProperties: "{ additionalProperty: '{{=$additionalProperty}}' }",
anyOf: "{}",
dependencies: "{ property: '{{= it.util.escapeQuotes($property) }}', missingProperty: '{{=$missingProperty}}', depsCount: {{=$deps.length}}, deps: '{{? $deps.length==1 }}{{= it.util.escapeQuotes($deps[0]) }}{{??}}{{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}}' }",
format: "{ format: '{{=it.util.escapeQuotes($schema)}}' }",
maximum: "{ comparison: '{{=$op}}', limit: {{=$schema}}, exclusive: {{=$exclusive}} }",
minimum: "{ comparison: '{{=$op}}', limit: {{=$schema}}, exclusive: {{=$exclusive}} }",
maxItems: "{ limit: {{=$schema}} }",
minItems: "{ limit: {{=$schema}} }",
maxLength: "{ limit: {{=$schema}} }",
minLength: "{ limit: {{=$schema}} }",
maxProperties:"{ limit: {{=$schema}} }",
minProperties:"{ limit: {{=$schema}} }",
multipleOf: "{ multipleOf: {{=$schema}} }",
not: "{}",
oneOf: "{}",
pattern: "{ pattern: '{{=it.util.escapeQuotes($schema)}}' }",
required: "{ missingProperty: '{{=$missingProperty}}' }",
type: "{ type: '{{? $isArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}' }",
uniqueItems: "{ i: i, j: j }",
custom: "{ keyword: '$rule.keyword' }"
} #}}

View File

@ -1,4 +1,5 @@
{{# def.definitions }}
{{# def.missing }}
{{# def.setup:'dependencies' }}
{{# def.setupNextLevel }}
@ -16,22 +17,21 @@
var {{=$errs}} = errors;
{{## def.checkPropertyDeps:
{{~ $deps:$dep:$i }}
{{?$i}} || {{?}}
{{=$data}}{{= it.util.getProperty($dep) }} === undefined
{{~}}
#}}
{{ var $currentErrorPath = it.errorPath; }}
var missing{{=$lvl}};
{{ for (var $property in $propertyDeps) { }}
if ({{=$data}}{{= it.util.getProperty($property) }} !== undefined) {
{{ $deps = $propertyDeps[$property]; }}
if ({{# def.checkPropertyDeps }}) {
if ({{# def.checkMissingProperty:$deps }}) {
{{# def.errorMissingProperty }}
{{# def.error:'dependencies' }}
} {{# def.elseIfValid }}
}
{{ } }}
{{ it.errorPath = $currentErrorPath; }}
{{ for (var $property in $schemaDeps) { }}
{{ var $sch = $schemaDeps[$property]; }}

20
lib/dot/missing.def Normal file
View File

@ -0,0 +1,20 @@
{{## def.checkMissingProperty:_properties:
{{~ _properties:_$property:$i }}
{{?$i}} || {{?}}
{{ var $prop = it.util.getProperty(_$property); }}
( {{=$data}}{{=$prop}} === undefined && (missing{{=$lvl}} = {{= it.util.toQuotedString(it.opts.jsonPointers ? _$property : $prop) }}) )
{{~}}
#}}
{{## def.errorMissingProperty:
{{
var $propertyPath = 'missing' + $lvl
, $missingProperty = '\' + ' + $propertyPath + ' + \'';
if (it.opts._errorDataPathProperty) {
it.errorPath = it.opts.jsonPointers
? it.util.getPathExpr($currentErrorPath, $propertyPath, true)
: $currentErrorPath + ' + ' + $propertyPath;
}
}}
#}}

View File

@ -7,7 +7,9 @@
{{ /* additionalProperties is schema */
$it.schema = $aProperties;
$it.schemaPath = it.schemaPath + '.additionalProperties';
$it.errorPath = it.errorPath;
$it.errorPath = it.opts._errorDataPathProperty
? it.errorPath
: it.util.getPathExpr(it.errorPath, 'key' + $lvl, it.opts.jsonPointers);
var $passData = $data + '[key' + $lvl + ']';
}}
@ -64,7 +66,10 @@ var valid{{=$it.level}} = true;
{{??}}
{{
var $currentErrorPath = it.errorPath;
it.errorPath = it.util.getPathExpr(it.errorPath, 'key' + $lvl, it.opts.jsonPointers);
var $additionalProperty = '\' + key' + $lvl + ' + \'';
if (it.opts._errorDataPathProperty) {
it.errorPath = it.util.getPathExpr(it.errorPath, 'key' + $lvl, it.opts.jsonPointers);
}
}}
{{? $noAdditional }}
{{? $removeAdditional }}
@ -137,7 +142,9 @@ var valid{{=$it.level}} = true;
{{
var $currentErrorPath = it.errorPath
, $missingProperty = it.util.escapeQuotes($propertyKey);
it.errorPath = it.util.getPath($currentErrorPath, $propertyKey, it.opts.jsonPointers);
if (it.opts._errorDataPathProperty) {
it.errorPath = it.util.getPath($currentErrorPath, $propertyKey, it.opts.jsonPointers);
}
}}
{{# def.error:'required' }}
{{ it.errorPath = $currentErrorPath; }}

View File

@ -1,21 +1,16 @@
{{# def.definitions }}
{{# def.missing }}
{{# def.setup:'required' }}
{{## def.checkRequired:
{{~ $required:$property:$i }}
{{? $i}} || {{?}}
{{ var $prop = it.util.getProperty($property); }}
( {{=$data}}{{=$prop}} === undefined && (missing{{=$lvl}} = {{= it.util.toQuotedString(it.opts.jsonPointers ? $property : $prop) }}) )
{{~}}
#}}
{{## def.setupLoop:
var schema{{=$lvl}} = validate.schema{{=$schemaPath}};
{{
var $i = 'i' + $lvl
, $propertyPath = 'schema' + $lvl + '[' + $i + ']'
, $missingProperty = '\' + "\'" + ' + $propertyPath + ' + "\'" + \'';
it.errorPath = it.util.getPathExpr($currentErrorPath, $propertyPath, it.opts.jsonPointers);
, $missingProperty = '\' + ' + $propertyPath + ' + \'';
if (it.opts._errorDataPathProperty) {
it.errorPath = it.util.getPathExpr($currentErrorPath, $propertyPath, it.opts.jsonPointers);
}
}}
#}}
@ -39,14 +34,8 @@
{{? $breakOnError }}
var missing{{=$lvl}};
{{? $required.length <= 20 }}
if ({{# def.checkRequired }}) {
{{
var $propertyPath = 'missing' + $lvl
, $missingProperty = '\' + ' + $propertyPath + ' + \'';
it.errorPath = it.opts.jsonPointers
? it.util.getPathExpr($currentErrorPath, $propertyPath, true)
: $currentErrorPath + ' + ' + $propertyPath;
}}
if ({{# def.checkMissingProperty:$required }}) {
{{# def.errorMissingProperty }}
{{# def.error:'required' }}
} else {
{{??}}
@ -65,8 +54,10 @@
{{~ $required:$property:$i }}
{{
var $prop = it.util.getProperty($property)
, $missingProperty = it.util.escapeQuotes($prop);
it.errorPath = it.util.getPath($currentErrorPath, $property, it.opts.jsonPointers);
, $missingProperty = it.util.escapeQuotes($property);
if (it.opts._errorDataPathProperty) {
it.errorPath = it.util.getPath($currentErrorPath, $property, it.opts.jsonPointers);
}
}}
if ({{=$data}}{{=$prop}} === undefined) {
{{# def.addError:'required' }}

View File

@ -47,6 +47,10 @@
var $typeSchema = it.schema.type;
}}
{{? it.RULES.macros }}
{{ it.expandMacros(); }}
{{?}}
{{~ it.RULES:$rulesGroup }}
{{? $shouldUseGroup($rulesGroup) }}
{{? $rulesGroup.type }}
@ -54,7 +58,11 @@
{{?}}
{{~ $rulesGroup.rules:$rule }}
{{? $shouldUseRule($rule) }}
{{= $rule.code(it) }}
{{? $rule.custom }}
{{# def.custom }}
{{??}}
{{= $rule.code(it) }}
{{?}}
{{? $breakOnError }}
{{ $closingBraces1 += '}'; }}
{{?}}

20
lib/v5.js Normal file
View File

@ -0,0 +1,20 @@
'use strict';
module.exports = {
enable: enableV5
};
function enableV5(ajv) {
ajv.addKeyword('constant', { macro: constantMacro });
ajv.addKeyword('contains', { macro: containsMacro });
}
function constantMacro(schema) {
return { enum: [schema] };
}
function containsMacro(schema) {
return { not: { items: { not: schema } } };
}

View File

@ -1,6 +1,6 @@
{
"name": "ajv",
"version": "1.4.10",
"version": "2.0.0",
"description": "Another JSON Schema Validator",
"main": "lib/ajv.js",
"files": [
@ -39,6 +39,7 @@
"json-stable-stringify": "^1.0.0"
},
"devDependencies": {
"brfs": "^0.0.8",
"browserify": "^11.0.1",
"chai": "^3.0.0",
"dot": "^1.0.3",
@ -59,6 +60,6 @@
"watch": "^0.16.0"
},
"peerDependencies": {
"ajv-i18n": "0.1.x"
"ajv-i18n": "^1.0.0"
}
}

View File

@ -6,7 +6,10 @@ var glob = require('glob')
, doT = require('dot')
, beautify = require('js-beautify').js_beautify;
var defs = fs.readFileSync(path.join(__dirname, '../lib/dot/definitions.def'));
var defs = {};
['definitions', 'custom', 'missing'].forEach(function (name) {
defs[name] = fs.readFileSync(path.join(__dirname, '../lib/dot/' + name + '.def'));
});
var files = glob.sync('../lib/dot/*.jst', { cwd: __dirname });
var dotjsPath = path.join(__dirname, '../lib/dotjs');
@ -21,7 +24,7 @@ files.forEach(function (f) {
var keyword = path.basename(f, '.jst');
var targetPath = path.join(dotjsPath, keyword + '.js');
var template = fs.readFileSync(path.join(__dirname, f));
var code = doT.compile(template, { definitions: defs });
var code = doT.compile(template, defs);
code = code.toString()
.replace(OUT_EMPTY_STRING, '')
.replace(FUNCTION_NAME, 'function generate_' + keyword + '(it) {');

View File

@ -8,4 +8,4 @@ browserify -r js-beautify -r ./lib/ajv.js:ajv -o .browser/ajv.beautify.js
find spec -type f -name '*.spec.js' | \
xargs -I {} sh -c \
'export f="{}"; browserify $f -t require-globify -x ajv -o $(echo $f | sed -e "s/spec/.browser/");'
'export f="{}"; browserify $f -t require-globify -t brfs -x ajv -o $(echo $f | sed -e "s/spec/.browser/");'

View File

@ -45,6 +45,24 @@ describe('Ajv', function () {
ajv.compile({ type: null });
});
});
it('should throw if compiled schema has an invalid JavaScript code', function() {
ajv.addKeyword('even', { inline: badEvenCode });
var schema = { even: true };
var validate = ajv.compile(schema);
validate(2) .should.equal(true);
validate(3) .should.equal(false);
var schema = { even: false };
should.throw(function() {
var validate = ajv.compile(schema);
});
function badEvenCode(it, schema) {
var op = schema ? '===' : '!==='; // invalid on purpose
return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0';
}
});
});

View File

@ -275,6 +275,20 @@ describe('compileAsync method', function() {
setTimeout(function() { callback(new Error('cant load')); });
}
});
it('if schema compilation throws some other exception', function (done) {
ajv.addKeyword('badkeyword', { compile: badCompile });
var schema = { badkeyword: true };
ajv.compileAsync(schema, function (err, validate) {
should.exist(err);
should.not.exist(validate);
done();
});
function badCompile(schema) {
throw new Error('cant compile keyword schema');
}
});
});

652
spec/custom.spec.js Normal file
View File

@ -0,0 +1,652 @@
'use strict';
var getAjvInstances = require('./ajv_instances')
, should = require('chai').should()
, equal = require('../lib/compile/equal')
, customRules = require('./custom_rules');
describe('Custom keywords', function () {
var ajv, instances;
beforeEach(function() {
instances = getAjvInstances({
allErrors: true,
verbose: true,
inlineRefs: false
});
ajv = instances[0];
});
describe('custom rules', function() {
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 and validate rule with "compiled" keyword validation', function() {
testEvenKeyword({ type: 'number', compile: compileEven });
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 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
? isDeepEqual
: isStrictEqual;
function isDeepEqual(data) { return equal(data, schema); }
function isStrictEqual(data) { return data === schema; }
}
function compileRange(schema, parentSchema) {
validateRangeSchema(schema, parentSchema);
var min = schema[0];
var max = schema[1];
return parentSchema.exclusiveRange === true
? function (data) { return data > min && data < max; }
: function (data) { return data >= min && data <= max; }
}
});
describe('macro rules', function() {
it('should add and validate rule with "macro" keyword', function() {
testEvenKeyword({ macro: macroEven });
});
it('should add and expand macro rule', function() {
testConstantKeyword({ macro: macroConstant });
});
it('should allow multiple schemas for the same macro keyword', function () {
testMultipleConstantKeyword({ macro: macroConstant });
});
it('should pass parent schema to "macro" keyword', function() {
testRangeKeyword({ macro: macroRange });
});
it('should allow multiple parent schemas for the same macro keyword', function () {
testMultipleRangeKeyword({ macro: macroRange });
});
it('should recursively expand macro keywords', function() {
instances.forEach(function (ajv) {
ajv.addKeyword('deepProperties', { macro: macroDeepProperties });
ajv.addKeyword('range', { macro: macroRange });
var schema = {
"deepProperties": {
"a.b.c": { "type": "number", "range": [2,4] },
"d.e.f.g": { "type": "string" }
}
};
/* This schema recursively expands to:
{
"allOf": [
{
"properties": {
"a": {
"properties": {
"b": {
"properties": {
"c": {
"type": "number",
"minimum": 2,
"exclusiveMinimum": false,
"maximum": 4,
"exclusiveMaximum": false
}
}
}
}
}
}
},
{
"properties": {
"d": {
"properties": {
"e": {
"properties": {
"f": {
"properties": {
"g": {
"type": "string"
}
}
}
}
}
}
}
}
}
]
}
*/
var validate = ajv.compile(schema);
shouldBeValid(validate, {
a: {b: {c: 3}},
d: {e: {f: {g: 'foo'}}}
});
shouldBeInvalid(validate, {
a: {b: {c: 5}}, // out of range
d: {e: {f: {g: 'foo'}}}
});
shouldBeInvalid(validate, {
a: {b: {c: 'bar'}}, // not number
d: {e: {f: {g: 'foo'}}}
});
shouldBeInvalid(validate, {
a: {b: {c: 3}},
d: {e: {f: {g: 2}}} // not string
});
function macroDeepProperties(schema) {
if (typeof schema != 'object')
throw new Error('schema of deepProperty should be an object');
var expanded = [];
for (var prop in schema) {
var path = prop.split('.');
var properties = {};
if (path.length == 1) {
properties[prop] = schema[prop];
} else {
var deepProperties = {};
deepProperties[path.slice(1).join('.')] = schema[prop];
properties[path[0]] = { "deepProperties": deepProperties };
}
expanded.push({ "properties": properties });
}
return expanded.length == 1 ? expanded[0] : { "allOf": expanded };
}
});
});
it('should correctly expand multiple macros on the same level', function() {
instances.forEach(function (ajv) {
ajv.addKeyword('range', { macro: macroRange });
ajv.addKeyword('even', { macro: macroEven });
var schema = {
"range": [4,6],
"even": true
};
var validate = ajv.compile(schema);
var numErrors = ajv.opts.allErrors ? 2 : 1;
shouldBeInvalid(validate, 2);
shouldBeInvalid(validate, 3, numErrors);
shouldBeValid(validate, 4);
shouldBeInvalid(validate, 5);
shouldBeValid(validate, 6);
shouldBeInvalid(validate, 7, numErrors);
shouldBeInvalid(validate, 8);
});
});
it('should use "allOf" keyword if macro schemas cannot be merged', function() {
instances.forEach(function (ajv) {
ajv.addKeyword('range', { macro: macroRange });
var schema = {
"range": [1,4],
"minimum": 2.5
};
var validate = ajv.compile(schema);
validate.schema.allOf .should.be.an('array');
validate.schema.allOf .should.have.length(2);
shouldBeValid(validate, 3);
shouldBeInvalid(validate, 2);
});
});
it('should correctly expand macros in subschemas', function() {
instances.forEach(function (ajv) {
ajv.addKeyword('range', { macro: macroRange });
var schema = {
"allOf": [
{ "range": [4,8] },
{ "range": [2,6] }
]
}
var validate = ajv.compile(schema);
shouldBeInvalid(validate, 2);
shouldBeInvalid(validate, 3);
shouldBeValid(validate, 4);
shouldBeValid(validate, 5);
shouldBeValid(validate, 6);
shouldBeInvalid(validate, 7);
shouldBeInvalid(validate, 8);
});
});
it('should correctly expand macros in macro expansions', function() {
instances.forEach(function (ajv) {
ajv.addKeyword('range', { macro: macroRange });
ajv.addKeyword('contains', { macro: macroContains });
var schema = {
"contains": {
"type": "number",
"range": [4,7],
"exclusiveRange": true
}
};
var validate = ajv.compile(schema);
shouldBeInvalid(validate, [1,2,3]);
shouldBeInvalid(validate, [2,3,4]);
shouldBeValid(validate, [3,4,5]); // only 5 is in range
shouldBeValid(validate, [6,7,8]); // only 6 is in range
shouldBeInvalid(validate, [7,8,9]);
shouldBeInvalid(validate, [8,9,10]);
function macroContains(schema) {
return { "not": { "items": { "not": schema } } };
}
});
});
it('should throw exception is macro expansion is an invalid schema', function() {
ajv.addKeyword('invalid', { macro: macroInvalid });
var schema = { "invalid": true };
should.throw(function() {
var validate = ajv.compile(schema);
});
function macroInvalid(schema) {
return { "type": "invalid" };
}
});
function macroEven(schema) {
if (schema === true) return { "multipleOf": 2 };
if (schema === false) return { "not": { "multipleOf": 2 } };
throw new Error('Schema for "even" keyword should be boolean');
}
function macroConstant(schema, parentSchema) {
return { "enum": [schema] };
}
function macroRange(schema, parentSchema) {
validateRangeSchema(schema, parentSchema);
var exclusive = !!parentSchema.exclusiveRange;
return {
minimum: schema[0],
exclusiveMinimum: exclusive,
maximum: schema[1],
exclusiveMaximum: exclusive
};
}
});
describe('inline rules', function() {
it('should add and validate rule with "inline" code keyword', function() {
testEvenKeyword({ type: 'number', inline: inlineEven });
});
it('should pass parent schema to "inline" keyword', function() {
testRangeKeyword({ type: 'number', inline: inlineRange, statements: true });
});
it('should define "inline" keyword as template', function() {
var inlineRangeTemplate = customRules.range;
testRangeKeyword({
type: 'number',
inline: inlineRangeTemplate,
statements: true
});
});
it('should allow defining optional errors', function() {
var inlineRangeTemplate = customRules.rangeWithErrors;
testRangeKeyword({
type: 'number',
inline: inlineRangeTemplate,
statements: true
}, true);
});
it('should allow defining required errors', function() {
var inlineRangeTemplate = customRules.rangeWithErrors;
testRangeKeyword({
type: 'number',
inline: inlineRangeTemplate,
statements: true,
errors: true
}, true);
});
function inlineEven(it, schema) {
var op = schema ? '===' : '!==';
return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0';
}
function inlineRange(it, schema, parentSchema) {
var min = schema[0]
, max = schema[1]
, data = 'data' + (it.dataLevel || '')
, gt = parentSchema.exclusiveRange ? ' > ' : ' >= '
, lt = parentSchema.exclusiveRange ? ' < ' : ' <= ';
return 'var valid' + it.level + ' = ' + data + gt + min + ' && ' + data + lt + max + ';';
}
});
function testEvenKeyword(definition) {
instances.forEach(function (ajv) {
ajv.addKeyword('even', definition);
var schema = { "even": true };
var validate = ajv.compile(schema);
shouldBeValid(validate, 2);
shouldBeValid(validate, 'abc');
shouldBeInvalid(validate, 2.5);
shouldBeInvalid(validate, 3);
});
}
function testConstantKeyword(definition) {
instances.forEach(function (ajv) {
ajv.addKeyword('constant', definition);
var schema = { "constant": "abc" };
var validate = ajv.compile(schema);
shouldBeValid(validate, 'abc');
shouldBeInvalid(validate, 2);
shouldBeInvalid(validate, {});
});
}
function testMultipleConstantKeyword(definition) {
instances.forEach(function (ajv) {
ajv.addKeyword('constant', definition);
var schema = {
"properties": {
"a": { "constant": 1 },
"b": { "constant": 1 }
},
"additionalProperties": { "constant": { "foo": "bar" } },
"items": { "constant": { "foo": "bar" } }
};
var validate = ajv.compile(schema);
shouldBeValid(validate, {a:1, b:1});
shouldBeInvalid(validate, {a:2, b:1});
shouldBeValid(validate, {a:1, c: {foo: 'bar'}});
shouldBeInvalid(validate, {a:1, c: {foo: 'baz'}});
shouldBeValid(validate, [{foo: 'bar'}]);
shouldBeValid(validate, [{foo: 'bar'}, {foo: 'bar'}]);
shouldBeInvalid(validate, [1]);
});
}
function testRangeKeyword(definition, customErrors) {
instances.forEach(function (ajv) {
ajv.addKeyword('range', definition);
var schema = { "range": [2, 4] };
var validate = ajv.compile(schema);
shouldBeValid(validate, 2);
shouldBeValid(validate, 3);
shouldBeValid(validate, 4);
shouldBeValid(validate, 'abc');
shouldBeInvalid(validate, 1.99);
if (customErrors) shouldBeRangeError(validate.errors[0], '', '>=', 2);
shouldBeInvalid(validate, 4.01);
if (customErrors) shouldBeRangeError(validate.errors[0], '', '<=', 4);
var schema = {
"properties": {
"foo": {
"range": [2, 4],
"exclusiveRange": true
}
}
};
var validate = ajv.compile(schema);
shouldBeValid(validate, { foo: 2.01 });
shouldBeValid(validate, { foo: 3 });
shouldBeValid(validate, { foo: 3.99 });
shouldBeInvalid(validate, { foo: 2 });
if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '>', 2, true);
shouldBeInvalid(validate, { foo: 4 });
if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '<', 4, true);
});
}
function testMultipleRangeKeyword(definition) {
instances.forEach(function (ajv) {
ajv.addKeyword('range', definition);
var schema = {
"properties": {
"a": { "range": [2, 4], "exclusiveRange": true },
"b": { "range": [2, 4], "exclusiveRange": false }
},
"additionalProperties": { "range": [5, 7] },
"items": { "range": [5, 7] }
};
var validate = ajv.compile(schema);
shouldBeValid(validate, {a:3.99, b:4});
shouldBeInvalid(validate, {a:4, b:4});
shouldBeValid(validate, {a:2.01, c: 7});
shouldBeInvalid(validate, {a:2.01, c: 7.01});
shouldBeValid(validate, [5, 6, 7]);
shouldBeInvalid(validate, [7.01]);
});
}
function shouldBeRangeError(error, dataPath, comparison, limit, exclusive) {
delete error.schema;
delete error.data;
error .should.eql({
keyword: 'range',
dataPath: dataPath,
message: 'should be ' + comparison + ' ' + limit,
params: {
comparison: comparison,
limit: limit,
exclusive: !!exclusive
}
});
}
function validateRangeSchema(schema, parentSchema) {
var schemaValid = Array.isArray(schema) && schema.length == 2
&& typeof schema[0] == 'number'
&& typeof schema[1] == 'number';
if (!schemaValid) throw new Error('Invalid schema for range keyword, should be array of 2 numbers');
var exclusiveRangeSchemaValid = parentSchema.exclusiveRange === undefined
|| typeof parentSchema.exclusiveRange == 'boolean';
if (!exclusiveRangeSchemaValid) throw new Error('Invalid schema for exclusiveRange keyword, should be bolean');
}
function shouldBeValid(validate, data) {
validate(data) .should.equal(true);
should.not.exist(validate.errors);
}
function shouldBeInvalid(validate, data, numErrors) {
validate(data) .should.equal(false);
validate.errors .should.have.length(numErrors || 1);
}
describe('addKeyword method', function() {
var TEST_TYPES = [ undefined, 'number', 'string', 'boolean', ['number', 'string']];
it('should throw if defined keyword is passed', function() {
testThrow(['minimum', 'maximum', 'multipleOf', 'minLength', 'maxLength']);
testThrowDuplicate('custom');
function testThrow(keywords) {
TEST_TYPES.forEach(function (dataType, index) {
should.throw(function(){
addKeyword(keywords[index], dataType);
});
});
}
function testThrowDuplicate(keywordPrefix) {
var index = 0;
TEST_TYPES.forEach(function (dataType1) {
TEST_TYPES.forEach(function (dataType2) {
var keyword = keywordPrefix + (index++);
addKeyword(keyword, dataType1);
should.throw(function() {
addKeyword(keyword, dataType2);
});
});
});
}
});
it('should throw if unknown type is passed', function() {
should.throw(function() {
addKeyword('custom1', 'wrongtype');
});
should.throw(function() {
addKeyword('custom2', ['number', 'wrongtype']);
});
should.throw(function() {
addKeyword('custom3', ['number', undefined]);
});
});
it('should throw if type is passed to macro keyword', function() {
should.throw(function() {
ajv.addKeyword(keyword, {
type: 'number',
macro: function() {}
});
});
});
function addKeyword(keyword, dataType) {
ajv.addKeyword(keyword, {
type: dataType,
validate: function() {}
});
}
});
});

View File

@ -0,0 +1,9 @@
'use strict';
var fs = require('fs')
, doT = require('dot');
module.exports = {
range: doT.compile(fs.readFileSync(__dirname + '/range.jst')),
rangeWithErrors: doT.compile(fs.readFileSync(__dirname + '/range_with_errors.jst'))
};

View File

@ -0,0 +1,8 @@
{{
var $data = 'data' + (it.dataLevel || '')
, $min = it.schema.range[0]
, $max = it.schema.range[1]
, $gt = it.schema.exclusiveRange ? '>' : '>='
, $lt = it.schema.exclusiveRange ? '<' : '<=';
}}
var valid{{=it.level}} = {{=$data}} {{=$gt}} {{=$min}} && {{=$data}} {{=$lt}} {{=$max}};

View File

@ -0,0 +1,42 @@
{{
var $data = 'data' + (it.dataLevel || '')
, $min = it.schema.range[0]
, $max = it.schema.range[1]
, $exclusive = !!it.schema.exclusiveRange
, $gt = $exclusive ? '>' : '>='
, $lt = $exclusive ? '<' : '<='
, $lvl = it.level
, $err = 'err' + $lvl;
}}
var minOk{{=$lvl}} = {{=$data}} {{=$gt}} {{=$min}};
var valid{{=$lvl}} = minOk{{=$lvl}} && {{=$data}} {{=$lt}} {{=$max}};
if (!valid{{=$lvl}}) {
var {{=$err}};
if (minOk{{=$lvl}}) {
{{=$err}} = {
keyword: 'range',
message: 'should be {{=$lt}} {{=$max}}',
params: {
comparison: '{{=$lt}}',
limit: {{=$max}},
exclusive: {{=$exclusive}}
}
};
} else {
{{=$err}} = {
keyword: 'range',
message: 'should be {{=$gt}} {{=$min}}',
params: {
comparison: '{{=$gt}}',
limit: {{=$min}},
exclusive: {{=$exclusive}}
}
};
}
errors++;
if (vErrors) vErrors[vErrors.length] = {{=$err}};
else vErrors = [{{=$err}}];
}

View File

@ -9,11 +9,15 @@ describe('Validation errors', function () {
var ajv, ajvJP, fullAjv;
beforeEach(function() {
ajv = Ajv();
ajvJP = Ajv({ jsonPointers: true });
fullAjv = Ajv({ allErrors: true, jsonPointers: true });
createInstances();
});
function createInstances(errorDataPath) {
ajv = Ajv({ errorDataPath: errorDataPath });
ajvJP = Ajv({ errorDataPath: errorDataPath, jsonPointers: true });
fullAjv = Ajv({ errorDataPath: errorDataPath, allErrors: true, jsonPointers: true });
}
it('error should include dataPath', function() {
testSchema1({
properties: {
@ -22,7 +26,7 @@ describe('Validation errors', function () {
});
});
it('error should include dataPath in refs', function() {
it('"refs" error should include dataPath', function() {
testSchema1({
definitions: {
num: { type: 'number' }
@ -34,115 +38,289 @@ describe('Validation errors', function () {
});
it('errors for additionalProperties should include property in dataPath', function() {
var schema = {
properties: {
foo: {},
bar: {}
},
additionalProperties: false
};
describe('"additionalProperties" errors', function() {
it('should include property in dataPath with option errorDataPath="property"', function() {
createInstances('property');
testAdditional('property');
});
var data = { foo: 1, bar: 2 }
, invalidData = { foo: 1, bar: 2, baz: 3, quux: 4 };
it('should NOT include property in dataPath WITHOUT option errorDataPath', function() {
testAdditional();
});
var validate = ajv.compile(schema);
shouldBeValid(validate, data);
shouldBeInvalid(validate, invalidData);
shouldBeError(validate.errors[0], 'additionalProperties', "['baz']");
function testAdditional(errorDataPath) {
var schema = {
properties: {
foo: {},
bar: {}
},
additionalProperties: false
};
var validateJP = ajvJP.compile(schema);
shouldBeValid(validateJP, data);
shouldBeInvalid(validateJP, invalidData);
shouldBeError(validateJP.errors[0], 'additionalProperties', "/baz");
var data = { foo: 1, bar: 2 }
, invalidData = { foo: 1, bar: 2, baz: 3, quux: 4 };
var fullValidate = fullAjv.compile(schema);
shouldBeValid(fullValidate, data);
shouldBeInvalid(fullValidate, invalidData, 2);
shouldBeError(fullValidate.errors[0], 'additionalProperties', '/baz');
shouldBeError(fullValidate.errors[1], 'additionalProperties', '/quux');
var path = pathFunc(errorDataPath);
var msg = msgFunc(errorDataPath);
fullValidate.errors
.filter(function(err) { return err.keyword == 'additionalProperties'; })
.map(function(err) { return fullAjv.opts.jsonPointers ? err.dataPath.substr(1) : err.dataPath.slice(2,-2); })
.forEach(function(p) { delete invalidData[p]; });
var validate = ajv.compile(schema);
shouldBeValid(validate, data);
shouldBeInvalid(validate, invalidData);
shouldBeError(validate.errors[0], 'additionalProperties', path("['baz']"), undefined, { additionalProperty: 'baz' });
invalidData .should.eql({ foo: 1, bar: 2 });
var validateJP = ajvJP.compile(schema);
shouldBeValid(validateJP, data);
shouldBeInvalid(validateJP, invalidData);
shouldBeError(validateJP.errors[0], 'additionalProperties', path("/baz"), undefined, { additionalProperty: 'baz' });
var fullValidate = fullAjv.compile(schema);
shouldBeValid(fullValidate, data);
shouldBeInvalid(fullValidate, invalidData, 2);
shouldBeError(fullValidate.errors[0], 'additionalProperties', path('/baz'), undefined, { additionalProperty: 'baz' });
shouldBeError(fullValidate.errors[1], 'additionalProperties', path('/quux'), undefined, { additionalProperty: 'quux' });
if (errorDataPath == 'property') {
fullValidate.errors
.filter(function(err) { return err.keyword == 'additionalProperties'; })
.map(function(err) { return fullAjv.opts.jsonPointers ? err.dataPath.substr(1) : err.dataPath.slice(2,-2); })
.forEach(function(p) { delete invalidData[p]; });
invalidData .should.eql({ foo: 1, bar: 2 });
}
}
});
it('errors for required should include missing property in dataPath', function() {
var schema = {
required: ['foo', 'bar', 'baz']
};
describe('"required" errors', function() {
it('should include missing property in dataPath with option errorDataPath="property"', function() {
createInstances('property');
testRequired('property');
});
it('should NOT include missing property in dataPath WITHOUT option errorDataPath', function() {
testRequired();
});
function testRequired(errorDataPath) {
var schema = {
required: ['foo', 'bar', 'baz']
};
_testRequired(errorDataPath, schema, '.');
}
it('large data/schemas with option errorDataPath="property"', function() {
createInstances('property');
testRequiredLargeSchema('property');
});
it('large data/schemas WITHOUT option errorDataPath', function() {
testRequiredLargeSchema();
});
function testRequiredLargeSchema(errorDataPath) {
var schema = { required: [] }
, data = {}
, invalidData1 = {}
, invalidData2 = {};
for (var i=0; i<100; i++) {
schema.required.push(''+i); // properties from '0' to '99' are required
data[i] = invalidData1[i] = invalidData2[i] = i;
}
delete invalidData1[1]; // property '1' will be missing
delete invalidData2[2]; // properties '2' and '198' will be missing
delete invalidData2[98];
var path = pathFunc(errorDataPath);
var msg = msgFunc(errorDataPath);
test();
var schema = { anyOf: [ schema ] };
test(1);
function test(extraErrors) {
extraErrors = extraErrors || 0;
var validate = ajv.compile(schema);
shouldBeValid(validate, data);
shouldBeInvalid(validate, invalidData1, 1 + extraErrors);
shouldBeError(validate.errors[0], 'required', path("['1']"), msg('1'), { missingProperty: '1' });
shouldBeInvalid(validate, invalidData2, 1 + extraErrors);
shouldBeError(validate.errors[0], 'required', path("['2']"), msg('2'), { missingProperty: '2' });
var validateJP = ajvJP.compile(schema);
shouldBeValid(validateJP, data);
shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors);
shouldBeError(validateJP.errors[0], 'required', path("/1"), msg('1'), { missingProperty: '1' });
shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors);
shouldBeError(validateJP.errors[0], 'required', path("/2"), msg('2'), { missingProperty: '2' });
var fullValidate = fullAjv.compile(schema);
shouldBeValid(fullValidate, data);
shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors);
shouldBeError(fullValidate.errors[0], 'required', path('/1'), msg('1'), { missingProperty: '1' });
shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors);
shouldBeError(fullValidate.errors[0], 'required', path('/2'), msg('2'), { missingProperty: '2' });
shouldBeError(fullValidate.errors[1], 'required', path('/98'), msg('98'), { missingProperty: '98' });
}
}
it('with "properties" with option errorDataPath="property"', function() {
createInstances('property');
testRequiredAndProperties('property');
});
it('with "properties" WITHOUT option errorDataPath', function() {
testRequiredAndProperties();
});
function testRequiredAndProperties(errorDataPath) {
var schema = {
properties: {
'foo': { type: 'number' },
'bar': { type: 'number' },
'baz': { type: 'number' },
},
required: ['foo', 'bar', 'baz']
};
_testRequired(errorDataPath, schema);
}
it('in "anyOf" with option errorDataPath="property"', function() {
createInstances('property');
testRequiredInAnyOf('property');
});
it('in "anyOf" WITHOUT option errorDataPath', function() {
testRequiredInAnyOf();
});
function testRequiredInAnyOf(errorDataPath) {
var schema = {
anyOf: [
{ required: ['foo', 'bar', 'baz'] }
]
};
_testRequired(errorDataPath, schema, '.', 1);
}
});
describe('"dependencies" errors', function() {
it('should include missing property in dataPath with option errorDataPath="property"', function() {
createInstances('property');
testDependencies('property');
});
it('should NOT include missing property in dataPath WITHOUT option errorDataPath', function() {
testDependencies();
});
function testDependencies(errorDataPath) {
var schema = {
dependencies: {
a: ['foo', 'bar', 'baz']
}
};
var data = { a: 0, foo: 1, bar: 2, baz: 3 }
, invalidData1 = { a: 0, foo: 1, baz: 3 }
, invalidData2 = { a: 0, bar: 2 };
var path = pathFunc(errorDataPath);
var msg = 'should have properties foo, bar, baz when property a is present';
var validate = ajv.compile(schema);
shouldBeValid(validate, data);
shouldBeInvalid(validate, invalidData1);
shouldBeError(validate.errors[0], 'dependencies', path('.bar'), msg, params('.bar'));
shouldBeInvalid(validate, invalidData2);
shouldBeError(validate.errors[0], 'dependencies', path('.foo'), msg, params('.foo'));
var validateJP = ajvJP.compile(schema);
shouldBeValid(validateJP, data);
shouldBeInvalid(validateJP, invalidData1);
shouldBeError(validateJP.errors[0], 'dependencies', path('/bar'), msg, params('bar'));
shouldBeInvalid(validateJP, invalidData2);
shouldBeError(validateJP.errors[0], 'dependencies', path('/foo'), msg, params('foo'));
var fullValidate = fullAjv.compile(schema);
shouldBeValid(fullValidate, data);
shouldBeInvalid(fullValidate, invalidData1);
shouldBeError(fullValidate.errors[0], 'dependencies', path('/bar'), msg, params('bar'));
shouldBeInvalid(fullValidate, invalidData2/*, 2*/);
shouldBeError(fullValidate.errors[0], 'dependencies', path('/foo'), msg, params('foo'));
// shouldBeError(fullValidate.errors[1], 'dependencies', path('/baz'), msg, params('baz'));
function params(missing) {
var p = {
property: 'a',
deps: 'foo, bar, baz',
depsCount: 3
};
p.missingProperty = missing;
return p;
}
}
});
function _testRequired(errorDataPath, schema, prefix, extraErrors) {
prefix = prefix || '';
extraErrors = extraErrors || 0;
var data = { foo: 1, bar: 2, baz: 3 }
, invalidData1 = { foo: 1, baz: 3 }
, invalidData2 = { bar: 2 };
var validate = ajv.compile(schema);
shouldBeValid(validate, data);
shouldBeInvalid(validate, invalidData1);
shouldBeError(validate.errors[0], 'required', '.bar', 'is a required property');
shouldBeInvalid(validate, invalidData2);
shouldBeError(validate.errors[0], 'required', '.foo', 'is a required property');
var validateJP = ajvJP.compile(schema);
shouldBeValid(validateJP, data);
shouldBeInvalid(validateJP, invalidData1);
shouldBeError(validateJP.errors[0], 'required', '/bar', 'is a required property');
shouldBeInvalid(validateJP, invalidData2);
shouldBeError(validateJP.errors[0], 'required', '/foo', 'is a required property');
var fullValidate = fullAjv.compile(schema);
shouldBeValid(fullValidate, data);
shouldBeInvalid(fullValidate, invalidData1);
shouldBeError(fullValidate.errors[0], 'required', '/bar', 'is a required property');
shouldBeInvalid(fullValidate, invalidData2, 2);
shouldBeError(fullValidate.errors[0], 'required', '/foo', 'is a required property');
shouldBeError(fullValidate.errors[1], 'required', '/baz', 'is a required property');
});
it('required validation and errors for large data/schemas', function() {
var schema = { required: [] }
, data = {}
, invalidData1 = {}
, invalidData2 = {};
for (var i=0; i<100; i++) {
schema.required.push(''+i); // properties from '0' to '99' are required
data[i] = invalidData1[i] = invalidData2[i] = i;
}
delete invalidData1[1]; // property '1' will be missing
delete invalidData2[2]; // properties '2' and '198' will be missing
delete invalidData2[98];
var path = pathFunc(errorDataPath);
var msg = msgFunc(errorDataPath);
var validate = ajv.compile(schema);
shouldBeValid(validate, data);
shouldBeInvalid(validate, invalidData1);
shouldBeError(validate.errors[0], 'required', "['1']", "is a required property");
shouldBeInvalid(validate, invalidData2);
shouldBeError(validate.errors[0], 'required', "['2']", "is a required property");
shouldBeInvalid(validate, invalidData1, 1 + extraErrors);
shouldBeError(validate.errors[0], 'required', path('.bar'), msg(prefix + 'bar'), { missingProperty: prefix + 'bar' });
shouldBeInvalid(validate, invalidData2, 1 + extraErrors);
shouldBeError(validate.errors[0], 'required', path('.foo'), msg(prefix + 'foo'), { missingProperty: prefix + 'foo' });
var validateJP = ajvJP.compile(schema);
shouldBeValid(validateJP, data);
shouldBeInvalid(validateJP, invalidData1);
shouldBeError(validateJP.errors[0], 'required', "/1", "is a required property");
shouldBeInvalid(validateJP, invalidData2);
shouldBeError(validateJP.errors[0], 'required', "/2", "is a required property");
shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors);
shouldBeError(validateJP.errors[0], 'required', path('/bar'), msg('bar'), { missingProperty: 'bar' });
shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors);
shouldBeError(validateJP.errors[0], 'required', path('/foo'), msg('foo'), { missingProperty: 'foo' });
var fullValidate = fullAjv.compile(schema);
shouldBeValid(fullValidate, data);
shouldBeInvalid(fullValidate, invalidData1);
shouldBeError(fullValidate.errors[0], 'required', '/1', "is a required property");
shouldBeInvalid(fullValidate, invalidData2, 2);
shouldBeError(fullValidate.errors[0], 'required', '/2', "is a required property");
shouldBeError(fullValidate.errors[1], 'required', '/98', "is a required property");
});
shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors);
shouldBeError(fullValidate.errors[0], 'required', path('/bar'), msg('bar'), { missingProperty: 'bar' });
shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors);
shouldBeError(fullValidate.errors[0], 'required', path('/foo'), msg('foo'), { missingProperty: 'foo' });
shouldBeError(fullValidate.errors[1], 'required', path('/baz'), msg('baz'), { missingProperty: 'baz' });
}
function pathFunc(errorDataPath) {
return function (dataPath) {
return errorDataPath == 'property' ? dataPath : '';
};
}
function msgFunc(errorDataPath) {
return function (prop) {
return errorDataPath == 'property'
? 'is a required property'
: 'should have required property \'' + prop + '\'';
};
}
it('errors for items should include item index without quotes in dataPath (#48)', function() {
it('"items" errors should include item index without quotes in dataPath (#48)', function() {
var schema1 = {
id: 'schema1',
type: 'array',
@ -223,11 +401,11 @@ describe('Validation errors', function () {
}
function shouldBeError(error, keyword, dataPath, message) {
function shouldBeError(error, keyword, dataPath, message, params) {
error.keyword .should.equal(keyword);
error.dataPath .should.equal(dataPath);
error.message .should.be.a('string');
if (message !== undefined)
error.message .should.equal(message);
if (message !== undefined) error.message .should.equal(message);
if (params !== undefined) error.params .should.eql(params);
}
});

View File

@ -4,7 +4,6 @@ var jsonSchemaTest = require('json-schema-test')
, getAjvInstances = require('./ajv_instances');
var isBrowser = typeof window == 'object';
var Ajv = require(isBrowser ? 'ajv' : '../lib/ajv');
var fullTest = isBrowser || !process.env.AJV_FAST_TEST;
var instances = getAjvInstances(fullTest ? {
@ -14,7 +13,6 @@ var instances = getAjvInstances(fullTest ? {
format: 'full',
inlineRefs: false,
jsonPointers: true,
i18n: true
} : { allErrors: true });
var remoteRefs = {

View File

@ -193,9 +193,9 @@ describe('Ajv Options', function () {
it('should not throw and fail validation with missingRef == "fail" if the ref is used', function() {
testMissingRefsFail(Ajv({ missingRefs: 'fail' }));
testMissingRefsFail(Ajv({ missingRefs: 'fail', verbose: true, i18n: true }));
testMissingRefsFail(Ajv({ missingRefs: 'fail', verbose: true }));
testMissingRefsFail(Ajv({ missingRefs: 'fail', allErrors: true }));
testMissingRefsFail(Ajv({ missingRefs: 'fail', allErrors: true, verbose: true, i18n: true }));
testMissingRefsFail(Ajv({ missingRefs: 'fail', allErrors: true, verbose: true }));
function testMissingRefsFail(ajv) {
var validate = ajv.compile({
@ -249,4 +249,29 @@ describe('Ajv Options', function () {
}
});
});
describe('v5', function() {
it('should define keywords "constant" and "contains"', function() {
testV5(Ajv({ v5: true }));
testV5(Ajv({ v5: true, allErrors: true }));
function testV5(ajv) {
var validate = ajv.compile({ constant: 2 });
validate(2) .should.equal(true);
validate(5) .should.equal(false);
validate('a') .should.equal(false);
var validate = ajv.compile({ contains: { minimum: 5 }});
validate([1,2,3,4]) .should.equal(false);
validate([3,4,5]) .should.equal(true);
validate([3,4,6]) .should.equal(true);
var validate = ajv.compile({ contains: { constant: 5 }});
validate([1,2,3,4]) .should.equal(false);
validate([3,4,6]) .should.equal(false);
validate([3,4,5]) .should.equal(true);
}
});
});
});

View File

@ -39,8 +39,7 @@ describe('resolve', function () {
instances = getAjvInstances({
allErrors: true,
verbose: true,
inlineRefs: false,
i18n: true
inlineRefs: false
});
});