Skip to content

Commit

Permalink
patternGroups keyword (v5 proposals)
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Nov 28, 2015
1 parent cf35958 commit 7d96e1b
Show file tree
Hide file tree
Showing 11 changed files with 529 additions and 55 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ NB: [Upgrading to version 2.0.0](https://github.com/epoberezkin/ajv/releases/tag
- i18n error messages support with [ajv-i18n](https://github.com/epoberezkin/ajv-i18n) package (version >= 1.0.0)
- [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)
- NEW: keywords `constant`, `contains` and `patternGroups` 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).

Expand Down Expand Up @@ -539,7 +539,7 @@ Defaults:
- _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)
- _v5_: add keywords `constant`, `contains` and `patternGroups` from [JSON-schema v5 proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals)


## Validation errors
Expand All @@ -566,6 +566,10 @@ Properties of `params` object in errors depend on the keyword that failed valida
- `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).
- `patternGroups` (with v5 option) - properties:
- `pattern`
- `reason` ("minimum"/"maximum"),
- `limit` (max/min allowed number of properties matching number)
- `dependencies` - properties:
- `property` (dependent property),
- `missingProperty` (required missing dependency - only the first one is reported currently)
Expand Down
47 changes: 25 additions & 22 deletions lib/dot/definitions.def
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@
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)'",
custom: "'should pass \"{{=$rule.keyword}}\" keyword validation'"
custom: "'should pass \"{{=$rule.keyword}}\" keyword validation'",
patternGroups: "'should NOT have {{=$moreOrLess}} than {{=$limit}} properties matching pattern \"{{=it.util.escapeQuotes($pgProperty)}}\"'"
} #}}


Expand All @@ -192,31 +193,33 @@
required: "validate.schema{{=$schemaPath}}",
type: "{{? $isArray }}['{{= $typeSchema.join(\"','\") }}']{{??}}'{{=$typeSchema}}'{{?}}",
uniqueItems: "{{=$schema}}",
custom: "validate.schema{{=$schemaPath}}"
custom: "validate.schema{{=$schemaPath}}",
patternGroups: "validate.schema{{=$schemaPath}}"
} #}}


{{## def._errorParams = {
$ref: "{ ref: '{{=it.util.escapeQuotes($schema)}}' }",
$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' }"
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' }",
patternGroups: "{ reason: '{{=$reason}}', limit: {{=$limit}}, pattern: '{{=it.util.escapeQuotes($pgProperty)}}' }"
} #}}
73 changes: 73 additions & 0 deletions lib/dot/properties.jst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
, $removeAdditional = it.opts.removeAdditional
, $checkAdditional = $noAdditional || $additionalIsSchema || $removeAdditional
, $requiredProperties = it.util.toHash(it.schema.required || []);

if (it.opts.v5) {
var $pgProperties = it.schema.patternGroups || {}
, $pgPropertyKeys = Object.keys($pgProperties);
}
}}


Expand All @@ -57,6 +62,11 @@ var valid{{=$it.level}} = true;
|| {{= it.usePattern($pProperty) }}.test(key{{=$lvl}})
{{~}}
{{?}}
{{? it.opts.v5 && $pgPropertyKeys && $pgPropertyKeys.length }}
{{~ $pgPropertyKeys:$pgProperty:$i }}
|| {{= it.usePattern($pgProperty) }}.test(key{{=$lvl}})
{{~}}
{{?}}
);

if (isAdditional{{=$lvl}}) {
Expand Down Expand Up @@ -195,6 +205,69 @@ var valid{{=$it.level}} = true;
{{?}} {{ /* def.nonEmptySchema */ }}
{{~}}


{{? it.opts.v5 }}
{{~ $pgPropertyKeys:$pgProperty }}
{{
var $pgSchema = $pgProperties[$pgProperty]
, $sch = $pgSchema.schema;
}}

{{? {{# def.nonEmptySchema:$sch}} }}
{{
$it.schema = $sch;
$it.schemaPath = it.schemaPath + '.patternGroups' + it.util.getProperty($pgProperty);
}}

var pgPropCount{{=$lvl}} = 0;

for (var key{{=$lvl}} in {{=$data}}) {
if ({{= it.usePattern($pgProperty) }}.test(key{{=$lvl}})) {
pgPropCount{{=$lvl}}++;

{{
$it.errorPath = it.util.getPathExpr(it.errorPath, 'key' + $lvl, it.opts.jsonPointers);
var $passData = $data + '[key' + $lvl + ']';
}}

{{ var $code = it.validate($it); }}
{{# def.optimizeValidate }}

{{? $breakOnError }} if (!valid{{=$it.level}}) break; {{?}}
}
{{? $breakOnError }} else valid{{=$it.level}} = true; {{?}}
}

{{# def.ifResultValid }}

{{
var $pgMin = $pgSchema.minimum
, $pgMax = $pgSchema.maximum;
}}
{{? $pgMin !== undefined || $pgMax !== undefined }}
var {{=$valid}} = true;
{{? $pgMin !== undefined }}
{{ var $limit = $pgMin, $reason = 'minimum', $moreOrLess = 'less'; }}
{{=$valid}} = pgPropCount{{=$lvl}} >= {{=$pgMin}};
{{# def.checkError:'patternGroups' }}
{{? $pgMax !== undefined }}
else
{{?}}
{{?}}

{{? $pgMax !== undefined }}
{{ var $limit = $pgMax, $reason = 'maximum', $moreOrLess = 'more'; }}
{{=$valid}} = pgPropCount{{=$lvl}} <= {{=$pgMax}};
{{# def.checkError:'patternGroups' }}
{{?}}

{{# def.ifValid }}
{{?}}
{{?}} {{ /* def.nonEmptySchema */ }}
{{~}}
{{?}}


{{? $breakOnError }}
{{= $closingBraces }}
if ({{=$errs}} == errors) {
Expand Down
8 changes: 5 additions & 3 deletions lib/dot/validate.jst
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,10 @@
return it.schema[$rule.keyword] !== undefined ||
( $rule.keyword == 'properties' &&
( it.schema.additionalProperties === false ||
typeof it.schema.additionalProperties == 'object' ||
( it.schema.patternProperties &&
Object.keys(it.schema.patternProperties).length )));
typeof it.schema.additionalProperties == 'object'
|| ( it.schema.patternProperties &&
Object.keys(it.schema.patternProperties).length )
|| ( it.opts.v5 && it.schema.patternGroups &&
Object.keys(it.schema.patternGroups).length )));
}
}}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ajv",
"version": "2.0.4",
"version": "2.1.0",
"description": "Another JSON Schema Validator",
"main": "lib/ajv.js",
"files": [
Expand Down
4 changes: 2 additions & 2 deletions spec/ajv_instances.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ var Ajv = require(typeof window == 'object' ? 'ajv' : '../lib/ajv')
module.exports = getAjvInstances;


function getAjvInstances(options) {
return _getAjvInstances(options, {});
function getAjvInstances(options, extraOpts) {
return _getAjvInstances(options, extraOpts || {});
}

function _getAjvInstances(opts, useOpts) {
Expand Down
25 changes: 0 additions & 25 deletions spec/options.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,29 +249,4 @@ 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);
}
});
});
});
47 changes: 47 additions & 0 deletions spec/v5.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';

var jsonSchemaTest = require('json-schema-test')
, getAjvInstances = require('./ajv_instances');

var isBrowser = typeof window == 'object';

var fullTest = isBrowser || !process.env.AJV_FAST_TEST;
var instances = getAjvInstances(fullTest ? {
beautify: true,
allErrors: true,
verbose: true,
format: 'full',
inlineRefs: false,
jsonPointers: true,
} : { allErrors: true }, { v5: true });


jsonSchemaTest(instances, {
description: 'v5 schemas tests of ' + instances.length + ' ajv instances with different options',
suites: testSuites(),
afterError: function (res) {
console.log('ajv options:', res.validator.opts);
},
cwd: __dirname,
hideFolder: 'v5/',
timeout: 90000
});


function testSuites() {
if (typeof window == 'object') {
var suites = {
'v5 proposals': require('./v5/{**/,}*.json', {mode: 'list'})
};
for (var suiteName in suites) {
suites[suiteName].forEach(function (suite) {
suite.test = suite.module;
});
}
} else {
var suites = {
'v5 proposals': './v5/{**/,}*.json'
}
}
return suites;
}
49 changes: 49 additions & 0 deletions spec/v5/constant.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[
{
"description": "constant keyword requires the value to be equal to some constant",
"schema": { "constant": 2 },
"tests": [
{
"description": "same value is valid",
"data": 2,
"valid": true
},
{
"description": "another value is invalid",
"data": 5,
"valid": false
},
{
"description": "another type is invalid",
"data": "a",
"valid": false
}
]
},
{
"description": "constant keyword requires the value to be equal to some object",
"schema": { "constant": { "foo": "bar", "baz": "bax" } },
"tests": [
{
"description": "same object is valid",
"data": { "foo": "bar", "baz": "bax" },
"valid": true
},
{
"description": "same object with different property order is valid",
"data": { "baz": "bax", "foo": "bar" },
"valid": true
},
{
"description": "another object is invalid",
"data": { "foo": "bar" },
"valid": false
},
{
"description": "another type is invalid",
"data": [ 1, 2 ],
"valid": false
}
]
}
]
49 changes: 49 additions & 0 deletions spec/v5/contains.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[
{
"description": "contains keyword requires the item matching schema to be present",
"schema": {
"contains": { "minimum": 5 }
},
"tests": [
{
"description": "array with item matching schema (5) is valid",
"data": [3, 4, 5],
"valid": true
},
{
"description": "array with item matching schema (6) is valid",
"data": [3, 4, 6],
"valid": true
},
{
"description": "array without item matching schema is invalid",
"data": [1, 2, 3, 4],
"valid": false
},
{
"skip": true,
"description": "not array is valid",
"data": {},
"valid": true
}
]
},
{
"description": "contains keyword with constant keyword requires a specific item to be present",
"schema": {
"contains": { "constant": 5 }
},
"tests": [
{
"description": "array with item 5 is valid",
"data": [3, 4, 5],
"valid": true
},
{
"description": "array without item 5 is invalid",
"data": [1, 2, 3, 4],
"valid": false
}
]
}
]
Loading

0 comments on commit 7d96e1b

Please sign in to comment.