json-dry
Advanced tools
Comparing version 1.0.10 to 1.0.11
@@ -0,1 +1,7 @@ | ||
## 1.0.11 (2019-11-22) | ||
* Split up `dryReplacer` function, added `replaceObject` function | ||
* Don't assign regenerated value if it's exactly the same, prevents HTMLCollection errors | ||
* Fix throwing an error when serializing an invalid date | ||
## 1.0.10 (2019-01-31) | ||
@@ -2,0 +8,0 @@ |
@@ -28,3 +28,3 @@ "use strict"; | ||
seen_path, | ||
is_root = true, | ||
flags = {is_root: true}, | ||
chain = [], | ||
@@ -38,11 +38,2 @@ path = [], | ||
var class_name, | ||
new_value, | ||
replaced, | ||
is_array, | ||
is_wrap, | ||
keys, | ||
key, | ||
i; | ||
// Process the value to a possible given replacer function | ||
@@ -58,2 +49,4 @@ if (replacer != null) { | ||
let is_wrap; | ||
// An explicitly false key means this dryReplaced was | ||
@@ -85,213 +78,266 @@ // recursively called with a replacement object | ||
case 'object': | ||
value = replaceObject(dryReplacer, value, chain, flags, is_wrap, holder, path, value_paths, key); | ||
break; | ||
if (value.constructor) { | ||
class_name = value.constructor.name; | ||
} else { | ||
class_name = 'Object'; | ||
case 'string': | ||
// Make sure regular strings don't start with the path delimiter | ||
if (!flags.is_root && value[0] == '~') { | ||
value = safe_special_char + value.slice(1); | ||
} | ||
// See if the chain needs popping | ||
while (len = chain.length) { | ||
break; | ||
// 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(); | ||
} | ||
case 'number': | ||
// Allow infinite values | ||
if (value && !isFinite(value)) { | ||
if (value > 0) { | ||
value = {dry: '+Infinity'}; | ||
} else { | ||
break; | ||
value = {dry: '-Infinity'}; | ||
} | ||
} | ||
break; | ||
} | ||
// Has the object been seen before? | ||
seen_path = value_paths.get(value); | ||
return value; | ||
} | ||
} | ||
if (seen_path) { | ||
/** | ||
* Actually replace the object | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.0 | ||
* @version 1.0.11 | ||
*/ | ||
function replaceObject(dryReplacer, value, chain, flags, is_wrap, holder, path, value_paths, key) { | ||
// If the path is still an array, | ||
// turn it into a string now | ||
if (typeof seen_path != 'string') { | ||
var class_name, | ||
seen_path, | ||
new_value, | ||
replaced, | ||
is_array, | ||
keys, | ||
last, | ||
temp, | ||
len, | ||
i; | ||
// 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); | ||
} | ||
} | ||
if (typeof value.constructor == 'function') { | ||
class_name = value.constructor.name; | ||
} else { | ||
class_name = 'Object'; | ||
} | ||
seen_path = special_char + seen_path.join(special_char); | ||
value_paths.set(value, seen_path); | ||
} | ||
// See if the chain needs popping | ||
while (len = chain.length) { | ||
// Replace the value with the path | ||
new_value = seen_path; | ||
// 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]) { | ||
// See if the new path is shorter | ||
len = 1; | ||
for (i = 0; i < path.length; i++) { | ||
len += 1 + path[i].length; | ||
} | ||
len += key.length; | ||
last = chain.pop(); | ||
if (len < seen_path.length) { | ||
temp = seen_path; | ||
seen_path = path.slice(0); | ||
// Only pop the path if the popped object isn't a wrapper | ||
if (last && !last.__is_wrap) { | ||
path.pop(); | ||
} | ||
} else { | ||
break; | ||
} | ||
} | ||
// The key of the current value still needs to be added | ||
seen_path.push(key); | ||
// Has the object been seen before? | ||
seen_path = value_paths.get(value); | ||
// 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); | ||
} | ||
} | ||
if (seen_path) { | ||
seen_path = special_char + seen_path.join(special_char); | ||
value_paths.set(value, seen_path); | ||
// If the path is still an array, | ||
// turn it into a string now | ||
if (typeof seen_path != 'string') { | ||
// This entry still has to refer to the longer path, | ||
// otherwise it'll refer to itself | ||
seen_path = temp; | ||
} | ||
// 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); | ||
} | ||
} | ||
value = new_value; | ||
seen_path = special_char + seen_path.join(special_char); | ||
value_paths.set(value, seen_path); | ||
} | ||
break; | ||
} | ||
// Replace the value with the path | ||
new_value = seen_path; | ||
if (!is_root && !is_wrap) { | ||
path.push(key); | ||
} else { | ||
is_root = false; | ||
} | ||
// See if the new path is shorter | ||
len = 1; | ||
for (i = 0; i < path.length; i++) { | ||
len += 1 + path[i].length; | ||
} | ||
len += key.length; | ||
// Make a copy of the current path array | ||
value_paths.set(value, path.slice(0)); | ||
if (len < seen_path.length) { | ||
temp = seen_path; | ||
seen_path = path.slice(0); | ||
if (driers[class_name] != null) { | ||
value = driers[class_name].fnc(holder, key, value); | ||
// The key of the current value still needs to be added | ||
seen_path.push(key); | ||
value = { | ||
dry: class_name, | ||
value: value | ||
}; | ||
// 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); | ||
} | ||
} | ||
if (driers[class_name].options.add_path !== false) { | ||
value.drypath = path.slice(0); | ||
} | ||
seen_path = special_char + seen_path.join(special_char); | ||
value_paths.set(value, seen_path); | ||
replaced = {'': value}; | ||
} else if (class_name == 'RegExp' && value.constructor == RegExp) { | ||
value = {dry: 'regexp', value: value.toString()}; | ||
replaced = {'': value}; | ||
} else if (class_name == 'Date' && value.constructor == Date) { | ||
value = {dry: 'date', value: value.toISOString()}; | ||
replaced = {'': value}; | ||
} else if (typeof value.toDry === 'function') { | ||
temp = value; | ||
value = value.toDry(); | ||
// This entry still has to refer to the longer path, | ||
// otherwise it'll refer to itself | ||
seen_path = temp; | ||
} | ||
// 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; | ||
} | ||
value = new_value; | ||
if (!value.dry_class) { | ||
value.dry_class = temp.constructor.name; | ||
} | ||
} | ||
} | ||
return value; | ||
} | ||
value.dry = 'toDry'; | ||
value.drypath = path.slice(0); | ||
replaced = {'': value}; | ||
} else if (typeof value.toJSON === 'function') { | ||
value = value.toJSON(); | ||
replaced = {'': value}; | ||
} else { | ||
is_array = Array.isArray(value); | ||
} | ||
if (!flags.is_root && !is_wrap) { | ||
path.push(key); | ||
} else { | ||
flags.is_root = false; | ||
} | ||
if (replaced) { | ||
// Push the replaced object on the chain | ||
chain.push(replaced); | ||
// Make a copy of the current path array | ||
value_paths.set(value, path.slice(0)); | ||
// Jsonify the replaced object | ||
value = dryReplacer(replaced, false, replaced['']); | ||
if (driers[class_name] != null) { | ||
value = driers[class_name].fnc(holder, key, value); | ||
// 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(); | ||
value = { | ||
dry: class_name, | ||
value: value | ||
}; | ||
// 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(); | ||
} | ||
if (driers[class_name].options.add_path !== false) { | ||
value.drypath = path.slice(0); | ||
} | ||
// Break out of the switch | ||
break; | ||
} | ||
replaced = {'': value}; | ||
} else if (class_name === 'RegExp' && value.constructor == RegExp) { | ||
value = {dry: 'regexp', value: value.toString()}; | ||
replaced = {'': value}; | ||
} else if (class_name === 'Date' && value.constructor == Date) { | ||
// Push this object on the chain | ||
chain.push(value); | ||
// Get numeric value first | ||
temp = value.valueOf(); | ||
if (is_array) { | ||
new_value = []; | ||
if (isNaN(temp)) { | ||
temp = 'invalid'; | ||
} else { | ||
temp = value.toISOString(); | ||
} | ||
for (i = 0; i < value.length; i++) { | ||
new_value[i] = dryReplacer(value, String(i), value[i]); | ||
} | ||
} else { | ||
new_value = {}; | ||
keys = Object.keys(value); | ||
value = {dry: 'date', value: temp}; | ||
replaced = {'': value}; | ||
} else if (typeof value.toDry === 'function') { | ||
temp = value; | ||
value = value.toDry(); | ||
for (i = 0; i < keys.length; i++) { | ||
key = keys[i]; | ||
new_value[key] = dryReplacer(value, key, value[key]); | ||
} | ||
// 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; | ||
} | ||
value = new_value; | ||
if (!value.dry_class) { | ||
value.dry_class = temp.constructor.name; | ||
} | ||
} | ||
} | ||
break; | ||
value.dry = 'toDry'; | ||
value.drypath = path.slice(0); | ||
replaced = {'': value}; | ||
} else if (typeof value.toJSON === 'function') { | ||
value = value.toJSON(); | ||
replaced = {'': value}; | ||
} else { | ||
is_array = Array.isArray(value); | ||
} | ||
case 'string': | ||
if (replaced) { | ||
// Push the replaced object on the chain | ||
chain.push(replaced); | ||
// Make sure regular strings don't start with the path delimiter | ||
if (!is_root && value[0] == '~') { | ||
value = safe_special_char + value.slice(1); | ||
} | ||
// Jsonify the replaced object | ||
value = dryReplacer(replaced, false, replaced['']); | ||
break; | ||
// 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(); | ||
case 'number': | ||
// Allow infinite values | ||
if (!isFinite(value)) { | ||
if (value > 0) { | ||
value = {dry: '+Infinity'}; | ||
} else { | ||
value = {dry: '-Infinity'}; | ||
} | ||
} | ||
break; | ||
// 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)) { | ||
path.pop(); | ||
} | ||
// Break out of the switch | ||
return value; | ||
} | ||
// 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 = recurseGeneralObject(dryReplacer, value); | ||
} | ||
value = new_value; | ||
return value; | ||
} | ||
/** | ||
* Recursively replace the given regular object | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 1.0.11 | ||
* @version 1.0.11 | ||
* | ||
* @param {Function} dryReplacer | ||
* @param {Object} value | ||
* | ||
* @return {Object} | ||
*/ | ||
function recurseGeneralObject(dryReplacer, value) { | ||
var new_value = {}, | ||
keys = Object.keys(value), | ||
key, | ||
i; | ||
for (i = 0; i < keys.length; i++) { | ||
key = keys[i]; | ||
new_value[key] = dryReplacer(value, key, value[key]); | ||
} | ||
return new_value; | ||
} | ||
/** | ||
* Generate reviver function | ||
@@ -457,2 +503,3 @@ * | ||
temp, | ||
key, | ||
len, | ||
@@ -474,3 +521,4 @@ i; | ||
for (i = 0; i < len; i++) { | ||
entry = obj[keys[i]]; | ||
key = keys[i]; | ||
entry = obj[key]; | ||
entry_type = typeof entry; | ||
@@ -486,3 +534,3 @@ | ||
if (wm.has(entry)) { | ||
target[keys[i]] = wm.get(entry); | ||
target[key] = wm.get(entry); | ||
continue; | ||
@@ -495,15 +543,15 @@ } | ||
if (custom_method && entry[custom_method]) { | ||
target[keys[i]] = entry[custom_method].apply(entry, extra_args); | ||
target[key] = entry[custom_method].apply(entry, extra_args); | ||
} else if (driers[name_type] != null) { | ||
// Look for a registered drier function | ||
temp = driers[name_type].fnc(obj, keys[i], entry); | ||
temp = driers[name_type].fnc(obj, key, entry); | ||
if (undriers[name_type]) { | ||
target[keys[i]] = undriers[name_type].fnc(target, keys[i], temp); | ||
target[key] = undriers[name_type].fnc(target, key, temp); | ||
} else { | ||
target[keys[i]] = temp; | ||
target[key] = temp; | ||
} | ||
} else if (entry.dryClone) { | ||
// Look for dryClone after | ||
target[keys[i]] = entry.dryClone(wm, custom_method); | ||
target[key] = entry.dryClone(wm, custom_method); | ||
} else if (entry.toDry) { | ||
@@ -519,9 +567,9 @@ // Perform the toDry function | ||
if (entry.constructor.unDry) { | ||
target[keys[i]] = entry.constructor.unDry(temp, custom_method || true); | ||
target[key] = entry.constructor.unDry(temp, custom_method || true); | ||
} else { | ||
// If there is no undry function, the clone will be a simple object | ||
target[keys[i]] = temp; | ||
target[key] = temp; | ||
} | ||
} else if (name_type == 'Date') { | ||
target[keys[i]] = new Date(entry); | ||
target[key] = new Date(entry); | ||
} else if (name_type == 'RegExp') { | ||
@@ -532,9 +580,9 @@ temp = entry.toString(); | ||
if (split) { | ||
target[keys[i]] = RegExp(split[1], split[2]); | ||
target[key] = RegExp(split[1], split[2]); | ||
} else { | ||
target[keys[i]] = RegExp(temp); | ||
target[key] = RegExp(temp); | ||
} | ||
} else if (typeof entry.clone == 'function') { | ||
// If it supplies a clone method, use that | ||
target[keys[i]] = entry.clone(); | ||
target[key] = entry.clone(); | ||
} else if (entry.toJSON) { | ||
@@ -547,14 +595,14 @@ temp = entry.toJSON(); | ||
target[keys[i]] = temp; | ||
target[key] = temp; | ||
} else { | ||
target[keys[i]] = real_clone(entry, custom_method, extra_args, wm); | ||
target[key] = real_clone(entry, custom_method, extra_args, wm); | ||
} | ||
} else { | ||
target[keys[i]] = real_clone(entry, custom_method, extra_args, wm); | ||
target[key] = real_clone(entry, custom_method, extra_args, wm); | ||
} | ||
// Remember this clone for later | ||
wm.set(entry, target[keys[i]]); | ||
wm.set(entry, target[key]); | ||
} else { | ||
target[keys[i]] = entry; | ||
target[key] = entry; | ||
} | ||
@@ -706,2 +754,6 @@ } | ||
if (!constructor) { | ||
console.log('Could not find constructor for', value); | ||
} | ||
return constructor; | ||
@@ -744,3 +796,3 @@ } | ||
* @since 0.1.4 | ||
* @version 1.0.8 | ||
* @version 1.0.11 | ||
* | ||
@@ -751,3 +803,4 @@ * @return {Object} | ||
var temp, | ||
var path, | ||
temp, | ||
key; | ||
@@ -759,6 +812,16 @@ | ||
if (!seen.get(current[key])) { | ||
temp = current_path.slice(0); | ||
temp.push(key); | ||
path = current_path.slice(0); | ||
path.push(key); | ||
current[key] = regenerate(root, current, current[key], seen, retrieve, undry_paths, old, temp); | ||
temp = regenerate(root, current, current[key], seen, retrieve, undry_paths, old, path); | ||
// @TODO: Values returned by `unDry` methods also get regenerated, | ||
// even though these could contain properties coming from somewhere else, | ||
// like live HTMLCollections. Assigning anything to that will throw an error. | ||
// This is a workaround to that proble: if the value is exactly the same, | ||
// it's not needed to assign it again, so it won't throw an error, | ||
// but it's not an ideal solution. | ||
if (temp !== current[key]) { | ||
current[key] = temp; | ||
} | ||
} | ||
@@ -1068,3 +1131,3 @@ } | ||
*/ | ||
function walk(obj, fnc, result) { | ||
function walk(obj, fnc, result, seen) { | ||
@@ -1077,2 +1140,8 @@ var is_root, | ||
if (!seen) { | ||
seen = new WeakMap(); | ||
} | ||
seen.set(obj, true); | ||
if (!result) { | ||
@@ -1094,7 +1163,11 @@ is_root = true; | ||
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, {}); | ||
if (!seen.get(obj[key])) { | ||
if (Array.isArray(obj[key])) { | ||
result[key] = walk(obj[key], fnc, [], seen); | ||
} else { | ||
result[key] = walk(obj[key], fnc, {}, seen); | ||
} | ||
} | ||
result[key] = fnc(key, result[key], result); | ||
@@ -1119,3 +1192,3 @@ } else { | ||
* @since 1.0.0 | ||
* @version 1.0.4 | ||
* @version 1.0.11 | ||
* | ||
@@ -1138,3 +1211,4 @@ * @param {Object} value | ||
key, | ||
old = {}; | ||
old = {}, | ||
i; | ||
@@ -1190,3 +1264,3 @@ // Create the reviver function | ||
for (var i = 0; i < undry_paths.extra_pass.length; i++) { | ||
for (i = 0; i < undry_paths.extra_pass.length; i++) { | ||
entry = undry_paths.extra_pass[i]; | ||
@@ -1193,0 +1267,0 @@ holder = entry[0]; |
{ | ||
"name": "json-dry", | ||
"description": "Don't repeat yourself, JSON: Add support for (circular) references, class instances, ...", | ||
"version": "1.0.10", | ||
"version": "1.0.11", | ||
"author": "Jelle De Loecker <jelle@develry.be>", | ||
@@ -6,0 +6,0 @@ "keywords": [ |
41501
1070