'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; };