Socket
Socket
Sign inDemoInstall

json-dry

Package Overview
Dependencies
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

json-dry - npm Package Compare versions

Comparing version 1.1.1 to 2.0.0

lib/index.d.ts

5

CHANGELOG.md

@@ -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"
}
}

@@ -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.
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc