You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
320 lines
9.5 KiB
320 lines
9.5 KiB
'use strict'; |
|
|
|
var urilib = require('url'); |
|
|
|
var attribute = require('./attribute'); |
|
var helpers = require('./helpers'); |
|
var scanSchema = require('./scan').scan; |
|
var ValidatorResult = helpers.ValidatorResult; |
|
var SchemaError = helpers.SchemaError; |
|
var SchemaContext = helpers.SchemaContext; |
|
//var anonymousBase = 'vnd.jsonschema:///'; |
|
var anonymousBase = '/'; |
|
|
|
/** |
|
* Creates a new Validator object |
|
* @name Validator |
|
* @constructor |
|
*/ |
|
var Validator = function Validator () { |
|
// Allow a validator instance to override global custom formats or to have their |
|
// own custom formats. |
|
this.customFormats = Object.create(Validator.prototype.customFormats); |
|
this.schemas = {}; |
|
this.unresolvedRefs = []; |
|
|
|
// Use Object.create to make this extensible without Validator instances stepping on each other's toes. |
|
this.types = Object.create(types); |
|
this.attributes = Object.create(attribute.validators); |
|
}; |
|
|
|
// Allow formats to be registered globally. |
|
Validator.prototype.customFormats = {}; |
|
|
|
// Hint at the presence of a property |
|
Validator.prototype.schemas = null; |
|
Validator.prototype.types = null; |
|
Validator.prototype.attributes = null; |
|
Validator.prototype.unresolvedRefs = null; |
|
|
|
/** |
|
* Adds a schema with a certain urn to the Validator instance. |
|
* @param schema |
|
* @param urn |
|
* @return {Object} |
|
*/ |
|
Validator.prototype.addSchema = function addSchema (schema, base) { |
|
var self = this; |
|
if (!schema) { |
|
return null; |
|
} |
|
var scan = scanSchema(base||anonymousBase, schema); |
|
var ourUri = base || schema.id; |
|
for(var uri in scan.id){ |
|
this.schemas[uri] = scan.id[uri]; |
|
} |
|
for(var uri in scan.ref){ |
|
this.unresolvedRefs.push(uri); |
|
} |
|
this.unresolvedRefs = this.unresolvedRefs.filter(function(uri){ |
|
return typeof self.schemas[uri]==='undefined'; |
|
}); |
|
return this.schemas[ourUri]; |
|
}; |
|
|
|
Validator.prototype.addSubSchemaArray = function addSubSchemaArray(baseuri, schemas) { |
|
if(!(schemas instanceof Array)) return; |
|
for(var i=0; i<schemas.length; i++){ |
|
this.addSubSchema(baseuri, schemas[i]); |
|
} |
|
}; |
|
|
|
Validator.prototype.addSubSchemaObject = function addSubSchemaArray(baseuri, schemas) { |
|
if(!schemas || typeof schemas!='object') return; |
|
for(var p in schemas){ |
|
this.addSubSchema(baseuri, schemas[p]); |
|
} |
|
}; |
|
|
|
|
|
|
|
/** |
|
* Sets all the schemas of the Validator instance. |
|
* @param schemas |
|
*/ |
|
Validator.prototype.setSchemas = function setSchemas (schemas) { |
|
this.schemas = schemas; |
|
}; |
|
|
|
/** |
|
* Returns the schema of a certain urn |
|
* @param urn |
|
*/ |
|
Validator.prototype.getSchema = function getSchema (urn) { |
|
return this.schemas[urn]; |
|
}; |
|
|
|
/** |
|
* Validates instance against the provided schema |
|
* @param instance |
|
* @param schema |
|
* @param [options] |
|
* @param [ctx] |
|
* @return {Array} |
|
*/ |
|
Validator.prototype.validate = function validate (instance, schema, options, ctx) { |
|
if (!options) { |
|
options = {}; |
|
} |
|
var propertyName = options.propertyName || 'instance'; |
|
// This will work so long as the function at uri.resolve() will resolve a relative URI to a relative URI |
|
var base = urilib.resolve(options.base||anonymousBase, schema.id||''); |
|
if(!ctx){ |
|
ctx = new SchemaContext(schema, options, propertyName, base, Object.create(this.schemas)); |
|
if (!ctx.schemas[base]) { |
|
ctx.schemas[base] = schema; |
|
} |
|
var found = scanSchema(base, schema); |
|
for(var n in found.id){ |
|
var sch = found.id[n]; |
|
ctx.schemas[n] = sch; |
|
} |
|
} |
|
if (schema) { |
|
var result = this.validateSchema(instance, schema, options, ctx); |
|
if (!result) { |
|
throw new Error('Result undefined'); |
|
} |
|
return result; |
|
} |
|
throw new SchemaError('no schema specified', schema); |
|
}; |
|
|
|
/** |
|
* @param Object schema |
|
* @return mixed schema uri or false |
|
*/ |
|
function shouldResolve(schema) { |
|
var ref = (typeof schema === 'string') ? schema : schema.$ref; |
|
if (typeof ref=='string') return ref; |
|
return false; |
|
} |
|
|
|
/** |
|
* Validates an instance against the schema (the actual work horse) |
|
* @param instance |
|
* @param schema |
|
* @param options |
|
* @param ctx |
|
* @private |
|
* @return {ValidatorResult} |
|
*/ |
|
Validator.prototype.validateSchema = function validateSchema (instance, schema, options, ctx) { |
|
var result = new ValidatorResult(instance, schema, options, ctx); |
|
|
|
// Support for the true/false schemas |
|
if(typeof schema==='boolean') { |
|
if(schema===true){ |
|
// `true` is always valid |
|
schema = {}; |
|
}else if(schema===false){ |
|
// `false` is always invalid |
|
schema = {type: []}; |
|
} |
|
}else if(!schema){ |
|
// This might be a string |
|
throw new Error("schema is undefined"); |
|
} |
|
|
|
if (schema['extends']) { |
|
if (schema['extends'] instanceof Array) { |
|
var schemaobj = {schema: schema, ctx: ctx}; |
|
schema['extends'].forEach(this.schemaTraverser.bind(this, schemaobj)); |
|
schema = schemaobj.schema; |
|
schemaobj.schema = null; |
|
schemaobj.ctx = null; |
|
schemaobj = null; |
|
} else { |
|
schema = helpers.deepMerge(schema, this.superResolve(schema['extends'], ctx)); |
|
} |
|
} |
|
|
|
// If passed a string argument, load that schema URI |
|
var switchSchema; |
|
if (switchSchema = shouldResolve(schema)) { |
|
var resolved = this.resolve(schema, switchSchema, ctx); |
|
var subctx = new SchemaContext(resolved.subschema, options, ctx.propertyPath, resolved.switchSchema, ctx.schemas); |
|
return this.validateSchema(instance, resolved.subschema, options, subctx); |
|
} |
|
|
|
var skipAttributes = options && options.skipAttributes || []; |
|
// Validate each schema attribute against the instance |
|
for (var key in schema) { |
|
if (!attribute.ignoreProperties[key] && skipAttributes.indexOf(key) < 0) { |
|
var validatorErr = null; |
|
var validator = this.attributes[key]; |
|
if (validator) { |
|
validatorErr = validator.call(this, instance, schema, options, ctx); |
|
} else if (options.allowUnknownAttributes === false) { |
|
// This represents an error with the schema itself, not an invalid instance |
|
throw new SchemaError("Unsupported attribute: " + key, schema); |
|
} |
|
if (validatorErr) { |
|
result.importErrors(validatorErr); |
|
} |
|
} |
|
} |
|
|
|
if (typeof options.rewrite == 'function') { |
|
var value = options.rewrite.call(this, instance, schema, options, ctx); |
|
result.instance = value; |
|
} |
|
return result; |
|
}; |
|
|
|
/** |
|
* @private |
|
* @param Object schema |
|
* @param SchemaContext ctx |
|
* @returns Object schema or resolved schema |
|
*/ |
|
Validator.prototype.schemaTraverser = function schemaTraverser (schemaobj, s) { |
|
schemaobj.schema = helpers.deepMerge(schemaobj.schema, this.superResolve(s, schemaobj.ctx)); |
|
} |
|
|
|
/** |
|
* @private |
|
* @param Object schema |
|
* @param SchemaContext ctx |
|
* @returns Object schema or resolved schema |
|
*/ |
|
Validator.prototype.superResolve = function superResolve (schema, ctx) { |
|
var ref; |
|
if(ref = shouldResolve(schema)) { |
|
return this.resolve(schema, ref, ctx).subschema; |
|
} |
|
return schema; |
|
} |
|
|
|
/** |
|
* @private |
|
* @param Object schema |
|
* @param Object switchSchema |
|
* @param SchemaContext ctx |
|
* @return Object resolved schemas {subschema:String, switchSchema: String} |
|
* @throws SchemaError |
|
*/ |
|
Validator.prototype.resolve = function resolve (schema, switchSchema, ctx) { |
|
switchSchema = ctx.resolve(switchSchema); |
|
// First see if the schema exists under the provided URI |
|
if (ctx.schemas[switchSchema]) { |
|
return {subschema: ctx.schemas[switchSchema], switchSchema: switchSchema}; |
|
} |
|
// Else try walking the property pointer |
|
var parsed = urilib.parse(switchSchema); |
|
var fragment = parsed && parsed.hash; |
|
var document = fragment && fragment.length && switchSchema.substr(0, switchSchema.length - fragment.length); |
|
if (!document || !ctx.schemas[document]) { |
|
throw new SchemaError("no such schema <" + switchSchema + ">", schema); |
|
} |
|
var subschema = helpers.objectGetPath(ctx.schemas[document], fragment.substr(1)); |
|
if(subschema===undefined){ |
|
throw new SchemaError("no such schema " + fragment + " located in <" + document + ">", schema); |
|
} |
|
return {subschema: subschema, switchSchema: switchSchema}; |
|
}; |
|
|
|
/** |
|
* Tests whether the instance if of a certain type. |
|
* @private |
|
* @param instance |
|
* @param schema |
|
* @param options |
|
* @param ctx |
|
* @param type |
|
* @return {boolean} |
|
*/ |
|
Validator.prototype.testType = function validateType (instance, schema, options, ctx, type) { |
|
if (typeof this.types[type] == 'function') { |
|
return this.types[type].call(this, instance); |
|
} |
|
if (type && typeof type == 'object') { |
|
var res = this.validateSchema(instance, type, options, ctx); |
|
return res === undefined || !(res && res.errors.length); |
|
} |
|
// Undefined or properties not on the list are acceptable, same as not being defined |
|
return true; |
|
}; |
|
|
|
var types = Validator.prototype.types = {}; |
|
types.string = function testString (instance) { |
|
return typeof instance == 'string'; |
|
}; |
|
types.number = function testNumber (instance) { |
|
// isFinite returns false for NaN, Infinity, and -Infinity |
|
return typeof instance == 'number' && isFinite(instance); |
|
}; |
|
types.integer = function testInteger (instance) { |
|
return (typeof instance == 'number') && instance % 1 === 0; |
|
}; |
|
types.boolean = function testBoolean (instance) { |
|
return typeof instance == 'boolean'; |
|
}; |
|
types.array = function testArray (instance) { |
|
return Array.isArray(instance); |
|
}; |
|
types['null'] = function testNull (instance) { |
|
return instance === null; |
|
}; |
|
types.date = function testDate (instance) { |
|
return instance instanceof Date; |
|
}; |
|
types.any = function testAny (instance) { |
|
return true; |
|
}; |
|
types.object = function testObject (instance) { |
|
// TODO: fix this - see #15 |
|
return instance && (typeof instance) === 'object' && !(instance instanceof Array) && !(instance instanceof Date); |
|
}; |
|
|
|
module.exports = Validator;
|
|
|