Support Map and Set objects

This commit is contained in:
Michaël Lemaire 2021-07-18 23:12:32 +02:00
parent 69a4825f5a
commit f76867da5e
9 changed files with 329 additions and 133 deletions

View file

@ -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
```

1
TODO.md Normal file
View file

@ -0,0 +1 @@
- Add protocol version, to handle breaking changes

15
doc/about.md Normal file
View file

@ -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.

2
doc/index Normal file
View file

@ -0,0 +1,2 @@
about
usage

49
doc/usage.md Normal file
View file

@ -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
```

View file

19
run Executable file
View file

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

View file

@ -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", () => {

View file

@ -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 (<any> 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) =>
<Object> {
$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];
}
}