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.
325 lines
11 KiB
325 lines
11 KiB
'use strict'; |
|
|
|
var uri = require('url'); |
|
|
|
var ValidationError = exports.ValidationError = function ValidationError (message, instance, schema, propertyPath, name, argument) { |
|
if (propertyPath) { |
|
this.property = propertyPath; |
|
} |
|
if (message) { |
|
this.message = message; |
|
} |
|
if (schema) { |
|
if (schema.id) { |
|
this.schema = schema.id; |
|
} else { |
|
this.schema = schema; |
|
} |
|
} |
|
if (instance) { |
|
this.instance = instance; |
|
} |
|
this.name = name; |
|
this.argument = argument; |
|
this.stack = this.toString(); |
|
}; |
|
|
|
ValidationError.prototype.toString = function toString() { |
|
return this.property + ' ' + this.message; |
|
}; |
|
|
|
var ValidatorResult = exports.ValidatorResult = function ValidatorResult(instance, schema, options, ctx) { |
|
this.instance = instance; |
|
this.schema = schema; |
|
this.propertyPath = ctx.propertyPath; |
|
this.errors = []; |
|
this.throwError = options && options.throwError; |
|
this.disableFormat = options && options.disableFormat === true; |
|
}; |
|
|
|
ValidatorResult.prototype.addError = function addError(detail) { |
|
var err; |
|
if (typeof detail == 'string') { |
|
err = new ValidationError(detail, this.instance, this.schema, this.propertyPath); |
|
} else { |
|
if (!detail) throw new Error('Missing error detail'); |
|
if (!detail.message) throw new Error('Missing error message'); |
|
if (!detail.name) throw new Error('Missing validator type'); |
|
err = new ValidationError(detail.message, this.instance, this.schema, this.propertyPath, detail.name, detail.argument); |
|
} |
|
|
|
if (this.throwError) { |
|
throw err; |
|
} |
|
this.errors.push(err); |
|
return err; |
|
}; |
|
|
|
ValidatorResult.prototype.importErrors = function importErrors(res) { |
|
if (typeof res == 'string' || (res && res.validatorType)) { |
|
this.addError(res); |
|
} else if (res && res.errors) { |
|
Array.prototype.push.apply(this.errors, res.errors); |
|
} |
|
}; |
|
|
|
function stringizer (v,i){ |
|
return i+': '+v.toString()+'\n'; |
|
} |
|
ValidatorResult.prototype.toString = function toString(res) { |
|
return this.errors.map(stringizer).join(''); |
|
}; |
|
|
|
Object.defineProperty(ValidatorResult.prototype, "valid", { get: function() { |
|
return !this.errors.length; |
|
} }); |
|
|
|
/** |
|
* Describes a problem with a Schema which prevents validation of an instance |
|
* @name SchemaError |
|
* @constructor |
|
*/ |
|
var SchemaError = exports.SchemaError = function SchemaError (msg, schema) { |
|
this.message = msg; |
|
this.schema = schema; |
|
Error.call(this, msg); |
|
Error.captureStackTrace(this, SchemaError); |
|
}; |
|
SchemaError.prototype = Object.create(Error.prototype, |
|
{ constructor: {value: SchemaError, enumerable: false} |
|
, name: {value: 'SchemaError', enumerable: false} |
|
}); |
|
|
|
var SchemaContext = exports.SchemaContext = function SchemaContext (schema, options, propertyPath, base, schemas) { |
|
this.schema = schema; |
|
this.options = options; |
|
this.propertyPath = propertyPath; |
|
this.base = base; |
|
this.schemas = schemas; |
|
}; |
|
|
|
SchemaContext.prototype.resolve = function resolve (target) { |
|
return uri.resolve(this.base, target); |
|
}; |
|
|
|
SchemaContext.prototype.makeChild = function makeChild(schema, propertyName){ |
|
var propertyPath = (propertyName===undefined) ? this.propertyPath : this.propertyPath+makeSuffix(propertyName); |
|
var base = uri.resolve(this.base, schema.id||''); |
|
var ctx = new SchemaContext(schema, this.options, propertyPath, base, Object.create(this.schemas)); |
|
if(schema.id && !ctx.schemas[base]){ |
|
ctx.schemas[base] = schema; |
|
} |
|
return ctx; |
|
} |
|
|
|
var FORMAT_REGEXPS = exports.FORMAT_REGEXPS = { |
|
'date-time': /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-(3[01]|0[1-9]|[12][0-9])[tT ](2[0-4]|[01][0-9]):([0-5][0-9]):(60|[0-5][0-9])(\.\d+)?([zZ]|[+-]([0-5][0-9]):(60|[0-5][0-9]))$/, |
|
'date': /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-(3[01]|0[1-9]|[12][0-9])$/, |
|
'time': /^(2[0-4]|[01][0-9]):([0-5][0-9]):(60|[0-5][0-9])$/, |
|
|
|
'email': /^(?:[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+\.)*[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+@(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!\.)){0,61}[a-zA-Z0-9]?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!$)){0,61}[a-zA-Z0-9]?)|(?:\[(?:(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\]))$/, |
|
'ip-address': /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, |
|
'ipv6': /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-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-Fa-f]{1,4}:){5}(((:[0-9A-Fa-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-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-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-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-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-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-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-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-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-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-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*$/, |
|
'uri': /^[a-zA-Z][a-zA-Z0-9+-.]*:[^\s]*$/, |
|
|
|
'color': /^(#?([0-9A-Fa-f]{3}){1,2}\b|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|(rgb\(\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*\))|(rgb\(\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*\)))$/, |
|
|
|
// hostname regex from: http://stackoverflow.com/a/1420225/5628 |
|
'hostname': /^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$/, |
|
'host-name': /^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$/, |
|
|
|
'alpha': /^[a-zA-Z]+$/, |
|
'alphanumeric': /^[a-zA-Z0-9]+$/, |
|
'utc-millisec': function (input) { |
|
return (typeof input === 'string') && parseFloat(input) === parseInt(input, 10) && !isNaN(input); |
|
}, |
|
'regex': function (input) { |
|
var result = true; |
|
try { |
|
new RegExp(input); |
|
} catch (e) { |
|
result = false; |
|
} |
|
return result; |
|
}, |
|
'style': /\s*(.+?):\s*([^;]+);?/g, |
|
'phone': /^\+(?:[0-9] ?){6,14}[0-9]$/ |
|
}; |
|
|
|
FORMAT_REGEXPS.regexp = FORMAT_REGEXPS.regex; |
|
FORMAT_REGEXPS.pattern = FORMAT_REGEXPS.regex; |
|
FORMAT_REGEXPS.ipv4 = FORMAT_REGEXPS['ip-address']; |
|
|
|
exports.isFormat = function isFormat (input, format, validator) { |
|
if (typeof input === 'string' && FORMAT_REGEXPS[format] !== undefined) { |
|
if (FORMAT_REGEXPS[format] instanceof RegExp) { |
|
return FORMAT_REGEXPS[format].test(input); |
|
} |
|
if (typeof FORMAT_REGEXPS[format] === 'function') { |
|
return FORMAT_REGEXPS[format](input); |
|
} |
|
} else if (validator && validator.customFormats && |
|
typeof validator.customFormats[format] === 'function') { |
|
return validator.customFormats[format](input); |
|
} |
|
return true; |
|
}; |
|
|
|
var makeSuffix = exports.makeSuffix = function makeSuffix (key) { |
|
key = key.toString(); |
|
// This function could be capable of outputting valid a ECMAScript string, but the |
|
// resulting code for testing which form to use would be tens of thousands of characters long |
|
// That means this will use the name form for some illegal forms |
|
if (!key.match(/[.\s\[\]]/) && !key.match(/^[\d]/)) { |
|
return '.' + key; |
|
} |
|
if (key.match(/^\d+$/)) { |
|
return '[' + key + ']'; |
|
} |
|
return '[' + JSON.stringify(key) + ']'; |
|
}; |
|
|
|
exports.deepCompareStrict = function deepCompareStrict (a, b) { |
|
if (typeof a !== typeof b) { |
|
return false; |
|
} |
|
if (a instanceof Array) { |
|
if (!(b instanceof Array)) { |
|
return false; |
|
} |
|
if (a.length !== b.length) { |
|
return false; |
|
} |
|
return a.every(function (v, i) { |
|
return deepCompareStrict(a[i], b[i]); |
|
}); |
|
} |
|
if (typeof a === 'object') { |
|
if (!a || !b) { |
|
return a === b; |
|
} |
|
var aKeys = Object.keys(a); |
|
var bKeys = Object.keys(b); |
|
if (aKeys.length !== bKeys.length) { |
|
return false; |
|
} |
|
return aKeys.every(function (v) { |
|
return deepCompareStrict(a[v], b[v]); |
|
}); |
|
} |
|
return a === b; |
|
}; |
|
|
|
function deepMerger (target, dst, e, i) { |
|
if (typeof e === 'object') { |
|
dst[i] = deepMerge(target[i], e) |
|
} else { |
|
if (target.indexOf(e) === -1) { |
|
dst.push(e) |
|
} |
|
} |
|
} |
|
|
|
function copyist (src, dst, key) { |
|
dst[key] = src[key]; |
|
} |
|
|
|
function copyistWithDeepMerge (target, src, dst, key) { |
|
if (typeof src[key] !== 'object' || !src[key]) { |
|
dst[key] = src[key]; |
|
} |
|
else { |
|
if (!target[key]) { |
|
dst[key] = src[key]; |
|
} else { |
|
dst[key] = deepMerge(target[key], src[key]) |
|
} |
|
} |
|
} |
|
|
|
function deepMerge (target, src) { |
|
var array = Array.isArray(src); |
|
var dst = array && [] || {}; |
|
|
|
if (array) { |
|
target = target || []; |
|
dst = dst.concat(target); |
|
src.forEach(deepMerger.bind(null, target, dst)); |
|
} else { |
|
if (target && typeof target === 'object') { |
|
Object.keys(target).forEach(copyist.bind(null, target, dst)); |
|
} |
|
Object.keys(src).forEach(copyistWithDeepMerge.bind(null, target, src, dst)); |
|
} |
|
|
|
return dst; |
|
}; |
|
|
|
module.exports.deepMerge = deepMerge; |
|
|
|
/** |
|
* Validates instance against the provided schema |
|
* Implements URI+JSON Pointer encoding, e.g. "%7e"="~0"=>"~", "~1"="%2f"=>"/" |
|
* @param o |
|
* @param s The path to walk o along |
|
* @return any |
|
*/ |
|
exports.objectGetPath = function objectGetPath(o, s) { |
|
var parts = s.split('/').slice(1); |
|
var k; |
|
while (typeof (k=parts.shift()) == 'string') { |
|
var n = decodeURIComponent(k.replace(/~0/,'~').replace(/~1/g,'/')); |
|
if (!(n in o)) return; |
|
o = o[n]; |
|
} |
|
return o; |
|
}; |
|
|
|
function pathEncoder (v) { |
|
return '/'+encodeURIComponent(v).replace(/~/g,'%7E'); |
|
} |
|
/** |
|
* Accept an Array of property names and return a JSON Pointer URI fragment |
|
* @param Array a |
|
* @return {String} |
|
*/ |
|
exports.encodePath = function encodePointer(a){ |
|
// ~ must be encoded explicitly because hacks |
|
// the slash is encoded by encodeURIComponent |
|
return a.map(pathEncoder).join(''); |
|
}; |
|
|
|
|
|
/** |
|
* Calculate the number of decimal places a number uses |
|
* We need this to get correct results out of multipleOf and divisibleBy |
|
* when either figure is has decimal places, due to IEEE-754 float issues. |
|
* @param number |
|
* @returns {number} |
|
*/ |
|
exports.getDecimalPlaces = function getDecimalPlaces(number) { |
|
|
|
var decimalPlaces = 0; |
|
if (isNaN(number)) return decimalPlaces; |
|
|
|
if (typeof number !== 'number') { |
|
number = Number(number); |
|
} |
|
|
|
var parts = number.toString().split('e'); |
|
if (parts.length === 2) { |
|
if (parts[1][0] !== '-') { |
|
return decimalPlaces; |
|
} else { |
|
decimalPlaces = Number(parts[1].slice(1)); |
|
} |
|
} |
|
|
|
var decimalParts = parts[0].split('.'); |
|
if (decimalParts.length === 2) { |
|
decimalPlaces += decimalParts[1].length; |
|
} |
|
|
|
return decimalPlaces; |
|
}; |
|
|
|
|