Skip to content

Commit

Permalink
time format; formatMaximum/formatMinimum and exclusiveFormatMaximum/e…
Browse files Browse the repository at this point in the history
…xclusiveFormatMinimum keywords from v5 proposals
  • Loading branch information
epoberezkin committed Dec 5, 2015
1 parent 6afdb67 commit d7fd822
Show file tree
Hide file tree
Showing 10 changed files with 541 additions and 28 deletions.
8 changes: 5 additions & 3 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`, `contains` and `patternGroups` from [JSON-schema v5 proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals) with [option v5](#options)
- NEW: keywords `constant`, `contains`, `patternGroups`, `formatMaximum`/`formatMinimum` and `exclusiveFormatMaximum`/`exclusiveFormatMinimum` 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 @@ -452,14 +452,16 @@ Remove added/cached schema. Even if schema is referenced by other schemas it can
Schema can be removed using key passed to `addSchema`, it's full reference (id) or using actual schema object that will be stable-stringified to remove schema from cache.


##### <a name="api-addformat"></a>.addFormat(String name, String|RegExp|Function format)
##### <a name="api-addformat"></a>.addFormat(String name, String|RegExp|Function|Object format)

Add custom format to validate strings. It can also be used to replace pre-defined formats for ajv instance.

Strings are converted to RegExp.

Function should return validation result as `true` or `false`.

If object is passed it should have properties `validate` and `compare`. `validate` can be a string, RegExp or a function as described above. `compare` is a comparison function that accepts two strings and compares them according to the format meaning. This function is used with keywords `formatMaximum`/`formatMinimum` (from [v5 proposals](https://github.com/json-schema/json-schema/wiki/v5-Proposals) - `v5` option should be used). It should return `1` if the first value is bigger than the second value, `-1` if it is smaller and `0` if it is equal.

Custom formats can be also added via `formats` option.


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


## Validation errors
Expand Down
68 changes: 58 additions & 10 deletions lib/compile/formats.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ var util = require('./util');

var DATE = /^\d\d\d\d-(\d\d)-(\d\d)$/;
var DAYS = [0,31,29,31,30,31,30,31,31,30,31,30,31];
var TIME = /^(\d\d):(\d\d):(\d\d)(?:\.\d+)?(?:z|[+-]\d\d:\d\d)$/;
var TIME = /^(\d\d):(\d\d):(\d\d)(\.\d+)?(z|[+-]\d\d:\d\d)?$/i;
var HOSTNAME = /^[a-z](?:(?:[-0-9a-z]{0,61})?[0-9a-z])?(\.[a-z](?:(?:[-0-9a-z]{0,61})?[0-9a-z])?)*$/i;
var URI = /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@\/?]|%[0-9a-f]{2})*)?(?:\#(?:[a-z0-9\-._~!$&'()*+,;=:@\/?]|%[0-9a-f]{2})*)?$/i;

Expand All @@ -13,15 +13,23 @@ module.exports = formats;

function formats(mode) {
mode = mode == 'full' ? 'full' : 'fast';
return util.copy(formats[mode]);
var formatDefs = util.copy(formats[mode]);
for (var fName in formats.compare) {
formatDefs[fName] = {
validate: formatDefs[fName],
compare: formats.compare[fName]
};
}
return formatDefs;
}


formats.fast = {
// date: http://tools.ietf.org/html/rfc3339#section-5.6
date: /^\d\d\d\d-[0-1]\d-[0-3]\d$/,
// date-time: http://tools.ietf.org/html/rfc3339#section-5.6
'date-time': /^\d\d\d\d-[0-1]\d-[0-3]\d[t ][0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?(?:z|[+-]\d\d:\d\d)$/i,
time: /^[0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?(?:z|[+-]\d\d:\d\d)?$/i,
'date-time': /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s][0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?(?:z|[+-]\d\d:\d\d)$/i,
// uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js
uri: /^(?:[a-z][a-z0-9+-.]*)?(?:\:|\/)\/?[^\s]*$/i,
// email (sources from jsen validator):
Expand All @@ -39,6 +47,7 @@ formats.fast = {

formats.full = {
date: date,
time: time,
'date-time': date_time,
uri: uri,
email: /^[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&''*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i,
Expand All @@ -49,6 +58,13 @@ formats.full = {
};


formats.compare = {
date: compareDate,
time: compareTime,
'date-time': compareDateTime
};


function date(str) {
// full-date from http://tools.ietf.org/html/rfc3339#section-5.6
var matches = str.match(DATE);
Expand All @@ -60,18 +76,23 @@ function date(str) {
}


function date_time(str) {
// http://tools.ietf.org/html/rfc3339#section-5.6
var dateTime = str.toLowerCase().split('t');
if (!date(dateTime[0])) return false;

var matches = dateTime[1].match(TIME);
function time(str, full) {
var matches = str.match(TIME);
if (!matches) return false;

var hour = matches[1];
var minute = matches[2];
var second = matches[3];
return hour <= 23 && minute <= 59 && second <= 59;
var timeZone = matches[5];
return hour <= 23 && minute <= 59 && second <= 59 && (!full || timeZone);
}


var DATE_TIME_SEPARATOR = /t|\s/i;
function date_time(str) {
// http://tools.ietf.org/html/rfc3339#section-5.6
var dateTime = str.split(DATE_TIME_SEPARATOR);
return date(dateTime[0]) && time(dateTime[1], true);
}


Expand All @@ -96,3 +117,30 @@ function regex(str) {
return false;
}
}


function compareDate(d1, d2) {
if (d1 > d2) return 1;
if (d1 < d2) return -1;
return 0;
}


function compareTime(t1, t2) {
t1 = t1.match(TIME);
t2 = t2.match(TIME);
if (!(t1 && t2)) return 0;
t1 = t1[1] + t1[2] + t1[3] + (t1[4]||'');
t2 = t2[1] + t2[2] + t2[3] + (t2[4]||'');
if (t1 > t2) return 1;
if (t1 < t2) return -1;
return 0;
}


function compareDateTime(dt1, dt2) {
dt1 = dt1.split(DATE_TIME_SEPARATOR);
dt2 = dt2.split(DATE_TIME_SEPARATOR);
return compareDate(dt1[0], dt2[0])
|| compareTime(dt1[1], dt2[1]);
}
2 changes: 1 addition & 1 deletion lib/dot/definitions.def
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,6 @@
required: "{ missingProperty: '{{=$missingProperty}}' }",
type: "{ type: '{{? $isArray }}{{= $typeSchema.join(\",\") }}{{??}}{{=$typeSchema}}{{?}}' }",
uniqueItems: "{ i: i, j: j }",
custom: "{ keyword: '$rule.keyword' }",
custom: "{ keyword: '{{=$rule.keyword}}' }",
patternGroups: "{ reason: '{{=$reason}}', limit: {{=$limit}}, pattern: '{{=it.util.escapeQuotes($pgProperty)}}' }"
} #}}
12 changes: 10 additions & 2 deletions lib/dot/format.jst
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
{{# def.definitions }}
{{# def.setup:'format' }}

{{ var $format = it.formats[$schema]; }}
{{
var $format = it.formats[$schema];
var $isObject = typeof $format == 'object'
&& !($format instanceof RegExp)
&& $format.validate;
if ($isObject) $format = $format.validate;
}}

{{## def.format: formats{{= it.util.getProperty($schema) }} #}}
{{## def.format:
formats{{= it.util.getProperty($schema) }}{{? $isObject }}.validate{{?}}
#}}

{{## def.checkFormat:
{{? typeof $format == 'function' }}
Expand Down
24 changes: 13 additions & 11 deletions lib/keyword.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@ module.exports = function addKeyword(keyword, definition) {
if (this.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);
if (definition) {
if (definition.macro) {
if (definition.type) throw new Error('type cannot be defined for macro keywords');
_addMacro(keyword, definition.macro);
} else {
if (dataType) checkDataType(dataType);
_addRule(keyword, dataType, definition);
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);
}
}
}

Expand Down
20 changes: 20 additions & 0 deletions lib/v5.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ module.exports = {
function enableV5(ajv) {
ajv.addKeyword('constant', { macro: constantMacro });
ajv.addKeyword('contains', { macro: containsMacro });
ajv.addKeyword('formatMaximum', { type: 'string', inline: inlineFormatLimit('maximum') });
ajv.addKeyword('formatMinimum', { type: 'string', inline: inlineFormatLimit('minimum') });
ajv.addKeyword('exclusiveFormatMaximum');
ajv.addKeyword('exclusiveFormatMinimum');
ajv.addKeyword('patternGroups');
}

function constantMacro(schema) {
Expand All @@ -18,3 +23,18 @@ function constantMacro(schema) {
function containsMacro(schema) {
return { not: { items: { not: schema } } };
}

function inlineFormatLimit(limit) {
return function(it, schema, parentSchema) {
var format = parentSchema.format;
var compare = it.formats[format].compare;
if (!compare) throw new Error('No format or no comparison for the format');
var exclusive = parentSchema[limit == 'minimum' ? 'exclusiveFormatMinimum' : 'exclusiveFormatMaximum'];
var data = 'data' + (it.dataLevel || '');
var op = limit == 'minimum' ? '>' : '<';
if (!exclusive) op += '=';
return 'formats' + it.util.getProperty(format) + '.compare('
+ data + ', ' + it.util.toQuotedString(schema)
+ ') ' + op + ' 0';
}
}
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.1.4",
"version": "2.2.0",
"description": "Another JSON Schema Validator",
"main": "lib/ajv.js",
"files": [
Expand Down
21 changes: 21 additions & 0 deletions spec/tests/rules/format.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,26 @@
"valid": true
}
]
},
{
"description": "validation of date strings",
"schema": {"format": "date"},
"tests": [
{
"description": "a valid date-time string",
"data": "1963-06-19",
"valid": true
},
{
"description": "an invalid date-time string",
"data": "06/19/1963",
"valid": false
},
{
"description": "only RFC3339 not all of ISO 8601 are valid",
"data": "2013-350",
"valid": false
}
]
}
]

0 comments on commit d7fd822

Please sign in to comment.