UNPKG

jsan

Version:

handle circular references when stringifying and parsing

213 lines (181 loc) 6.74 kB
var pathGetter = require('./path-getter'); var utils = require('./utils'); var WMap = typeof WeakMap !== 'undefined'? WeakMap: function() { var keys = []; var values = []; return { set: function(key, value) { keys.push(key); values.push(value); }, get: function(key) { for (var i = 0; i < keys.length; i++) { if (keys[i] === key) { return values[i]; } } } } }; // Based on https://github.com/douglascrockford/JSON-js/blob/master/cycle.js exports.decycle = function decycle(object, options, replacer, map) { 'use strict'; map = map || new WMap(); var noCircularOption = !Object.prototype.hasOwnProperty.call(options, 'circular'); var withRefs = options.refs !== false; return (function derez(_value, path, key) { // The derez recurses through the object, producing the deep copy. var i, // The loop counter name, // Property name nu; // The new object or array // typeof null === 'object', so go on if this value is really an object but not // one of the weird builtin objects. var value = typeof replacer === 'function' ? replacer(key || '', _value) : _value; if (options.date && value instanceof Date) { return {$jsan: 'd' + value.getTime()}; } if (options.regex && value instanceof RegExp) { return {$jsan: 'r' + utils.getRegexFlags(value) + ',' + value.source}; } if (options['function'] && typeof value === 'function') { return {$jsan: 'f' + utils.stringifyFunction(value, options['function'])} } if (options['nan'] && typeof value === 'number' && isNaN(value)) { return {$jsan: 'n'} } if (options['infinity']) { if (Number.POSITIVE_INFINITY === value) return {$jsan: 'i'} if (Number.NEGATIVE_INFINITY === value) return {$jsan: 'y'} } if (options['undefined'] && value === undefined) { return {$jsan: 'u'} } if (options['error'] && value instanceof Error) { return {$jsan: 'e' + value.message} } if (options['symbol'] && typeof value === 'symbol') { var symbolKey = Symbol.keyFor(value) if (symbolKey !== undefined) { return {$jsan: 'g' + symbolKey} } // 'Symbol(foo)'.slice(7, -1) === 'foo' return {$jsan: 's' + value.toString().slice(7, -1)} } if (options['map'] && typeof Map === 'function' && value instanceof Map && typeof Array.from === 'function') { return {$jsan: 'm' + JSON.stringify(decycle(Array.from(value), options, replacer, map))} } if (options['set'] && typeof Set === 'function' && value instanceof Set && typeof Array.from === 'function') { return {$jsan: 'l' + JSON.stringify(decycle(Array.from(value), options, replacer, map))} } if (value && typeof value.toJSON === 'function') { try { value = value.toJSON(key); } catch (error) { var keyString = (key || '$'); return "toJSON failed for '" + (map.get(value) || keyString) + "'"; } } if (typeof value === 'object' && value !== null && !(value instanceof Boolean) && !(value instanceof Date) && !(value instanceof Number) && !(value instanceof RegExp) && !(value instanceof String) && !(typeof value === 'symbol') && !(value instanceof Error)) { // If the value is an object or array, look to see if we have already // encountered it. If so, return a $ref/path object. if (typeof value === 'object') { var foundPath = map.get(value); if (foundPath) { if (noCircularOption && withRefs) { return {$jsan: foundPath}; } // This is only a true circular reference if the parent path is inside of foundPath // drop the last component of the current path and check if it starts with foundPath var parentPath = path.split('.').slice(0, -1).join('.'); if (parentPath.indexOf(foundPath) === 0) { if (!noCircularOption) { return typeof options.circular === 'function'? options.circular(value, path, foundPath): options.circular; } return {$jsan: foundPath}; } if (withRefs) return {$jsan: foundPath}; } map.set(value, path); } // If it is an array, replicate the array. if (Object.prototype.toString.apply(value) === '[object Array]') { nu = []; for (i = 0; i < value.length; i += 1) { nu[i] = derez(value[i], path + '[' + i + ']', i); } } else { // If it is an object, replicate the object. nu = {}; for (name in value) { if (Object.prototype.hasOwnProperty.call(value, name)) { var nextPath = /^\w+$/.test(name) ? '.' + name : '[' + JSON.stringify(name) + ']'; nu[name] = name === '$jsan' ? [derez(value[name], path + nextPath)] : derez(value[name], path + nextPath, name); } } } return nu; } return value; }(object, '$')); }; exports.retrocycle = function retrocycle($) { 'use strict'; return (function rez(value) { // The rez function walks recursively through the object looking for $jsan // properties. When it finds one that has a value that is a path, then it // replaces the $jsan object with a reference to the value that is found by // the path. var i, item, name, path; if (value && typeof value === 'object') { if (Object.prototype.toString.apply(value) === '[object Array]') { for (i = 0; i < value.length; i += 1) { item = value[i]; if (item && typeof item === 'object') { if (item.$jsan) { value[i] = utils.restore(item.$jsan, $); } else { rez(item); } } } } else { for (name in value) { // base case passed raw object if(typeof value[name] === 'string' && name === '$jsan'){ return utils.restore(value.$jsan, $); break; } else { if (name === '$jsan') { value[name] = value[name][0]; } if (typeof value[name] === 'object') { item = value[name]; if (item && typeof item === 'object') { if (item.$jsan) { value[name] = utils.restore(item.$jsan, $); } else { rez(item); } } } } } } } return value; }($)); };