rebuild-recursive.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. /**
  2. * Module dependencies
  3. */
  4. var util = require('util');
  5. var _ = require('@sailshq/lodash');
  6. var Readable = require('stream').Readable;
  7. var getDisplayType = require('../get-display-type');
  8. /**
  9. * rebuildRecursive()
  10. *
  11. * Rebuild a potentially-recursively-deep value, running
  12. * the specified `handleLeafTransform` lifecycle callback
  13. * (aka transformer function) for every primitive (i.e. string,
  14. * number, boolean, null, function).
  15. *
  16. * Note that this is very similar to the sanitize helper, except
  17. * that it does not make any assumptions about how to handle functions
  18. * or primitives.
  19. *
  20. * @param {Anything} val
  21. *
  22. * @param {Function} handleLeafTransform [run AFTER stringification of Errors, Dates, etc.]
  23. * @param {Anything} leafVal
  24. * @param {String} leafType [either 'string', 'number', 'boolean', 'null', or 'lamda']
  25. * @return {Anything} [transformed version of `leafVal`]
  26. *
  27. * @param {Function} handleCompositeTransform [run BEFORE recursion and stripping of undefined items/props]
  28. * @param {Dictionary|Array} compositeVal
  29. * @param {String} leafType [either 'array' or 'dictionary']
  30. * @return {Dictionary|Array} [transformed version of `compositeVal`-- MUST BE A DICTONARY OR ARRAY THAT IS SAFE TO RECURSIVELY DIVE INTO!!!]
  31. *
  32. * @returns {JSON}
  33. */
  34. module.exports = function rebuildRecursive(val, handleLeafTransform, handleCompositeTransform) {
  35. // If an invalid transformer function was provided, throw a usage error.
  36. if (!_.isFunction(handleLeafTransform)){
  37. throw new Error('Usage: A transformer function must be provided as the second argument when rebuilding. Instead, got: '+util.inspect(handleLeafTransform, {depth:null}));
  38. }
  39. // If `val` is undefined at the top level, leave it as `undefined`.
  40. if (_.isUndefined(val)) {
  41. return undefined;
  42. }
  43. // The only reason this outer wrapper self-calling function exists
  44. // is to isolate the inline function below (the cycleReplacer)
  45. return (function _rebuild() {
  46. var stack = [];
  47. var keys = [];
  48. // This was modified from @isaacs' json-stringify-safe
  49. // (see https://github.com/isaacs/json-stringify-safe/commit/02cfafd45f06d076ac4bf0dd28be6738a07a72f9#diff-c3fcfbed30e93682746088e2ce1a4a24)
  50. var cycleReplacer = function(unused, value) {
  51. if (stack[0] === value) { return '[Circular ~]'; }
  52. return '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']';
  53. };
  54. // This is a self-invoking recursive function.
  55. return (function _recursiveRebuildIt (thisVal, key) {
  56. // Handle circle jerks
  57. if (stack.length > 0) {
  58. var self = this;
  59. var thisPos = stack.indexOf(self);
  60. ~thisPos ? stack.splice(thisPos + 1) : stack.push(self);
  61. ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key);
  62. if (~stack.indexOf(thisVal)) {
  63. thisVal = cycleReplacer.call(self, key, thisVal);
  64. }
  65. }
  66. else { stack.push(thisVal); }
  67. // If this is an array, we'll recursively rebuild and strip undefined items.
  68. if (_.isArray(thisVal)) {
  69. // But first, run the composite transform handler, if one was provided.
  70. if (!_.isUndefined(handleCompositeTransform)) {
  71. thisVal = handleCompositeTransform(thisVal, 'array');
  72. }
  73. // Now recursively rebuild and strip undefined items.
  74. return _.reduce(thisVal,function (memo, item, i) {
  75. if (!_.isUndefined(item)) {
  76. memo.push(_recursiveRebuildIt.call(thisVal, item, i));
  77. }
  78. return memo;
  79. }, []);
  80. }
  81. // Serialize errors, regexps, and dates to strings, then
  82. // allow those strings to be handled by the transformer
  83. // function from userland:
  84. else if (_.isError(thisVal)){
  85. thisVal = thisVal.stack;
  86. thisVal = handleLeafTransform(thisVal, 'string');
  87. }
  88. else if (_.isRegExp(thisVal)){
  89. thisVal = thisVal.toString();
  90. thisVal = handleLeafTransform(thisVal, 'string');
  91. }
  92. else if (_.isDate(thisVal)){
  93. thisVal = thisVal.toJSON();
  94. thisVal = handleLeafTransform(thisVal, 'string');
  95. }
  96. // But allow functions, strings, numbers, booleans, and `null` to
  97. // be handled by the transformer function provided from userland:
  98. else if (_.isFunction(thisVal)){
  99. thisVal = handleLeafTransform(thisVal, 'lamda');
  100. }
  101. else if (!_.isObject(thisVal)) {
  102. // There are a few special cases which are always
  103. // handled the same way-- these get transformed to zero,
  104. // then passed to the transformer function.
  105. // They are `NaN`, `Infinity`, `-Infinity`, and `-0`:
  106. if (_.isNaN(thisVal)) {
  107. thisVal = 0;
  108. thisVal = handleLeafTransform(thisVal, 'number');
  109. }
  110. else if (thisVal === Infinity) {
  111. thisVal = 0;
  112. thisVal = handleLeafTransform(thisVal, 'number');
  113. }
  114. else if (thisVal === -Infinity) {
  115. thisVal = 0;
  116. thisVal = handleLeafTransform(thisVal, 'number');
  117. }
  118. else if (thisVal === 0) {
  119. // (this coerces -0 to +0)
  120. thisVal = 0;
  121. thisVal = handleLeafTransform(thisVal, 'number');
  122. }
  123. // Otherwise, this is a normal primitive, so it just
  124. // goes through the transformer as-is, using rttc.getDisplayType()
  125. // to determine the second argument.
  126. else {
  127. thisVal = handleLeafTransform(thisVal, getDisplayType(thisVal));
  128. }
  129. }
  130. // Handle objects (which might be dictionaries and arrays,
  131. // or crazy things like streams):
  132. else if (_.isObject(thisVal)) {
  133. // Reject readable streams out of hand
  134. if (thisVal instanceof Readable) {
  135. return null;
  136. }
  137. // Reject buffers out of hand
  138. if (thisVal instanceof Buffer) {
  139. return null;
  140. }
  141. // Reject `RttcRefPlaceholders` out of hand
  142. // (this is a special case so there is a placeholder value that ONLY validates stricly against the "ref" type)
  143. // (note that like anything else, RttcRefPlaceholders nested inside of a JSON/generic dict/generic array get sanitized into JSON-compatible things)
  144. if (_.isObject(thisVal.constructor) && thisVal.constructor.name === 'RttcRefPlaceholder') {
  145. return null;
  146. }
  147. // Now we're about to take the the recursive step..!
  148. //
  149. // But first, run the composite transform handler, if one was provided.
  150. if (!_.isUndefined(handleCompositeTransform)) {
  151. thisVal = handleCompositeTransform(thisVal, 'dictionary');
  152. }
  153. // Then recursively rebuild and strip undefined keys.
  154. return _.reduce(_.keys(thisVal),function (memo, key) {
  155. var subVal = thisVal[key];
  156. if (!_.isUndefined(subVal)) {
  157. memo[key] = _recursiveRebuildIt.call(thisVal, subVal, key);
  158. }
  159. return memo;
  160. }, {});
  161. }
  162. // If the transformer function set the new `thisVal` to `undefined` or
  163. // left/set it to a function, then use `null` instead.
  164. // (because `null` is JSON serializable).
  165. if (_.isUndefined(thisVal) || _.isFunction(thisVal)) {
  166. thisVal = null;
  167. }
  168. // This check is just for convenience/to avoid common mistakes.
  169. // Note that the transformer function could have technically
  170. // returned a circular object that would cause an error
  171. // when/if stringified as JSON. Or it might have nested undefineds,
  172. // functions, Dates, etc., which would cause it to look different
  173. // after undergoing JSON serialization.
  174. //
  175. // We do not handle these cases for performance reasons.
  176. // It is up to userland code to provide a reasonable transformer
  177. // that returns JSON serializable things.
  178. return thisVal;
  179. })(val, '');
  180. // ^^Note that we pass in the empty string for the top-level
  181. // "key" to satisfy Mr. isaac's cycle replacer
  182. })();
  183. };