1
0
Fork 0

Allow to serialize reference to a constructor in the namespace

This commit is contained in:
Michaël Lemaire 2021-08-15 19:11:32 +02:00
parent 9e0d1d623e
commit 95b3cf9fcc
5 changed files with 189 additions and 141 deletions

View File

@ -23,7 +23,15 @@ uncompressed.
In deno: In deno:
```typescript ```typescript
import { Serializer } from "https://code.thunderk.net/typescript/serializer/raw/branch/master/mod.ts"; import { Serializer } from "https://js.thunderk.net/serializer/mod.ts";
```
In browser:
```html
<script type="module">
import { Serializer } from "https://js.thunderk.net/serializer/mod.js";
</script>
``` ```
## Use ## Use

View File

@ -1 +1,3 @@
# TODO
- Add protocol version, to handle breaking changes - Add protocol version, to handle breaking changes

View File

@ -3,7 +3,15 @@
In deno: In deno:
```typescript ```typescript
import { Serializer } from "https://code.thunderk.net/typescript/serializer/raw/branch/master/mod.ts"; import { Serializer } from "https://js.thunderk.net/serializer/mod.ts";
```
In browser:
```html
<script type="module">
import { Serializer } from "https://js.thunderk.net/serializer/mod.js";
</script>
``` ```
## Use ## Use

View File

@ -1,8 +1,9 @@
import { import {
describe,
expect, expect,
it, it,
mock, mock,
} from "https://code.thunderk.net/typescript/devtools/raw/1.2.2/testing.ts"; } from "https://code.thunderk.net/typescript/devtools/raw/1.3.0/testing.ts";
import { Serializer } from "./serializer.ts"; import { Serializer } from "./serializer.ts";
class TestSerializerObj1 { class TestSerializerObj1 {
@ -62,145 +63,152 @@ function checkReversability(
return loaded; return loaded;
} }
it("serializes simple objects", () => { describe(Serializer, () => {
var obj = { it("serializes simple objects", () => {
"a": 5, var obj = {
"b": null, "a": 5,
"c": [{ "a": 2 }, "test"], "b": null,
"d": new Set([1, 4]), "c": [{ "a": 2 }, "test"],
"e": new Map([["z", 8], ["t", 2]]), "d": new Set([1, 4]),
}; "e": new Map([["z", 8], ["t", 2]]),
const result = checkReversability(obj); };
expect(result["a"]).toBe(5); const result = checkReversability(obj);
expect(result["b"]).toBe(null); expect(result["a"]).toBe(5);
expect(result["c"][0]["a"]).toBe(2); expect(result["b"]).toBe(null);
expect(result["d"].has(1)).toBe(true); expect(result["c"][0]["a"]).toBe(2);
expect(result["d"].has(2)).toBe(false); expect(result["d"].has(1)).toBe(true);
expect(result["e"].get("z")).toBe(8); expect(result["d"].has(2)).toBe(false);
expect(result["e"].get("k")).toBeUndefined(); expect(result["e"].get("z")).toBe(8);
expect(result["e"].get("k")).toBeUndefined();
checkReversability(new Set(["a", new Set([1, "b"])])); checkReversability(new Set(["a", new Set([1, "b"])]));
checkReversability(new Map([["a", new Map([[1, "test"]])]])); checkReversability(new Map([["a", new Map([[1, "test"]])]]));
});
it("restores objects constructed from class", () => {
let loaded = checkReversability(new TestSerializerObj1(5));
expect(loaded.a).toBe(5);
expect(loaded).toBeInstanceOf(TestSerializerObj1);
loaded = checkReversability(new TestSerializerObj1(5), TEST_NS_FLAT);
expect(loaded.a).toBe(5);
expect(loaded).toBeInstanceOf(TestSerializerObj1);
loaded = checkReversability(new TestSerializerObj1(5), TEST_NS_RENAME);
expect(loaded.a).toBe(5);
expect(loaded).toBeInstanceOf(TestSerializerObj1);
});
it("stores one version of the same object", () => {
var a = new TestSerializerObj1(8);
var b = new TestSerializerObj1(8);
var c = {
"r": a,
"s": ["test", a],
"t": a,
"u": b,
};
var loaded = checkReversability(c);
expect(loaded.t).toBe(loaded.r);
expect(loaded.s[1]).toBe(loaded.r);
expect(loaded.u).not.toBe(loaded.r);
});
it("handles circular references", () => {
var a: any = { b: {} };
a.b.c = a;
checkReversability(a, undefined, (loaded) => {
expect(Object.keys(loaded)).toEqual(["b"]);
expect(Object.keys(loaded.b)).toEqual(["c"]);
expect(Object.keys(loaded.b.c)).toEqual(["b"]);
expect(Object.keys(loaded.b.c.b)).toEqual(["c"]);
expect(loaded.b.c).toBe(loaded);
expect(loaded.b.c.b).toBe(loaded.b);
}); });
});
it("ignores some classes", () => { it("restores objects constructed from class", () => {
let serializer = new Serializer(TEST_NS); let loaded = checkReversability(new TestSerializerObj1(5));
serializer.addIgnoredClass("TestSerializerObj1"); expect(loaded.a).toBe(5);
expect(loaded).toBeInstanceOf(TestSerializerObj1);
let data = serializer.serialize({ a: 5, b: new TestSerializerObj1() }); loaded = checkReversability(new TestSerializerObj1(5), TEST_NS_FLAT);
let loaded = serializer.unserialize(data); expect(loaded.a).toBe(5);
expect(loaded).toBeInstanceOf(TestSerializerObj1);
expect(loaded).toEqual({ a: 5, b: undefined }); loaded = checkReversability(new TestSerializerObj1(5), TEST_NS_RENAME);
expect(loaded.a).toBe(5);
expect(loaded).toBeInstanceOf(TestSerializerObj1);
});
serializer = new Serializer(TEST_NS); it("serializes reference to class type", () => {
serializer.addIgnoredClass(TestSerializerObj1); let loaded = checkReversability(TestSerializerObj1);
expect(loaded).toBe(TestSerializerObj1);
});
data = serializer.serialize({ a: 5, b: new TestSerializerObj1() }); it("stores one version of the same object", () => {
loaded = serializer.unserialize(data); var a = new TestSerializerObj1(8);
var b = new TestSerializerObj1(8);
var c = {
"r": a,
"s": ["test", a],
"t": a,
"u": b,
};
var loaded = checkReversability(c);
expect(loaded.t).toBe(loaded.r);
expect(loaded.s[1]).toBe(loaded.r);
expect(loaded.u).not.toBe(loaded.r);
});
expect(loaded).toEqual({ a: 5, b: undefined }); it("handles circular references", () => {
var a: any = { b: {} };
a.b.c = a;
serializer = new Serializer(TEST_NS_RENAME); checkReversability(a, undefined, (loaded) => {
serializer.addIgnoredClass(TestSerializerObj1); expect(Object.keys(loaded)).toEqual(["b"]);
expect(Object.keys(loaded.b)).toEqual(["c"]);
expect(Object.keys(loaded.b.c)).toEqual(["b"]);
expect(Object.keys(loaded.b.c.b)).toEqual(["c"]);
expect(loaded.b.c).toBe(loaded);
expect(loaded.b.c.b).toBe(loaded.b);
});
});
data = serializer.serialize({ a: 5, b: new TestSerializerObj1() }); it("ignores some classes", () => {
loaded = serializer.unserialize(data);
expect(loaded).toEqual({ a: 5, b: undefined });
});
it("ignores functions", () => {
let serializer = new Serializer(TEST_NS);
let data = serializer.serialize({ obj: new TestSerializerObj2() });
let loaded = serializer.unserialize(data);
let expected = <any> new TestSerializerObj2();
expected.a = undefined;
expected.b[0] = undefined;
expect(loaded).toEqual({ obj: expected });
});
it("calls specific postUnserialize", () => {
let serializer = new Serializer(TEST_NS);
let data = serializer.serialize({ obj: new TestSerializerObj3() });
let loaded = serializer.unserialize(data);
let expected = new TestSerializerObj3();
expected.a = [1, 2, 3];
expect(loaded).toEqual({ obj: expected });
});
it("handles missing classes", () => {
mock(console, "error", undefined, (mock_error) => {
let serializer = new Serializer(TEST_NS); let serializer = new Serializer(TEST_NS);
let data = serializer.serialize({ obj: new TestSerializerObj4() }); serializer.addIgnoredClass("TestSerializerObj1");
let data = serializer.serialize({ a: 5, b: new TestSerializerObj1() });
let loaded = serializer.unserialize(data); let loaded = serializer.unserialize(data);
expect(loaded).toEqual({ obj: { a: 0 } });
expect(mock_error).toHaveBeenCalledWith( expect(loaded).toEqual({ a: 5, b: undefined });
"Can't find class",
"TestSerializerObj4", serializer = new Serializer(TEST_NS);
); serializer.addIgnoredClass(TestSerializerObj1);
data = serializer.serialize({ a: 5, b: new TestSerializerObj1() });
loaded = serializer.unserialize(data);
expect(loaded).toEqual({ a: 5, b: undefined });
serializer = new Serializer(TEST_NS_RENAME);
serializer.addIgnoredClass(TestSerializerObj1);
data = serializer.serialize({ a: 5, b: new TestSerializerObj1() });
loaded = serializer.unserialize(data);
expect(loaded).toEqual({ a: 5, b: undefined });
});
it("ignores functions", () => {
let serializer = new Serializer(TEST_NS);
let data = serializer.serialize({ obj: new TestSerializerObj2() });
let loaded = serializer.unserialize(data);
let expected: any = new TestSerializerObj2();
expected.a = undefined;
expected.b[0] = undefined;
expect(loaded).toEqual({ obj: expected });
});
it("calls specific postUnserialize", () => {
let serializer = new Serializer(TEST_NS);
let data = serializer.serialize({ obj: new TestSerializerObj3() });
let loaded = serializer.unserialize(data);
let expected = new TestSerializerObj3();
expected.a = [1, 2, 3];
expect(loaded).toEqual({ obj: expected });
});
it("handles missing classes", () => {
mock(console, "error", undefined, (mock_error) => {
let serializer = new Serializer(TEST_NS);
let data = serializer.serialize({ obj: new TestSerializerObj4() });
let loaded = serializer.unserialize(data);
expect(loaded).toEqual({ obj: { a: 0 } });
expect(mock_error).toHaveBeenCalledWith(
"Can't find class",
"TestSerializerObj4",
);
});
});
it("uses namespace alias to protect from property mangling", () => {
const data = {
a: new TestSerializerObj1(1),
b: new TestSerializerObj1(2),
c: [new TestSerializerObj1(3)],
};
const serializer1 = new Serializer(TEST_NS);
const serializer2 = new Serializer(TEST_NS_RENAME);
const dumped1 = serializer1.serialize(data);
const dumped2 = serializer2.serialize(data);
expect(dumped1.length).toBeGreaterThan(dumped2.length);
expect(serializer1.unserialize(dumped1)).toEqual(data);
expect(serializer2.unserialize(dumped2)).toEqual(data);
}); });
}); });
it("uses namespace alias to protect from property mangling", () => {
const data = {
a: new TestSerializerObj1(1),
b: new TestSerializerObj1(2),
c: [new TestSerializerObj1(3)],
};
const serializer1 = new Serializer(TEST_NS);
const serializer2 = new Serializer(TEST_NS_RENAME);
const dumped1 = serializer1.serialize(data);
const dumped2 = serializer2.serialize(data);
expect(dumped1.length).toBeGreaterThan(dumped2.length);
expect(serializer1.unserialize(dumped1)).toEqual(data);
expect(serializer2.unserialize(dumped2)).toEqual(data);
});

View File

@ -41,18 +41,32 @@ export class Serializer {
// Collect objects // Collect objects
var objects: Object[] = []; var objects: Object[] = [];
var stats: any = {}; var stats: any = {};
function add(value: any): void {
if (objects.indexOf(value) < 0) {
objects.push(value);
}
}
crawl(obj, (value) => { crawl(obj, (value) => {
if (isObject(value)) { if (isObject(value)) {
var vtype = classname(value); var vtype = classname(value);
if (vtype != "" && this.ignored.indexOf(vtype) < 0) { if (vtype != "" && this.ignored.indexOf(vtype) < 0) {
stats[vtype] = (stats[vtype] || 0) + 1; stats[vtype] = (stats[vtype] || 0) + 1;
if (objects.indexOf(value) < 0) { add(value);
objects.push(value);
}
return value; return value;
} else { } else {
return STOP_CRAWLING; return STOP_CRAWLING;
} }
} else if (typeof value == "function") {
const found = Object.entries(this.namespace).find(([_, cons]) =>
cons === value
);
if (found && this.ignored.indexOf(found[0]) < 0) {
// Keep references to constructors in the namespace
add(value);
}
return STOP_CRAWLING;
} else { } else {
return value; return value;
} }
@ -63,7 +77,8 @@ export class Serializer {
var fobjects = objects.map((value) => this.encodeObject(value)); var fobjects = objects.map((value) => this.encodeObject(value));
return JSON.stringify(fobjects, (key, value) => { return JSON.stringify(fobjects, (key, value) => {
if ( if (
key != "$f" && isObject(value) && !value.hasOwnProperty("$c") && key != "$f" && (isObject(value) || typeof value == "function") &&
!value.hasOwnProperty("$c") &&
!value.hasOwnProperty("$i") !value.hasOwnProperty("$i")
) { ) {
return { $i: objects.indexOf(value) }; return { $i: objects.indexOf(value) };
@ -132,6 +147,10 @@ export class Serializer {
$c: ctype, $c: ctype,
$f: Array.from(obj), $f: Array.from(obj),
}; };
} else if (typeof obj == "function") {
return {
$c: obj.name,
};
} else { } else {
return { return {
$c: this.namespace_rev[ctype] || ctype, $c: this.namespace_rev[ctype] || ctype,
@ -149,8 +168,10 @@ export class Serializer {
return new Set(objdata.$f); return new Set(objdata.$f);
} else if (ctype == "Map") { } else if (ctype == "Map") {
return new Map(objdata.$f); return new Map(objdata.$f);
} else { } else if (objdata.$f) {
return Object.assign(this.constructObject(ctype), objdata.$f); return Object.assign(this.constructObject(ctype), objdata.$f);
} else {
return this.namespace[ctype];
} }
} }
} }
@ -159,7 +180,8 @@ export class Serializer {
* Check if the argument is an instance of a class * Check if the argument is an instance of a class
*/ */
function isObject(value: any): boolean { function isObject(value: any): boolean {
return value instanceof Object && !Array.isArray(value); return typeof value == "object" && value instanceof Object &&
!Array.isArray(value);
} }
/** /**
@ -174,7 +196,7 @@ function crawl(
callback: (item: any) => any, callback: (item: any) => any,
replace = false, replace = false,
memo: any[] = [], memo: any[] = [],
) { ): any {
if (isObject(obj)) { if (isObject(obj)) {
if (memo.indexOf(obj) >= 0) { if (memo.indexOf(obj) >= 0) {
return obj; return obj;
@ -183,11 +205,11 @@ function crawl(
} }
} }
if (obj !== undefined && obj !== null && typeof obj != "function") { if (obj !== undefined && obj !== null) {
const result = callback(obj); const result = callback(obj);
if (result === STOP_CRAWLING) { if (result === STOP_CRAWLING) {
return; return undefined;
} }
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
@ -244,5 +266,5 @@ function crawl(
* Get the class name of an object. * Get the class name of an object.
*/ */
function classname(obj: Object): string { function classname(obj: Object): string {
return (<any> obj.constructor).name; return obj.constructor.name;
} }