Support Map and Set objects
This commit is contained in:
parent
69a4825f5a
commit
f76867da5e
66
README.md
66
README.md
|
@ -1,3 +1,69 @@
|
||||||
# typescript/serializer
|
# typescript/serializer
|
||||||
|
|
||||||
[![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/serializer?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=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
|
||||||
|
```
|
||||||
|
|
15
doc/about.md
Normal file
15
doc/about.md
Normal 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.
|
49
doc/usage.md
Normal file
49
doc/usage.md
Normal 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
|
||||||
|
```
|
19
run
Executable file
19
run
Executable 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
|
|
@ -67,8 +67,17 @@ it("serializes simple objects", () => {
|
||||||
"a": 5,
|
"a": 5,
|
||||||
"b": null,
|
"b": null,
|
||||||
"c": [{ "a": 2 }, "test"],
|
"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", () => {
|
it("restores objects constructed from class", () => {
|
||||||
|
|
299
serializer.ts
299
serializer.ts
|
@ -1,7 +1,160 @@
|
||||||
const merge = Object.assign;
|
|
||||||
|
|
||||||
const STOP_CRAWLING = {};
|
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
|
* Check if the argument is an instance of a class
|
||||||
*/
|
*/
|
||||||
|
@ -44,13 +197,24 @@ function crawl(
|
||||||
obj[index] = value;
|
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) {
|
} else if (obj instanceof Object) {
|
||||||
let subresult: any = {};
|
let subresult: any = {};
|
||||||
for (let key in obj) {
|
for (let key in obj) {
|
||||||
subresult[key] = crawl(obj[key], callback, replace, memo);
|
subresult[key] = crawl(obj[key], callback, replace, memo);
|
||||||
}
|
}
|
||||||
if (replace) {
|
if (replace) {
|
||||||
merge(obj, subresult);
|
Object.assign(obj, subresult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,132 +230,3 @@ function crawl(
|
||||||
function classname(obj: Object): string {
|
function classname(obj: Object): string {
|
||||||
return (<any> obj.constructor).name;
|
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue