/** * Module dependencies */ var util = require('util'); var _ = require('@sailshq/lodash'); var Readable = require('stream').Readable; var getDisplayType = require('../get-display-type'); /** * rebuildRecursive() * * Rebuild a potentially-recursively-deep value, running * the specified `handleLeafTransform` lifecycle callback * (aka transformer function) for every primitive (i.e. string, * number, boolean, null, function). * * Note that this is very similar to the sanitize helper, except * that it does not make any assumptions about how to handle functions * or primitives. * * @param {Anything} val * * @param {Function} handleLeafTransform [run AFTER stringification of Errors, Dates, etc.] * @param {Anything} leafVal * @param {String} leafType [either 'string', 'number', 'boolean', 'null', or 'lamda'] * @return {Anything} [transformed version of `leafVal`] * * @param {Function} handleCompositeTransform [run BEFORE recursion and stripping of undefined items/props] * @param {Dictionary|Array} compositeVal * @param {String} leafType [either 'array' or 'dictionary'] * @return {Dictionary|Array} [transformed version of `compositeVal`-- MUST BE A DICTONARY OR ARRAY THAT IS SAFE TO RECURSIVELY DIVE INTO!!!] * * @returns {JSON} */ module.exports = function rebuildRecursive(val, handleLeafTransform, handleCompositeTransform) { // If an invalid transformer function was provided, throw a usage error. if (!_.isFunction(handleLeafTransform)){ throw new Error('Usage: A transformer function must be provided as the second argument when rebuilding. Instead, got: '+util.inspect(handleLeafTransform, {depth:null})); } // If `val` is undefined at the top level, leave it as `undefined`. if (_.isUndefined(val)) { return undefined; } // The only reason this outer wrapper self-calling function exists // is to isolate the inline function below (the cycleReplacer) return (function _rebuild() { var stack = []; var keys = []; // This was modified from @isaacs' json-stringify-safe // (see https://github.com/isaacs/json-stringify-safe/commit/02cfafd45f06d076ac4bf0dd28be6738a07a72f9#diff-c3fcfbed30e93682746088e2ce1a4a24) var cycleReplacer = function(unused, value) { if (stack[0] === value) { return '[Circular ~]'; } return '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']'; }; // This is a self-invoking recursive function. return (function _recursiveRebuildIt (thisVal, key) { // Handle circle jerks if (stack.length > 0) { var self = this; var thisPos = stack.indexOf(self); ~thisPos ? stack.splice(thisPos + 1) : stack.push(self); ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key); if (~stack.indexOf(thisVal)) { thisVal = cycleReplacer.call(self, key, thisVal); } } else { stack.push(thisVal); } // If this is an array, we'll recursively rebuild and strip undefined items. if (_.isArray(thisVal)) { // But first, run the composite transform handler, if one was provided. if (!_.isUndefined(handleCompositeTransform)) { thisVal = handleCompositeTransform(thisVal, 'array'); } // Now recursively rebuild and strip undefined items. return _.reduce(thisVal,function (memo, item, i) { if (!_.isUndefined(item)) { memo.push(_recursiveRebuildIt.call(thisVal, item, i)); } return memo; }, []); } // Serialize errors, regexps, and dates to strings, then // allow those strings to be handled by the transformer // function from userland: else if (_.isError(thisVal)){ thisVal = thisVal.stack; thisVal = handleLeafTransform(thisVal, 'string'); } else if (_.isRegExp(thisVal)){ thisVal = thisVal.toString(); thisVal = handleLeafTransform(thisVal, 'string'); } else if (_.isDate(thisVal)){ thisVal = thisVal.toJSON(); thisVal = handleLeafTransform(thisVal, 'string'); } // But allow functions, strings, numbers, booleans, and `null` to // be handled by the transformer function provided from userland: else if (_.isFunction(thisVal)){ thisVal = handleLeafTransform(thisVal, 'lamda'); } else if (!_.isObject(thisVal)) { // There are a few special cases which are always // handled the same way-- these get transformed to zero, // then passed to the transformer function. // They are `NaN`, `Infinity`, `-Infinity`, and `-0`: if (_.isNaN(thisVal)) { thisVal = 0; thisVal = handleLeafTransform(thisVal, 'number'); } else if (thisVal === Infinity) { thisVal = 0; thisVal = handleLeafTransform(thisVal, 'number'); } else if (thisVal === -Infinity) { thisVal = 0; thisVal = handleLeafTransform(thisVal, 'number'); } else if (thisVal === 0) { // (this coerces -0 to +0) thisVal = 0; thisVal = handleLeafTransform(thisVal, 'number'); } // Otherwise, this is a normal primitive, so it just // goes through the transformer as-is, using rttc.getDisplayType() // to determine the second argument. else { thisVal = handleLeafTransform(thisVal, getDisplayType(thisVal)); } } // Handle objects (which might be dictionaries and arrays, // or crazy things like streams): else if (_.isObject(thisVal)) { // Reject readable streams out of hand if (thisVal instanceof Readable) { return null; } // Reject buffers out of hand if (thisVal instanceof Buffer) { return null; } // Reject `RttcRefPlaceholders` out of hand // (this is a special case so there is a placeholder value that ONLY validates stricly against the "ref" type) // (note that like anything else, RttcRefPlaceholders nested inside of a JSON/generic dict/generic array get sanitized into JSON-compatible things) if (_.isObject(thisVal.constructor) && thisVal.constructor.name === 'RttcRefPlaceholder') { return null; } // Now we're about to take the the recursive step..! // // But first, run the composite transform handler, if one was provided. if (!_.isUndefined(handleCompositeTransform)) { thisVal = handleCompositeTransform(thisVal, 'dictionary'); } // Then recursively rebuild and strip undefined keys. return _.reduce(_.keys(thisVal),function (memo, key) { var subVal = thisVal[key]; if (!_.isUndefined(subVal)) { memo[key] = _recursiveRebuildIt.call(thisVal, subVal, key); } return memo; }, {}); } // If the transformer function set the new `thisVal` to `undefined` or // left/set it to a function, then use `null` instead. // (because `null` is JSON serializable). if (_.isUndefined(thisVal) || _.isFunction(thisVal)) { thisVal = null; } // This check is just for convenience/to avoid common mistakes. // Note that the transformer function could have technically // returned a circular object that would cause an error // when/if stringified as JSON. Or it might have nested undefineds, // functions, Dates, etc., which would cause it to look different // after undergoing JSON serialization. // // We do not handle these cases for performance reasons. // It is up to userland code to provide a reasonable transformer // that returns JSON serializable things. return thisVal; })(val, ''); // ^^Note that we pass in the empty string for the top-level // "key" to satisfy Mr. isaac's cycle replacer })(); };