2019-09-13 11:57:21 +00:00
|
|
|
const merge = Object.assign;
|
|
|
|
|
|
|
|
const STOP_CRAWLING = {};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the argument is an instance of a class
|
|
|
|
*/
|
|
|
|
function isObject(value: any): boolean {
|
|
|
|
return value instanceof Object && !Array.isArray(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Recursively crawl through an object, yielding any defined value found along the way
|
|
|
|
*
|
|
|
|
* If *replace* is set to true, the current object is replaced (in array or object attribute) by the result of the callback
|
|
|
|
*
|
|
|
|
* *memo* is used to prevent circular references to be traversed
|
|
|
|
*/
|
2020-05-13 09:18:22 +00:00
|
|
|
function crawl(
|
|
|
|
obj: any,
|
|
|
|
callback: (item: any) => any,
|
|
|
|
replace = false,
|
|
|
|
memo: any[] = [],
|
|
|
|
) {
|
2019-09-13 11:57:21 +00:00
|
|
|
if (isObject(obj)) {
|
|
|
|
if (memo.indexOf(obj) >= 0) {
|
|
|
|
return obj;
|
|
|
|
} else {
|
|
|
|
memo.push(obj);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (obj !== undefined && obj !== null && typeof obj != "function") {
|
|
|
|
let result = callback(obj);
|
|
|
|
|
|
|
|
if (result === STOP_CRAWLING) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Array.isArray(obj)) {
|
2020-05-13 09:18:22 +00:00
|
|
|
let subresult = obj.map((value) => crawl(value, callback, replace, memo));
|
2019-09-13 11:57:21 +00:00
|
|
|
if (replace) {
|
|
|
|
subresult.forEach((value, index) => {
|
|
|
|
obj[index] = value;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
} else {
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the class name of an object.
|
|
|
|
*/
|
|
|
|
function classname(obj: Object): string {
|
2020-05-13 09:18:22 +00:00
|
|
|
return (<any> obj.constructor).name;
|
2019-09-13 11:57:21 +00:00
|
|
|
}
|
|
|
|
|
2020-05-13 09:18:22 +00:00
|
|
|
type SerializerNamespace = { [name: string]: { new (): any } };
|
2019-11-07 22:59:34 +00:00
|
|
|
|
2019-09-13 11:57:21 +00:00
|
|
|
/**
|
|
|
|
* A deep serializer of javascript objects.
|
|
|
|
*/
|
|
|
|
export class Serializer {
|
2019-11-07 22:59:34 +00:00
|
|
|
private namespace: SerializerNamespace;
|
|
|
|
private namespace_rev: { [classname: string]: string };
|
2019-09-13 11:57:21 +00:00
|
|
|
private ignored: string[] = [];
|
|
|
|
|
|
|
|
constructor(namespace: any) {
|
2019-11-07 22:59:34 +00:00
|
|
|
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;
|
|
|
|
}
|
2019-09-13 11:57:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a class to the ignore list
|
|
|
|
*/
|
2020-05-13 09:18:22 +00:00
|
|
|
addIgnoredClass(cl: string | { new (): any }) {
|
2019-11-07 22:59:34 +00:00
|
|
|
this.ignored.push(typeof cl === "string" ? cl : cl.name);
|
2019-09-13 11:57:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 = {};
|
2020-05-13 09:18:22 +00:00
|
|
|
crawl(obj, (value) => {
|
2019-09-13 11:57:21 +00:00
|
|
|
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
|
2020-05-13 09:18:22 +00:00
|
|
|
var fobjects = objects.map((value) =>
|
|
|
|
<Object> {
|
|
|
|
$c: this.namespace_rev[classname(value)] || classname(value),
|
|
|
|
$f: merge({}, value),
|
|
|
|
}
|
|
|
|
);
|
2019-09-13 11:57:21 +00:00
|
|
|
return JSON.stringify(fobjects, (key, value) => {
|
2020-05-13 09:18:22 +00:00
|
|
|
if (
|
|
|
|
key != "$f" && isObject(value) && !value.hasOwnProperty("$c") &&
|
|
|
|
!value.hasOwnProperty("$i")
|
|
|
|
) {
|
2019-09-13 11:57:21 +00:00
|
|
|
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
|
2020-05-13 09:18:22 +00:00
|
|
|
var objects = fobjects.map((objdata: any) =>
|
|
|
|
merge(this.constructObject(objdata["$c"]), objdata["$f"])
|
|
|
|
);
|
2019-09-13 11:57:21 +00:00
|
|
|
|
|
|
|
// Reconnect links
|
2020-05-13 09:18:22 +00:00
|
|
|
crawl(objects, (value) => {
|
|
|
|
if (value instanceof Object && value.hasOwnProperty("$i")) {
|
|
|
|
return objects[value["$i"]];
|
2019-09-13 11:57:21 +00:00
|
|
|
} else {
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
}, true);
|
|
|
|
|
|
|
|
// Post unserialize hooks
|
2020-05-13 09:18:22 +00:00
|
|
|
crawl(objects[0], (value) => {
|
|
|
|
if (
|
|
|
|
value instanceof Object && typeof value.postUnserialize === "function"
|
|
|
|
) {
|
2019-09-13 11:57:21 +00:00
|
|
|
value.postUnserialize();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// First object was the root
|
|
|
|
return objects[0];
|
|
|
|
}
|
|
|
|
}
|