index.js
/*
Copyright, Feb 2016, AnyWhichWay
MIT License (since some CDNs and users must have some type of license and MIT in pretty un-restrictive)
Substantive portions based on:
cycle.js
2013-02-19 douglas crockford
Public Domain.
*/
(function() {
"use strict";
var cycler = {};
function isArray(value) {
return Array.isArray(value) || value instanceof Array;
}
// AnyWhichWay, Feb 2016, isolates code for tagging objects with $class
// during decycle. See resurrect for the converse.
function augment(context, original, decycled) {
var classname = original.constructor.name;
// look in context if classname not available
if (!classname || classname === "") {
Object.keys(context).some(function(name) {
if (context[name] === original.constructor) {
classname = name;
return true;
}
});
}
// add the $class info to array or object
if (classname && classname.length > 0) {
if (isArray(decycled)) {
decycled.push({
$class : classname
});
return decycled;
}
decycled.$class = classname;
return decycled;
}
}
function getContext(context) {
return (context ? context : (typeof (window) !== "undefined" ? window : global));
}
function isDecyclable(value) {
return typeof(value)==="object" && value
&& !(value instanceof Boolean) && !(value instanceof Date)
&& !(value instanceof Number) && !(value instanceof RegExp)
&& !(value instanceof String);
}
cycler.decycle = function decycle(object, context) {
// Make a deep copy of an object or array, assuring that there is at
// most one instance of each object or array in the resulting structure. The
// duplicate references (which might be forming cycles) are replaced
// with an object of the form
// {$ref: PATH}
// where the PATH is a JSONPath string that locates the first occurance.
// So,
// var a = [];
// a[0] = a;
// return JSON.stringify(JSON.decycle(a));
// produces the string '[{"$ref":"$"}]'.
// Add a $class property to objects or element to arrays so that they
// can be restored as their original kind.
// JSONPath is used to locate the unique object. $ indicates the top
// level of the object or array. [NUMBER] or [STRING] indicates a
// child member or property.
// AnyWhichWay, Feb 2016, establish context
context = getContext(context);
// AnyWhichWay, Feb 201, replaced objects and paths arrays with Map
var objects = new Map();
return (function derez(value, path) {
// The derez recurses through the object, producing the deep copy.
var pathfound, // AnyWhichWay added Feb 2016
nu = (isArray(value) ? [] : {});
// AnyWhichWay, Feb 2016, converted test to function call
if (isDecyclable(value)) {
// If the value is an object or array, look to see if we have
// encountered it. If so, return a $ref/path object.
// AnyWhichWay, Feb 2016 replaced array loops with Map get
pathfound = objects.get(value);
if (pathfound) {
return {
$ref : pathfound
};
}
// Otherwise, accumulate the unique value and its path.
// AnyWhichWay, Feb 2016 replace array objects and paths with Map
objects.set(value, path);
Object.keys(value).forEach(
function(key) {
nu[key] = derez(value[key], path
+ "["
+ (isArray(nu) ? key : JSON
.stringify(key)) + "]");
});
// AnyWhichWay, Feb 2016 augment with $class
return augment(context, value, nu);
}
// otherwise, just return value
return value;
}(object, "$"));
};
function getConstructor(context, item) {
// process objects and return possibly modified item
var value;
if (item && item.$class) {
if (typeof (context[item.$class]) === "function") {
value = context[item.$class];
delete item.$class;
return value;
}
return Object; // don't delete item.$class since it will be useful for debugging scope issues
}
if (isArray(item)
&& item[item.length - 1].$class
&& Object.keys(item[item.length - 1]).length === 1) {
if (typeof (context[item[item.length - 1].$class]) === "function") {
value = context[item[item.length - 1].$class];
delete item[item.length - 1].$class;
return value;
}
return Object;
}
}
// AnyWhichWay, Feb 2016, isolates code for resurrecting objects as their
// original type see augment for inverse
// AnyWhichWay, Feb 2016, isolates code for resurrecting objects as their
// original type see augment for inverse. Optimized May, 2016
function resurrect(context, item) {
var cons = getConstructor(context, item);
// process objects and return possibly modified item
if (cons) {
var properties = {constructor: {enumerable:false,configurable:true,writable:true,value:cons}};
Object.keys(item).forEach(function(key, i) {
// hide class spec if it exists
if(key==="$class") {
properties[key] = {configurable:true,writable:true,value:item[key]};
} else if (i !== item.length - 1 || !isArray(item)) {
properties[key] = {configurable:true,writable:true,enumerable:true,value:item[key]};
}
});
return Object.create(cons.prototype,properties);
}
return item;
}
cycler.retrocycle = function retrocycle($, context) {
// Restore an object that was reduced by decycle. Members whose values
// are objects of the form {$ref: PATH}
// are replaced with references to the value found by the PATH. This
// will restore cycles. The object will be mutated.
// AnyWhichWay, Feb 2016
// Objects containing $class member are converted to the class specified
// Arrays with last member {$class: <some kind>} are converted to the
// specified class of array
// A dynamic Function is used to locate the values described by a PATH.
// Root object is kept in a $ variable. A regular expression is used to
// assure that the PATH is well formed. The regexp contain nested
// * quantifiers. That has been known to have extremely bad performance
// problems on some browsers for very long strings. A PATH should be
// reasonably short. A PATH is allowed to belong to a very restricted
// subset of Goessner's JSONPath.
// So,
// var s = '[{"$ref":"$"}]';
// return JSON.retrocycle(JSON.parse(s));
// produces an array containing a single element which is the array
// itself.
// AnyWhichWay, May 2016, just return if not object
if(typeof($)!=="object" || !$) {
return $;
}
// AnyWhichWay, Feb 2016, establish the context
context = getContext(context);
// AnyWhichWay, Feb 2016 do any required top-level conversion from
// POJO's to $classs
$ = resurrect(context, $);
var px = /^\$(?:\[(?:\d+|\"(?:[^\\\"\u0000-\u001f]|\\([\\\"\/bfnrt]|u[0-9a-zA-Z]{4}))*\")\])*$/;
(function rez(value) {
// Modified by AnyWhichWay, Feb 2016
// The rez function walks recursively through the object looking for
// $ref and $class properties or array values. When it finds a $ref value
// that is a path, then it replaces the $ref object with a reference to the value
// that is found by the path. When it finds a $class value that names a function in
// the global scope, it assumes the function is a constructor and uses it to create an
// object which replaces the JSON such that it is restored with the appropriate
// class semantics and capability rather than just a general object. If no
// constructor exists, a POJO is used.
// AnyWhichWay, Feb 2016, replaced separate array and object loops with forEach
Object.keys(value).forEach(
function(name) {
value[name] = resurrect(context, value[name]);
if (value[name] && typeof value[name] === "object"
&& typeof value[name].$ref === "string"
&& px.test(value[name].$ref)) {
value[name] = Function("dollar",
"var $ = dollar; return " + value[name].$ref)($);
} else if (value[name] && typeof value[name] === "object") {
rez(value[name]);
}
});
}($));
return $;
};
if(typeof(module)!=="undefined") {
module.exports = cycler;
}
if(typeof(window)!=="undefined") {
window.Cycler = cycler;
}
})();