Normalized on tk-base

This commit is contained in:
Michaël Lemaire 2019-09-13 13:57:21 +02:00
parent 49e2c36673
commit f1c07354c2
14 changed files with 6973 additions and 2527 deletions

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
max_line_length = off

6
.gitignore vendored
View file

@ -1,5 +1,5 @@
.venv
dist
node_modules
coverage
.rts2_cache_*
.coverage
/dist/

View file

@ -1,5 +0,0 @@
language: node_js
node_js:
- "node"
after_success:
- "npm run codecov"

View file

@ -1,11 +1,6 @@
tk-serializer
=============
[![Build status](https://img.shields.io/travis/thunderk/tk-serializer.svg)](https://travis-ci.org/thunderk/tk-serializer)
[![Code coverage](https://img.shields.io/codecov/c/github/thunderk/tk-serializer.svg)](https://codecov.io/gh/thunderk/tk-serializer)
[![NPM version](https://img.shields.io/npm/v/tk-serializer.svg)](https://www.npmjs.com/package/tk-serializer)
[![Minified size](https://img.shields.io/bundlephobia/min/tk-serializer.svg)](https://bundlephobia.com/result?p=tk-serializer)
About
-----
@ -15,19 +10,39 @@ Deep objects state may be serialized to a string, and reconstructed back.
Class instances are reconstructed properly, as long as they are in the provided namespace. Circular references are handled.
Be warned that resulting serialized value may be quite large.
Typescript definitions are included.
Install
-------
npm install tk-serializer
Import in node:
```shell
npm install tk-serializer
```
```javascript
import { Serializer } from "tk-serializer";
```
Import in browser:
```html
<script src="https://unpkg.com/tk-serializer"></script>
```
```javascript
const Serializer = tkSerializer.Serializer;
```
Use
---
Suppose you have 2 classes Class1 and Class2, whose instances you want to serialize.
```typescript
import { Serializer } from "tk-serializer";
Suppose you have 2 classes Class1 and Class2, whose instances you want to serialize:
```javascript
const namespace = {
Class1,
Class2
@ -44,7 +59,7 @@ let serializer = new Serializer(namespace);
serializer.addIgnoredClass("Class3");
// Serialize the object to a string
let state: string = serializer.serialize(obj);
let state = serializer.serialize(obj);
// Reconstruct the object back (*c* will be undefined)
let nobj = serializer.unserialize(state);

View file

@ -3,7 +3,7 @@
# source activate_node
vdir="./.venv"
expected="10.15.0"
expected="10.16.3"
if [ \! -f "./activate_node" ]
then

View file

@ -1,24 +1,23 @@
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"
]
transform: {
"^.+\\.ts$": "ts-jest"
},
moduleFileExtensions: [
"ts",
"js",
"json",
"node"
],
restoreMocks: true,
collectCoverage: true,
collectCoverageFrom: [
"src/**/*.ts",
"!src/**/*.test.ts",
],
coverageDirectory: ".coverage",
coverageReporters: [
"lcovonly",
"html",
"text-summary"
]
}

8777
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,35 +1,32 @@
{
"name": "tk-serializer",
"version": "1.0.1",
"version": "1.1.0",
"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",
"codecov": "codecov"
},
"files": [
"dist"
],
"author": {
"name": "Michael Lemaire",
"email": "michael@thunderk.net",
"name": "Michaël Lemaire",
"url": "https://thunderk.net"
},
"repository": {
"type": "git",
"url": "https://github.com/thunderk/tk-serializer.git"
"url": "https://code.thunderk.net/tslib/tk-serializer.git"
},
"license": "MIT",
"license": "ISC",
"source": "src/index.ts",
"main": "dist/tk-serializer.js",
"umd:main": "dist/tk-serializer.js",
"types": "dist/src/index.d.ts",
"scripts": {
"build": "npx tk-base build",
"test": "npx tk-base test",
"prepare": "npm run build",
"prepublishOnly": "npm test",
"normalize": "npx tk-base normalize"
},
"files": [
"/src",
"/dist"
],
"devDependencies": {
"@types/jest": "^23.3.10",
"codecov": "^3.1.0",
"jest": "^23.6.0",
"ts-jest": "^23.10.5",
"ts-node": "^7.0.1",
"typescript": "^3.2.2"
"tk-base": "^0.1.2"
}
}

1
src/index.ts Normal file
View file

@ -0,0 +1 @@
export { Serializer } from "./serializer";

View file

@ -1,165 +0,0 @@
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 {
private namespace: any;
private 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
*/
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: 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/serializer.test.ts Normal file
View file

@ -0,0 +1,105 @@
import { Serializer } from "./serializer";
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 });
});
});

165
src/serializer.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 {
private namespace: any;
private 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
*/
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: 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];
}
}

View file

@ -1,105 +0,0 @@
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 });
});
});

View file

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