diff --git a/README.md b/README.md index dd49b35..7260bd5 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,15 @@ uncompressed. In deno: ```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 + ``` ## Use diff --git a/TODO.md b/TODO.md index f321421..49a13f2 100644 --- a/TODO.md +++ b/TODO.md @@ -1 +1,3 @@ +# TODO + - Add protocol version, to handle breaking changes diff --git a/doc/usage.md b/doc/usage.md index 1c265ca..3a9e9e4 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -3,7 +3,15 @@ In deno: ```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 + ``` ## Use diff --git a/serializer.test.ts b/serializer.test.ts index b3ed531..c49efbe 100644 --- a/serializer.test.ts +++ b/serializer.test.ts @@ -1,8 +1,9 @@ import { + describe, expect, it, 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"; class TestSerializerObj1 { @@ -62,145 +63,152 @@ function checkReversability( return loaded; } -it("serializes simple objects", () => { - var obj = { - "a": 5, - "b": null, - "c": [{ "a": 2 }, "test"], - "d": new Set([1, 4]), - "e": new Map([["z", 8], ["t", 2]]), - }; - const result = checkReversability(obj); - expect(result["a"]).toBe(5); - expect(result["b"]).toBe(null); - expect(result["c"][0]["a"]).toBe(2); - expect(result["d"].has(1)).toBe(true); - expect(result["d"].has(2)).toBe(false); - expect(result["e"].get("z")).toBe(8); - expect(result["e"].get("k")).toBeUndefined(); +describe(Serializer, () => { + it("serializes simple objects", () => { + var obj = { + "a": 5, + "b": null, + "c": [{ "a": 2 }, "test"], + "d": new Set([1, 4]), + "e": new Map([["z", 8], ["t", 2]]), + }; + const result = checkReversability(obj); + expect(result["a"]).toBe(5); + expect(result["b"]).toBe(null); + expect(result["c"][0]["a"]).toBe(2); + expect(result["d"].has(1)).toBe(true); + expect(result["d"].has(2)).toBe(false); + expect(result["e"].get("z")).toBe(8); + expect(result["e"].get("k")).toBeUndefined(); - checkReversability(new Set(["a", new Set([1, "b"])])); - 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); + checkReversability(new Set(["a", new Set([1, "b"])])); + checkReversability(new Map([["a", new Map([[1, "test"]])]])); }); -}); -it("ignores some classes", () => { - let serializer = new Serializer(TEST_NS); - serializer.addIgnoredClass("TestSerializerObj1"); + it("restores objects constructed from class", () => { + let loaded = checkReversability(new TestSerializerObj1(5)); + expect(loaded.a).toBe(5); + expect(loaded).toBeInstanceOf(TestSerializerObj1); - let data = serializer.serialize({ a: 5, b: new TestSerializerObj1() }); - let loaded = serializer.unserialize(data); + loaded = checkReversability(new TestSerializerObj1(5), TEST_NS_FLAT); + 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); - serializer.addIgnoredClass(TestSerializerObj1); + it("serializes reference to class type", () => { + let loaded = checkReversability(TestSerializerObj1); + expect(loaded).toBe(TestSerializerObj1); + }); - data = serializer.serialize({ a: 5, b: new TestSerializerObj1() }); - loaded = serializer.unserialize(data); + 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); + }); - 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); - serializer.addIgnoredClass(TestSerializerObj1); + 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); + }); + }); - 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 = 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) => { + it("ignores some classes", () => { 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); - expect(loaded).toEqual({ obj: { a: 0 } }); - expect(mock_error).toHaveBeenCalledWith( - "Can't find class", - "TestSerializerObj4", - ); + + expect(loaded).toEqual({ a: 5, b: undefined }); + + 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); -}); diff --git a/serializer.ts b/serializer.ts index d529e77..fda2cc0 100644 --- a/serializer.ts +++ b/serializer.ts @@ -41,18 +41,32 @@ export class Serializer { // Collect objects var objects: Object[] = []; var stats: any = {}; + + function add(value: any): void { + if (objects.indexOf(value) < 0) { + objects.push(value); + } + } + crawl(obj, (value) => { if (isObject(value)) { var vtype = classname(value); if (vtype != "" && this.ignored.indexOf(vtype) < 0) { stats[vtype] = (stats[vtype] || 0) + 1; - if (objects.indexOf(value) < 0) { - objects.push(value); - } + add(value); return value; } else { 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 { return value; } @@ -63,7 +77,8 @@ export class Serializer { var fobjects = objects.map((value) => this.encodeObject(value)); return JSON.stringify(fobjects, (key, value) => { if ( - key != "$f" && isObject(value) && !value.hasOwnProperty("$c") && + key != "$f" && (isObject(value) || typeof value == "function") && + !value.hasOwnProperty("$c") && !value.hasOwnProperty("$i") ) { return { $i: objects.indexOf(value) }; @@ -132,6 +147,10 @@ export class Serializer { $c: ctype, $f: Array.from(obj), }; + } else if (typeof obj == "function") { + return { + $c: obj.name, + }; } else { return { $c: this.namespace_rev[ctype] || ctype, @@ -149,8 +168,10 @@ export class Serializer { return new Set(objdata.$f); } else if (ctype == "Map") { return new Map(objdata.$f); - } else { + } else if (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 */ 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, replace = false, memo: any[] = [], -) { +): any { if (isObject(obj)) { if (memo.indexOf(obj) >= 0) { 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); if (result === STOP_CRAWLING) { - return; + return undefined; } if (Array.isArray(obj)) { @@ -244,5 +266,5 @@ function crawl( * Get the class name of an object. */ function classname(obj: Object): string { - return ( obj.constructor).name; + return obj.constructor.name; }