Initial extract

This commit is contained in:
Michaël Lemaire 2018-12-30 23:22:16 +01:00
commit aa7db7c3d9
9 changed files with 5969 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.venv
dist
node_modules
out

0
README.md Normal file
View file

14
activate_node Normal file
View file

@ -0,0 +1,14 @@
# Activation script for virtual nodejs environment
# Usage:
# source activate_node
if [ \! -f "./activate_node" ]
then
echo "Not in project directory"
exit 1
fi
vdir="./.venv"
test -x "${vdir}/bin/nodeenv" || ( python3 -m venv "${vdir}" && "${vdir}/bin/pip" install --upgrade nodeenv )
test -e "${vdir}/node/bin/activate" || "${vdir}/bin/nodeenv" --node=10.15.0 --force "${vdir}/node"
source "${vdir}/node/bin/activate"

24
jest.config.js Normal file
View file

@ -0,0 +1,24 @@
module.exports = {
transform: {
"^.+\\.ts$": "ts-jest"
},
testRegex: "src/test\\.ts$",
testURL: "http://localhost:8014",
moduleFileExtensions: [
"ts",
"js",
"json",
"node"
],
restoreMocks: true,
collectCoverage: true,
collectCoverageFrom: [
"src/main.ts",
],
coverageDirectory: "out/coverage",
coverageReporters: [
"lcovonly",
"html",
"text-summary"
]
}

5605
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

25
package.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "tk-serializer",
"version": "0.1.30",
"description": "Serializer of javascript objects",
"main": "dist/main.js",
"types": "dist/main.d.ts",
"scripts": {
"build": "tsc",
"test": "jest",
"prepare" : "npm run build",
"prepublishOnly" : "npm test"
},
"files": [
"dist"
],
"author": "Michael Lemaire",
"license": "MIT",
"devDependencies": {
"@types/jest": "^23.3.10",
"jest": "^23.6.0",
"ts-jest": "^23.10.5",
"ts-node": "^7.0.1",
"typescript": "^3.2.2"
}
}

165
src/main.ts Normal file
View file

@ -0,0 +1,165 @@
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
*/
function crawl(obj: any, callback: (item: any) => any, replace = false, memo: any[] = []) {
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)) {
let subresult = obj.map(value => crawl(value, callback, replace, memo));
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 {
return (<any>obj.constructor).name;
}
/**
* A deep serializer of javascript objects.
*/
export class Serializer {
namespace: any;
ignored: string[] = [];
constructor(namespace: any) {
this.namespace = namespace;
}
/**
* Add a class to the ignore list
*/
addIgnoredClass(name: string) {
this.ignored.push(name);
}
/**
* Construct an object from a constructor name
*/
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: 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];
}
}

105
src/test.ts Normal file
View file

@ -0,0 +1,105 @@
import {Serializer} from "./main";
class TestSerializerObj1 {
a: number;
constructor(a = 0) {
this.a = a;
}
}
class TestSerializerObj2 {
a = () => 1
b = [(obj: any) => 2]
}
class TestSerializerObj3 {
a = [1, 2];
postUnserialize() {
this.a.push(3);
}
}
const TEST_NS = {
TestSerializerObj1,
TestSerializerObj2,
TestSerializerObj3
};
describe("Serializer", () => {
function checkReversability(obj: any, namespace = TEST_NS): any {
var serializer = new Serializer(namespace);
var data = serializer.serialize(obj);
serializer = new Serializer(namespace);
var loaded = serializer.unserialize(data);
expect(loaded).toEqual(obj);
return loaded;
}
it("serializes simple objects", () => {
var obj = {
"a": 5,
"b": null,
"c": [{ "a": 2 }, "test"]
};
checkReversability(obj);
});
it("restores objects constructed from class", () => {
var loaded = checkReversability(new TestSerializerObj1(5));
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);
});
it("ignores some classes", () => {
var serializer = new Serializer(TEST_NS);
serializer.addIgnoredClass("TestSerializerObj1");
var data = serializer.serialize({ a: 5, b: new TestSerializerObj1() });
var 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 });
});
});

26
tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"strict": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": true,
"lib": [
"es6",
"dom"
],
"target": "es6",
"module": "es6",
"declaration": true,
"outDir": "dist"
},
"include": [
"src/main.ts"
],
"exclude": [
"dist",
"out",
"node_modules"
]
}