jsan
Version:
handle circular references when stringifying and parsing
213 lines (181 loc) • 6.74 kB
JavaScript
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;
}($));
};