annotate.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. 'use strict';
  2. const Clone = require('@hapi/hoek/lib/clone');
  3. const Common = require('./common');
  4. const internals = {
  5. annotations: Symbol('annotations')
  6. };
  7. exports.error = function (stripColorCodes) {
  8. if (!this._original ||
  9. typeof this._original !== 'object') {
  10. return this.details[0].message;
  11. }
  12. const redFgEscape = stripColorCodes ? '' : '\u001b[31m';
  13. const redBgEscape = stripColorCodes ? '' : '\u001b[41m';
  14. const endColor = stripColorCodes ? '' : '\u001b[0m';
  15. const obj = Clone(this._original);
  16. for (let i = this.details.length - 1; i >= 0; --i) { // Reverse order to process deepest child first
  17. const pos = i + 1;
  18. const error = this.details[i];
  19. const path = error.path;
  20. let node = obj;
  21. for (let j = 0; ; ++j) {
  22. const seg = path[j];
  23. if (Common.isSchema(node)) {
  24. node = node.clone(); // joi schemas are not cloned by hoek, we have to take this extra step
  25. }
  26. if (j + 1 < path.length &&
  27. typeof node[seg] !== 'string') {
  28. node = node[seg];
  29. }
  30. else {
  31. const refAnnotations = node[internals.annotations] || { errors: {}, missing: {} };
  32. node[internals.annotations] = refAnnotations;
  33. const cacheKey = seg || error.context.key;
  34. if (node[seg] !== undefined) {
  35. refAnnotations.errors[cacheKey] = refAnnotations.errors[cacheKey] || [];
  36. refAnnotations.errors[cacheKey].push(pos);
  37. }
  38. else {
  39. refAnnotations.missing[cacheKey] = pos;
  40. }
  41. break;
  42. }
  43. }
  44. }
  45. const replacers = {
  46. key: /_\$key\$_([, \d]+)_\$end\$_"/g,
  47. missing: /"_\$miss\$_([^|]+)\|(\d+)_\$end\$_": "__missing__"/g,
  48. arrayIndex: /\s*"_\$idx\$_([, \d]+)_\$end\$_",?\n(.*)/g,
  49. specials: /"\[(NaN|Symbol.*|-?Infinity|function.*|\(.*)]"/g
  50. };
  51. let message = internals.safeStringify(obj, 2)
  52. .replace(replacers.key, ($0, $1) => `" ${redFgEscape}[${$1}]${endColor}`)
  53. .replace(replacers.missing, ($0, $1, $2) => `${redBgEscape}"${$1}"${endColor}${redFgEscape} [${$2}]: -- missing --${endColor}`)
  54. .replace(replacers.arrayIndex, ($0, $1, $2) => `\n${$2} ${redFgEscape}[${$1}]${endColor}`)
  55. .replace(replacers.specials, ($0, $1) => $1);
  56. message = `${message}\n${redFgEscape}`;
  57. for (let i = 0; i < this.details.length; ++i) {
  58. const pos = i + 1;
  59. message = `${message}\n[${pos}] ${this.details[i].message}`;
  60. }
  61. message = message + endColor;
  62. return message;
  63. };
  64. // Inspired by json-stringify-safe
  65. internals.safeStringify = function (obj, spaces) {
  66. return JSON.stringify(obj, internals.serializer(), spaces);
  67. };
  68. internals.serializer = function () {
  69. const keys = [];
  70. const stack = [];
  71. const cycleReplacer = (key, value) => {
  72. if (stack[0] === value) {
  73. return '[Circular ~]';
  74. }
  75. return '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']';
  76. };
  77. return function (key, value) {
  78. if (stack.length > 0) {
  79. const thisPos = stack.indexOf(this);
  80. if (~thisPos) {
  81. stack.length = thisPos + 1;
  82. keys.length = thisPos + 1;
  83. keys[thisPos] = key;
  84. }
  85. else {
  86. stack.push(this);
  87. keys.push(key);
  88. }
  89. if (~stack.indexOf(value)) {
  90. value = cycleReplacer.call(this, key, value);
  91. }
  92. }
  93. else {
  94. stack.push(value);
  95. }
  96. if (value) {
  97. const annotations = value[internals.annotations];
  98. if (annotations) {
  99. if (Array.isArray(value)) {
  100. const annotated = [];
  101. for (let i = 0; i < value.length; ++i) {
  102. if (annotations.errors[i]) {
  103. annotated.push(`_$idx$_${annotations.errors[i].sort().join(', ')}_$end$_`);
  104. }
  105. annotated.push(value[i]);
  106. }
  107. value = annotated;
  108. }
  109. else {
  110. for (const errorKey in annotations.errors) {
  111. value[`${errorKey}_$key$_${annotations.errors[errorKey].sort().join(', ')}_$end$_`] = value[errorKey];
  112. value[errorKey] = undefined;
  113. }
  114. for (const missingKey in annotations.missing) {
  115. value[`_$miss$_${missingKey}|${annotations.missing[missingKey]}_$end$_`] = '__missing__';
  116. }
  117. }
  118. return value;
  119. }
  120. }
  121. if (value === Infinity ||
  122. value === -Infinity ||
  123. Number.isNaN(value) ||
  124. typeof value === 'function' ||
  125. typeof value === 'symbol') {
  126. return '[' + value.toString() + ']';
  127. }
  128. return value;
  129. };
  130. };