Skip to content

Commit 9a01d2a

Browse files
committed
json ref replacement improved
1 parent 21d2961 commit 9a01d2a

2 files changed

Lines changed: 148 additions & 15 deletions

File tree

__tests__/helpers/helperFunctions.test.ts

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { test, expect } from '@jest/globals';
2-
import { stringifyIfNotString, jsonStringifyKeepMeta, wrapInObjectIfNotObject, jsonStringifyPossiblyCircular, removeCircularReferencesFromObject } from './../../helpers/helperFunctions';
2+
import { stringifyIfNotString, jsonStringifyKeepMeta, wrapInObjectIfNotObject, jsonReplacerWithPath, jsonRefReplacer, parseRefJSON, jsonStringifyPossiblyCircular, removeCircularReferencesFromObject } from './../../helpers/helperFunctions';
33
import { makeRandomString } from './../../helpers/random';
44
import { CONFIG } from '../../config';
55

@@ -67,6 +67,47 @@ test("wrapInObjectIfNotObject test", () => {
6767
expect(test7).toEqual({a: 1, b: 2, c: 3});
6868
});
6969

70+
test("jsonReplacerWithPath test", () => {
71+
const a = { a1: 1, a2: 1 };
72+
const b = { b1: 2, b2: [1, a] };
73+
const c = { c1: 3, c2: b };
74+
75+
const test1: string[] = [];
76+
const s = JSON.stringify(c, jsonReplacerWithPath(function(this: any, field: string, value: any, path: string) {
77+
test1.push(path);
78+
return value;
79+
}));
80+
81+
expect(test1).toEqual(["", "c1", "c2", "c2.b1", "c2.b2", "c2.b2[0]", "c2.b2[1]", "c2.b2[1].a1", "c2.b2[1].a2"])
82+
expect(s).toEqual(JSON.stringify(c));
83+
});
84+
85+
test("jsonRefReplacer test", () => {
86+
// Creating object with duplicate references
87+
const a: any = { a1: 1, a2: 2 };
88+
const b: any = { b1: 3, b2: "4" };
89+
const obj: any = { o1: { o2: a }, b, a }; // duplicate reference
90+
a.a3 = [1 ,2, b]; // circular reference
91+
b.b3 = a; // circular reference
92+
93+
const test1 = JSON.stringify(obj, jsonRefReplacer());
94+
expect(test1).toEqual(`{"o1":{"o2":{"a1":1,"a2":2,"a3":[1,2,{"b1":3,"b2":"4","b3":"#REF:$.o1.o2"}]}},"b":"#REF:$.o1.o2.a3[2]","a":"#REF:$.o1.o2"}`);
95+
});
96+
97+
test("parseRefJSON test", () => {
98+
// Creating object with duplicate references
99+
const a: any = { a1: 1, a2: 2 };
100+
const b: any = { b1: 3, b2: "4" };
101+
const obj: any = { o1: { o2: a }, b, a }; // duplicate reference
102+
a.a3 = [1, 2, b]; // circular reference
103+
b.b3 = a; // circular reference
104+
105+
const s = JSON.stringify(obj, jsonRefReplacer());
106+
107+
const test1 = parseRefJSON(s);
108+
expect(test1).toEqual(obj);
109+
});
110+
70111
test("jsonStringifyPossiblyCircular test", () => {
71112
const test1 = jsonStringifyPossiblyCircular([]);
72113
const test2 = jsonStringifyPossiblyCircular({});
@@ -84,7 +125,7 @@ test("jsonStringifyPossiblyCircular test", () => {
84125
expect(test3).toEqual(`{"a":1,"b":2,"c":3}`);
85126
expect(test4).toEqual(`{"a":{"x":1,"y":2},"b":{"x":0,"y":-1}}`);
86127
expect(test4Reversed).toEqual(data4);
87-
expect(test5).toEqual(`{"data":"123"}`);
128+
expect(test5).toEqual(`{"data":"123","circ":"#REF:$"}`);
88129
});
89130

90131
test("removeCircularReferencesFromObject test", () => {
@@ -110,12 +151,17 @@ test("removeCircularReferencesFromObject test", () => {
110151

111152
const test6 = removeCircularReferencesFromObject(innerRecursiveLinksObject);
112153

154+
// console.log("test6", test6);
155+
113156
expect(test1).toEqual(a);
114157
expect(test2).toEqual(b);
115158
expect(test3).toEqual(c);
116159
expect(test4).toEqual(d);
117-
expect(test5).toEqual({data: "123"});
118-
expect(test6).toEqual({aRefersToB: { x: 10, linkB: { y: 100 } }});
160+
expect(test5).toEqual({data: "123", circ: "#REF:$"});
161+
expect(test6).toEqual({
162+
aRefersToB: { x: 10, linkB: { linkA: "#REF:$.aRefersToB", y: 100 } },
163+
bRefersToA: "#REF:$.aRefersToB.linkB"
164+
});
119165
});
120166

121167
test("jsonStringifyKeepMeta test", () => {
@@ -153,5 +199,5 @@ test("jsonStringifyKeepMeta test", () => {
153199

154200
const test4 = jsonStringifyKeepMeta(data3, true);
155201
expect(test4.ok).toBe(true);
156-
expect(test4.result).toEqual(`{"a":101,"b":"abc","c":true,"d":null,"f":[1,2,3],"g":{"x":10,"y":12},"circularObject":{"data":"123"}}`);
202+
expect(test4.result).toEqual(`{"a":101,"b":"abc","c":true,"d":null,"f":[1,2,3],"g":{"x":10,"y":12},"circularObject":{"data":"123","circ":"#REF:$.circularObject"}}`);
157203
});

helpers/helperFunctions.ts

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,106 @@ export const emptyMobxStoreMonitor: MobxStoreMonitor = [
5151
() => {}
5252
];
5353

54-
export const jsonStringifyPossiblyCircular = (data: ObjectT<any>) => {
55-
const seen = new WeakSet();
54+
// JSON.stringify replacer function constructor that adds full path to current position as last arg of replacer
55+
// Explanation fo replacerWithPath decorator:
56+
// > 'this' inside 'return function' point to field parent object
57+
// (JSON.stringify execute replacer like that)
58+
// > 'path' contains path to current field based on parent ('this') path
59+
// previously saved in Map
60+
// > during path generation we check is parent ('this') array or object
61+
// and chose: "[field]" or ".field"
62+
// > in Map we store current 'path' for given 'field' only if it
63+
// is obj or arr in this way path to each parent is stored in Map.
64+
// We don't need to store path to simple types (number, bool, ...) because they never will have children
65+
// > value === Object(value) -> is true if value is object or array
66+
// > path for main object parent is set as 'undefined.' so we cut out that
67+
// prefix at the end ad call replacer with that path
68+
// Test example:
69+
// let a = { a1: 1, a2: 1 };
70+
// let b = { b1: 2, b2: [1, a] };
71+
// let c = { c1: 3, c2: b };
72+
73+
// let s = JSON.stringify(c, replacerWithPath(function(field, value, path) {
74+
// // "this" has same value as in replacer without decoration
75+
// console.log(path);
76+
// return value;
77+
// }));
78+
export const jsonReplacerWithPath = (replacer: ((this: any, key: string, value: any, path: string) => any)) => {
79+
const m = new Map();
80+
81+
return function(this: any, field: string, value: any) {
82+
const path = m.get(this) + (Array.isArray(this) ? `[${field}]` : '.' + field);
83+
84+
if (value === Object(value))
85+
m.set(value, path);
86+
87+
// @ts-ignore (4th arg is additional one, thats a whole point)
88+
return replacer.call(this, field, value, path.replace(/undefined\.\.?/,''));
89+
}
90+
}
91+
92+
// JSON.stringify replacer function constructor for ref replacing (uses same idea as replacerWithPathDecorator)
93+
export const jsonRefReplacer = () => {
94+
const m = new Map();
95+
const v = new Map();
96+
let init: any = null;
97+
98+
return function(this: any, field: string, value: any) {
99+
const p = m.get(this) + (Array.isArray(this) ? `[${field}]` : '.' + field);
100+
const isComplex = value === Object(value);
101+
102+
if (isComplex)
103+
m.set(value, p);
104+
105+
const pp = v.get(value) || '';
106+
const path = p.replace(/undefined\.\.?/,'');
107+
let val = pp ? `#REF:${pp[0] == '[' ? '$' : '$.'}${pp}` : value;
108+
109+
!init ? (init = value) : (val === init ? val = "#REF:$" : 0);
110+
if (!pp && isComplex)
111+
v.set(value, path);
112+
113+
return val;
114+
}
115+
}
116+
117+
// JSON.parse but for objects that were stringified with jsonRefReplacer
118+
export const parseRefJSON = (json: string) => {
119+
const objToPath = new Map();
120+
const pathToObj = new Map();
121+
let o = JSON.parse(json);
122+
123+
const traverse = (parent: any, field?: string) => {
124+
let obj = parent;
125+
let path = '#REF:$';
126+
127+
if (field !== undefined) {
128+
obj = parent[field];
129+
path = objToPath.get(parent) + (Array.isArray(parent) ? `[${field}]` : `${field ? '.' + field : ''}`);
130+
}
131+
132+
objToPath.set(obj, path);
133+
pathToObj.set(path, obj);
134+
135+
let ref = pathToObj.get(obj);
136+
// @ts-ignore
137+
if (ref) parent[field] = ref;
138+
139+
for (let f in obj)
140+
if (obj === Object(obj))
141+
traverse(obj, f);
142+
}
143+
144+
traverse(o);
145+
146+
return o;
147+
}
56148

149+
150+
export const jsonStringifyPossiblyCircular = (data: ObjectT<any>) => {
57151
return JSON.stringify(
58152
data,
59-
(k, v) => {
60-
if (typeof v === 'object' && v) {
61-
if (seen.has(v))
62-
return;
63-
seen.add(v);
64-
}
65-
return v;
66-
}
153+
jsonRefReplacer()
67154
);
68155
}
69156

0 commit comments

Comments
 (0)