json-dry
Advanced tools
Comparing version 0.1.6 to 1.0.0
1132
lib/json-dry.js
@@ -0,163 +1,761 @@ | ||
"use strict"; | ||
var special_char = '~', | ||
safe_special_char = '\\x7e', | ||
escaped_safe_special_char = '\\' + safe_special_char, | ||
special_char_rg = RegExp(safe_special_char, 'g'), | ||
safe_special_char_rg = RegExp(escaped_safe_special_char, 'g'), | ||
get_regex = /^\/(.*)\/(.*)/, | ||
undriers = {}, | ||
driers = {}; | ||
/** | ||
* Copyright (C) 2013 by WebReflection | ||
* Modified by Jelle De Loecker | ||
* Generate a replacer function | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in | ||
* all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
* THE SOFTWARE. | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.0 | ||
* @version 1.0.0 | ||
* | ||
* @param {Object} root | ||
* @param {Function} replacer | ||
* | ||
* @return {Function} | ||
*/ | ||
function createDryReplacer(root, replacer) { | ||
var | ||
// should be a not so common char | ||
// possibly one JSON does not encode | ||
// possibly one encodeURIComponent does not encode | ||
// right now this char is '~' but this might change in the future | ||
specialChar = '~', | ||
safeSpecialChar = '\\x' + ( | ||
'0' + specialChar.charCodeAt(0).toString(16) | ||
).slice(-2), | ||
escapedSafeSpecialChar = '\\' + safeSpecialChar, | ||
specialCharRG = new RegExp(safeSpecialChar, 'g'), | ||
safeSpecialCharRG = new RegExp(escapedSafeSpecialChar, 'g'), | ||
var value_paths = new WeakMap(), | ||
seen_path, | ||
is_root = true, | ||
chain = [], | ||
path = [], | ||
temp, | ||
last, | ||
len; | ||
safeStartWithSpecialCharRG = new RegExp('(?:^|[^\\\\])' + escapedSafeSpecialChar), | ||
return function dryReplacer(holder, key, value) { | ||
indexOf = [].indexOf || function(v){ | ||
for(var i=this.length;i--&&this[i]!==v;); | ||
return i; | ||
}, | ||
$String = String, // there's no way to drop warnings in JSHint | ||
// about new String ... well, I need that here! | ||
// faked, and happy linter! | ||
iso8061 = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/, | ||
getregex = /^\/(.*)\/(.*)/ | ||
; | ||
var class_name, | ||
new_value, | ||
replaced, | ||
is_array, | ||
is_wrap, | ||
keys, | ||
key, | ||
i; | ||
function generateReplacer(rootValue, replacer) { | ||
var path = [], | ||
seen = [rootValue], // Registry of ALL objects | ||
mapp = [specialChar], // The main map to use | ||
// Process the value to a possible given replacer function | ||
if (replacer != null) { | ||
value = replacer.call(holder, key, value); | ||
} | ||
seens = {}, // Registry of objects per constructor | ||
seensmapp = {}, // Map of seens to seen id | ||
// All falsy values can be returned as-is | ||
if (!value) { | ||
return value; | ||
} | ||
isObject = (typeof rootValue === 'object'), | ||
chain = [], | ||
i; | ||
// An explicitly false key means this dryReplaced was | ||
// recursively called with a replacement object | ||
if (key === false) { | ||
key = ''; | ||
is_wrap = true; | ||
if (typeof rootValue == 'object') { | ||
seens[rootValue.constructor.name] = [rootValue]; | ||
seensmapp[rootValue.constructor.name] = [0]; | ||
} | ||
// Wrappers get added to the object chain, but not the path | ||
// We need to be able to identify them later on | ||
holder.__is_wrap = true; | ||
var prevchild = 0; | ||
return function DryReplacer(key, value) { | ||
// See if the wrapped value is an object | ||
if (holder[''] != null && typeof holder[''] === 'object') { | ||
holder.__is_object = true; | ||
} | ||
} | ||
var valType, j, nameType; | ||
// the replacer has rights to decide | ||
// if a new object should be returned | ||
// or if there's some key to drop | ||
// let's call it here rather than "too late" | ||
if (replacer && typeof replacer == 'function') value = replacer.call(this, key, value); | ||
switch (typeof value) { | ||
valType = typeof value; | ||
case 'function': | ||
// If no drier is created, return now. | ||
// Else: fall through | ||
if (!driers.Function) { | ||
return; | ||
} | ||
// did you know ? Safari passes keys as integers for arrays | ||
if (key !== '' || !isObject) { | ||
if (valType === 'object' && value) { | ||
case 'object': | ||
// Get the constructor name | ||
nameType = value.constructor.name; | ||
if (value.constructor) { | ||
class_name = value.constructor.name; | ||
} else { | ||
class_name = 'Object'; | ||
} | ||
if (!seens[nameType]) { | ||
seens[nameType] = []; | ||
seensmapp[nameType] = []; | ||
// See if the chain needs popping | ||
while (len = chain.length) { | ||
// If the current object at the end of the chain does not | ||
// match the current holder, move one up | ||
// Don't mess with the chain if this is a wrap object | ||
if (!is_wrap && holder !== chain[len-1]) { | ||
last = chain.pop(); | ||
// Only pop the path if the popped object isn't a wrapper | ||
if (last && !last.__is_wrap) { | ||
path.pop(); | ||
} | ||
} else { | ||
break; | ||
} | ||
} | ||
if (chain.length) { | ||
// See if the current is actually a property of the current | ||
// active item in the chain | ||
if (chain[chain.length-1][key] !== value) { | ||
// Has the object been seen before? | ||
seen_path = value_paths.get(value); | ||
// If it's not, the current active item is probably done | ||
chain.pop(); | ||
if (seen_path) { | ||
// Se we should also remove it from the path | ||
path.pop(); | ||
// If the path is still an array, | ||
// turn it into a string now | ||
if (typeof seen_path != 'string') { | ||
// First iterate over the pieces and escape them | ||
for (i = 0; i < seen_path.length; i++) { | ||
if (seen_path[i].indexOf(special_char) > -1) { | ||
seen_path[i] = seen_path[i].replace(special_char_rg, safe_special_char); | ||
} | ||
} | ||
seen_path = special_char + seen_path.join(special_char); | ||
value_paths.set(value, seen_path); | ||
} | ||
// Replace the value with the path | ||
new_value = seen_path; | ||
// See if the new path is shorter | ||
len = 0; | ||
for (i = 0; i < path.length; i++) { | ||
len += 1 + path.length; | ||
} | ||
len += key.length; | ||
if (len < seen_path.length) { | ||
temp = seen_path; | ||
seen_path = path.slice(0); | ||
// The key of the current value still needs to be added | ||
seen_path.push(key); | ||
// First iterate over the pieces and escape them | ||
for (i = 0; i < seen_path.length; i++) { | ||
if (seen_path[i].indexOf(special_char) > -1) { | ||
seen_path[i] = seen_path[i].replace(special_char_rg, safe_special_char); | ||
} | ||
} | ||
seen_path = special_char + seen_path.join(special_char); | ||
value_paths.set(value, seen_path); | ||
// This entry still has to refer to the longer path, | ||
// otherwise it'll refer to itself | ||
seen_path = temp; | ||
} | ||
value = new_value; | ||
break; | ||
} | ||
// Push the current object to the chain | ||
chain.push(value); | ||
if (!is_root && !is_wrap) { | ||
path.push(key); | ||
} else { | ||
is_root = false; | ||
} | ||
// See if this object has been seen before | ||
i = seens[nameType].indexOf(value); | ||
if (i < 0) { | ||
// Make a copy of the current path array | ||
value_paths.set(value, path.slice(0)); | ||
// Store the object in the seen array and return the index | ||
i = seen.push(value) - 1; | ||
j = seens[nameType].push(value); | ||
seensmapp[nameType][j] = i; | ||
if (driers[class_name] != null) { | ||
value = driers[class_name].fnc(holder, key, value); | ||
if (value.constructor.name == 'RegExp') { | ||
value = {dry: 'regexp', value: value.toString()}; | ||
value = { | ||
dry: class_name, | ||
value: value | ||
}; | ||
if (driers[class_name].options.add_path !== false) { | ||
value.drypath = path.slice(0); | ||
} | ||
// key cannot contain specialChar but could be not a string | ||
path.push(('' + key).replace(specialCharRG, safeSpecialChar)); | ||
replaced = {'': value}; | ||
} else if (class_name == 'RegExp') { | ||
value = {dry: 'regexp', value: value.toString()}; | ||
replaced = {'': value}; | ||
} else if (class_name == 'Date') { | ||
value = {dry: 'date', value: value.toISOString()}; | ||
replaced = {'': value}; | ||
} else if (typeof value.toDry === 'function') { | ||
temp = value; | ||
value = value.toDry(); | ||
mapp[i] = specialChar + path.join(specialChar); | ||
// If no path was supplied in the toDry, | ||
// get some more class information | ||
if (!value.path) { | ||
if (temp.constructor) { | ||
if (!value.namespace && temp.constructor.namespace) { | ||
value.namespace = temp.constructor.namespace; | ||
} | ||
if (!value.dry_class) { | ||
value.dry_class = temp.constructor.name; | ||
} | ||
} | ||
} | ||
value.dry = 'toDry'; | ||
value.drypath = path.slice(0); | ||
replaced = {'': value}; | ||
} else if (typeof value.toJSON === 'function') { | ||
value = value.toJSON(); | ||
replaced = {'': value}; | ||
} else { | ||
value = mapp[seensmapp[nameType][j]]; | ||
is_array = Array.isArray(value); | ||
} | ||
} else { | ||
path.pop(); | ||
if (valType === 'string') { | ||
// ensure no special char involved on deserialization | ||
// in this case only first char is important | ||
// no need to replace all value (better performance) | ||
value = value .replace(safeSpecialChar, escapedSafeSpecialChar) | ||
.replace(specialChar, safeSpecialChar); | ||
} else if (valType === 'number') { | ||
if (replaced) { | ||
// Push the replaced object on the chain | ||
chain.push(replaced); | ||
// Allow infinite values | ||
if (!isFinite(value)) { | ||
if (value > 0) { | ||
value = {dry: '+Infinity'}; | ||
// Jsonify the replaced object | ||
value = dryReplacer(replaced, false, replaced['']); | ||
// At least one part of the path & chain will have | ||
// to be popped off. This is needed for toJSON calls | ||
// that return primitive values | ||
temp = chain.pop(); | ||
// Don't pop off anything from the path if the last item | ||
// from the chain was a wrapper for an object, | ||
// because then it'll already be popped of | ||
if (!(temp && temp.__is_wrap && temp.__is_object)) { | ||
temp = path.pop(); | ||
} | ||
// Break out of the switch | ||
break; | ||
} | ||
// Push this object on the chain | ||
chain.push(value); | ||
if (is_array) { | ||
new_value = []; | ||
for (i = 0; i < value.length; i++) { | ||
new_value[i] = dryReplacer(value, String(i), value[i]); | ||
} | ||
} else { | ||
new_value = {}; | ||
keys = Object.keys(value); | ||
for (i = 0; i < keys.length; i++) { | ||
key = keys[i]; | ||
new_value[key] = dryReplacer(value, key, value[key]); | ||
} | ||
} | ||
value = new_value; | ||
break; | ||
case 'string': | ||
// Make sure regular strings don't start with the path delimiter | ||
if (!is_root && value[0] == '~') { | ||
value = safe_special_char + value.slice(1); | ||
} | ||
break; | ||
case 'number': | ||
// Allow infinite values | ||
if (!isFinite(value)) { | ||
if (value > 0) { | ||
value = {dry: '+Infinity'}; | ||
} else { | ||
value = {dry: '-Infinity'}; | ||
} | ||
} | ||
break; | ||
} | ||
return value; | ||
} | ||
} | ||
/** | ||
* Generate reviver function | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.0 | ||
* @version 1.0.0 | ||
* | ||
* @param {Function} reviver | ||
* @param {Object} undry_paths | ||
* | ||
* @return {Function} | ||
*/ | ||
function generateReviver(reviver, undry_paths) { | ||
return function dryReviver(key, value) { | ||
var val_type = typeof value, | ||
constructor, | ||
temp; | ||
if (val_type === 'string') { | ||
if (value[0] === special_char) { | ||
// This is actually a path that needs to be replaced. | ||
// Put in a String object for now | ||
return new String(value.slice(1)); | ||
} else if (value[0] == '\\' && value[1] == 'x' && value[2] == '7' && value[3] == 'e') { | ||
value = special_char + value.slice(4); | ||
} | ||
} else if (value && value.dry != null) { | ||
switch (value.dry) { | ||
case 'date': | ||
if (value.value) { | ||
return new Date(value.value); | ||
} | ||
break; | ||
case 'regexp': | ||
if (value.value) { | ||
return RegExp.apply(undefined, get_regex.exec(value.value).slice(1)); | ||
} | ||
break; | ||
case '+Infinity': | ||
return Infinity; | ||
case '-Infinity': | ||
return -Infinity; | ||
case 'toDry': | ||
constructor = findClass(value); | ||
// Undry this element, but don't put it in the parsed object yet | ||
if (constructor && typeof constructor.unDry === 'function') { | ||
value.undried = constructor.unDry(value.value); | ||
} else { | ||
value.undried = value.value; | ||
} | ||
if (value.drypath) { | ||
undry_paths[value.drypath.join(special_char)] = value; | ||
} else { | ||
return value.undried; | ||
} | ||
break; | ||
default: | ||
if (typeof value.value !== 'undefined') { | ||
if (undriers[value.dry]) { | ||
value.undried = undriers[value.dry].fnc(this, key, value.value); | ||
} else { | ||
value = {dry: '-Infinity'}; | ||
value.undried = value.value; | ||
} | ||
if (value.drypath) { | ||
undry_paths[value.drypath.join(special_char)] = value; | ||
} else { | ||
return value.undried; | ||
} | ||
} | ||
} | ||
} | ||
if (reviver == null) { | ||
return value; | ||
} | ||
return reviver.call(this, key, value); | ||
}; | ||
}; | ||
/** | ||
* Deep clone an object | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 1.0.0 | ||
* @version 1.0.0 | ||
* | ||
* @param {Object} obj | ||
* @param {String} custom_method Custom method to use if available | ||
* @param {Array} extra_args Extra arguments for the custom method | ||
* @param {WeakMap} wm | ||
* | ||
* @return {Object} | ||
*/ | ||
function clone(obj, custom_method, extra_args, wm) { | ||
var custom_args, | ||
entry_type, | ||
name_type, | ||
target, | ||
entry, | ||
split, | ||
keys, | ||
temp, | ||
len, | ||
i; | ||
if (custom_method instanceof WeakMap) { | ||
wm = custom_method; | ||
custom_method = null; | ||
} else if (!Array.isArray(extra_args)) { | ||
wm = extra_args; | ||
extra_args = null; | ||
} | ||
if (wm == null) { | ||
wm = new WeakMap(); | ||
return clone({'_': obj}, custom_method, wm)['_']; | ||
} | ||
if (Array.isArray(obj)) { | ||
target = []; | ||
} else { | ||
target = {}; | ||
} | ||
if (custom_method) { | ||
custom_args = [wm].concat(extra_args); | ||
} | ||
keys = Object.keys(obj); | ||
len = keys.length; | ||
// Remember the root object and its clone | ||
wm.set(obj, target); | ||
for (i = 0; i < len; i++) { | ||
entry = obj[keys[i]]; | ||
entry_type = typeof entry; | ||
if (entry && (entry_type == 'object' || entry_type == 'function')) { | ||
if (entry_type == 'function' && !driers.Function) { | ||
continue; | ||
} | ||
// If this has been cloned before, use that | ||
if (wm.has(entry)) { | ||
target[keys[i]] = wm.get(entry); | ||
continue; | ||
} | ||
if (entry.constructor) { | ||
name_type = entry.constructor.name; | ||
if (custom_method && entry[custom_method]) { | ||
target[keys[i]] = entry[custom_method].apply(entry, custom_args); | ||
} else if (driers[name_type] != null) { | ||
// Look for a registered drier function | ||
temp = driers[name_type].fnc(obj, keys[i], entry); | ||
if (undriers[name_type]) { | ||
target[keys[i]] = undriers[name_type].fnc(target, keys[i], temp); | ||
} else { | ||
target[keys[i]] = temp; | ||
} | ||
} else if (entry.dryClone) { | ||
// Look for dryClone after | ||
target[keys[i]] = entry.dryClone(wm, custom_method); | ||
} else if (entry.toDry) { | ||
// Perform the toDry function | ||
temp = entry.toDry(); | ||
// Clone the value, | ||
// because returned objects aren't necesarilly cloned yet | ||
temp = clone(temp.value, custom_method, wm); | ||
// Perform the undry function | ||
if (entry.constructor.unDry) { | ||
target[keys[i]] = entry.constructor.unDry(temp); | ||
} else { | ||
// If there is no undry function, the clone will be a simple object | ||
target[keys[i]] = temp; | ||
} | ||
} else if (name_type == 'Date') { | ||
target[keys[i]] = new Date(entry); | ||
} else if (name_type == 'RegExp') { | ||
temp = entry.toString(); | ||
split = temp.match(/^\/(.*?)\/([gim]*)$/); | ||
if (split) { | ||
target[keys[i]] = RegExp(split[1], split[2]); | ||
} else { | ||
target[keys[i]] = RegExp(temp); | ||
} | ||
} else if (typeof entry.clone == 'function') { | ||
// If it supplies a clone method, use that | ||
target[keys[i]] = entry.clone(); | ||
} else if (entry.toJSON) { | ||
temp = entry.toJSON(); | ||
if (temp && typeof temp == 'object') { | ||
temp = clone(temp, custom_method, wm); | ||
} | ||
target[keys[i]] = temp; | ||
} else { | ||
target[keys[i]] = clone(entry, custom_method, wm); | ||
} | ||
} else { | ||
target[keys[i]] = clone(entry, custom_method, wm); | ||
} | ||
// Remember this clone for later | ||
wm.set(entry, target[keys[i]]); | ||
} else { | ||
target[keys[i]] = entry; | ||
} | ||
} | ||
return value; | ||
return target; | ||
} | ||
/** | ||
* Register a drier | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 1.0.0 | ||
* @version 1.0.0 | ||
* | ||
* @param {Function|String} constructor What constructor to listen to | ||
* @param {Function} fnc | ||
* @param {Object} options | ||
*/ | ||
function registerDrier(constructor, fnc, options) { | ||
var path; | ||
if (typeof constructor == 'function') { | ||
path = constructor.name; | ||
} else { | ||
path = constructor; | ||
} | ||
driers[path] = { | ||
fnc : fnc, | ||
options : options || {} | ||
}; | ||
} | ||
/** | ||
* Register an undrier | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 1.0.0 | ||
* @version 1.0.0 | ||
* | ||
* @param {Function|String} constructor What constructor to listen to | ||
* @param {Function} fnc | ||
* @param {Object} options | ||
*/ | ||
function registerUndrier(constructor, fnc, options) { | ||
var path; | ||
if (typeof constructor == 'function') { | ||
path = constructor.name; | ||
} else { | ||
path = constructor; | ||
} | ||
undriers[path] = { | ||
fnc : fnc, | ||
options : options || {} | ||
}; | ||
} | ||
/** | ||
* Register a class that can be serialized/revived | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 1.0.0 | ||
* @version 1.0.0 | ||
* | ||
* @param {String} name The optional name of the class | ||
* @param {Function|String} constructor What constructor to listen to | ||
*/ | ||
function registerClass(name, constructor) { | ||
if (typeof name == 'function') { | ||
constructor = name; | ||
name = constructor.name; | ||
} | ||
exports.Classes[name] = constructor; | ||
} | ||
/** | ||
* Find a class | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 1.0.0 | ||
* @version 1.0.0 | ||
* | ||
* @param {String} value The name of the class | ||
*/ | ||
function findClass(value) { | ||
var constructor; | ||
// Return nothing for falsy values | ||
if (!value) { | ||
return null; | ||
} | ||
// Look for a regular class when it's just a string | ||
if (typeof value == 'string') { | ||
if (exports.Classes[value]) { | ||
return exports.Classes[value]; | ||
} | ||
return null; | ||
} | ||
if (value.path) { | ||
return fromPath(exports.Classes, value.path); | ||
} else { | ||
if (value.namespace) { | ||
constructor = fromPath(exports.Classes, value.namespace); | ||
} else { | ||
constructor = exports.Classes; | ||
} | ||
if (value.dry_class) { | ||
constructor = fromPath(constructor, value.dry_class); | ||
} | ||
} | ||
return constructor; | ||
} | ||
/** | ||
* Regenerate an array | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.4 | ||
* @version 1.0.0 | ||
* | ||
* @return {Array} | ||
*/ | ||
function regenerateArray(root, current, seen, retrieve, undry_paths) { | ||
var length = current.length, | ||
i; | ||
for (i = 0; i < length; i++) { | ||
// Only regenerate if it's not yet seen | ||
if (!seen.get(current[i])) { | ||
current[i] = regenerate(root, current[i], seen, retrieve, undry_paths); | ||
} | ||
} | ||
return current; | ||
}; | ||
/** | ||
* Regenerate an object | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.4 | ||
* @version 1.0.0 | ||
* | ||
* @return {Object} | ||
*/ | ||
function regenerateObject(root, current, seen, retrieve, undry_paths) { | ||
var key; | ||
for (key in current) { | ||
if (current.hasOwnProperty(key)) { | ||
// Only regenerate if it's not already seen | ||
if (!seen.get(current[key])) { | ||
current[key] = regenerate(root, current[key], seen, retrieve, undry_paths); | ||
} | ||
} | ||
} | ||
return current; | ||
}; | ||
/** | ||
* Regenerate a value | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.4 | ||
* @version 1.0.0 | ||
* | ||
* @return {Mixed} | ||
*/ | ||
function regenerate(root, current, seen, retrieve, undry_paths) { | ||
var temp; | ||
if (current && typeof current == 'object') { | ||
// Remember this object has been regenerated already | ||
seen.set(current, true); | ||
if (current instanceof Array) { | ||
return regenerateArray(root, current, seen, retrieve, undry_paths); | ||
} | ||
if (current instanceof String) { | ||
if (current.length) { | ||
current = current.toString(); | ||
if (undry_paths[current]) { | ||
return undry_paths[current].undried; | ||
} | ||
if (retrieve.hasOwnProperty(current)) { | ||
temp = retrieve[current]; | ||
} else { | ||
temp = retrieve[current] = retrieveFromPath(root, current.split(special_char)); | ||
} | ||
return temp; | ||
} else { | ||
return root; | ||
} | ||
} | ||
return regenerateObject(root, current, seen, retrieve, undry_paths); | ||
} | ||
return current; | ||
}; | ||
/** | ||
* Retrieve from path. | ||
* Set the given value, but only if the containing object exists. | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.4 | ||
* @version 1.0.0 | ||
* | ||
* @param {Object} current The object to look in | ||
* @param {Array} keys The path to look for | ||
* @param {Mixed} value Optional value to set | ||
* | ||
* @return {Mixed} | ||
*/ | ||
function retrieveFromPath(current, keys) { | ||
var length = keys.length, | ||
prev, | ||
key, | ||
@@ -167,13 +765,18 @@ i; | ||
for (i = 0; i < length; i++) { | ||
key = keys[i]; | ||
// Normalize the key | ||
key = keys[i].replace(safeSpecialCharRG, specialChar); | ||
if (key.indexOf(safe_special_char) > -1) { | ||
key = key.replace(safe_special_char_rg, special_char); | ||
} | ||
prev = current; | ||
if (current) { | ||
current = current[key]; | ||
if (current.hasOwnProperty(key)) { | ||
current = current[key]; | ||
} else { | ||
return undefined; | ||
} | ||
} else { | ||
if (console) { | ||
console.error('Could not find path ' + keys.join('.')); | ||
} | ||
return undefined; | ||
@@ -186,136 +789,227 @@ } | ||
function generateReviver(reviver) { | ||
return function(key, value) { | ||
/** | ||
* Extract something from an object by the path | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.0 | ||
* @version 1.0.0 | ||
* | ||
* @param {Object} obj | ||
* @param {String} path | ||
* | ||
* @return {Mixed} | ||
*/ | ||
function fromPath(obj, path) { | ||
var valType = typeof value; | ||
var pieces, | ||
here, | ||
len, | ||
i; | ||
if (valType == 'string') { | ||
if (value.charAt(0) === specialChar) { | ||
return new $String(value.slice(1)); | ||
} else if(value.match(iso8061)) { | ||
return new Date(value); | ||
if (typeof path == 'string') { | ||
pieces = path.split('.'); | ||
} else { | ||
pieces = path; | ||
} | ||
here = obj; | ||
// Go over every piece in the path | ||
for (i = 0; i < pieces.length; i++) { | ||
if (here != null) { | ||
if (here.hasOwnProperty(pieces[i])) { | ||
here = here[pieces[i]]; | ||
} else { | ||
return null; | ||
} | ||
} else if (value && valType == 'object' && typeof value.dry !== 'undefined') { | ||
if (value.dry == 'regexp' && value.value) { | ||
return RegExp.apply(undefined, getregex.exec(value.value).slice(1)); | ||
} else if (value.dry == '+Infinity') { | ||
return Infinity; | ||
} else if (value.dry == '-Infinity') { | ||
return -Infinity; | ||
} | ||
} else { | ||
break; | ||
} | ||
if (key === '') value = regenerate(value, value, {}); | ||
// again, only one needed, do not use the RegExp for this replacement | ||
// only keys need the RegExp | ||
if (valType == 'string') value = value .replace(safeStartWithSpecialCharRG, specialChar) | ||
.replace(escapedSafeSpecialChar, safeSpecialChar); | ||
return reviver ? reviver.call(this, key, value) : value; | ||
}; | ||
} | ||
} | ||
function regenerateArray(root, current, retrieve) { | ||
for (var i = 0, length = current.length; i < length; i++) { | ||
current[i] = regenerate(root, current[i], retrieve); | ||
} | ||
return current; | ||
return here; | ||
} | ||
function regenerateObject(root, current, retrieve) { | ||
for (var key in current) { | ||
if (current.hasOwnProperty(key)) { | ||
current[key] = regenerate(root, current[key], retrieve); | ||
/** | ||
* Set something on the given path | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 1.0.0 | ||
* @version 1.0.0 | ||
* | ||
* @param {Object} obj | ||
* @param {Array} path | ||
* | ||
* @return {Mixed} | ||
*/ | ||
function setPath(obj, keys, value) { | ||
var here, | ||
i; | ||
here = obj; | ||
for (i = 0; i < keys.length - 1; i++) { | ||
if (here != null) { | ||
if (here.hasOwnProperty(keys[i])) { | ||
here = here[keys[i]]; | ||
} else { | ||
return null; | ||
} | ||
} | ||
} | ||
return current; | ||
} | ||
function regenerate(root, current, retrieve) { | ||
return current instanceof Array ? | ||
// fast Array reconstruction | ||
regenerateArray(root, current, retrieve) : | ||
( | ||
current instanceof $String ? | ||
( | ||
// root is an empty string | ||
current.length ? | ||
( | ||
retrieve.hasOwnProperty(current) ? | ||
retrieve[current] : | ||
retrieve[current] = retrieveFromPath( | ||
root, current.split(specialChar) | ||
) | ||
) : | ||
root | ||
) : | ||
( | ||
current instanceof Object ? | ||
// dedicated Object parser | ||
regenerateObject(root, current, retrieve) : | ||
// value as it is | ||
current | ||
) | ||
) | ||
; | ||
here[keys[keys.length - 1]] = value; | ||
} | ||
function stringifyRecursion(value, replacer, space) { | ||
return JSON.stringify(value, generateReplacer(value, replacer), space); | ||
/** | ||
* Convert an object to a DRY object, ready for stringifying | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 1.0.0 | ||
* @version 1.0.0 | ||
* | ||
* @param {Object} value | ||
* @param {Function} replacer | ||
* | ||
* @return {Object} | ||
*/ | ||
function toDryObject(value, replacer) { | ||
var root = {'': value}; | ||
return createDryReplacer(root, replacer)(root, '', value); | ||
} | ||
function parseRecursion(text, reviver) { | ||
return JSON.parse(text, generateReviver(reviver)); | ||
/** | ||
* Convert directly to a string | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 1.0.0 | ||
* @version 1.0.0 | ||
* | ||
* @param {Object} value | ||
* @param {Function} replacer | ||
* | ||
* @return {Object} | ||
*/ | ||
function stringify(value, replacer, space) { | ||
return JSON.stringify(toDryObject(value, replacer), null, space); | ||
} | ||
/** | ||
* Determine if an object is empty or not. | ||
* Only own properties are valid. | ||
* Map an object | ||
* | ||
* @author Jelle De Loecker <jelle@codedor.be> | ||
* @since 0.1.4 | ||
* @version 0.1.4 | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.4.2 | ||
* @version 1.0.0 | ||
* | ||
* @param {Object} obj The object to walk over | ||
* @param {Function} fnc The function to perform on every entry | ||
* | ||
* @return {Object} | ||
*/ | ||
function isEmptyObject(val) { | ||
function walk(obj, fnc, result) { | ||
var key; | ||
var is_root, | ||
keys, | ||
key, | ||
ret, | ||
i; | ||
// Go over the keys in this object | ||
for (key in val) { | ||
if (!result) { | ||
is_root = true; | ||
// As soon as we encounter a key in this value that actually | ||
// belongs to the object itself, we return false, | ||
// because it's not empty | ||
if (Object.hasOwnProperty.call(val, key)) { | ||
return false; | ||
if (Array.isArray(obj)) { | ||
result = []; | ||
} else { | ||
result = {}; | ||
} | ||
} | ||
return true; | ||
keys = Object.keys(obj); | ||
for (i = 0; i < keys.length; i++) { | ||
key = keys[i]; | ||
if (typeof obj[key] == 'object' && obj[key] != null) { | ||
if (Array.isArray(obj[key])) { | ||
result[key] = walk(obj[key], fnc, []); | ||
} else { | ||
result[key] = walk(obj[key], fnc, {}); | ||
} | ||
result[key] = fnc(key, result[key], result); | ||
} else { | ||
// Fire the function | ||
result[key] = fnc(key, obj[key], obj); | ||
} | ||
} | ||
if (is_root) { | ||
result = fnc('', result); | ||
} | ||
return result; | ||
} | ||
/** | ||
* Determine if an object is empty or not, | ||
* with a special check for arrays | ||
* Convert from a dried object | ||
* | ||
* @author Jelle De Loecker <jelle@codedor.be> | ||
* @since 0.1.4 | ||
* @version 0.1.4 | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 1.0.0 | ||
* @version 1.0.0 | ||
* | ||
* @param {Object} value | ||
* | ||
* @return {Object} | ||
*/ | ||
function isEmpty(val) { | ||
function parse(object, reviver) { | ||
if (Array.isArray(val)) { | ||
return !val.length; | ||
var undry_paths = {}, | ||
retrieve = {}, | ||
reviver, | ||
result, | ||
path; | ||
// Create the reviver function | ||
reviver = generateReviver(reviver, undry_paths); | ||
if (typeof object == 'string') { | ||
object = JSON.parse(object); | ||
} | ||
return isEmptyObject(val); | ||
} | ||
if (!object || typeof object != 'object') { | ||
return object; | ||
} | ||
this.stringify = stringifyRecursion; | ||
this.parse = parseRecursion; | ||
this.isEmptyObject = isEmptyObject; | ||
this.isEmpty = isEmpty; | ||
result = walk(object, reviver); | ||
this.info = {path: false}; | ||
if (result == null) { | ||
return result; | ||
} | ||
if (typeof __dirname !== 'undefined') { | ||
this.info.path = __dirname + '/json-dry.js'; | ||
for (path in undry_paths) { | ||
undry_paths[path].undried = regenerate(result, undry_paths[path].undried, new WeakMap(), retrieve, undry_paths); | ||
} | ||
// Only now we can resolve paths | ||
result = regenerate(result, result, new WeakMap(), retrieve, undry_paths); | ||
// Now we can replace all the undried values | ||
for (path in undry_paths) { | ||
setPath(result, undry_paths[path].drypath, undry_paths[path].undried); | ||
} | ||
if (result.undried != null && result.dry) { | ||
return result.undried; | ||
} | ||
return result; | ||
} | ||
exports.stringify = stringify; | ||
exports.toObject = toDryObject; | ||
exports.parse = parse; | ||
exports.clone = clone; | ||
exports.Classes = {}; | ||
exports.registerClass = registerClass; | ||
exports.registerUndrier = registerUndrier; | ||
exports.registerDrier = registerDrier; |
{ | ||
"name": "json-dry", | ||
"description": "JSON generator & parser with circular, date and regex support", | ||
"version": "0.1.6", | ||
"author": "Jelle De Loecker <jelle@codedor.be>", | ||
"keywords": ["json", "circular", "serialization", "deserialization"], | ||
"description": "Don't repeat yourself, JSON: Add support for (circular) references, class instances, ...", | ||
"version": "1.0.0", | ||
"author": "Jelle De Loecker <jelle@develry.be>", | ||
"keywords": [ | ||
"json", | ||
"circular", | ||
"serialization", | ||
"deserialization" | ||
], | ||
"main": "./lib/json-dry.js", | ||
"repository": "git@github.com:skerit/json-dry.git", | ||
"scripts": { | ||
"test" : "node_modules/.bin/mocha --reporter spec", | ||
"coverage" : "./node_modules/istanbul/lib/cli.js cover _mocha", | ||
"report-coverage" : "cat ./coverage/lcov.info | coveralls" | ||
}, | ||
"license": "MIT", | ||
"devDependencies": { | ||
"istanbul" : "^0.4.5", | ||
"mocha" : "1.20.x", | ||
"protoblast" : "skerit/protoblast#3218106" | ||
}, | ||
"engines": { | ||
"node": ">=0.8" | ||
"node": ">=6.4" | ||
} | ||
} |
200
README.md
@@ -1,34 +0,194 @@ | ||
JSON-dry | ||
======== | ||
# JSON-dry | ||
[![NPM version](http://img.shields.io/npm/v/json-dry.svg)](https://npmjs.org/package/json-dry) | ||
[![Build Status](https://travis-ci.org/skerit/json-dry.svg?branch=master)](https://travis-ci.org/skerit/json-dry) | ||
[![Coverage Status](https://coveralls.io/repos/github/skerit/json-dry/badge.svg?branch=master)](https://coveralls.io/github/skerit/json-dry?branch=master) | ||
JSON-dry allows you to stringify objects containing circular references, | ||
dates and regexes. | ||
dates, regexes, ... | ||
Multiple references to the same object are also correctly converted. | ||
It can also be used to serialize and revive instances of your own classes. | ||
JSON-dry is based on circular-json | ||
## Installation | ||
$ npm install json-dry | ||
# Usage | ||
## Usage | ||
### Basic example | ||
This is a basic example of stringifying an object (containing multiple references to the same object) and parsing it again. | ||
```js | ||
var dry = require('json-dry'), | ||
obj, | ||
ref; | ||
var Dry = require('json-dry'); | ||
ref = { | ||
text: 'This object is referred to', | ||
number: 1, | ||
date: new Date(), | ||
regex: /test/i | ||
// The object we'll serialize later | ||
var obj = {}; | ||
// The object we'll make multiple references to | ||
var ref = { | ||
date : new Date(), | ||
regex : /test/i | ||
}; | ||
obj = { | ||
alpha: 'test', | ||
extra: ref, | ||
again: ref, | ||
three: ref | ||
// Now we'll make multiple references: | ||
// `reference_one` and `reference_two` both point to the same object | ||
// `date` refers to a `Date` object | ||
obj.reference_one = ref; | ||
obj.reference_two = ref; | ||
obj.date = ref.date; | ||
// Stringify the object | ||
var dried = Dry.stringify(obj); | ||
// { | ||
// "reference_one": { | ||
// "date": { | ||
// "dry": "date", | ||
// "value": "2018-01-14T17:45:57.989Z" | ||
// }, | ||
// "regex": { | ||
// "dry": "regexp", | ||
// "value": "/test/i" | ||
// } | ||
// }, | ||
// "reference_two": "~reference_one", | ||
// "date": "~reference_one~date" | ||
// } | ||
// Now we'll revive it again | ||
var undried = Dry.parse(dried); | ||
// { reference_one: { date: 2018-01-14T17:56:43.149Z, regex: /test/i }, | ||
// reference_two: { date: 2018-01-14T17:56:43.149Z, regex: /test/i }, | ||
// date: 2018-01-14T17:58:50.427Z } | ||
// See if they're the same objects (as it should) | ||
undried.reference_one == undried.reference_two; | ||
// true | ||
// The date outside of the reference object is also the same reference | ||
undried.reference_one.date == undried.date; | ||
// true | ||
``` | ||
### Reviving instances | ||
Let's create an example class you might want to serialize and revive: | ||
```js | ||
// The class constructor | ||
function Person(options) { | ||
this.firstname = options.firstname; | ||
this.lastname = options.lastname; | ||
} | ||
// A simple method that prints out the full name | ||
Person.prototype.fullname = function fullname() { | ||
return this.firstname + ' ' + this.lastname; | ||
}; | ||
dry.stringify(obj); | ||
// Create an object | ||
var jelle = new Person({firstname: 'Jelle', lastname: 'De Loecker'}); | ||
// Test out the fullname method | ||
jelle.fullname(); | ||
// returns "Jelle De Loecker" | ||
``` | ||
So now we've created a very basic class, let's register the class and add the **2** required methods for serializing & reviving. | ||
```js | ||
// We need to register the class | ||
Dry.registerClass(Person); | ||
// Add the `toDry` method that will be called upon when serializing/stringifying | ||
Person.prototype.toDry = function toDry() { | ||
return { | ||
value: { | ||
firstname : this.firstname, | ||
lastname : this.lastname | ||
} | ||
}; | ||
}; | ||
// Now add the `unDry` method as a **static** method, on the constructor | ||
Person.unDry = function unDry(value) { | ||
// How you do this is up to you. | ||
// You can call the constructor for this simple class, | ||
// or you can use Object.create, ... | ||
var result = new Person(value); | ||
return result; | ||
}; | ||
``` | ||
Now let's try stringifying it: | ||
```js | ||
var dried = Dry.stringify(jelle); | ||
// {"value":{"firstname":"Jelle","lastname":"De Loecker"},"dry_class":"Person","dry":"toDry","drypath":[]} | ||
// And parse it again | ||
var undried = Dry.parse(dried); | ||
// Person { firstname: 'Jelle', lastname: 'De Loecker' } | ||
// And it works | ||
undried.fullname(); | ||
// returns "Jelle De Loecker" | ||
``` | ||
### toObject | ||
While `Dry.stringify` will return you with a json-valid string, `Dry.toObject` will give you a valid simplified object. | ||
In fact: `Dry.stringify` is just a function that performs `JON.stringify` on `Dry.toObject`'s output. | ||
**Why would you want to use this?** Things like `Workers` and `IndexedDB` communicate data using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). So instead of performing expensive stringify operations you can just use these objects. | ||
## Cloning objects & instances | ||
JSON-Dry offers a specialized `clone` method. While in theory you could clone an object by drying end reviving it, like so: | ||
```js | ||
var cloned = Dry.parse(Dry.toObject(jelle)) | ||
``` | ||
This is 14x slower than using `clone`, because `toObject` needs to generate paths, escape certain string values and create wrapper objects. These expensive things can be ignored when cloning: | ||
```js | ||
var cloned = Dry.clone(jelle); | ||
``` | ||
### Clone methods | ||
If you've added a `toDry` and `unDry` method to your class, by default the `clone` method will use those to create the clone. | ||
However, you can also create another method that gets precedence: | ||
#### dryClone | ||
```js | ||
Person.prototype.dryClone = function dryClone(seen_map, custom_method) { | ||
return new Person({ | ||
firstname : this.firstname, | ||
lastname : this.lastname | ||
}); | ||
} | ||
``` | ||
#### Custom clone methods | ||
The `clone` method takes an extra parameter called `custom_method`. If you're cloning something that has a function property with the same name, that'll be used. | ||
This can be used when you want to redact certain parts, for example: | ||
```js | ||
Person.prototype.specialOccasionClone = function specialOccasionClone(seen_map, custom_method) { | ||
return new Person({ | ||
firstname : this.firstname[0] + '.', // Only add the first letter of the name | ||
lastname : this.lastname | ||
}); | ||
}; | ||
var special_clone = Dry.clone(jelle, 'specialOccasionClone'); | ||
special_clone.fullname(); | ||
// Returns "J. De Loecker" | ||
``` |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
30143
6
836
1
195
3
1