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.

274 lines
7.9 KiB

4 years ago
  1. 'use strict';
  2. module.exports = {
  3. copy: copy,
  4. checkDataType: checkDataType,
  5. checkDataTypes: checkDataTypes,
  6. coerceToTypes: coerceToTypes,
  7. toHash: toHash,
  8. getProperty: getProperty,
  9. escapeQuotes: escapeQuotes,
  10. equal: require('fast-deep-equal'),
  11. ucs2length: require('./ucs2length'),
  12. varOccurences: varOccurences,
  13. varReplace: varReplace,
  14. cleanUpCode: cleanUpCode,
  15. finalCleanUpCode: finalCleanUpCode,
  16. schemaHasRules: schemaHasRules,
  17. schemaHasRulesExcept: schemaHasRulesExcept,
  18. schemaUnknownRules: schemaUnknownRules,
  19. toQuotedString: toQuotedString,
  20. getPathExpr: getPathExpr,
  21. getPath: getPath,
  22. getData: getData,
  23. unescapeFragment: unescapeFragment,
  24. unescapeJsonPointer: unescapeJsonPointer,
  25. escapeFragment: escapeFragment,
  26. escapeJsonPointer: escapeJsonPointer
  27. };
  28. function copy(o, to) {
  29. to = to || {};
  30. for (var key in o) to[key] = o[key];
  31. return to;
  32. }
  33. function checkDataType(dataType, data, negate) {
  34. var EQUAL = negate ? ' !== ' : ' === '
  35. , AND = negate ? ' || ' : ' && '
  36. , OK = negate ? '!' : ''
  37. , NOT = negate ? '' : '!';
  38. switch (dataType) {
  39. case 'null': return data + EQUAL + 'null';
  40. case 'array': return OK + 'Array.isArray(' + data + ')';
  41. case 'object': return '(' + OK + data + AND +
  42. 'typeof ' + data + EQUAL + '"object"' + AND +
  43. NOT + 'Array.isArray(' + data + '))';
  44. case 'integer': return '(typeof ' + data + EQUAL + '"number"' + AND +
  45. NOT + '(' + data + ' % 1)' +
  46. AND + data + EQUAL + data + ')';
  47. default: return 'typeof ' + data + EQUAL + '"' + dataType + '"';
  48. }
  49. }
  50. function checkDataTypes(dataTypes, data) {
  51. switch (dataTypes.length) {
  52. case 1: return checkDataType(dataTypes[0], data, true);
  53. default:
  54. var code = '';
  55. var types = toHash(dataTypes);
  56. if (types.array && types.object) {
  57. code = types.null ? '(': '(!' + data + ' || ';
  58. code += 'typeof ' + data + ' !== "object")';
  59. delete types.null;
  60. delete types.array;
  61. delete types.object;
  62. }
  63. if (types.number) delete types.integer;
  64. for (var t in types)
  65. code += (code ? ' && ' : '' ) + checkDataType(t, data, true);
  66. return code;
  67. }
  68. }
  69. var COERCE_TO_TYPES = toHash([ 'string', 'number', 'integer', 'boolean', 'null' ]);
  70. function coerceToTypes(optionCoerceTypes, dataTypes) {
  71. if (Array.isArray(dataTypes)) {
  72. var types = [];
  73. for (var i=0; i<dataTypes.length; i++) {
  74. var t = dataTypes[i];
  75. if (COERCE_TO_TYPES[t]) types[types.length] = t;
  76. else if (optionCoerceTypes === 'array' && t === 'array') types[types.length] = t;
  77. }
  78. if (types.length) return types;
  79. } else if (COERCE_TO_TYPES[dataTypes]) {
  80. return [dataTypes];
  81. } else if (optionCoerceTypes === 'array' && dataTypes === 'array') {
  82. return ['array'];
  83. }
  84. }
  85. function toHash(arr) {
  86. var hash = {};
  87. for (var i=0; i<arr.length; i++) hash[arr[i]] = true;
  88. return hash;
  89. }
  90. var IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i;
  91. var SINGLE_QUOTE = /'|\\/g;
  92. function getProperty(key) {
  93. return typeof key == 'number'
  94. ? '[' + key + ']'
  95. : IDENTIFIER.test(key)
  96. ? '.' + key
  97. : "['" + escapeQuotes(key) + "']";
  98. }
  99. function escapeQuotes(str) {
  100. return str.replace(SINGLE_QUOTE, '\\$&')
  101. .replace(/\n/g, '\\n')
  102. .replace(/\r/g, '\\r')
  103. .replace(/\f/g, '\\f')
  104. .replace(/\t/g, '\\t');
  105. }
  106. function varOccurences(str, dataVar) {
  107. dataVar += '[^0-9]';
  108. var matches = str.match(new RegExp(dataVar, 'g'));
  109. return matches ? matches.length : 0;
  110. }
  111. function varReplace(str, dataVar, expr) {
  112. dataVar += '([^0-9])';
  113. expr = expr.replace(/\$/g, '$$$$');
  114. return str.replace(new RegExp(dataVar, 'g'), expr + '$1');
  115. }
  116. var EMPTY_ELSE = /else\s*{\s*}/g
  117. , EMPTY_IF_NO_ELSE = /if\s*\([^)]+\)\s*\{\s*\}(?!\s*else)/g
  118. , EMPTY_IF_WITH_ELSE = /if\s*\(([^)]+)\)\s*\{\s*\}\s*else(?!\s*if)/g;
  119. function cleanUpCode(out) {
  120. return out.replace(EMPTY_ELSE, '')
  121. .replace(EMPTY_IF_NO_ELSE, '')
  122. .replace(EMPTY_IF_WITH_ELSE, 'if (!($1))');
  123. }
  124. var ERRORS_REGEXP = /[^v.]errors/g
  125. , REMOVE_ERRORS = /var errors = 0;|var vErrors = null;|validate.errors = vErrors;/g
  126. , REMOVE_ERRORS_ASYNC = /var errors = 0;|var vErrors = null;/g
  127. , RETURN_VALID = 'return errors === 0;'
  128. , RETURN_TRUE = 'validate.errors = null; return true;'
  129. , RETURN_ASYNC = /if \(errors === 0\) return data;\s*else throw new ValidationError\(vErrors\);/
  130. , RETURN_DATA_ASYNC = 'return data;'
  131. , ROOTDATA_REGEXP = /[^A-Za-z_$]rootData[^A-Za-z0-9_$]/g
  132. , REMOVE_ROOTDATA = /if \(rootData === undefined\) rootData = data;/;
  133. function finalCleanUpCode(out, async) {
  134. var matches = out.match(ERRORS_REGEXP);
  135. if (matches && matches.length == 2) {
  136. out = async
  137. ? out.replace(REMOVE_ERRORS_ASYNC, '')
  138. .replace(RETURN_ASYNC, RETURN_DATA_ASYNC)
  139. : out.replace(REMOVE_ERRORS, '')
  140. .replace(RETURN_VALID, RETURN_TRUE);
  141. }
  142. matches = out.match(ROOTDATA_REGEXP);
  143. if (!matches || matches.length !== 3) return out;
  144. return out.replace(REMOVE_ROOTDATA, '');
  145. }
  146. function schemaHasRules(schema, rules) {
  147. if (typeof schema == 'boolean') return !schema;
  148. for (var key in schema) if (rules[key]) return true;
  149. }
  150. function schemaHasRulesExcept(schema, rules, exceptKeyword) {
  151. if (typeof schema == 'boolean') return !schema && exceptKeyword != 'not';
  152. for (var key in schema) if (key != exceptKeyword && rules[key]) return true;
  153. }
  154. function schemaUnknownRules(schema, rules) {
  155. if (typeof schema == 'boolean') return;
  156. for (var key in schema) if (!rules[key]) return key;
  157. }
  158. function toQuotedString(str) {
  159. return '\'' + escapeQuotes(str) + '\'';
  160. }
  161. function getPathExpr(currentPath, expr, jsonPointers, isNumber) {
  162. var path = jsonPointers // false by default
  163. ? '\'/\' + ' + expr + (isNumber ? '' : '.replace(/~/g, \'~0\').replace(/\\//g, \'~1\')')
  164. : (isNumber ? '\'[\' + ' + expr + ' + \']\'' : '\'[\\\'\' + ' + expr + ' + \'\\\']\'');
  165. return joinPaths(currentPath, path);
  166. }
  167. function getPath(currentPath, prop, jsonPointers) {
  168. var path = jsonPointers // false by default
  169. ? toQuotedString('/' + escapeJsonPointer(prop))
  170. : toQuotedString(getProperty(prop));
  171. return joinPaths(currentPath, path);
  172. }
  173. var JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/;
  174. var RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/;
  175. function getData($data, lvl, paths) {
  176. var up, jsonPointer, data, matches;
  177. if ($data === '') return 'rootData';
  178. if ($data[0] == '/') {
  179. if (!JSON_POINTER.test($data)) throw new Error('Invalid JSON-pointer: ' + $data);
  180. jsonPointer = $data;
  181. data = 'rootData';
  182. } else {
  183. matches = $data.match(RELATIVE_JSON_POINTER);
  184. if (!matches) throw new Error('Invalid JSON-pointer: ' + $data);
  185. up = +matches[1];
  186. jsonPointer = matches[2];
  187. if (jsonPointer == '#') {
  188. if (up >= lvl) throw new Error('Cannot access property/index ' + up + ' levels up, current level is ' + lvl);
  189. return paths[lvl - up];
  190. }
  191. if (up > lvl) throw new Error('Cannot access data ' + up + ' levels up, current level is ' + lvl);
  192. data = 'data' + ((lvl - up) || '');
  193. if (!jsonPointer) return data;
  194. }
  195. var expr = data;
  196. var segments = jsonPointer.split('/');
  197. for (var i=0; i<segments.length; i++) {
  198. var segment = segments[i];
  199. if (segment) {
  200. data += getProperty(unescapeJsonPointer(segment));
  201. expr += ' && ' + data;
  202. }
  203. }
  204. return expr;
  205. }
  206. function joinPaths (a, b) {
  207. if (a == '""') return b;
  208. return (a + ' + ' + b).replace(/' \+ '/g, '');
  209. }
  210. function unescapeFragment(str) {
  211. return unescapeJsonPointer(decodeURIComponent(str));
  212. }
  213. function escapeFragment(str) {
  214. return encodeURIComponent(escapeJsonPointer(str));
  215. }
  216. function escapeJsonPointer(str) {
  217. return str.replace(/~/g, '~0').replace(/\//g, '~1');
  218. }
  219. function unescapeJsonPointer(str) {
  220. return str.replace(/~1/g, '/').replace(/~0/g, '~');
  221. }