Initial extract
This commit is contained in:
commit
aa7db7c3d9
9 changed files with 5969 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.venv
|
||||
dist
|
||||
node_modules
|
||||
out
|
||||
|
0
README.md
Normal file
0
README.md
Normal file
14
activate_node
Normal file
14
activate_node
Normal 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
24
jest.config.js
Normal 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
5605
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
25
package.json
Normal file
25
package.json
Normal 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
165
src/main.ts
Normal 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
105
src/test.ts
Normal 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
26
tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue