helpers.js 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. 'use strict';
  2. var uri = require('url');
  3. var ValidationError = exports.ValidationError = function ValidationError (message, instance, schema, propertyPath, name, argument) {
  4. if (propertyPath) {
  5. this.property = propertyPath;
  6. }
  7. if (message) {
  8. this.message = message;
  9. }
  10. if (schema) {
  11. if (schema.id) {
  12. this.schema = schema.id;
  13. } else {
  14. this.schema = schema;
  15. }
  16. }
  17. if (instance) {
  18. this.instance = instance;
  19. }
  20. this.name = name;
  21. this.argument = argument;
  22. this.stack = this.toString();
  23. };
  24. ValidationError.prototype.toString = function toString() {
  25. return this.property + ' ' + this.message;
  26. };
  27. var ValidatorResult = exports.ValidatorResult = function ValidatorResult(instance, schema, options, ctx) {
  28. this.instance = instance;
  29. this.schema = schema;
  30. this.propertyPath = ctx.propertyPath;
  31. this.errors = [];
  32. this.throwError = options && options.throwError;
  33. this.disableFormat = options && options.disableFormat === true;
  34. };
  35. ValidatorResult.prototype.addError = function addError(detail) {
  36. var err;
  37. if (typeof detail == 'string') {
  38. err = new ValidationError(detail, this.instance, this.schema, this.propertyPath);
  39. } else {
  40. if (!detail) throw new Error('Missing error detail');
  41. if (!detail.message) throw new Error('Missing error message');
  42. if (!detail.name) throw new Error('Missing validator type');
  43. err = new ValidationError(detail.message, this.instance, this.schema, this.propertyPath, detail.name, detail.argument);
  44. }
  45. if (this.throwError) {
  46. throw err;
  47. }
  48. this.errors.push(err);
  49. return err;
  50. };
  51. ValidatorResult.prototype.importErrors = function importErrors(res) {
  52. if (typeof res == 'string' || (res && res.validatorType)) {
  53. this.addError(res);
  54. } else if (res && res.errors) {
  55. var errs = this.errors;
  56. res.errors.forEach(function (v) {
  57. errs.push(v);
  58. });
  59. }
  60. };
  61. ValidatorResult.prototype.toString = function toString(res) {
  62. return this.errors.map(function(v,i){ return i+': '+v.toString()+'\n'; }).join('');
  63. };
  64. Object.defineProperty(ValidatorResult.prototype, "valid", { get: function() {
  65. return !this.errors.length;
  66. } });
  67. /**
  68. * Describes a problem with a Schema which prevents validation of an instance
  69. * @name SchemaError
  70. * @constructor
  71. */
  72. var SchemaError = exports.SchemaError = function SchemaError (msg, schema) {
  73. this.message = msg;
  74. this.schema = schema;
  75. Error.call(this, msg);
  76. Error.captureStackTrace(this, SchemaError);
  77. };
  78. SchemaError.prototype = Object.create(Error.prototype,
  79. { constructor: {value: SchemaError, enumerable: false}
  80. , name: {value: 'SchemaError', enumerable: false}
  81. });
  82. var SchemaContext = exports.SchemaContext = function SchemaContext (schema, options, propertyPath, base, schemas) {
  83. this.schema = schema;
  84. this.options = options;
  85. this.propertyPath = propertyPath;
  86. this.base = base;
  87. this.schemas = schemas;
  88. };
  89. SchemaContext.prototype.resolve = function resolve (target) {
  90. return uri.resolve(this.base, target);
  91. };
  92. SchemaContext.prototype.makeChild = function makeChild(schema, propertyName){
  93. var propertyPath = (propertyName===undefined) ? this.propertyPath : this.propertyPath+makeSuffix(propertyName);
  94. var base = uri.resolve(this.base, schema.id||'');
  95. var ctx = new SchemaContext(schema, this.options, propertyPath, base, Object.create(this.schemas));
  96. if(schema.id && !ctx.schemas[base]){
  97. ctx.schemas[base] = schema;
  98. }
  99. return ctx;
  100. }
  101. var FORMAT_REGEXPS = exports.FORMAT_REGEXPS = {
  102. '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]))$/,
  103. 'date': /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-(3[01]|0[1-9]|[12][0-9])$/,
  104. 'time': /^(2[0-4]|[01][0-9]):([0-5][0-9]):(60|[0-5][0-9])$/,
  105. '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])\]))$/,
  106. '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]?)$/,
  107. '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*$/,
  108. 'uri': /^[a-zA-Z][a-zA-Z0-9+-.]*:[^\s]*$/,
  109. '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*\)))$/,
  110. // hostname regex from: http://stackoverflow.com/a/1420225/5628
  111. '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])?)*\.?$/,
  112. '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])?)*\.?$/,
  113. 'alpha': /^[a-zA-Z]+$/,
  114. 'alphanumeric': /^[a-zA-Z0-9]+$/,
  115. 'utc-millisec': function (input) {
  116. return (typeof input === 'string') && parseFloat(input) === parseInt(input, 10) && !isNaN(input);
  117. },
  118. 'regex': function (input) {
  119. var result = true;
  120. try {
  121. new RegExp(input);
  122. } catch (e) {
  123. result = false;
  124. }
  125. return result;
  126. },
  127. 'style': /\s*(.+?):\s*([^;]+);?/g,
  128. 'phone': /^\+(?:[0-9] ?){6,14}[0-9]$/
  129. };
  130. FORMAT_REGEXPS.regexp = FORMAT_REGEXPS.regex;
  131. FORMAT_REGEXPS.pattern = FORMAT_REGEXPS.regex;
  132. FORMAT_REGEXPS.ipv4 = FORMAT_REGEXPS['ip-address'];
  133. exports.isFormat = function isFormat (input, format, validator) {
  134. if (typeof input === 'string' && FORMAT_REGEXPS[format] !== undefined) {
  135. if (FORMAT_REGEXPS[format] instanceof RegExp) {
  136. return FORMAT_REGEXPS[format].test(input);
  137. }
  138. if (typeof FORMAT_REGEXPS[format] === 'function') {
  139. return FORMAT_REGEXPS[format](input);
  140. }
  141. } else if (validator && validator.customFormats &&
  142. typeof validator.customFormats[format] === 'function') {
  143. return validator.customFormats[format](input);
  144. }
  145. return true;
  146. };
  147. var makeSuffix = exports.makeSuffix = function makeSuffix (key) {
  148. key = key.toString();
  149. // This function could be capable of outputting valid a ECMAScript string, but the
  150. // resulting code for testing which form to use would be tens of thousands of characters long
  151. // That means this will use the name form for some illegal forms
  152. if (!key.match(/[.\s\[\]]/) && !key.match(/^[\d]/)) {
  153. return '.' + key;
  154. }
  155. if (key.match(/^\d+$/)) {
  156. return '[' + key + ']';
  157. }
  158. return '[' + JSON.stringify(key) + ']';
  159. };
  160. exports.deepCompareStrict = function deepCompareStrict (a, b) {
  161. if (typeof a !== typeof b) {
  162. return false;
  163. }
  164. if (a instanceof Array) {
  165. if (!(b instanceof Array)) {
  166. return false;
  167. }
  168. if (a.length !== b.length) {
  169. return false;
  170. }
  171. return a.every(function (v, i) {
  172. return deepCompareStrict(a[i], b[i]);
  173. });
  174. }
  175. if (typeof a === 'object') {
  176. if (!a || !b) {
  177. return a === b;
  178. }
  179. var aKeys = Object.keys(a);
  180. var bKeys = Object.keys(b);
  181. if (aKeys.length !== bKeys.length) {
  182. return false;
  183. }
  184. return aKeys.every(function (v) {
  185. return deepCompareStrict(a[v], b[v]);
  186. });
  187. }
  188. return a === b;
  189. };
  190. module.exports.deepMerge = function deepMerge (target, src) {
  191. var array = Array.isArray(src);
  192. var dst = array && [] || {};
  193. if (array) {
  194. target = target || [];
  195. dst = dst.concat(target);
  196. src.forEach(function (e, i) {
  197. if (typeof e === 'object') {
  198. dst[i] = deepMerge(target[i], e)
  199. } else {
  200. if (target.indexOf(e) === -1) {
  201. dst.push(e)
  202. }
  203. }
  204. });
  205. } else {
  206. if (target && typeof target === 'object') {
  207. Object.keys(target).forEach(function (key) {
  208. dst[key] = target[key];
  209. });
  210. }
  211. Object.keys(src).forEach(function (key) {
  212. if (typeof src[key] !== 'object' || !src[key]) {
  213. dst[key] = src[key];
  214. }
  215. else {
  216. if (!target[key]) {
  217. dst[key] = src[key];
  218. } else {
  219. dst[key] = deepMerge(target[key], src[key])
  220. }
  221. }
  222. });
  223. }
  224. return dst;
  225. };
  226. /**
  227. * Validates instance against the provided schema
  228. * Implements URI+JSON Pointer encoding, e.g. "%7e"="~0"=>"~", "~1"="%2f"=>"/"
  229. * @param o
  230. * @param s The path to walk o along
  231. * @return any
  232. */
  233. exports.objectGetPath = function objectGetPath(o, s) {
  234. var parts = s.split('/').slice(1);
  235. var k;
  236. while (typeof (k=parts.shift()) == 'string') {
  237. var n = decodeURIComponent(k.replace(/~0/,'~').replace(/~1/g,'/'));
  238. if (!(n in o)) return;
  239. o = o[n];
  240. }
  241. return o;
  242. };
  243. /**
  244. * Accept an Array of property names and return a JSON Pointer URI fragment
  245. * @param Array a
  246. * @return {String}
  247. */
  248. exports.encodePath = function encodePointer(a){
  249. // ~ must be encoded explicitly because hacks
  250. // the slash is encoded by encodeURIComponent
  251. return a.map(function(v){ return '/'+encodeURIComponent(v).replace(/~/g,'%7E'); }).join('');
  252. };