diff --git a/README.md b/README.md index 8cdeb28..2d51a6e 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ ajv compiles schemas to functions and caches them in both cases (using stringifi ## Options -- _allErrors_: if true, jv will continue validating all rules collecting all errors (false by default) +- _allErrors_: if true, ajv checks all rules collecting all errors, otherwise it will return after the first error (false by default) - _verbose_: include the reference to the validated data in the errors (false by default) - _format_: if false, the formats won't be validated (true by default) -- _uniqueItems_: if false, `uniqueItems` keyword will be ignored (true by default) -- _unicode_: if true, the lengths of strings with unicode pairs will be correct (false by default) - not implemented yet. +- _uniqueItems_: if false, `uniqueItems` keyword will be ignored (true by default, i.e. uniqueItems is checked) +- _unicode_: if true, the lengths of strings with unicode pairs will be correct and each pair will be counted as one character (false by default, as it is slower). diff --git a/lib/compile/formats.js b/lib/compile/formats.js index 48827db..a811b67 100644 --- a/lib/compile/formats.js +++ b/lib/compile/formats.js @@ -1,14 +1,14 @@ 'use strict'; module.exports = { - date: date, - '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])?$/, - hostname: hostname, - ipv4: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/, - ipv6: /^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$/i, - regex: regex + date: date, + '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])?$/, + hostname: hostname, + ipv4: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/, + ipv6: /^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$/i, + regex: regex }; var DATE = /^\d\d\d\d-(\d\d)-(\d\d)$/; @@ -19,48 +19,48 @@ var URI = /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9 function date(str) { - // full-date from http://tools.ietf.org/html/rfc3339#section-5.6 - var matches = str.match(DATE); - if (!matches) return false; + // full-date from http://tools.ietf.org/html/rfc3339#section-5.6 + var matches = str.match(DATE); + if (!matches) return false; - var month = +matches[1]; - var day = +matches[2]; - return month >= 1 && month <= 12 && day >= 1 && day <= DAYS[month]; + var month = +matches[1]; + var day = +matches[2]; + return month >= 1 && month <= 12 && day >= 1 && day <= DAYS[month]; } 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; + // 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); - if (!matches) return false; + var matches = dateTime[1].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 hour = matches[1]; + var minute = matches[2]; + var second = matches[3]; + return hour <= 23 && minute <= 59 && second <= 59; } function hostname(str) { - // http://tools.ietf.org/html/rfc1034#section-3.5 - return str.length <= 255 && HOSTNAME.test(str); + // http://tools.ietf.org/html/rfc1034#section-3.5 + return str.length <= 255 && HOSTNAME.test(str); } function uri(str) { - // http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "." - return str.indexOf('.') >= 0 && URI.test(str); + // http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "." + return str.indexOf('.') >= 0 && URI.test(str); } function regex(str) { - try { - new RegExp(str); - return true; - } catch(e) { - return false; - } + try { + new RegExp(str); + return true; + } catch(e) { + return false; + } } diff --git a/lib/compile/index.js b/lib/compile/index.js index b7fa319..9b92d67 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -1,54 +1,54 @@ 'use strict'; var doT = require('dot') - , fs = require('fs') - , stableStringify = require('json-stable-stringify') - , formats = require('./formats') - , resolve = require('./resolve') - , equal = require('./equal'); + , fs = require('fs') + , stableStringify = require('json-stable-stringify') + , formats = require('./formats') + , resolve = require('./resolve') + , equal = require('./equal'); var RULES = require('./rules') - , validateTemplate = fs.readFileSync(__dirname + '/validate.dot.js') - , validateGenerator = doT.compile(validateTemplate); + , validateTemplate = fs.readFileSync(__dirname + '/validate.dot.js') + , validateGenerator = doT.compile(validateTemplate); module.exports = compile; function compile(schema) { - var self = this; - var validateCode = validateGenerator({ - isRoot: true, - schema: schema, - schemaPath: '', - RULES: RULES, - validate: validateGenerator, - copy: copy, - toHash: toHash, - resolveRef: resolveRef, - checkDataType: checkDataType, - checkDataTypes: checkDataTypes, - escapeQuotes: escapeQuotes, - stableStringify: stableStringify, - opts: this.opts - }); - // console.log('\n\n\n *** \n', validateCode); - var validate; - eval('validate = ' + validateCode); + var self = this; + var validateCode = validateGenerator({ + isRoot: true, + schema: schema, + schemaPath: '', + RULES: RULES, + validate: validateGenerator, + copy: copy, + toHash: toHash, + resolveRef: resolveRef, + checkDataType: checkDataType, + checkDataTypes: checkDataTypes, + escapeQuotes: escapeQuotes, + stableStringify: stableStringify, + opts: this.opts + }); + // console.log('\n\n\n *** \n', validateCode); + var validate; + eval('validate = ' + validateCode); - validate.schema = schema; - validate.errors = []; + validate.schema = schema; + validate.errors = []; - return validate; + return validate; - function resolveRef(ref) { - return resolve.call(self, compile, schema, ref); - } + function resolveRef(ref) { + return resolve.call(self, compile, schema, ref); + } - function validateRef(ref, data) { - var v = ref == '#' ? validate : self._schemas[ref]; - var valid = v(data); - return { valid: valid, errors: v.errors }; - } + function validateRef(ref, data) { + var v = ref == '#' ? validate : self._schemas[ref]; + var valid = v(data); + return { valid: valid, errors: v.errors }; + } } @@ -58,58 +58,78 @@ function compile(schema) { function copy(o, to) { - to = to || {}; - for (var key in o) to[key] = o[key]; - return to; + to = to || {}; + for (var key in o) to[key] = o[key]; + return to; } function checkDataType(dataType, lvl) { - var data = 'data' + lvl; - switch (dataType) { - case 'null': return data + ' === null'; - case 'array': return 'Array.isArray(' + data + ')'; - case 'object': return '(' + data + ' && typeof ' + data + ' == "object" && !Array.isArray(' + data + '))'; - case 'integer': return '(typeof ' + data + ' == "number" && !(' + data + ' % 1))' - default: return 'typeof ' + data + ' == "' + dataType + '"'; - } + var data = 'data' + lvl; + switch (dataType) { + case 'null': return data + ' === null'; + case 'array': return 'Array.isArray(' + data + ')'; + case 'object': return '(' + data + ' && typeof ' + data + ' == "object" && !Array.isArray(' + data + '))'; + case 'integer': return '(typeof ' + data + ' == "number" && !(' + data + ' % 1))' + default: return 'typeof ' + data + ' == "' + dataType + '"'; + } } function checkDataTypes(dataTypes, lvl) { - var data = 'data' + lvl; - switch (dataTypes.length) { - case 0: return 'true'; - case 1: return checkDataType(dataTypes[0], lvl); - default: - var code = '' - var types = toHash(dataTypes); - if (types.array && types.object) { - code = types.null ? '(': '(' + data + ' && ' - code += 'typeof ' + data + ' == "object")'; - delete types.null; - delete types.array; - delete types.object; - } - if (types.number) delete types.integer; - for (var t in types) - code += (code ? '||' : '' ) + checkDataType(t, lvl); + var data = 'data' + lvl; + switch (dataTypes.length) { + case 0: return 'true'; + case 1: return checkDataType(dataTypes[0], lvl); + default: + var code = '' + var types = toHash(dataTypes); + if (types.array && types.object) { + code = types.null ? '(': '(' + data + ' && ' + code += 'typeof ' + data + ' == "object")'; + delete types.null; + delete types.array; + delete types.object; + } + if (types.number) delete types.integer; + for (var t in types) + code += (code ? '||' : '' ) + checkDataType(t, lvl); - return code; + return code; + } +} + + +// https://mathiasbynens.be/notes/javascript-encoding +// https://github.com/bestiejs/punycode.js - punycode.ucs2.decode +function ucs2length(str) { + var length = 0 + , len = str.length + , pos = 0 + , value; + while (pos < len) { + length++; + value = str.charCodeAt(pos++); + if (value >= 0xD800 && value <= 0xDBFF && pos < len) { + // high surrogate, and there is a next character + value = str.charCodeAt(pos); + if ((value & 0xFC00) == 0xDC00) pos++; // low surrogate } + } + return length; } function toHash(arr, func) { - var hash = {}; - arr.forEach(function (item) { - if (func) item = func(item); - hash[item] = true; - }); - return hash; + var hash = {}; + arr.forEach(function (item) { + if (func) item = func(item); + hash[item] = true; + }); + return hash; } function escapeQuotes(str) { - return str.replace(/"/g, '\\"'); + return str.replace(/"/g, '\\"'); } diff --git a/lib/compile/resolve.js b/lib/compile/resolve.js index 19192af..c706a5c 100644 --- a/lib/compile/resolve.js +++ b/lib/compile/resolve.js @@ -2,30 +2,30 @@ module.exports = function resolve(compile, rootSchema, ref) { - var schema = rootSchema; - if (ref[0] != '#') - schema = undefined; - else if (ref != '#') { - if (this._schemas[ref]) - schema = this._schemas[ref]; - else { - var parts = ref.split('/'); - for (var i = 1; i < parts.length; i++) { - if (!schema) break; - var part = unescape(parts[i]); - schema = schema[part]; - if (schema.$ref) - schema = resolve.call(this, compile, rootSchema, schema.$ref); - } - if (schema) this._schemas[ref] = compile.call(this, schema); - } + var schema = rootSchema; + if (ref[0] != '#') + schema = undefined; + else if (ref != '#') { + if (this._schemas[ref]) + schema = this._schemas[ref]; + else { + var parts = ref.split('/'); + for (var i = 1; i < parts.length; i++) { + if (!schema) break; + var part = unescape(parts[i]); + schema = schema[part]; + if (schema.$ref) + schema = resolve.call(this, compile, rootSchema, schema.$ref); + } + if (schema) this._schemas[ref] = compile.call(this, schema); } - return schema; + } + return schema; }; function unescape(str) { - return decodeURIComponent(str) - .replace(/~1/g, '/') - .replace(/~0/g, '~'); + return decodeURIComponent(str) + .replace(/~1/g, '/') + .replace(/~0/g, '~'); } diff --git a/lib/compile/rules/definitions.def.js b/lib/compile/rules/definitions.def.js index b64a676..40598e1 100644 --- a/lib/compile/rules/definitions.def.js +++ b/lib/compile/rules/definitions.def.js @@ -34,6 +34,15 @@ #}} +{{## def.strLength: + {{? it.opts.unicode }} + ucs2length(data{{=$dataLvl}}) + {{??}} + data{{=$dataLvl}}.length + {{?}} +#}} + + {{## def.cleanUp: {{ out = out.replace(/if \(valid[0-9]*\) \{\s*\}/g, ''); }} #}} diff --git a/lib/compile/rules/maxLength.dot.js b/lib/compile/rules/maxLength.dot.js index 4168762..8557c74 100644 --- a/lib/compile/rules/maxLength.dot.js +++ b/lib/compile/rules/maxLength.dot.js @@ -1,6 +1,6 @@ {{# def.definitions }} {{# def.setup:'maxLength' }} -var valid{{=$lvl}} = data{{=$dataLvl}}.length <= {{=$schema}}; +var valid{{=$lvl}} = {{# def.strLength }} <= {{=$schema}}; {{# def.checkError:'maxLength' }} diff --git a/lib/compile/rules/minLength.dot.js b/lib/compile/rules/minLength.dot.js index 477b004..d7cadb7 100644 --- a/lib/compile/rules/minLength.dot.js +++ b/lib/compile/rules/minLength.dot.js @@ -1,6 +1,6 @@ {{# def.definitions }} {{# def.setup:'minLength' }} -var valid{{=$lvl}} = data{{=$dataLvl}}.length >= {{=$schema}}; +var valid{{=$lvl}} = {{# def.strLength }} >= {{=$schema}}; {{# def.checkError:'minLength' }} diff --git a/package.json b/package.json index e2565ac..1ea90b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ajv", - "version": "0.2.0", + "version": "0.2.1", "description": "Another JSON schema Validator", "main": "lib/ajv.js", "scripts": { diff --git a/spec/json-schema.spec.js b/spec/json-schema.spec.js index 71b6578..b054c25 100644 --- a/spec/json-schema.spec.js +++ b/spec/json-schema.spec.js @@ -6,18 +6,10 @@ var glob = require('glob') var ONLY_RULES, SKIP_RULES; // ONLY_RULES = [ -// 'type', -// 'not', -// 'allOf', -// 'anyOf', -// 'oneOf', -// 'enum', -// 'maximum', 'minimum', -// 'multipleOf', -// 'maxLength', 'minLength', 'pattern', +// 'type', 'not', 'allOf', 'anyOf', 'oneOf', 'enum', +// 'maximum', 'minimum', 'multipleOf', 'maxLength', 'minLength', 'pattern', // 'properties', 'patternProperties', 'additionalProperties', -// 'dependencies', -// 'required', +// 'dependencies', 'required', // 'maxProperties', 'minProperties', 'maxItems', 'minItems', // 'items', 'additionalItems', 'uniqueItems', // 'optional/format', 'optional/bignum', @@ -31,8 +23,8 @@ SKIP_RULES = [ var Ajv = require('../lib/ajv') - , ajv = Ajv() - , fullAjv = Ajv({ allErrors: true, verbose: true }); + , ajv = Ajv({ unicode: true }) + , fullAjv = Ajv({ unicode: true, allErrors: true, verbose: true }); var remoteRefs = { 'http://localhost:1234/integer.json': require('./JSON-Schema-Test-Suite/remotes/integer.json'), @@ -67,7 +59,7 @@ describe('JSON-Schema tests', function () { var fullValidate = fullAjv.compile(testSet.schema); testSet.tests.forEach(function (test) { - // if (test.description != 'valid') return; + // if (test.description != 'one supplementary Unicode code point is not long enough') return; // console.log(testSet.schema, '\n\n***\n\n', validate.toString()); it(test.description, function() { var valid = validate(test.data);