Comparing version 1.1.1 to 2.0.0
@@ -0,1 +1,6 @@ | ||
## 2.0.0 (2023-01-14) | ||
* Rewrite serialization & parser logic | ||
* Store any value that is referred to more than once in a separate root property | ||
## 1.1.1 (2022-08-25) | ||
@@ -2,0 +7,0 @@ |
1622
lib/json-dry.js
"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 = {}; | ||
const GET_REGEX = /^\/(.*)\/(.*)/, | ||
UNDRIERS = {}, | ||
DRIERS = {}, | ||
REFS = '~refs', | ||
ROOT = '~root'; | ||
let global_this; | ||
/** | ||
* Represent a value | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
class Value { | ||
if (typeof globalThis != 'undefined') { | ||
global_this = globalThis; | ||
} else if (typeof window != 'undefined') { | ||
global_this = window; | ||
} else if (typeof global != 'undefined') { | ||
global_this = global; | ||
} else { | ||
global_this = {}; | ||
/** | ||
* Construct the value instance | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
* | ||
* @param {Object} holder The object that holds the value | ||
* @param {String} key The key this value is held under | ||
* @param {*} value The actual value to replace | ||
* @param {Value} parent The optional parent Value instance | ||
*/ | ||
constructor(holder, key, value, parent) { | ||
this.holder = holder; | ||
this.key = key; | ||
this.value = value; | ||
this.parent = parent; | ||
if (parent) { | ||
parent.values.set(value, this); | ||
} | ||
} | ||
/** | ||
* Return the dried representation of this value. | ||
* Calling this more than once will make it return a reference. | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
driedValue() { | ||
if (!this.is_processed) { | ||
this.is_processed = true; | ||
this.replaced_value = this.initialReplace(this.value); | ||
if (this.is_registered) { | ||
this.getReference(); | ||
} | ||
} else { | ||
if (!this.ref_count) { | ||
this.ref_count = 0; | ||
} | ||
this.ref_count++; | ||
return this.getReference(); | ||
} | ||
return this.replaced_value; | ||
} | ||
/** | ||
* Default replace behaviour | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
initialReplace() { | ||
return this.value; | ||
} | ||
/** | ||
* Get a reference to this value | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
* | ||
* @return {Object} | ||
*/ | ||
getReference() { | ||
if (!this.is_registered) { | ||
this.id = this.root.duplicates.push(this) - 1; | ||
this.is_registered = true; | ||
this.reference = {'~r': this.id}; | ||
} | ||
// Replace the first one with the reference too | ||
if (!this.replaced_first_instance && this.parent && this.parent.replaced_value) { | ||
this.parent.replaced_value[this.key] = this.reference; | ||
this.replaced_first_instance = true; | ||
} | ||
return this.reference; | ||
} | ||
/** | ||
* Return the undried version of this value | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
* | ||
* @param {Object} holder The holder object the undried value will be put in | ||
* @param {String|Number} key The property key the undried value will be put in | ||
* | ||
* @return {*} | ||
*/ | ||
undriedValue(holder, key) { | ||
if (!this.is_processed) { | ||
this.is_processed = true; | ||
this.replaced_value = this.initialRevive(this.value); | ||
} | ||
return this.replaced_value; | ||
} | ||
/** | ||
* Default revive behaviour | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
initialRevive() { | ||
return this.value; | ||
} | ||
/** | ||
* Get the root `values` array | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
get values() { | ||
return this.parent.values; | ||
} | ||
/** | ||
* Get the `RootValue` instance | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
get root() { | ||
return this.parent.root; | ||
} | ||
} | ||
/** | ||
* Generate a replacer function | ||
* Represent the root | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.0 | ||
* @version 1.0.1 | ||
* | ||
* @param {Object} root | ||
* @param {Function} replacer | ||
* | ||
* @return {Function} | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
function createDryReplacer(root, replacer) { | ||
class RootValue { | ||
values = new Map(); | ||
has_replacer = false; | ||
current_type = undefined; | ||
duplicates = []; | ||
var value_paths = new WeakMap(), | ||
seen_path, | ||
flags = {is_root: true}, | ||
chain = [], | ||
path = [], | ||
temp, | ||
last, | ||
len; | ||
/** | ||
* Construct the root instance | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
* | ||
* @param {*} root The actual value that needs to be dried | ||
* @param {Function} replacer Optional replacer function | ||
*/ | ||
constructor(root, replacer) { | ||
this.value = root; | ||
this.root = this; | ||
return function dryReplacer(holder, key, value) { | ||
if (replacer) { | ||
this.replacer = replacer; | ||
this.has_replacer = true; | ||
} | ||
// Process the value to a possible given replacer function | ||
if (replacer != null) { | ||
value = replacer.call(holder, key, value); | ||
this.values.set(root, this); | ||
} | ||
/** | ||
* Replace the given value | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.1.0 | ||
* @version 2.0.0 | ||
* | ||
* @param {Object} holder The object that holds the value | ||
* @param {String} key The key this value is held under | ||
* @param {*} value The actual value to replace | ||
*/ | ||
replace(holder, key, value, parent) { | ||
if (this.has_replacer) { | ||
value = this.replacer(key, value); | ||
} | ||
@@ -59,395 +221,774 @@ | ||
let is_wrap; | ||
switch (typeof value) { | ||
case 'number': | ||
if (!isFinite(value)) { | ||
break; | ||
} | ||
case 'boolean': | ||
return value; | ||
} | ||
// An explicitly false key means this dryReplaced was | ||
// recursively called with a replacement object | ||
if (key === false) { | ||
key = ''; | ||
is_wrap = true; | ||
let result = this.values.get(value); | ||
// 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; | ||
if (!result) { | ||
result = this.createValueWrapper(holder, key, value, parent); | ||
} | ||
// See if the wrapped value is an object | ||
if (holder[''] != null && typeof holder[''] === 'object') { | ||
holder.__is_object = true; | ||
return result.driedValue(); | ||
} | ||
/** | ||
* Revive the given value | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
* | ||
* @param {Object} original_holder The object that holds the value | ||
* @param {String} key The key this value is held under | ||
* @param {*} value The actual value to replace | ||
* @param {Value} parent | ||
* @param {Object} new_holder | ||
*/ | ||
revive(original_holder, key, value, parent, new_holder) { | ||
let result; | ||
// Only check non-falsy values | ||
if (value) { | ||
let type = typeof value; | ||
if (type == 'number' || type == 'boolean' || type == 'string') { | ||
// Primitives don't need special reviving | ||
result = value; | ||
} else { | ||
result = this.createReviverValueWrapper(original_holder, key, value, parent); | ||
result = result.undriedValue(new_holder, key); | ||
} | ||
} else { | ||
result = value; | ||
} | ||
switch (typeof value) { | ||
// Make sure the "replacer" (in this case, a "reviver") is executed | ||
if (this.has_replacer) { | ||
result = this.replacer(key, result); | ||
} | ||
case 'function': | ||
// If no drier is created, return now. | ||
// Else: fall through | ||
if (!driers.Function) { | ||
return; | ||
} | ||
return result; | ||
} | ||
case 'object': | ||
value = replaceObject(dryReplacer, value, chain, flags, is_wrap, holder, path, value_paths, key); | ||
break; | ||
/** | ||
* Create a reviver wrapper | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
* | ||
* @param {Object} holder The object that holds the value | ||
* @param {String} key The key this value is held under | ||
* @param {*} value The actual value to replace | ||
*/ | ||
createReviverValueWrapper(holder, key, value, parent) { | ||
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); | ||
} | ||
if (!value) { | ||
return new Value(holder, key, value, parent); | ||
} | ||
break; | ||
let type = typeof value; | ||
case 'number': | ||
// Allow infinite values | ||
if (value && !isFinite(value)) { | ||
if (value > 0) { | ||
value = {dry: '+Infinity'}; | ||
} else { | ||
value = {dry: '-Infinity'}; | ||
} | ||
let result; | ||
if (type == 'object') { | ||
let dry_type = value.dry, | ||
ref = value['~r']; | ||
if (typeof ref == 'number') { | ||
result = new RefValue(holder, key, value, parent); | ||
} else if (typeof dry_type == 'string') { | ||
if (dry_type == 'root') { | ||
result = this; | ||
} else if (dry_type == 'toDry') { | ||
result = new CustomReviverValue(holder, key, value, parent); | ||
} else if (UNDRIERS[dry_type]) { | ||
result = new UndrierValue(holder, key, value, parent); | ||
} else if (dry_type === '+Infinity') { | ||
return new NumberValue(holder, key, Infinity, parent); | ||
} else if (dry_type === '-Infinity') { | ||
return new NumberValue(holder, key, -Infinity, parent); | ||
} else if (dry_type === 'regexp') { | ||
return new RegExpValue(holder, key, value, parent); | ||
} else if (dry_type === 'date') { | ||
return new DateValue(holder, key, value, parent); | ||
} else if (dry_type === 'escape') { | ||
return new EscapedObjectValue(holder, key, value.value, parent); | ||
} else { | ||
result = new UnknownUndrierValue(holder, key, value, parent); | ||
} | ||
break; | ||
} | ||
} | ||
return value; | ||
if (!result) { | ||
result = this.createValueWrapper(holder, key, value, parent); | ||
} | ||
return result; | ||
} | ||
} | ||
/** | ||
* 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) { | ||
/** | ||
* Create a value wrapper | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
* | ||
* @param {Object} holder The object that holds the value | ||
* @param {String} key The key this value is held under | ||
* @param {*} value The actual value to replace | ||
*/ | ||
createValueWrapper(holder, key, value, parent) { | ||
var class_name, | ||
seen_path, | ||
new_value, | ||
replaced, | ||
is_array, | ||
keys, | ||
last, | ||
temp, | ||
len, | ||
i; | ||
let current_type = typeof value; | ||
if (typeof value.constructor == 'function') { | ||
class_name = value.constructor.name; | ||
} else { | ||
class_name = 'Object'; | ||
switch (current_type) { | ||
case 'function': | ||
return new FunctionValue(holder, key, value, parent); | ||
case 'object': | ||
if (Array.isArray(value)) { | ||
return new ArrayValue(holder, key, value, parent); | ||
} else { | ||
return new ObjectValue(holder, key, value, parent); | ||
} | ||
case 'string': | ||
return new StringValue(holder, key, value, parent); | ||
case 'number': | ||
return new NumberValue(holder, key, value, parent); | ||
default: | ||
return new Value(holder, key, value, parent); | ||
} | ||
} | ||
// See if the chain needs popping | ||
while (len = chain.length) { | ||
/** | ||
* Return the dried representation of this root value | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
driedValue() { | ||
// 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]) { | ||
let $root; | ||
last = chain.pop(); | ||
if (!this.is_processed) { | ||
this.is_processed = true; | ||
this.real_root = this.createValueWrapper(null, null, this.value, this); | ||
this.values.set(this.value, this); | ||
// Only pop the path if the popped object isn't a wrapper | ||
if (last && !last.__is_wrap) { | ||
path.pop(); | ||
// Trigger the initial drying | ||
$root = this.real_root.driedValue(); | ||
} else { | ||
if (!this.ref_count) { | ||
this.ref_count = 0; | ||
} | ||
this.ref_count++; | ||
return this.getReference(); | ||
} | ||
let duplicate, | ||
refs = [], | ||
i; | ||
for (i = 0; i < this.duplicates.length; i++) { | ||
duplicate = this.duplicates[i]; | ||
refs.push(duplicate.replaced_value); | ||
} | ||
let result; | ||
if (refs.length > 0) { | ||
result = { | ||
[REFS]: refs, | ||
[ROOT]: $root, | ||
}; | ||
} else { | ||
break; | ||
result = $root; | ||
} | ||
this.replaced_value = result; | ||
return this.replaced_value; | ||
} | ||
// Has the object been seen before? | ||
seen_path = value_paths.get(value); | ||
/** | ||
* Return the reference object to this root value | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
getReference() { | ||
return {'~r': -1}; | ||
} | ||
if (seen_path) { | ||
/** | ||
* Return the undried value of this root | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
undriedValue(holder, key) { | ||
// If the path is still an array, | ||
// turn it into a string now | ||
if (typeof seen_path != 'string') { | ||
if (!this.is_processed) { | ||
// 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); | ||
let value = this.value; | ||
if (!value) { | ||
return value; | ||
} | ||
let type = typeof value; | ||
switch (type) { | ||
case 'string': | ||
case 'number': | ||
case 'boolean': | ||
return value; | ||
}; | ||
this.is_busy = true; | ||
this.is_processed = true; | ||
if (value[REFS] && value[ROOT]) { | ||
let original_refs = value[REFS], | ||
length = original_refs.length, | ||
$refs = [], | ||
i; | ||
for (i = 0; i < length; i++) { | ||
$refs.push(this.createReviverValueWrapper(null, null, original_refs[i], this)); | ||
} | ||
this.duplicates = $refs; | ||
this.value = value = value[ROOT]; | ||
} | ||
seen_path = special_char + seen_path.join(special_char); | ||
value_paths.set(value, seen_path); | ||
this.real_root = this.createReviverValueWrapper(null, null, value, this); | ||
this.values.set(value, this); | ||
this.replaced_value = this.real_root.undriedValue(holder, key); | ||
if (this.when_done) { | ||
while (this.when_done.length) { | ||
this.when_done.shift()(); | ||
} | ||
} | ||
this.is_busy = false; | ||
} | ||
// Replace the value with the path | ||
new_value = seen_path; | ||
return this.real_root.undriedValue(holder, key); | ||
} | ||
// See if the new path is shorter | ||
len = 1; | ||
for (i = 0; i < path.length; i++) { | ||
len += 1 + path[i].length; | ||
/** | ||
* Get the when-done scheduler | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
getWhenDoneScheduler() { | ||
if (!this.when_done_scheduler) { | ||
this.when_done = []; | ||
this.when_done_scheduler = (fnc) => this.when_done.push(fnc); | ||
} | ||
len += key.length; | ||
return this.when_done_scheduler; | ||
} | ||
} | ||
if (len < seen_path.length) { | ||
temp = seen_path; | ||
seen_path = path.slice(0); | ||
/** | ||
* Represent an object value | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
class ObjectValue extends Value { | ||
// The key of the current value still needs to be added | ||
seen_path.push(key); | ||
initialReplace(value) { | ||
let replace_again = false, | ||
class_name, | ||
result; | ||
// 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'; | ||
} | ||
if (DRIERS[class_name] != null) { | ||
result = DRIERS[class_name].fnc(this.holder, this.key, value); | ||
result = { | ||
dry : class_name, | ||
value : result, | ||
}; | ||
replace_again = true; | ||
} else if (class_name === 'RegExp' && value.constructor == RegExp) { | ||
result = {dry: 'regexp', value: value.toString()}; | ||
} else if (class_name === 'Date' && value.constructor == Date) { | ||
// Get numeric value first | ||
let temp = value.valueOf(); | ||
if (isNaN(temp)) { | ||
temp = 'invalid'; | ||
} else { | ||
temp = value.toISOString(); | ||
} | ||
result = {dry: 'date', value: temp}; | ||
} else if (typeof value.toDry === 'function') { | ||
let temp = value; | ||
value = value.toDry(); | ||
// 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'; | ||
result = value; | ||
replace_again = true; | ||
} else if (typeof value.toJSON === 'function') { | ||
result = value.toJSON(); | ||
replace_again = true; | ||
} else { | ||
result = this.simpleReplace(value); | ||
} | ||
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; | ||
if (replace_again && result) { | ||
result = this.root.replace(this.holder, this.key, result, this.parent); | ||
} | ||
value = new_value; | ||
this.replaced_value = result; | ||
return value; | ||
return result; | ||
} | ||
if (!flags.is_root && !is_wrap) { | ||
path.push(key); | ||
} else { | ||
flags.is_root = false; | ||
} | ||
simpleReplace(value) { | ||
// Make a copy of the current path array | ||
value_paths.set(value, path.slice(0)); | ||
let needs_escape = false, | ||
new_value = {}, | ||
target_key, | ||
keys = Object.keys(value), | ||
key, | ||
i; | ||
if (driers[class_name] != null) { | ||
value = driers[class_name].fnc(holder, key, value); | ||
this.replaced_value = new_value; | ||
value = { | ||
dry: class_name, | ||
value: value | ||
}; | ||
for (i = 0; i < keys.length; i++) { | ||
target_key = key = keys[i]; | ||
if (driers[class_name].options.add_path !== false) { | ||
value.drypath = path.slice(0); | ||
if (key == '~r') { | ||
needs_escape = true; | ||
target_key = '~~r'; | ||
} | ||
new_value[target_key] = this.root.replace(value, key, value[key], this); | ||
} | ||
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) { | ||
if (needs_escape) { | ||
new_value = {dry: 'escape', value: new_value}; | ||
} | ||
// Get numeric value first | ||
temp = value.valueOf(); | ||
return new_value; | ||
} | ||
if (isNaN(temp)) { | ||
temp = 'invalid'; | ||
} else { | ||
temp = value.toISOString(); | ||
initialRevive(value) { | ||
let new_value = {}, | ||
revived, | ||
keys = Object.keys(value), | ||
key, | ||
i; | ||
this.replaced_value = new_value; | ||
for (i = 0; i < keys.length; i++) { | ||
key = keys[i]; | ||
revived = this.root.revive(value, key, value[key], this, new_value); | ||
if (!new_value.hasOwnProperty(key)) { | ||
new_value[key] = revived; | ||
} | ||
} | ||
value = {dry: 'date', value: temp}; | ||
replaced = {'': value}; | ||
} else if (typeof value.toDry === 'function') { | ||
temp = value; | ||
value = value.toDry(); | ||
return new_value; | ||
} | ||
} | ||
// 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; | ||
/** | ||
* Represent an object value that has values that need to be escaped | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
class EscapedObjectValue extends ObjectValue { | ||
initialRevive(value) { | ||
let new_value = {}, | ||
revived, | ||
keys = Object.keys(value), | ||
key, | ||
i; | ||
this.replaced_value = new_value; | ||
for (i = 0; i < keys.length; i++) { | ||
key = keys[i]; | ||
revived = this.root.revive(value, key, value[key], this, new_value); | ||
if (!new_value.hasOwnProperty(key)) { | ||
if (key === '~~r') { | ||
key = '~r'; | ||
} | ||
if (!value.dry_class) { | ||
value.dry_class = temp.constructor.name; | ||
} | ||
new_value[key] = revived; | ||
} | ||
} | ||
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); | ||
return new_value; | ||
} | ||
if (replaced) { | ||
// Push the replaced object on the chain | ||
chain.push(replaced); | ||
} | ||
// Jsonify the replaced object | ||
value = dryReplacer(replaced, false, replaced['']); | ||
/** | ||
* Represent an array value | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
class ArrayValue extends ObjectValue { | ||
// 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(); | ||
simpleReplace(value) { | ||
let length = value.length, | ||
new_value = new Array(length), | ||
replaced, | ||
i; | ||
this.replaced_value = new_value; | ||
for (i = 0; i < length; i++) { | ||
replaced = this.root.replace(value, i, value[i], this); | ||
// 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(); | ||
// It's possible that a reference was added instead, | ||
// so check if it's undefined first | ||
if (new_value[i] === undefined) { | ||
new_value[i] = replaced; | ||
} | ||
} | ||
// Break out of the switch | ||
return value; | ||
return new_value; | ||
} | ||
// Push this object on the chain | ||
chain.push(value); | ||
initialRevive(value) { | ||
if (is_array) { | ||
new_value = []; | ||
let new_value = [], | ||
length = value.length, | ||
revived, | ||
i; | ||
this.replaced_value = new_value; | ||
for (i = 0; i < value.length; i++) { | ||
new_value[i] = dryReplacer(value, String(i), value[i]); | ||
for (i = 0; i < length; i++) { | ||
revived = this.root.revive(value, i, value[i], this, new_value); | ||
new_value.push(revived); | ||
} | ||
} else { | ||
new_value = recurseGeneralObject(dryReplacer, value); | ||
return new_value; | ||
} | ||
} | ||
value = new_value; | ||
/** | ||
* Represent a function value | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
class FunctionValue extends Value { | ||
return value; | ||
} | ||
/** | ||
* Recursively replace the given regular object | ||
* Represent a string value | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 1.0.11 | ||
* @version 1.0.11 | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
class StringValue extends Value { | ||
driedValue() { | ||
if (this.value.length < 16) { | ||
return this.value; | ||
} | ||
return super.driedValue(); | ||
} | ||
} | ||
/** | ||
* Represent a number value | ||
* | ||
* @param {Function} dryReplacer | ||
* @param {Object} value | ||
* | ||
* @return {Object} | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
function recurseGeneralObject(dryReplacer, value) { | ||
class NumberValue extends Value { | ||
driedValue() { | ||
let value = this.value; | ||
var new_value = {}, | ||
keys = Object.keys(value), | ||
key, | ||
i; | ||
// Allow infinite values | ||
if (value && !isFinite(value)) { | ||
if (value > 0) { | ||
value = {dry: '+Infinity'}; | ||
} else { | ||
value = {dry: '-Infinity'}; | ||
} | ||
} | ||
for (i = 0; i < keys.length; i++) { | ||
key = keys[i]; | ||
new_value[key] = dryReplacer(value, key, value[key]); | ||
return value; | ||
} | ||
} | ||
return new_value; | ||
/** | ||
* Represent a regexp value | ||
* (For undrying) | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
class RegExpValue extends Value { | ||
initialRevive(value) { | ||
if (value.value) { | ||
return RegExp.apply(undefined, GET_REGEX.exec(value.value).slice(1)); | ||
} | ||
} | ||
} | ||
/** | ||
* Generate reviver function | ||
* Represent a date value | ||
* (For undrying) | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.0 | ||
* @version 1.0.2 | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
class DateValue extends Value { | ||
initialRevive(value) { | ||
if (value.value) { | ||
return new Date(value.value); | ||
} | ||
} | ||
} | ||
/** | ||
* Represent a reference | ||
* (Only used during undrying) | ||
* | ||
* @param {Function} reviver | ||
* @param {Map} undry_paths | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
class RefValue extends Value { | ||
undriedValue(holder, key) { | ||
let index = this.value['~r']; | ||
// "-1" is a reference to the root value | ||
if (index === -1) { | ||
return this.root.undriedValue(holder, key); | ||
} | ||
let value = this.root.duplicates[index]; | ||
if (!value) { | ||
return value; | ||
} | ||
let result = value.undriedValue(holder, key); | ||
return result; | ||
} | ||
} | ||
/** | ||
* Represent a value that needs reviving using registered undriers | ||
* (Only used during undrying) | ||
* | ||
* @return {Function} | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
function generateReviver(reviver, undry_paths) { | ||
class UndrierValue extends Value { | ||
return function dryReviver(key, value) { | ||
undriedValue(holder, key) { | ||
var val_type = typeof value, | ||
constructor, | ||
temp; | ||
if (this.is_busy) { | ||
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); | ||
if (!this.placeholders) { | ||
this.placeholders = []; | ||
} | ||
} else if (value && value.dry != null) { | ||
switch (value.dry) { | ||
let placeholder = Symbol(); | ||
case 'date': | ||
if (value.value) { | ||
return new Date(value.value); | ||
} | ||
break; | ||
this.placeholders.push([holder, key, placeholder]); | ||
case 'regexp': | ||
if (value.value) { | ||
return RegExp.apply(undefined, get_regex.exec(value.value).slice(1)); | ||
} | ||
break; | ||
return placeholder; | ||
} | ||
case '+Infinity': | ||
return Infinity; | ||
return super.undriedValue(holder, key); | ||
} | ||
case '-Infinity': | ||
return -Infinity; | ||
initialRevive(value) { | ||
case 'toDry': | ||
constructor = findClass(value); | ||
this.is_busy = true; | ||
// Undry this element, but don't put it in the parsed object yet | ||
if (constructor && typeof constructor.unDry === 'function') { | ||
value.unDryConstructor = constructor; | ||
} else { | ||
value.undried = value.value; | ||
} | ||
let result = this.reviveWithReviver(value); | ||
if (value.drypath) { | ||
undry_paths.set(value.drypath.join(special_char), value); | ||
} else { | ||
return value.undried; | ||
} | ||
break; | ||
this.is_busy = false; | ||
default: | ||
if (typeof value.value !== 'undefined') { | ||
if (undriers[value.dry]) { | ||
value.unDryFunction = undriers[value.dry].fnc; | ||
if (this.placeholders) { | ||
let placeholder, | ||
holder, | ||
entry, | ||
key; | ||
if (!value.drypath) { | ||
// No path given? Then do the undrying right now | ||
value.undried = value.unDryFunction(this, key, value.value) | ||
} | ||
while (this.placeholders.length) { | ||
entry = this.placeholders.shift(); | ||
} else { | ||
value.undried = value.value; | ||
holder = entry[0]; | ||
key = entry[1]; | ||
placeholder = entry[2]; | ||
holder[key] = result; | ||
if (holder.$replaced) { | ||
let fixer = (key, value, holder) => { | ||
if (value === placeholder) { | ||
holder[key] = result; | ||
} | ||
}; | ||
if (value.drypath) { | ||
undry_paths.set(value.drypath.join(special_char), value); | ||
} else { | ||
return value.undried; | ||
} | ||
// The holder might have already been replaced with something | ||
// else (`$replaced` property), but we can't assume that | ||
// replacement expects the result to be stored under the same key | ||
// So we need to walk over it and check every property | ||
while (holder.$replaced) { | ||
holder = holder.$replaced; | ||
walk(holder, fixer); | ||
} | ||
} | ||
} | ||
} | ||
if (reviver == null) { | ||
return value; | ||
return result; | ||
} | ||
reviveWithReviver(value) { | ||
let undrier = UNDRIERS[value.dry].fnc, | ||
result = this.root.revive(null, null, value.value, this); | ||
if (result) { | ||
let new_result = undrier(this.holder, this.key, result); | ||
if (result && typeof result == 'object') { | ||
result.$replaced = new_result; | ||
} | ||
result = new_result; | ||
} | ||
return reviver.call(this, key, value); | ||
}; | ||
}; | ||
return result; | ||
} | ||
} | ||
/** | ||
* Represent a value that needs reviving using registered undriers, | ||
* but of which we don't have an undrier available | ||
* (Only used during undrying) | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
class UnknownUndrierValue extends UndrierValue { | ||
reviveWithReviver(value) { | ||
return this.root.revive(null, null, value.value, this); | ||
} | ||
} | ||
/** | ||
* Represent a value that needs reviving | ||
* (Only used during undrying) | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 2.0.0 | ||
* @version 2.0.0 | ||
*/ | ||
class CustomReviverValue extends UndrierValue { | ||
reviveWithReviver(value) { | ||
value = (new ObjectValue(this.holder, this.key, value, this)).undriedValue(); | ||
let constructor = findClass(value), | ||
result = value.value; | ||
// Undry this element, but don't put it in the parsed object yet | ||
if (constructor && typeof constructor.unDry === 'function') { | ||
let force = false, | ||
scheduler; | ||
if (constructor.unDry.length > 2) { | ||
scheduler = this.root.getWhenDoneScheduler(); | ||
} | ||
let new_result = constructor.unDry(result, force, scheduler); | ||
result.$replaced = new_result; | ||
result = new_result; | ||
} | ||
return result; | ||
} | ||
} | ||
/** | ||
* Deep clone an object | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 1.0.0 | ||
@@ -525,3 +1066,3 @@ * @version 1.1.0 | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 1.0.0 | ||
@@ -569,3 +1110,3 @@ * @version 1.1.0 | ||
if (entry_type == 'function' && !driers.Function) { | ||
if (entry_type == 'function' && !DRIERS.Function) { | ||
continue; | ||
@@ -585,8 +1126,8 @@ } | ||
target[key] = entry[custom_method].apply(entry, extra_args); | ||
} else if (driers[name_type] != null) { | ||
} else if (DRIERS[name_type] != null) { | ||
// Look for a registered drier function | ||
temp = driers[name_type].fnc(obj, key, entry); | ||
temp = DRIERS[name_type].fnc(obj, key, entry); | ||
if (undriers[name_type]) { | ||
target[key] = undriers[name_type].fnc(target, key, temp); | ||
if (UNDRIERS[name_type]) { | ||
target[key] = UNDRIERS[name_type].fnc(target, key, temp); | ||
} else { | ||
@@ -663,3 +1204,3 @@ target[key] = temp; | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 1.0.0 | ||
@@ -682,3 +1223,3 @@ * @version 1.0.0 | ||
driers[path] = { | ||
DRIERS[path] = { | ||
fnc : fnc, | ||
@@ -692,3 +1233,3 @@ options : options || {} | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 1.0.0 | ||
@@ -711,3 +1252,3 @@ * @version 1.0.0 | ||
undriers[path] = { | ||
UNDRIERS[path] = { | ||
fnc : fnc, | ||
@@ -721,3 +1262,3 @@ options : options || {} | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 1.0.0 | ||
@@ -753,3 +1294,3 @@ * @version 1.0.5 | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 1.0.0 | ||
@@ -814,296 +1355,5 @@ * @version 1.0.9 | ||
/** | ||
* Regenerate an array | ||
* Extract something from an object by the path | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.4 | ||
* @version 1.0.8 | ||
* | ||
* @return {Array} | ||
*/ | ||
function regenerateArray(root, holder, current, seen, retrieve, undry_paths, old, current_path) { | ||
var length = current.length, | ||
temp, | ||
i; | ||
for (i = 0; i < length; i++) { | ||
// Only regenerate if it's not yet seen | ||
if (!seen.get(current[i])) { | ||
temp = current_path.slice(0); | ||
temp.push(i); | ||
current[i] = regenerate(root, current, current[i], seen, retrieve, undry_paths, old, temp); | ||
} | ||
} | ||
return current; | ||
}; | ||
/** | ||
* Regenerate an object | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.4 | ||
* @version 1.1.1 | ||
* | ||
* @return {Object} | ||
*/ | ||
function regenerateObject(root, holder, current, seen, retrieve, undry_paths, old, current_path) { | ||
// Do not regenerate the global object | ||
if (current === global_this) { | ||
return current; | ||
} | ||
let path, | ||
temp, | ||
key; | ||
for (key in current) { | ||
if (current.hasOwnProperty(key)) { | ||
// Only regenerate if it's not already seen | ||
if (!seen.get(current[key])) { | ||
path = current_path.slice(0); | ||
path.push(key); | ||
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]) { | ||
if (current[key] && current[key] instanceof String) { | ||
// String object instances are path/references, | ||
// so we can just overwrite it with the regenerated result | ||
current[key] = temp; | ||
} else { | ||
throw new Error('Failed to regenerate "' + key + '"'); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return current; | ||
}; | ||
/** | ||
* Regenerate a value | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 1.1.1 | ||
* @version 1.1.1 | ||
* | ||
* @param {Object} root The root object being regenerated | ||
* @param {Object} holder The parent object of the current value | ||
* @param {Mixed} current The current value to regenerate | ||
* @param {WeakMap} seen All the (original) values that have already been seen | ||
* @param {Object} retrieve Value of references that have already been regenerated | ||
* @param {Map} undry_paths Regenerated values that still need to be revived with some function | ||
* @param {Object} old Old, unregenerated wrapper entries are stored here in case | ||
* references still exist to its children | ||
* @param {Array} current_path The path of the current value being regenerated | ||
* | ||
* @return {Mixed} | ||
*/ | ||
function regenerate(root, holder, current, seen, retrieve, undry_paths, old, current_path) { | ||
try { | ||
return _regenerate(root, holder, current, seen, retrieve, undry_paths, old, current_path); | ||
} catch (err) { | ||
// This makes sure the initially wrapped error & path is thrown | ||
if (err.is_dry_error) { | ||
throw err; | ||
} | ||
let message = 'Failed to regenerate "' + current_path.join('.') + '": '; | ||
let new_error = new Error(message + err.message); | ||
new_error.code = err.code; | ||
new_error.stack = message + err.stack; | ||
new_error.is_dry_error = true; | ||
new_error.root = root; | ||
new_error.holder = holder; | ||
throw new_error; | ||
} | ||
} | ||
/** | ||
* Regenerate a value | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.4 | ||
* @version 1.0.8 | ||
* | ||
* @return {Mixed} | ||
*/ | ||
function _regenerate(root, holder, current, seen, retrieve, undry_paths, old, current_path) { | ||
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, holder, current, seen, retrieve, undry_paths, old, current_path); | ||
} | ||
if (current instanceof String) { | ||
if (current.length > -1) { | ||
current = current.toString(); | ||
if (temp = undry_paths.get(current)) { | ||
if (typeof temp.undried != 'undefined') { | ||
return temp.undried; | ||
} | ||
if (!holder) { | ||
throw new Error('Unable to resolve recursive reference'); | ||
} | ||
undry_paths.extra_pass.push([holder, temp, current_path]); | ||
return temp; | ||
} | ||
if (retrieve.hasOwnProperty(current)) { | ||
temp = retrieve[current]; | ||
} else { | ||
temp = retrieve[current] = retrieveFromPath(root, current.split(special_char)); | ||
if (typeof temp == 'undefined') { | ||
temp = retrieve[current] = getFromOld(old, current.split(special_char)); | ||
} | ||
} | ||
// Because we always regenerate parsed objects first | ||
// (JSON-dry parsing goes from string » object » regenerated object) | ||
// keys of regular objects can appear out-of-order, so we need to parse them | ||
if (temp && temp instanceof String) { | ||
// Unset the String as a valid result | ||
retrieve[current] = null; | ||
// Regenerate the string again | ||
// (We have to create a new instance, because it's already been "seen") | ||
temp = retrieve[current] = regenerate(root, holder, new String(temp), seen, retrieve, undry_paths, old, current_path); | ||
} | ||
return temp; | ||
} else { | ||
return root; | ||
} | ||
} | ||
return regenerateObject(root, holder, current, seen, retrieve, undry_paths, old, current_path); | ||
} | ||
return current; | ||
}; | ||
/** | ||
* Find path in an "old" object | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 1.0.10 | ||
* @version 1.0.10 | ||
* | ||
* @param {Object} old The object to look in | ||
* @param {Array} pieces The path to look for | ||
* | ||
* @return {Mixed} | ||
*/ | ||
function getFromOld(old, pieces) { | ||
var length = pieces.length, | ||
result, | ||
path, | ||
rest, | ||
i; | ||
for (i = 0; i < length; i++) { | ||
path = pieces.slice(0, length - i).join('.'); | ||
result = old[path]; | ||
if (typeof result != 'undefined') { | ||
if (i == 0) { | ||
return result; | ||
} | ||
rest = pieces.slice(pieces.length - i); | ||
result = retrieveFromPath(result, rest); | ||
if (typeof result != 'undefined') { | ||
return result; | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* 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.10 | ||
* | ||
* @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, | ||
i; | ||
// Keys [''] always means the root | ||
if (length == 1 && keys[0] === '') { | ||
return current; | ||
} | ||
for (i = 0; i < length; i++) { | ||
key = keys[i]; | ||
// Normalize the key | ||
if (typeof key == 'number') { | ||
// Allow | ||
} else if (key.indexOf(safe_special_char) > -1) { | ||
key = key.replace(safe_special_char_rg, special_char); | ||
} | ||
prev = current; | ||
if (current) { | ||
if (current.hasOwnProperty(key)) { | ||
current = current[key]; | ||
} else { | ||
return undefined; | ||
} | ||
} else { | ||
return undefined; | ||
} | ||
} | ||
return current; | ||
} | ||
/** | ||
* Extract something from an object by the path | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.0 | ||
@@ -1151,3 +1401,3 @@ * @version 1.0.0 | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 1.0.0 | ||
@@ -1192,5 +1442,5 @@ * @version 1.0.2 | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 1.0.0 | ||
* @version 1.0.0 | ||
* @version 2.0.0 | ||
* | ||
@@ -1203,5 +1453,5 @@ * @param {Object} value | ||
function toDryObject(value, replacer) { | ||
var root = {'': value}; | ||
return createDryReplacer(root, replacer)(root, '', value); | ||
} | ||
let root = new RootValue(value, replacer); | ||
return root.driedValue(); | ||
}; | ||
@@ -1211,5 +1461,5 @@ /** | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 1.0.0 | ||
* @version 1.0.0 | ||
* @version 2.0.0 | ||
* | ||
@@ -1222,3 +1472,4 @@ * @param {Object} value | ||
function stringify(value, replacer, space) { | ||
return JSON.stringify(toDryObject(value, replacer), null, space); | ||
let obj = toDryObject(value, replacer); | ||
return JSON.stringify(obj, null, space); | ||
} | ||
@@ -1229,3 +1480,3 @@ | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.4.2 | ||
@@ -1296,5 +1547,5 @@ * @version 1.0.0 | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 1.0.0 | ||
* @version 1.1.0 | ||
* @version 2.0.0 | ||
* | ||
@@ -1305,124 +1556,11 @@ * @param {Object} value | ||
*/ | ||
function parse(object, reviver) { | ||
function parse(value, reviver) { | ||
var undry_paths = new Map(), | ||
when_done = [], | ||
retrieve = {}, | ||
reviver, | ||
result, | ||
holder, | ||
entry, | ||
temp, | ||
seen, | ||
path, | ||
key, | ||
old = {}, | ||
i; | ||
// Create the reviver function | ||
reviver = generateReviver(reviver, undry_paths); | ||
if (typeof object == 'string') { | ||
object = JSON.parse(object); | ||
if (typeof value === 'string') { | ||
value = JSON.parse(value); | ||
} | ||
if (!object || typeof object != 'object') { | ||
return object; | ||
} | ||
let root = new RootValue(value, reviver); | ||
let result = root.undriedValue(); | ||
result = walk(object, reviver); | ||
if (result == null) { | ||
return result; | ||
} | ||
function whenDone(fnc) { | ||
if (!fnc) return; | ||
when_done.push(fnc); | ||
} | ||
// To remember which objects have already been revived | ||
seen = new WeakMap(); | ||
// Maybe paths need another round of undrying | ||
undry_paths.extra_pass = []; | ||
// Iterate over all the values that require some kind of function to be revived | ||
undry_paths.forEach(function eachEntry(entry, path) { | ||
var path_array = entry.drypath, | ||
path_string = path_array.join('.'); | ||
// Regenerate this replacement wrapper first | ||
regenerate(result, null, entry, seen, retrieve, undry_paths, old, path_array.slice(0)); | ||
if (entry.unDryConstructor) { | ||
entry.undried = entry.unDryConstructor.unDry(entry.value, false, whenDone); | ||
} else if (entry.unDryFunction) { | ||
entry.undried = entry.unDryFunction(entry, null, entry.value, whenDone); | ||
} else { | ||
entry.undried = entry.value; | ||
} | ||
// Remember the old wrapper entry, some other references | ||
// may still point to it's children | ||
old[path_string] = entry; | ||
if (entry.drypath && entry.drypath.length) { | ||
setPath(result, entry.drypath, entry.undried); | ||
} | ||
}); | ||
for (i = 0; i < undry_paths.extra_pass.length; i++) { | ||
entry = undry_paths.extra_pass[i]; | ||
holder = entry[0]; | ||
temp = entry[1]; | ||
path = entry[2]; | ||
for (key in holder) { | ||
if (holder[key] == temp) { | ||
holder[key] = temp.undried; | ||
break; | ||
} | ||
} | ||
path.pop(); | ||
// Annoying workaround for some circular references | ||
if (path.length && path[path.length - 1] == 'value') { | ||
path.pop(); | ||
} | ||
if (path.length) { | ||
// Get the other holder | ||
holder = retrieveFromPath(result, path); | ||
// If the holder object was not found in the result, | ||
// it was probably a child of ANOTHER holder that has already been undried & replaces | ||
// Just get the value from the object containing old references | ||
if (!holder) { | ||
holder = getFromOld(old, path); | ||
} | ||
for (key in holder) { | ||
if (holder[key] == temp) { | ||
holder[key] = temp.undried; | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
// Only now we can resolve paths | ||
result = regenerate(result, result, result, seen, retrieve, undry_paths, old, []); | ||
if (result.undried != null && result.dry) { | ||
result = result.undried; | ||
} | ||
for (i = 0; i < when_done.length; i++) { | ||
when_done[i](); | ||
} | ||
return result; | ||
@@ -1429,0 +1567,0 @@ } |
{ | ||
"name": "json-dry", | ||
"description": "Don't repeat yourself, JSON: Add support for (circular) references, class instances, ...", | ||
"version": "1.1.1", | ||
"version": "2.0.0", | ||
"author": "Jelle De Loecker <jelle@elevenways.be>", | ||
@@ -16,3 +16,3 @@ "keywords": [ | ||
"test" : "mocha --exit --reporter spec --bail --timeout 5000 --file test/dry.js", | ||
"coverage" : "./node_modules/istanbul/lib/cli.js cover _mocha", | ||
"coverage" : "nyc --reporter=text --reporter=lcov mocha --exit --timeout 20000 --bail --file test/dry.js", | ||
"report-coverage" : "codecov" | ||
@@ -22,3 +22,3 @@ }, | ||
"devDependencies": { | ||
"codecov" : "~3.7.0", | ||
"codecov" : "~3.8.3", | ||
"nyc" : "^15.1.0", | ||
@@ -29,4 +29,4 @@ "mocha" : "~8.0.1", | ||
"engines": { | ||
"node": ">=6.4" | ||
"node": ">=14.0" | ||
} | ||
} |
123
README.md
@@ -1,12 +0,41 @@ | ||
# JSON-dry | ||
<h1 align="center"> | ||
<b>JSON-DRY</b> | ||
</h1> | ||
<div align="center"> | ||
<!-- CI - Github Actions --> | ||
<a href="https://github.com/11ways/json-dry/actions/workflows/unit_test.yaml"> | ||
<img src="https://github.com/11ways/json-dry/actions/workflows/unit_test.yaml/badge.svg" alt="Node.js CI (Linux, MacOS, Windows)" /> | ||
</a> | ||
[![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) | ||
<!-- Coverage - Codecov --> | ||
<a href="https://codecov.io/gh/11ways/json-dry"> | ||
<img src="https://img.shields.io/codecov/c/github/11ways/json-dry/master.svg" alt="Codecov Coverage report" /> | ||
</a> | ||
JSON-dry allows you to stringify objects containing circular references, | ||
dates, regexes, ... | ||
<!-- DM - Snyk --> | ||
<a href="https://snyk.io/test/github/11ways/json-dry?targetFile=package.json"> | ||
<img src="https://snyk.io/test/github/11ways/json-dry/badge.svg?targetFile=package.json" alt="Known Vulnerabilities" /> | ||
</a> | ||
</div> | ||
It can also be used to serialize and revive instances of your own classes. | ||
<div align="center"> | ||
<!-- Version - npm --> | ||
<a href="https://www.npmjs.com/package/json-dry"> | ||
<img src="https://img.shields.io/npm/v/json-dry.svg" alt="Latest version on npm" /> | ||
</a> | ||
<!-- License - MIT --> | ||
<a href="https://github.com/11ways/json-dry#license"> | ||
<img src="https://img.shields.io/github/license/11ways/json-dry.svg" alt="Project license" /> | ||
</a> | ||
</div> | ||
<br> | ||
<div align="center"> | ||
Serialize objects while preserving references and custom class instances | ||
</div> | ||
<div align="center"> | ||
<sub> | ||
Coded with ❤️ by <a href="#authors">Eleven Ways</a>. | ||
</sub> | ||
</div> | ||
@@ -25,3 +54,2 @@ ## Table of contents | ||
* [Project history](#project-history) | ||
* [Project future](#project-future) | ||
* [Versioning](#versioning) | ||
@@ -31,3 +59,11 @@ * [License](#license) | ||
## Version 2.x! | ||
First of all: **Version 2.x of `json-dry` is not able to parse output from version 1.x, if that output contains references!** | ||
The way references are made & revived has changed completely. | ||
All other syntax has remained the same. | ||
If you did not use `json-dry` to store serialized objects long-term (so just on-the-fly, for communication) then it's probably safe to upgrade. | ||
## Installation | ||
@@ -45,9 +81,9 @@ | ||
```js | ||
var Dry = require('json-dry'); | ||
let Dry = require('json-dry'); | ||
// The object we'll serialize later | ||
var obj = {}; | ||
let obj = {}; | ||
// The object we'll make multiple references to | ||
var ref = { | ||
let ref = { | ||
date : new Date(), | ||
@@ -65,20 +101,26 @@ regex : /test/i | ||
// Stringify the object | ||
var dried = Dry.stringify(obj); | ||
let dried = Dry.stringify(obj); | ||
// { | ||
// "reference_one": { | ||
// "date": { | ||
// "~refs": [ | ||
// { | ||
// "date": {"~r": 1}, | ||
// "regex": { | ||
// "dry": "regexp", | ||
// "value": "/test/i" | ||
// } | ||
// }, | ||
// { | ||
// "dry": "date", | ||
// "value": "2018-01-14T17:45:57.989Z" | ||
// }, | ||
// "regex": { | ||
// "dry": "regexp", | ||
// "value": "/test/i" | ||
// "value": "2023-01-14T12:00:35.194Z" | ||
// } | ||
// }, | ||
// "reference_two": "~reference_one", | ||
// "date": "~reference_one~date" | ||
// ], | ||
// "~root": { | ||
// "reference_one": {"~r": 0}, | ||
// "reference_two": {"~r": 0}, | ||
// "date": {"~r": 1} | ||
// } | ||
// } | ||
// Now we'll revive it again | ||
var undried = Dry.parse(dried); | ||
let undried = Dry.parse(dried); | ||
// { reference_one: { date: 2018-01-14T17:56:43.149Z, regex: /test/i }, | ||
@@ -115,3 +157,3 @@ // reference_two: { date: 2018-01-14T17:56:43.149Z, regex: /test/i }, | ||
// Create an object | ||
var jelle = new Person({firstname: 'Jelle', lastname: 'De Loecker'}); | ||
let jelle = new Person({firstname: 'Jelle', lastname: 'De Loecker'}); | ||
@@ -152,7 +194,7 @@ // Test out the fullname method | ||
```js | ||
var dried = Dry.stringify(jelle); | ||
let 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); | ||
let undried = Dry.parse(dried); | ||
// Person { firstname: 'Jelle', lastname: 'De Loecker' } | ||
@@ -208,9 +250,9 @@ | ||
```js | ||
var cloned = Dry.parse(Dry.toObject(jelle)) | ||
let 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: | ||
This is a lot slower than using `clone`, because `toObject` needs to do extra work that can be ignored when cloning: | ||
```js | ||
var cloned = Dry.clone(jelle); | ||
let cloned = Dry.clone(jelle); | ||
``` | ||
@@ -252,3 +294,3 @@ | ||
var special_clone = Dry.clone(jelle, 'specialOccasionClone'); | ||
let special_clone = Dry.clone(jelle, 'specialOccasionClone'); | ||
special_clone.fullname(); | ||
@@ -264,18 +306,6 @@ // Returns "J. De Loecker" | ||
After version 0.1.6 I integrated `json-dry` into my [protoblast](https://github.com/skerit/protoblast) library, and development continued in there. But now it has deserved its own repository once again. | ||
The versions of `json-dry` before `2.0.0` used references to the path where the object was first seen, like `~paths~to~the~first~reference`. Unfortunately sometimes objects were nested so deep that these reference paths were a lot longer than the serialized version of the object itself. | ||
This version is a rewrite of earlier versions. `circular-json` used (and still uses) multiple arrays to keep track of already used objects, but `json-dry` now uses `WeakMap`s, something that makes the code easier to maintain and is also faster. | ||
That's why in this new version, objects that are used more than once are stored in the `~refs` array. This way all references to objects can be simple numbers, instead of paths. | ||
`circular-json` was also implemented as a `replacer` and `reviver` function to `JSON.stringify` and `JSON.parse` respectively. `json-dry` actually creates a new object before `stringifying` it. | ||
Because multiple references are represented as `~paths~to~the~first~reference`, the size of the JSON string can be a lot smaller. Can be, though, because sometimes reference paths are longer than the object they are refering to. | ||
Because of this, as soon as `json-dry` encounters a new path that is smaller than the previous one, it'll use that in the future. This helps a bit, though more improvements could be made in the future. | ||
## Project future | ||
* Possibly use objects or arrays instead of string primitives for references. This would speed up serializing and parsing, but be a bit more verbose. Tell me what you think in [issue #2](https://github.com/skerit/json-dry/issues/2) | ||
## Versioning | ||
@@ -289,6 +319,1 @@ | ||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details | ||
## Acknowledgments | ||
Many thanks to [WebReflection](https://github.com/WebReflection/), whose [circular-json](https://github.com/WebReflection/circular-json) was the basis for earlier versions of this project. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
49089
7
1350
310
1