Comparing version 0.2.9 to 0.3.0
'use strict'; | ||
var compileSchema = require('./compile') | ||
, resolve = require('./compile/resolve') | ||
, stableStringify = require('json-stable-stringify') | ||
@@ -8,2 +9,4 @@ | ||
var META_SCHEMA_ID = 'http://json-schema.org/draft-04/schema'; | ||
/** | ||
@@ -21,2 +24,4 @@ * Creates validator instance. | ||
this._schemas = {}; | ||
this._refs = {}; | ||
this._missing = {}; | ||
this._byJson = {}; | ||
@@ -32,3 +37,3 @@ | ||
if (this.opts.meta !== false) | ||
addSchema(require('./refs/json-schema-draft-04.json')); | ||
addSchema(require('./refs/json-schema-draft-04.json'), META_SCHEMA_ID, true); | ||
@@ -38,8 +43,15 @@ /** | ||
* Schema will be compiled and cached (using serialized JSON as key. [json-stable-stringify](https://github.com/substack/json-stable-stringify) is used to serialize. | ||
* @param {String|Object} schema | ||
* @param {String|Object} schemaKeyRef key, ref or schema object | ||
* @param {Any} data to be validated | ||
* @return {Boolean} validation result. Errors from the last validation will be available in `jv.errors` (and also in compiled schema: `schema.errors`). | ||
*/ | ||
function validate(schema, data) { | ||
var v = _addSchema(schema); | ||
function validate(schemaKeyRef, data) { | ||
if (typeof schemaKeyRef == 'string') { | ||
var v = getSchema(schemaKeyRef); | ||
if (!v) { | ||
v = getRef(schemaKeyRef); | ||
if (!v) throw new Error('no schema with key or ref "' + schemaKeyRef + '"'); | ||
} | ||
} else v = _addSchema(schemaKeyRef); | ||
return v(data); | ||
@@ -61,14 +73,13 @@ } | ||
* Adds schema to the instance. | ||
* @param {String|Object|Array} schema schema or array of schemas. If array is passed, `name` will be ignored. | ||
* @param {String} id Optional schema id. Will be used in addition to `schema.id` to find schema by `$ref`. | ||
* @return {Object} compiled schema with method `validate` that accepts `data`. | ||
* @param {Object|Array} schema schema or array of schemas. If array is passed, `key` will be ignored. | ||
* @param {String} key Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`. | ||
* @return {Function} compiled schema with method `validate` that accepts `data`. | ||
*/ | ||
function addSchema(schema, id) { | ||
if (Array.isArray(schema)) return schema.map(addSchema); | ||
if (!id && !schema.id) throw new Error('no schema id'); | ||
if (self._schemas[id] || self._schemas[schema.id]) | ||
throw new Error('schema already exists'); | ||
var validate = _addSchema(schema); | ||
if (id) self._schemas[id] = validate; | ||
if (schema.id) self._schemas[schema.id] = validate; | ||
function addSchema(schema, key, _skipValidation) { | ||
if (Array.isArray(schema)) | ||
return schema.map(function(sch) { return addSchema(sch); }); | ||
// can key/id have # inside? | ||
var key = resolve.normalizeId(key || schema.id); | ||
checkUnique(key); | ||
var validate = self._schemas[key] = _addSchema(schema, _skipValidation); | ||
return validate; | ||
@@ -78,18 +89,56 @@ } | ||
function validateSchema(schema) { | ||
var $schema = schema.$schema || META_SCHEMA_ID; | ||
return validate($schema, schema); | ||
} | ||
/** | ||
* Get schema from the instance by by `id` | ||
* @param {String} id `schema.id` or `id` that was passed to `addSchema` (schema will be availbale by its internal id even if the id was passed). | ||
* @return {Object} compiled schema with property `schema` and method `validate`. | ||
* Get compiled schema from the instance by `key`. | ||
* @param {String} key `key` that was passed to `addSchema` (or `schema.id`). | ||
* @return {Function} schema validating function (with property `schema`). | ||
*/ | ||
function getSchema(id) { | ||
return self._schemas[id]; | ||
function getSchema(key) { | ||
key = resolve.normalizeId(key); | ||
return self._schemas[key]; | ||
} | ||
function _addSchema(schema) { | ||
if (typeof schema == 'string') schema = JSON.parse(schema); | ||
if (typeof schema != 'object') throw new Error('schema has invalid type'); | ||
/** | ||
* Get compiled schema from the instance by `id`. | ||
* @param {String} id `schema.id` or any reference in any of previously passed schemas. | ||
* @return {Function} schema validating function (with property `schema`). | ||
*/ | ||
function getRef(ref) { | ||
ref = resolve.normalizeId(ref); | ||
// TODO | ||
return self._refs[ref]; | ||
} | ||
function _addSchema(schema, skipValidation) { | ||
if (typeof schema != 'object') throw new Error('schema should be object'); | ||
var str = stableStringify(schema); | ||
return (self._byJson[str] = self._byJson[str] || compileSchema.call(self, schema)); | ||
if (self._byJson[str]) return self._byJson[str]; | ||
var id = resolve.normalizeId(schema.id); | ||
if (id) checkUnique(id); | ||
// var ok = skipValidation || self.opts.validateSchema === false | ||
// || validateSchema(schema); | ||
// if (!ok) throw new Error('schema is invalid'); | ||
resolve.ids.call(self, schema); | ||
var validate = self._refs[id] = self._byJson[str] = compileSchema.call(self, schema); | ||
return validate; | ||
} | ||
function checkUnique(id) { | ||
var schemaRef = self._refs[id]; | ||
if (self._schemas[id] || (schemaRef && !schemaRef.missing)) | ||
throw new Error('schema with key or id "' + id + '" already exists'); | ||
} | ||
} | ||
@@ -96,0 +145,0 @@ |
@@ -32,2 +32,3 @@ 'use strict'; | ||
util: util, | ||
resolve: resolve, | ||
resolveRef: resolveRef, | ||
@@ -51,10 +52,13 @@ opts: this.opts | ||
function resolveRef(ref) { | ||
function resolveRef(baseId, ref) { | ||
ref = resolve.url(baseId, ref); | ||
if (refs[ref]) return refs[ref]; | ||
var v = resolve.call(self, compile, _rootSchema, ref); | ||
if (v) { | ||
var id = refVal.length; | ||
var refId = refVal.length; | ||
refVal.push(v); | ||
refs[ref] = id; | ||
return id; | ||
refs[ref] = refId; | ||
return refId; | ||
} else { | ||
// TODO register missing ref, create empty entry in refs and return id | ||
} | ||
@@ -61,0 +65,0 @@ } |
'use strict'; | ||
var url = require('url') | ||
, equal = require('./equal'); | ||
module.exports = function resolve(compile, rootSchema, ref) { | ||
if (this._schemas[ref]) return this._schemas[ref]; | ||
if (ref[0] != '#') return; | ||
var schema = _resolve(rootSchema, ref); | ||
if (schema) return this._schemas[ref] = compile.call(this, schema, rootSchema); | ||
module.exports = resolve; | ||
resolve.normalizeId = normalizeId; | ||
resolve.fullPath = getFullPath; | ||
resolve.url = resolveUrl; | ||
resolve.ids = resolveIds; | ||
resolve.missing = resolveMissing; | ||
function resolve(compile, rootSchema, ref) { | ||
var refVal = this._refs[ref]; | ||
if (typeof refVal == 'string') refVal = this._refs[refVal]; | ||
if (typeof refVal == 'function') return refVal; | ||
var refVal = this._schemas[ref]; | ||
if (typeof refVal == 'function') return refVal; | ||
var schema = _resolve.call(this, rootSchema, ref); | ||
if (schema) return this._refs[ref] = compile.call(this, schema, rootSchema); | ||
}; | ||
@@ -13,10 +27,34 @@ | ||
function _resolve(rootSchema, ref) { | ||
var schema = rootSchema | ||
, parts = ref.split('/'); | ||
var p = url.parse(ref, false, true) | ||
, refPath = _getFullPath(p) | ||
, baseId = getFullPath(rootSchema.id); | ||
if (refPath !== baseId) { | ||
// rootSchema = undefined; TODO this breaks resolution in meta-schema | ||
var refVal = this._refs[refPath]; | ||
if (typeof refVal == 'string') refVal = this._refs[refVal]; | ||
if (typeof refVal == 'function') rootSchema = refVal.schema; | ||
else { | ||
var refVal = this._schemas[normalizeId(refPath)]; | ||
if (typeof refVal == 'function') rootSchema = refVal.schema; | ||
} | ||
if (!rootSchema) return; | ||
baseId = getFullPath(rootSchema.id); | ||
} | ||
p.hash = p.hash || ''; | ||
if (p.hash.slice(0,2) != '#/') return; | ||
var parts = p.hash.split('/'); | ||
var schema = rootSchema; | ||
for (var i = 1; i < parts.length; i++) { | ||
if (!schema) break; | ||
var part = unescape(parts[i]); | ||
schema = schema[part]; | ||
if (schema.$ref) | ||
schema = _resolve(rootSchema, schema.$ref); | ||
var part = parts[i]; | ||
if (part) { | ||
part = unescapeFragment(part); | ||
schema = schema[part]; | ||
if (!schema) break; | ||
if (schema.id) baseId = resolveUrl(baseId, schema.id); | ||
if (schema.$ref) { | ||
var $ref = resolveUrl(baseId, schema.$ref); | ||
schema = _resolve.call(this, rootSchema, $ref); | ||
} | ||
} | ||
} | ||
@@ -27,3 +65,3 @@ if (schema != rootSchema) return schema; | ||
function unescape(str) { | ||
function unescapeFragment(str) { | ||
return decodeURIComponent(str) | ||
@@ -33,1 +71,69 @@ .replace(/~1/g, '/') | ||
} | ||
function escapeFragment(str) { | ||
var str = str.replace(/~/g, '~0').replace(/\//g, '~1'); | ||
return encodeURIComponent(str); | ||
} | ||
function getFullPath(id, normalize) { | ||
if (normalize !== false) id = normalizeId(id); | ||
var p = url.parse(id, false, true); | ||
return _getFullPath(p); | ||
} | ||
function _getFullPath(p) { | ||
return (p.protocol||'') + (p.protocol?'//':'') + (p.host||'') + (p.path||'') + '#'; | ||
} | ||
var TRAILING_SLASH_HASH = /#\/?$/; | ||
function normalizeId(id) { | ||
return id ? id.replace(TRAILING_SLASH_HASH, '') : ''; | ||
} | ||
function resolveUrl(baseId, id) { | ||
id = normalizeId(id); | ||
return url.resolve(baseId, id); | ||
} | ||
function resolveIds(schema) { | ||
var id = normalizeId(schema.id); | ||
_resolveIds.call(this, schema, getFullPath(id, false), id); | ||
} | ||
function _resolveIds(schema, fullPath, baseId) { | ||
if (Array.isArray(schema)) | ||
for (var i=0; i<schema.length; i++) | ||
_resolveIds.call(this, schema[i], fullPath+'/'+i, baseId); | ||
else if (schema && typeof schema == 'object') { | ||
if (typeof schema.id == 'string') { | ||
var id = baseId = baseId | ||
? url.resolve(baseId, schema.id) | ||
: getFullPath(schema.id); | ||
var refVal = this._refs[id]; | ||
if (typeof refVal == 'string') refVal = this._refs[refVal]; | ||
if (refVal && refVal.schema) { | ||
if (!equal(schema, refVal.schema)) | ||
throw new Error('id "' + id + '" resolves to more than one schema'); | ||
} else if (id != normalizeId(fullPath)) | ||
this._refs[id] = fullPath; | ||
// TODO check and resolve missing | ||
} | ||
for (var key in schema) | ||
_resolveIds.call(this, schema[key], fullPath+'/'+escapeFragment(key), baseId); | ||
} | ||
} | ||
function resolveMissing(schema, schemaRef) { | ||
} |
@@ -7,3 +7,3 @@ {{# def.definitions }} | ||
{{??}} | ||
{{ $id = it.resolveRef($schema); }} | ||
{{ $id = it.resolveRef(it.baseId, $schema); }} | ||
{{? $id === undefined }} | ||
@@ -10,0 +10,0 @@ {{# def.error:'$ref' }} |
@@ -20,2 +20,3 @@ {{# def.definitions }} | ||
, $data = 'data'; | ||
it.baseId = it.resolve.fullPath(it.schema.id); | ||
delete it.isRoot; | ||
@@ -31,2 +32,4 @@ }} | ||
, $data = 'data' + ($dataLvl || ''); | ||
if (it.schema.id) it.baseId = it.resolve.url(it.baseId, it.schema.id); | ||
}} | ||
@@ -113,3 +116,3 @@ | ||
function $shouldUseRule($rule) { | ||
var $use = it.schema[$rule.keyword]; | ||
var $use = it.schema[$rule.keyword] !== undefined; | ||
if (!$use && $rule.keyword == 'properties') { | ||
@@ -116,0 +119,0 @@ var $pProperties = it.schema.patternProperties |
{ | ||
"name": "ajv", | ||
"version": "0.2.9", | ||
"version": "0.3.0", | ||
"description": "Another JSON schema Validator", | ||
@@ -29,2 +29,3 @@ "main": "lib/ajv.js", | ||
"devDependencies": { | ||
"chai": "^3.0.0", | ||
"glob": "^5.0.10", | ||
@@ -31,0 +32,0 @@ "js-beautify": "^1.5.6", |
@@ -6,5 +6,17 @@ # ajv - Another JSON schema Validator | ||
## JSON-schema standard | ||
ajv implements full JSON-schema draft 4 standard: | ||
- all validation keywords | ||
- full support of remote refs (remote schemas have to be pre-loaded) | ||
- correct string lengths for strings with unicode pairs (can be turned off) | ||
- formats defined by JSON-schema (can be turned off) | ||
ajv passes all the tests from [JSON Schema Test Suite](https://github.com/json-schema/JSON-Schema-Test-Suite) (apart from the one that requires that `1.0` is not an integer). | ||
## TODO | ||
- remote refs | ||
- resolve missing remote refs when schemas are added | ||
- custom formats (via options) | ||
@@ -45,7 +57,8 @@ - schema validation before compilation | ||
- _allErrors_: check all rules collecting all errors. Default is to return after the first error. | ||
- _verbose_: include the reference to the validated data in the errors. | ||
- _verbose_: include the reference to the part of the schema and validated data in errors (false by default). | ||
- _format_: validate formats (true by default). | ||
- _meta_: add [meta-schema](http://json-schema.org/documentation.html) so it can be used by other schemas (true by default). | ||
- _uniqueItems_: validate `uniqueItems` (true by default). | ||
- _unicode_: calculate correct length of strings with unicode pairs (true by default - string lengths are calculated correctly but it is slower). Pass `false` to use `string.length` 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). `npm install js-beautify` to use this option. | ||
- _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. | ||
@@ -52,0 +65,0 @@ |
'use strict'; | ||
var Ajv = require('../lib/ajv') | ||
, should = require('chai').should(); | ||
describe('Ajv', function () { | ||
var ajv; | ||
beforeEach(function() { | ||
ajv = Ajv(); | ||
}); | ||
it('should create instance', function() { | ||
ajv .should.be.instanceof(Ajv); | ||
}); | ||
describe('compile method', function() { | ||
it('should compile schema and return validating function', function() { | ||
var validate = ajv.compile({ type: 'integer' }); | ||
validate .should.be.a('function'); | ||
validate(1) .should.equal(true); | ||
validate(1.1) .should.equal(false); | ||
validate('1') .should.equal(false); | ||
}); | ||
it('should cache compiled functions for the same schema', function() { | ||
var v1 = ajv.compile({ id: '//e.com/int.json', type: 'integer', minimum: 1 }); | ||
var v2 = ajv.compile({ id: '//e.com/int.json', minimum: 1, type: 'integer' }); | ||
v1 .should.equal(v2); | ||
}) | ||
it('should throw if different schema has the same id', function() { | ||
ajv.compile({ id: '//e.com/int.json', type: 'integer' }); | ||
should.throw(function() { | ||
ajv.compile({ id: '//e.com/int.json', type: 'integer', minimum: 1 }); | ||
}); | ||
}) | ||
it.skip('should throw if schema is invalid', function() { | ||
should.throw(function() { | ||
ajv.compile({ type: null }); | ||
}); | ||
}); | ||
}); | ||
describe('validate method', function() { | ||
it('should compile schema and validate data against it', function() { | ||
ajv.validate({ type: 'integer' }, 1) .should.equal(true); | ||
ajv.validate({ type: 'integer' }, '1') .should.equal(false); | ||
ajv.validate({ type: 'string' }, 'a') .should.equal(true); | ||
ajv.validate({ type: 'string' }, 1) .should.equal(false); | ||
}); | ||
it('should validate against previously compiled schema by id (also see addSchema)', function() { | ||
ajv.validate({ id: '//e.com/int.json', type: 'integer' }, 1) .should.equal(true); | ||
ajv.validate('//e.com/int.json', 1) .should.equal(true); | ||
ajv.validate('//e.com/int.json', '1') .should.equal(false); | ||
ajv.compile({ id: '//e.com/str.json', type: 'string' }) .should.be.a('function'); | ||
ajv.validate('//e.com/str.json', 'a') .should.equal(true); | ||
ajv.validate('//e.com/str.json', 1) .should.equal(false); | ||
}); | ||
}); | ||
describe('addSchema method', function() { | ||
it('should add and compile schema with key', function() { | ||
var validate = ajv.addSchema({ type: 'integer' }, 'int'); | ||
validate .should.be.a('function'); | ||
validate(1) .should.equal(true); | ||
validate(1.1) .should.equal(false); | ||
validate('1') .should.equal(false); | ||
ajv.validate('int', 1) .should.equal(true); | ||
ajv.validate('int', '1') .should.equal(false); | ||
}); | ||
it('should add and compile schema without key', function() { | ||
var validate = ajv.addSchema({ type: 'integer' }); | ||
ajv.validate('', 1) .should.equal(true); | ||
ajv.validate('', '1') .should.equal(false); | ||
}); | ||
it('should add and compile schema with id', function() { | ||
var validate = ajv.addSchema({ id: '//e.com/int.json', type: 'integer' }); | ||
ajv.validate('//e.com/int.json', 1) .should.equal(true); | ||
ajv.validate('//e.com/int.json', '1') .should.equal(false); | ||
}); | ||
it('should normalize schema keys and ids', function() { | ||
var validate = ajv.addSchema({ id: '//e.com/int.json#', type: 'integer' }, 'int#'); | ||
ajv.validate('int', 1) .should.equal(true); | ||
ajv.validate('int', '1') .should.equal(false); | ||
ajv.validate('//e.com/int.json', 1) .should.equal(true); | ||
ajv.validate('//e.com/int.json', '1') .should.equal(false); | ||
ajv.validate('int#/', 1) .should.equal(true); | ||
ajv.validate('int#/', '1') .should.equal(false); | ||
ajv.validate('//e.com/int.json#/', 1) .should.equal(true); | ||
ajv.validate('//e.com/int.json#/', '1') .should.equal(false); | ||
}); | ||
it('should add and compile array of schemas with ids', function() { | ||
var validators = ajv.addSchema([ | ||
{ id: '//e.com/int.json', type: 'integer' }, | ||
{ id: '//e.com/str.json', type: 'string' } | ||
]); | ||
validators .should.be.an('array').with.length(2); | ||
validators[0](1) .should.equal(true); | ||
validators[0]('1') .should.equal(false); | ||
validators[1]('a') .should.equal(true); | ||
validators[1](1) .should.equal(false); | ||
ajv.validate('//e.com/int.json', 1) .should.equal(true); | ||
ajv.validate('//e.com/int.json', '1') .should.equal(false); | ||
ajv.validate('//e.com/str.json', 'a') .should.equal(true); | ||
ajv.validate('//e.com/str.json', 1) .should.equal(false); | ||
}); | ||
it('should throw on duplicate key', function() { | ||
ajv.addSchema({ type: 'integer' }, 'int'); | ||
should.throw(function() { | ||
ajv.addSchema({ type: 'integer', minimum: 1 }, 'int'); | ||
}); | ||
}); | ||
it('should throw on duplicate normalized key', function() { | ||
ajv.addSchema({ type: 'number' }, 'num'); | ||
should.throw(function() { | ||
ajv.addSchema({ type: 'integer' }, 'num#'); | ||
}); | ||
should.throw(function() { | ||
ajv.addSchema({ type: 'integer' }, 'num#/'); | ||
}); | ||
}); | ||
it('should allow only one schema without key and id', function() { | ||
ajv.addSchema({ type: 'number' }); | ||
should.throw(function() { | ||
ajv.addSchema({ type: 'integer' }); | ||
}); | ||
should.throw(function() { | ||
ajv.addSchema({ type: 'integer' }, ''); | ||
}); | ||
should.throw(function() { | ||
ajv.addSchema({ type: 'integer' }, '#'); | ||
}); | ||
}); | ||
}); | ||
describe('getSchema method', function() { | ||
it('should return compiled schema by key', function() { | ||
var validate = ajv.addSchema({ type: 'integer' }, 'int'); | ||
var v = ajv.getSchema('int'); | ||
v .should.equal(validate); | ||
v(1) .should.equal(true); | ||
v('1') .should.equal(false); | ||
}); | ||
it('should return compiled schema by id or ref', function() { | ||
var validate = ajv.addSchema({ id: '//e.com/int.json', type: 'integer' }); | ||
var v = ajv.getSchema('//e.com/int.json'); | ||
v .should.equal(validate); | ||
v(1) .should.equal(true); | ||
v('1') .should.equal(false); | ||
}); | ||
it('should return compiled schema without key or with empty key', function() { | ||
var validate = ajv.addSchema({ type: 'integer' }); | ||
var v = ajv.getSchema(''); | ||
v .should.equal(validate); | ||
v(1) .should.equal(true); | ||
v('1') .should.equal(false); | ||
var v = ajv.getSchema(); | ||
v .should.equal(validate); | ||
}); | ||
}); | ||
}); |
@@ -17,2 +17,4 @@ 'use strict'; | ||
// 'ref', | ||
// 'refRemote', | ||
// 'definitions' | ||
// 'schemas/complex' | ||
@@ -22,3 +24,3 @@ // ]; | ||
SKIP_RULES = [ | ||
'refRemote', | ||
// 'refRemote', | ||
'optional/zeroTerminatedFloats', | ||
@@ -30,4 +32,4 @@ 'schemas/complex' | ||
var Ajv = require('../lib/ajv') | ||
, ajv = Ajv() | ||
, fullAjv = Ajv({ allErrors: true, verbose: true }); | ||
, ajv = Ajv({ beautify: true }) | ||
, fullAjv = Ajv({ allErrors: true, verbose: true, beautify: true }); | ||
@@ -46,4 +48,6 @@ var remoteRefs = { | ||
addTests('JSON-Schema tests draft4', './json-schema-test-suite/tests/draft4/{**/,}*.json'); | ||
addTests('Advanced schema tests', './tests/{**/,}*.json'); | ||
describe('Schema validation tests', function() { | ||
addTests('JSON-Schema tests draft4', './json-schema-test-suite/tests/draft4/{**/,}*.json'); | ||
addTests('Advanced schema tests', './tests/{**/,}*.json'); | ||
}); | ||
@@ -63,3 +67,3 @@ | ||
testSets.forEach(function (testSet) { | ||
// if (testSet.description != 'allOf with base schema') return; | ||
// if (testSet.description != 'remote ref, containing refs itself') return; | ||
describe(testSet.description, function() { | ||
@@ -74,3 +78,3 @@ var validate, fullValidate; | ||
testSet.tests.forEach(function (test) { | ||
// if (test.description != 'one supplementary Unicode code point is not long enough') return; | ||
// if (test.description != 'changed scope ref valid') return; | ||
// console.log(testSet.schema, '\n\n***\n\n', validate.toString()); | ||
@@ -77,0 +81,0 @@ it(test.description, function() { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
236068
113
7113
70
4