From f76867da5ed694fe66c8c0df5107f14e861597b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Sun, 18 Jul 2021 23:12:32 +0200 Subject: [PATCH] Support Map and Set objects --- README.md | 66 ++++++++++ TODO.md | 1 + doc/about.md | 15 +++ doc/index | 2 + doc/usage.md | 49 ++++++++ all.ts => mod.ts | 0 run | 19 +++ serializer.test.ts | 11 +- serializer.ts | 299 +++++++++++++++++++++++++-------------------- 9 files changed, 329 insertions(+), 133 deletions(-) create mode 100644 TODO.md create mode 100644 doc/about.md create mode 100644 doc/index create mode 100644 doc/usage.md rename all.ts => mod.ts (100%) create mode 100755 run diff --git a/README.md b/README.md index 74d3f23..dd49b35 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,69 @@ # typescript/serializer [![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/serializer?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=serializer) + +## About + +This library offers a generic serialization system for Javascript. + +Deep objects state may be serialized to a string, and reconstructed back. + +Supported data types include: + +- Primitive types (number, bool, string...) +- Set and Map standard objects +- Class instances, as long as the class list is exhaustively provided to the + serializer + +Be warned that the resulting serialized value may be quite large and +uncompressed. + +## Import + +In deno: + +```typescript +import { Serializer } from "https://code.thunderk.net/typescript/serializer/raw/branch/master/mod.ts"; +``` + +## Use + +Suppose you have 2 classes Class1 and Class2, whose instances you want to +serialize: + +```typescript +Class1 { /* [...] */ } +Class2 { /* [...] */ } + +// Here is the example object we want to serialize to a string +const obj = { + a: [1, "a", new Class1()], + b: new Class2("x"), + c: new Class3(), +}; + +// We prepare the serializer, providing the class namespace +const namespace = { + Class1, + Class2, +}; +const serializer = new Serializer(namespace); + +// Optionally, some class instances may be ignored (they will be replaced by *undefined*) +serializer.addIgnoredClass(Class3); + +// Serialize the object to a string +const state = serializer.serialize(obj); + +// Reconstruct the object back +const nobj = serializer.unserialize(state); + +console.log(nobj.a[0]); +// output: 1 + +console.log(nobj.b instance of Class2); +// output: true + +console.log(nobj.c); +// output: undefined +``` diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..f321421 --- /dev/null +++ b/TODO.md @@ -0,0 +1 @@ +- Add protocol version, to handle breaking changes diff --git a/doc/about.md b/doc/about.md new file mode 100644 index 0000000..07cf6ab --- /dev/null +++ b/doc/about.md @@ -0,0 +1,15 @@ +## About + +This library offers a generic serialization system for Javascript. + +Deep objects state may be serialized to a string, and reconstructed back. + +Supported data types include: + +- Primitive types (number, bool, string...) +- Set and Map standard objects +- Class instances, as long as the class list is exhaustively provided to the + serializer + +Be warned that the resulting serialized value may be quite large and +uncompressed. diff --git a/doc/index b/doc/index new file mode 100644 index 0000000..c50505a --- /dev/null +++ b/doc/index @@ -0,0 +1,2 @@ +about +usage diff --git a/doc/usage.md b/doc/usage.md new file mode 100644 index 0000000..1c265ca --- /dev/null +++ b/doc/usage.md @@ -0,0 +1,49 @@ +## Import + +In deno: + +```typescript +import { Serializer } from "https://code.thunderk.net/typescript/serializer/raw/branch/master/mod.ts"; +``` + +## Use + +Suppose you have 2 classes Class1 and Class2, whose instances you want to +serialize: + +```typescript +Class1 { /* [...] */ } +Class2 { /* [...] */ } + +// Here is the example object we want to serialize to a string +const obj = { + a: [1, "a", new Class1()], + b: new Class2("x"), + c: new Class3(), +}; + +// We prepare the serializer, providing the class namespace +const namespace = { + Class1, + Class2, +}; +const serializer = new Serializer(namespace); + +// Optionally, some class instances may be ignored (they will be replaced by *undefined*) +serializer.addIgnoredClass(Class3); + +// Serialize the object to a string +const state = serializer.serialize(obj); + +// Reconstruct the object back +const nobj = serializer.unserialize(state); + +console.log(nobj.a[0]); +// output: 1 + +console.log(nobj.b instance of Class2); +// output: true + +console.log(nobj.c); +// output: undefined +``` diff --git a/all.ts b/mod.ts similarity index 100% rename from all.ts rename to mod.ts diff --git a/run b/run new file mode 100755 index 0000000..74d1c6d --- /dev/null +++ b/run @@ -0,0 +1,19 @@ +#!/bin/sh +# Simplified run tool for deno commands + +if test $# -eq 0 +then + echo "Usage: $0 [file or command]" + exit 1 +elif echo $1 | grep -q '.*.ts' +then + denocmd=run + denoargs=$1 + shift +else + denocmd=$1 + shift +fi + +denoargs="$(cat config/$denocmd.flags 2> /dev/null) $denoargs $@" +exec deno $denocmd $denoargs diff --git a/serializer.test.ts b/serializer.test.ts index f5947e0..d0e8615 100644 --- a/serializer.test.ts +++ b/serializer.test.ts @@ -67,8 +67,17 @@ it("serializes simple objects", () => { "a": 5, "b": null, "c": [{ "a": 2 }, "test"], + "d": new Set([1, 4]), + "e": new Map([["z", 8], ["t", 2]]), }; - checkReversability(obj); + 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(); }); it("restores objects constructed from class", () => { diff --git a/serializer.ts b/serializer.ts index 139dc11..1bb6575 100644 --- a/serializer.ts +++ b/serializer.ts @@ -1,7 +1,160 @@ -const merge = Object.assign; - const STOP_CRAWLING = {}; +type SerializerNamespace = { [name: string]: { new (): any } }; + +/** + * A deep serializer of javascript objects. + */ +export class Serializer { + private namespace: SerializerNamespace; + private namespace_rev: { [classname: string]: string }; + private ignored: string[] = []; + private stats: any = {}; + + constructor(namespace: any) { + if (Array.isArray(namespace)) { + this.namespace = {}; + for (let construct of namespace) { + this.namespace[construct.name] = construct; + } + } else { + this.namespace = namespace; + } + + this.namespace_rev = {}; + for (let [key, value] of Object.entries(this.namespace)) { + this.namespace_rev[value.name] = key; + } + } + + /** + * Add a class to the ignore list + */ + addIgnoredClass(cl: string | { new (): any }) { + this.ignored.push(typeof cl === "string" ? cl : cl.name); + } + + /** + * Serialize an object to a string + */ + serialize(obj: any): string { + // Collect objects + var objects: Object[] = []; + var stats: any = {}; + 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); + } + return value; + } else { + return STOP_CRAWLING; + } + } else { + return value; + } + }); + this.stats = stats; + + // Serialize objects list, transforming deeper objects to links + var fobjects = objects.map((value) => this.encodeObject(value)); + return JSON.stringify(fobjects, (key, value) => { + if ( + key != "$f" && isObject(value) && !value.hasOwnProperty("$c") && + !value.hasOwnProperty("$i") + ) { + return { $i: objects.indexOf(value) }; + } else { + return value; + } + }); + } + + /** + * Unserialize a string to an object + */ + unserialize(data: string): any { + // Unserialize objects list + var fobjects = JSON.parse(data); + + // Reconstruct objects + var objects = fobjects.map((objdata: any) => this.decodeObject(objdata)); + + // Reconnect links + crawl(objects, (value) => { + if (value instanceof Object && value.hasOwnProperty("$i")) { + return objects[value["$i"]]; + } else { + return value; + } + }, true); + + // Post unserialize hooks + crawl(objects[0], (value) => { + if ( + value instanceof Object && typeof value.postUnserialize === "function" + ) { + value.postUnserialize(); + } + }); + + // First object was the root + return objects[0]; + } + + /** + * Construct an object from a constructor name + */ + private constructObject(ctype: string): Object { + if (ctype == "Object") { + return {}; + } else { + let cl = this.namespace[ctype]; + if (cl) { + return Object.create(cl.prototype); + } else { + console.error("Can't find class", ctype); + return {}; + } + } + } + + /** + * Transform an object into encoded representation + */ + private encodeObject(obj: Object) { + const ctype = classname(obj); + if (obj instanceof Set || obj instanceof Map) { + return { + $c: ctype, + $f: Array.from(obj), + }; + } else { + return { + $c: this.namespace_rev[ctype] || ctype, + $f: Object.assign({}, obj), + }; + } + } + + /** + * Decode back an encoded representation to a full object + */ + private decodeObject(objdata: any) { + const ctype = objdata.$c; + if (ctype == "Set") { + return new Set(objdata.$f); + } else if (ctype == "Map") { + return new Map(objdata.$f); + } else { + return Object.assign(this.constructObject(ctype), objdata.$f); + } + } +} + /** * Check if the argument is an instance of a class */ @@ -44,13 +197,24 @@ function crawl( obj[index] = value; }); } + } else if (obj instanceof Set) { + let subresult = new Set(); + for (let item of obj) { + subresult.add(crawl(item, callback, replace, memo)); + } + if (replace) { + obj.clear(); + for (let item of subresult) { + obj.add(item); + } + } } else if (obj instanceof Object) { let subresult: any = {}; for (let key in obj) { subresult[key] = crawl(obj[key], callback, replace, memo); } if (replace) { - merge(obj, subresult); + Object.assign(obj, subresult); } } @@ -66,132 +230,3 @@ function crawl( function classname(obj: Object): string { return ( obj.constructor).name; } - -type SerializerNamespace = { [name: string]: { new (): any } }; - -/** - * A deep serializer of javascript objects. - */ -export class Serializer { - private namespace: SerializerNamespace; - private namespace_rev: { [classname: string]: string }; - private ignored: string[] = []; - - constructor(namespace: any) { - if (Array.isArray(namespace)) { - this.namespace = {}; - for (let construct of namespace) { - this.namespace[construct.name] = construct; - } - } else { - this.namespace = namespace; - } - - this.namespace_rev = {}; - for (let [key, value] of Object.entries(this.namespace)) { - this.namespace_rev[value.name] = key; - } - } - - /** - * Add a class to the ignore list - */ - addIgnoredClass(cl: string | { new (): any }) { - this.ignored.push(typeof cl === "string" ? cl : cl.name); - } - - /** - * Construct an object from a constructor name - */ - private constructObject(ctype: string): Object { - if (ctype == "Object") { - return {}; - } else { - let cl = this.namespace[ctype]; - if (cl) { - return Object.create(cl.prototype); - } else { - console.error("Can't find class", ctype); - return {}; - } - } - } - - /** - * Serialize an object to a string - */ - serialize(obj: any): string { - // Collect objects - var objects: Object[] = []; - var stats: any = {}; - 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); - } - return value; - } else { - return STOP_CRAWLING; - } - } else { - return value; - } - }); - //console.log("Serialize stats", stats); - - // Serialize objects list, transforming deeper objects to links - var fobjects = objects.map((value) => - { - $c: this.namespace_rev[classname(value)] || classname(value), - $f: merge({}, value), - } - ); - return JSON.stringify(fobjects, (key, value) => { - if ( - key != "$f" && isObject(value) && !value.hasOwnProperty("$c") && - !value.hasOwnProperty("$i") - ) { - return { $i: objects.indexOf(value) }; - } else { - return value; - } - }); - } - - /** - * Unserialize a string to an object - */ - unserialize(data: string): any { - // Unserialize objects list - var fobjects = JSON.parse(data); - - // Reconstruct objects - var objects = fobjects.map((objdata: any) => - merge(this.constructObject(objdata["$c"]), objdata["$f"]) - ); - - // Reconnect links - crawl(objects, (value) => { - if (value instanceof Object && value.hasOwnProperty("$i")) { - return objects[value["$i"]]; - } else { - return value; - } - }, true); - - // Post unserialize hooks - crawl(objects[0], (value) => { - if ( - value instanceof Object && typeof value.postUnserialize === "function" - ) { - value.postUnserialize(); - } - }); - - // First object was the root - return objects[0]; - } -}