From 57ce414ca81682a71288018a4d9001604002ec23 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Tue, 1 Mar 2022 11:29:26 +0100 Subject: validate repository responses against JSON schemas * compute_scripts.awk (include_file): don't enforce specific path format on #INCLUDE'd files * .gitmodules, schemas: add Haketilo JSON schemas subrepo * html/install.js (InstallView): import schema validator and run it against downloaded mapping and resource definitions * html/repo_query.js (RepoEntry): import schema validator and run it against obtained query results * test/haketilo_test/unit/test_install.py (test_install_normal_usage, test_install_dialogs): use underscore instead of hyphen in item identifiers * test/haketilo_test/unit/test_install.py (test_install_dialogs): adapt error message test cases to new handling method of invalid JSON instanced * test/haketilo_test/unit/test_repo_query.py (test_repo_query_normal_usage): use underscore instead of hyphen in item identifiers * test/haketilo_test/unit/test_repo_query.py (test_repo_query_messages): use higher sample unsupported schema version to avoid having to modify the test case soon * test/haketilo_test/world_wide_library.py: use underscore instead of hyphen in item identifiers * common/jsonschema.js, common/jsonschema: adapt tdegrunt's jsonschema and include in Haketilo, load schema documents from schemas/ --- common/jsonschema/helpers.js | 429 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 common/jsonschema/helpers.js (limited to 'common/jsonschema/helpers.js') diff --git a/common/jsonschema/helpers.js b/common/jsonschema/helpers.js new file mode 100644 index 0000000..f6a72a6 --- /dev/null +++ b/common/jsonschema/helpers.js @@ -0,0 +1,429 @@ +/* SPDX-License-Identifier: MIT + * + * jsonschema is licensed under MIT license. + * + * Copyright (C) 2012-2015 Tom de Grunt + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#IMPORT common/jsonschema/urllib_mock.js AS uri + +function ValidationError (message, instance, schema, path, name, argument) { + if(Array.isArray(path)){ + this.path = path; + this.property = path.reduce(function(sum, item){ + return sum + makeSuffix(item); + }, 'instance'); + }else if(path !== undefined){ + this.property = path; + } + if (message) { + this.message = message; + } + if (schema) { + var id = schema.$id || schema.id; + this.schema = id || schema; + } + if (instance !== undefined) { + this.instance = instance; + } + this.name = name; + this.argument = argument; + this.stack = this.toString(); +}; +#EXPORT ValidationError + +ValidationError.prototype.toString = function toString() { + return this.property + ' ' + this.message; +}; + +function ValidatorResult(instance, schema, options, ctx) { + this.instance = instance; + this.schema = schema; + this.options = options; + this.path = ctx.path; + this.propertyPath = ctx.propertyPath; + this.errors = []; + this.throwError = options && options.throwError; + this.throwFirst = options && options.throwFirst; + this.throwAll = options && options.throwAll; + 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.path); + } 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.path, detail.name, detail.argument); + } + + this.errors.push(err); + if (this.throwFirst) { + throw new ValidatorResultError(this); + }else if(this.throwError){ + throw 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; +} }); + +#EXPORT ValidatorResult + +function ValidatorResultError(result) { + if(Error.captureStackTrace){ + Error.captureStackTrace(this, ValidatorResultError); + } + this.instance = result.instance; + this.schema = result.schema; + this.options = result.options; + this.errors = result.errors; +} +ValidatorResultError.prototype = new Error(); +ValidatorResultError.prototype.constructor = ValidatorResultError; +ValidatorResultError.prototype.name = "Validation Error"; +#EXPORT ValidatorResultError + +/** + * Describes a problem with a Schema which prevents validation of an instance + * @name SchemaError + * @constructor + */ +function SchemaError (msg, schema) { + this.message = msg; + this.schema = schema; + Error.call(this, msg); + if(Error.captureStackTrace){ + Error.captureStackTrace(this, SchemaError); + } +}; +SchemaError.prototype = Object.create(Error.prototype, + { + constructor: {value: SchemaError, enumerable: false}, + name: {value: 'SchemaError', enumerable: false}, + }); +#EXPORT SchemaError + +function SchemaContext (schema, options, path, base, schemas) { + this.schema = schema; + this.options = options; + if(Array.isArray(path)){ + this.path = path; + this.propertyPath = path.reduce(function(sum, item){ + return sum + makeSuffix(item); + }, 'instance'); + }else{ + this.propertyPath = path; + } + 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 path = (propertyName===undefined) ? this.path : this.path.concat([propertyName]); + var id = schema.$id || schema.id; + var base = uri.resolve(this.base, id||''); + var ctx = new SchemaContext(schema, this.options, path, base, Object.create(this.schemas)); + if(id && !ctx.schemas[base]){ + ctx.schemas[base] = schema; + } + return ctx; +}; + +#EXPORT SchemaContext + +const FORMAT_REGEXPS = { + // 7.3.1. Dates, Times, and Duration + '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])$/, + 'duration': /P(T\d+(H(\d+M(\d+S)?)?|M(\d+S)?|S)|\d+(D|M(\d+D)?|Y(\d+M(\d+D)?)?)(T\d+(H(\d+M(\d+S)?)?|M(\d+S)?|S))?|\d+W)/i, + + // 7.3.2. Email Addresses + // TODO: fix the email production + '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])\]))$/, + 'idn-email': /^("(?:[!#-\[\]-\u{10FFFF}]|\\[\t -\u{10FFFF}])*"|[!#-'*+\-/-9=?A-Z\^-\u{10FFFF}](?:\.?[!#-'*+\-/-9=?A-Z\^-\u{10FFFF}])*)@([!#-'*+\-/-9=?A-Z\^-\u{10FFFF}](?:\.?[!#-'*+\-/-9=?A-Z\^-\u{10FFFF}])*|\[[!-Z\^-\u{10FFFF}]*\])$/u, + + // 7.3.3. Hostnames + + // 7.3.4. IP Addresses + '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]?)$/, + // FIXME whitespace is invalid + '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*$/, + + // 7.3.5. Resource Identifiers + // TODO: A more accurate regular expression for "uri" goes: + // [A-Za-z][+\-.0-9A-Za-z]*:((/(/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?)?)?#(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|(/(/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~])|/?%[0-9A-Fa-f]{2}|[!$&-.0-;=?-Z_a-z~])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*(#(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*)?|/(/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+(:\d*)?|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?:\d*|\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)?)? + 'uri': /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/, + 'uri-reference': /^(((([A-Za-z][+\-.0-9A-Za-z]*(:%[0-9A-Fa-f]{2}|:[!$&-.0-;=?-Z_a-z~]|[/?])|\?)(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|([A-Za-z][+\-.0-9A-Za-z]*:?)?)|([A-Za-z][+\-.0-9A-Za-z]*:)?\/((%[0-9A-Fa-f]{2}|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|(\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?)?))#(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|(([A-Za-z][+\-.0-9A-Za-z]*)?%[0-9A-Fa-f]{2}|[!$&-.0-9;=@_~]|[A-Za-z][+\-.0-9A-Za-z]*[!$&-*,;=@_~])(%[0-9A-Fa-f]{2}|[!$&-.0-9;=@-Z_a-z~])*((([/?](%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*)?#|[/?])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*)?|([A-Za-z][+\-.0-9A-Za-z]*(:%[0-9A-Fa-f]{2}|:[!$&-.0-;=?-Z_a-z~]|[/?])|\?)(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|([A-Za-z][+\-.0-9A-Za-z]*:)?\/((%[0-9A-Fa-f]{2}|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~])*|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~])+(:\d*)?|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?:\d*|\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~]+)?|[.0-:A-Fa-f]+)\])?)?|[A-Za-z][+\-.0-9A-Za-z]*:?)?$/, + 'iri': /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/, + 'iri-reference': /^(((([A-Za-z][+\-.0-9A-Za-z]*(:%[0-9A-Fa-f]{2}|:[!$&-.0-;=?-Z_a-z~-\u{10FFFF}]|[/?])|\?)(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|([A-Za-z][+\-.0-9A-Za-z]*:?)?)|([A-Za-z][+\-.0-9A-Za-z]*:)?\/((%[0-9A-Fa-f]{2}|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~-\u{10FFFF}])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~-\u{10FFFF}])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|(\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~-\u{10FFFF}])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?)?))#(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|(([A-Za-z][+\-.0-9A-Za-z]*)?%[0-9A-Fa-f]{2}|[!$&-.0-9;=@_~-\u{10FFFF}]|[A-Za-z][+\-.0-9A-Za-z]*[!$&-*,;=@_~-\u{10FFFF}])(%[0-9A-Fa-f]{2}|[!$&-.0-9;=@-Z_a-z~-\u{10FFFF}])*((([/?](%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*)?#|[/?])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*)?|([A-Za-z][+\-.0-9A-Za-z]*(:%[0-9A-Fa-f]{2}|:[!$&-.0-;=?-Z_a-z~-\u{10FFFF}]|[/?])|\?)(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|([A-Za-z][+\-.0-9A-Za-z]*:)?\/((%[0-9A-Fa-f]{2}|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~-\u{10FFFF}])+|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?)(:\d*)?[/?]|[!$&-.0-;=?-Z_a-z~-\u{10FFFF}])(%[0-9A-Fa-f]{2}|[!$&-;=?-Z_a-z~-\u{10FFFF}])*|\/((%[0-9A-Fa-f]{2}|[!$&-.0-9;=A-Z_a-z~-\u{10FFFF}])+(:\d*)?|(\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?:\d*|\[(([Vv][0-9A-Fa-f]+\.[!$&-.0-;=A-Z_a-z~-\u{10FFFF}]+)?|[.0-:A-Fa-f]+)\])?)?|[A-Za-z][+\-.0-9A-Za-z]*:?)?$/u, + 'uuid': /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i, + + // 7.3.6. uri-template + 'uri-template': /(%[0-9a-f]{2}|[!#$&(-;=?@\[\]_a-z~]|\{[!#&+,./;=?@|]?(%[0-9a-f]{2}|[0-9_a-z])(\.?(%[0-9a-f]{2}|[0-9_a-z]))*(:[1-9]\d{0,3}|\*)?(,(%[0-9a-f]{2}|[0-9_a-z])(\.?(%[0-9a-f]{2}|[0-9_a-z]))*(:[1-9]\d{0,3}|\*)?)*\})*/iu, + + // 7.3.7. JSON Pointers + 'json-pointer': /^(\/([\x00-\x2e0-@\[-}\x7f]|~[01])*)*$/iu, + 'relative-json-pointer': /^\d+(#|(\/([\x00-\x2e0-@\[-}\x7f]|~[01])*)*)$/iu, + + // 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])?)*\.?$/, + + 'utc-millisec': function (input) { + return (typeof input === 'string') && parseFloat(input) === parseInt(input, 10) && !isNaN(input); + }, + + // 7.3.8. regex + 'regex': function (input) { + var result = true; + try { + new RegExp(input); + } catch (e) { + result = false; + } + return result; + }, + + // Other definitions + // "style" was removed from JSON Schema in draft-4 and is deprecated + 'style': /[\r\n\t ]*[^\r\n\t ][^:]*:[\r\n\t ]*[^\r\n\t ;]*[\r\n\t ]*;?/, + // "color" was removed from JSON Schema in draft-4 and is deprecated + '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*\)))$/, + 'phone': /^\+(?:[0-9] ?){6,14}[0-9]$/, + 'alpha': /^[a-zA-Z]+$/, + 'alphanumeric': /^[a-zA-Z0-9]+$/, +}; + +FORMAT_REGEXPS.regexp = FORMAT_REGEXPS.regex; +FORMAT_REGEXPS.pattern = FORMAT_REGEXPS.regex; +FORMAT_REGEXPS.ipv4 = FORMAT_REGEXPS['ip-address']; + +#EXPORT FORMAT_REGEXPS + +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; +}; + +#EXPORT isFormat + +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) + ']'; +}; +#EXPORT makeSuffix + +function deepCompareStrict (a, b) { + if (typeof a !== typeof b) { + return false; + } + if (Array.isArray(a)) { + if (!Array.isArray(b)) { + 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; +}; +#EXPORT deepCompareStrict + +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; +} +#EXPORT 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 + */ +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; +}; +#EXPORT objectGetPath + +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} + */ +function encodePointer(a){ + // ~ must be encoded explicitly because hacks + // the slash is encoded by encodeURIComponent + return a.map(pathEncoder).join(''); +}; +#EXPORT encodePointer AS encodePath + + +/** + * 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} + */ +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; +}; +#EXPORT getDecimalPlaces + +function isSchema(val){ + return (typeof val === 'object' && val) || (typeof val === 'boolean'); +}; +#EXPORT isSchema -- cgit v1.2.3