Switched to deno

This commit is contained in:
Michaël Lemaire 2020-05-13 11:18:22 +02:00
parent b9388a8f11
commit f915a4b889
15 changed files with 238 additions and 8993 deletions

View file

@ -1,10 +1,9 @@
root = true
[*]
[*.{ts,json}]
charset = utf-8
end_of_line = lf
insert_final_newline = 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

8
.gitignore vendored
View file

@ -1,5 +1,3 @@
.venv
node_modules
.rts2_cache_*
.coverage
/dist/
deno.d.ts
.vscode
.local

View file

@ -1,11 +0,0 @@
image: node:latest
cache:
paths:
- node_modules/
test:
before_script:
- npm install
script:
- npm test

View file

@ -1,73 +0,0 @@
tk-serializer
=============
[![pipeline status](https://gitlab.com/thunderk/tk-serializer/badges/master/pipeline.svg)](https://gitlab.com/thunderk/tk-serializer/commits/master)
[![coverage report](https://gitlab.com/thunderk/tk-serializer/badges/master/coverage.svg)](https://gitlab.com/thunderk/tk-serializer/commits/master)
[![npm version](https://img.shields.io/npm/v/tk-serializer.svg)](https://npmjs.com/tk-serializer)
[![npm size](https://img.shields.io/bundlephobia/min/tk-serializer.svg)](https://bundlephobia.com/result?p=tk-serializer)
About
-----
This library offers a generic serialization system for Javascript.
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.
Issues can be reported on [GitLab](https://gitlab.com/thunderk/tk-serializer/issues).
Install
-------
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:
```javascript
const namespace = {
Class1,
Class2
};
const obj = {
a: [1, "a", new Class1()],
b: new Class2("x"),
c: new Class3()
};
let 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
let state = serializer.serialize(obj);
// Reconstruct the object back (*c* will be undefined)
let nobj = serializer.unserialize(state);
```

View file

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

1
all.ts Normal file
View file

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

View file

@ -1,27 +0,0 @@
module.exports = {
transform: {
"^.+\\.ts$": "ts-jest"
},
moduleFileExtensions: [
"ts",
"js",
"json",
"node"
],
watchPathIgnorePatterns: [
"<rootDir>/dist/",
"<rootDir>/node_modules/",
],
restoreMocks: true,
collectCoverage: true,
collectCoverageFrom: [
"src/**/*.ts",
"!src/**/*.test.ts",
],
coverageDirectory: ".coverage",
coverageReporters: [
"lcovonly",
"html",
"text-summary"
]
}

8604
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,43 +0,0 @@
{
"name": "tk-serializer",
"version": "1.2.0",
"description": "Typescript/Javascript serializer, with full object reconstruction",
"author": {
"name": "Michaël Lemaire",
"url": "https://thunderk.net"
},
"repository": {
"type": "git",
"url": "https://code.thunderk.net/tslib/tk-serializer.git"
},
"license": "ISC",
"source": "src/index.ts",
"main": "dist/tk-serializer.umd.js",
"module": "dist/tk-serializer.modern.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "microbundle build -f modern,umd",
"test": "jest",
"prepare": "npm run build",
"prepublishOnly": "npm test",
"normalize": "tk-base",
"dev": "run-p dev:*",
"dev:test": "jest --watchAll",
"dev:build": "microbundle watch -f modern,umd",
"dev:serve": "live-server --host=localhost --port=5000 --no-browser --ignorePattern='.*\\.d\\.ts' dist"
},
"files": [
"/src",
"/dist"
],
"devDependencies": {
"tk-base": "^0.2.5"
},
"bugs": {
"url": "https://gitlab.com/thunderk/tk-serializer/issues"
},
"homepage": "https://code.thunderk.net/tslib/tk-serializer",
"keywords": [
"typescript"
]
}

195
serializer.test.ts Normal file
View file

@ -0,0 +1,195 @@
import {
it,
expect,
patch,
} from "https://code.thunderk.net/typescript/deno_tools/raw/1.0.0/testing.ts";
import { Serializer } from "./serializer.ts";
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);
}
}
class TestSerializerObj4 extends TestSerializerObj1 {
}
const TEST_NS = {
TestSerializerObj1,
TestSerializerObj2,
TestSerializerObj3,
};
const TEST_NS_FLAT = [
TestSerializerObj1,
TestSerializerObj2,
TestSerializerObj3,
];
const TEST_NS_RENAME = {
ob1: TestSerializerObj1,
ob2: TestSerializerObj2,
ob3: TestSerializerObj3,
};
function checkReversability(
obj: any,
namespace: any = TEST_NS,
equality?: (loaded: any, orig: any) => void,
): any {
var serializer = new Serializer(namespace);
var data = serializer.serialize(obj);
serializer = new Serializer(namespace);
var loaded = serializer.unserialize(data);
if (equality) {
equality(loaded, obj);
} else {
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", () => {
let loaded = checkReversability(new TestSerializerObj1(5));
expect(loaded.a).toBe(5);
expect(loaded).toBeInstanceOf(TestSerializerObj1);
loaded = checkReversability(new TestSerializerObj1(5), TEST_NS_FLAT);
expect(loaded.a).toBe(5);
expect(loaded).toBeInstanceOf(TestSerializerObj1);
loaded = checkReversability(new TestSerializerObj1(5), TEST_NS_RENAME);
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, undefined, (loaded) => {
expect(Object.keys(loaded)).toEqual(["b"]);
expect(Object.keys(loaded.b)).toEqual(["c"]);
expect(Object.keys(loaded.b.c)).toEqual(["b"]);
expect(Object.keys(loaded.b.c.b)).toEqual(["c"]);
expect(loaded.b.c).toBe(loaded);
expect(loaded.b.c.b).toBe(loaded.b);
});
});
it("ignores some classes", () => {
let serializer = new Serializer(TEST_NS);
serializer.addIgnoredClass("TestSerializerObj1");
let data = serializer.serialize({ a: 5, b: new TestSerializerObj1() });
let loaded = serializer.unserialize(data);
expect(loaded).toEqual({ a: 5, b: undefined });
serializer = new Serializer(TEST_NS);
serializer.addIgnoredClass(TestSerializerObj1);
data = serializer.serialize({ a: 5, b: new TestSerializerObj1() });
loaded = serializer.unserialize(data);
expect(loaded).toEqual({ a: 5, b: undefined });
serializer = new Serializer(TEST_NS_RENAME);
serializer.addIgnoredClass(TestSerializerObj1);
data = serializer.serialize({ a: 5, b: new TestSerializerObj1() });
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 });
});
it("handles missing classes", () => {
const mock_error = patch(console, "error", () => {});
let serializer = new Serializer(TEST_NS);
mock_error.exec(() => {
let data = serializer.serialize({ obj: new TestSerializerObj4() });
let loaded = serializer.unserialize(data);
expect(loaded).toEqual({ obj: { a: 0 } });
expect(mock_error.fn).toHaveBeenCalledWith(
"Can't find class",
"TestSerializerObj4",
);
});
});
it("uses namespace alias to protect from property mangling", () => {
const data = {
a: new TestSerializerObj1(1),
b: new TestSerializerObj1(2),
c: [new TestSerializerObj1(3)],
};
const serializer1 = new Serializer(TEST_NS);
const serializer2 = new Serializer(TEST_NS_RENAME);
const dumped1 = serializer1.serialize(data);
const dumped2 = serializer2.serialize(data);
expect(dumped1.length).toBeGreaterThan(dumped2.length);
expect(serializer1.unserialize(dumped1)).toEqual(data);
expect(serializer2.unserialize(dumped2)).toEqual(data);
});

View file

@ -16,7 +16,12 @@ function isObject(value: any): boolean {
*
* *memo* is used to prevent circular references to be traversed
*/
function crawl(obj: any, callback: (item: any) => any, replace = false, memo: any[] = []) {
function crawl(
obj: any,
callback: (item: any) => any,
replace = false,
memo: any[] = [],
) {
if (isObject(obj)) {
if (memo.indexOf(obj) >= 0) {
return obj;
@ -33,7 +38,7 @@ function crawl(obj: any, callback: (item: any) => any, replace = false, memo: an
}
if (Array.isArray(obj)) {
let subresult = obj.map(value => crawl(value, callback, replace, memo));
let subresult = obj.map((value) => crawl(value, callback, replace, memo));
if (replace) {
subresult.forEach((value, index) => {
obj[index] = value;
@ -59,10 +64,10 @@ function crawl(obj: any, callback: (item: any) => any, replace = false, memo: an
* Get the class name of an object.
*/
function classname(obj: Object): string {
return (<any>obj.constructor).name;
return (<any> obj.constructor).name;
}
type SerializerNamespace = { [name: string]: { new(): any } };
type SerializerNamespace = { [name: string]: { new (): any } };
/**
* A deep serializer of javascript objects.
@ -91,7 +96,7 @@ export class Serializer {
/**
* Add a class to the ignore list
*/
addIgnoredClass(cl: string | { new(): any }) {
addIgnoredClass(cl: string | { new (): any }) {
this.ignored.push(typeof cl === "string" ? cl : cl.name);
}
@ -119,7 +124,7 @@ export class Serializer {
// Collect objects
var objects: Object[] = [];
var stats: any = {};
crawl(obj, value => {
crawl(obj, (value) => {
if (isObject(value)) {
var vtype = classname(value);
if (vtype != "" && this.ignored.indexOf(vtype) < 0) {
@ -138,9 +143,17 @@ export class Serializer {
//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) });
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")) {
if (
key != "$f" && isObject(value) && !value.hasOwnProperty("$c") &&
!value.hasOwnProperty("$i")
) {
return { $i: objects.indexOf(value) };
} else {
return value;
@ -156,20 +169,24 @@ export class Serializer {
var fobjects = JSON.parse(data);
// Reconstruct objects
var objects = fobjects.map((objdata: any) => merge(this.constructObject(objdata['$c']), objdata['$f']));
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']];
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") {
crawl(objects[0], (value) => {
if (
value instanceof Object && typeof value.postUnserialize === "function"
) {
value.postUnserialize();
}
});

View file

@ -1,7 +0,0 @@
import * as index from "./index";
describe("index", () => {
it("exposes Serializer", () => {
expect(index.Serializer).toBeTruthy();
});
})

View file

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

View file

@ -1,172 +0,0 @@
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);
}
}
class TestSerializerObj4 extends TestSerializerObj1 {
}
const TEST_NS = {
TestSerializerObj1,
TestSerializerObj2,
TestSerializerObj3,
};
const TEST_NS_FLAT = [
TestSerializerObj1,
TestSerializerObj2,
TestSerializerObj3,
];
const TEST_NS_RENAME = {
ob1: TestSerializerObj1,
ob2: TestSerializerObj2,
ob3: TestSerializerObj3,
}
describe("Serializer", () => {
function checkReversability(obj: any, namespace: any = 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", () => {
let loaded = checkReversability(new TestSerializerObj1(5));
expect(loaded.a).toBe(5);
expect(loaded).toBeInstanceOf(TestSerializerObj1);
loaded = checkReversability(new TestSerializerObj1(5), TEST_NS_FLAT);
expect(loaded.a).toBe(5);
expect(loaded).toBeInstanceOf(TestSerializerObj1);
loaded = checkReversability(new TestSerializerObj1(5), TEST_NS_RENAME);
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", () => {
let serializer = new Serializer(TEST_NS);
serializer.addIgnoredClass("TestSerializerObj1");
let data = serializer.serialize({ a: 5, b: new TestSerializerObj1() });
let loaded = serializer.unserialize(data);
expect(loaded).toEqual({ a: 5, b: undefined });
serializer = new Serializer(TEST_NS);
serializer.addIgnoredClass(TestSerializerObj1);
data = serializer.serialize({ a: 5, b: new TestSerializerObj1() });
loaded = serializer.unserialize(data);
expect(loaded).toEqual({ a: 5, b: undefined });
serializer = new Serializer(TEST_NS_RENAME);
serializer.addIgnoredClass(TestSerializerObj1);
data = serializer.serialize({ a: 5, b: new TestSerializerObj1() });
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 });
});
it("handles missing classes", () => {
jest.spyOn(console, "error").mockImplementation();
let serializer = new Serializer(TEST_NS);
let data = serializer.serialize({ obj: new TestSerializerObj4() });
let loaded = serializer.unserialize(data);
expect(loaded).toEqual({ obj: { a: 0 } });
expect(console.error).toHaveBeenCalledWith("Can't find class", "TestSerializerObj4");
});
it("uses namespace alias to protect from property mangling", () => {
const data = {
a: new TestSerializerObj1(1),
b: new TestSerializerObj1(2),
c: [new TestSerializerObj1(3)],
};
const serializer1 = new Serializer(TEST_NS);
const serializer2 = new Serializer(TEST_NS_RENAME);
const dumped1 = serializer1.serialize(data);
const dumped2 = serializer2.serialize(data);
expect(dumped1.length).toBeGreaterThan(dumped2.length);
expect(serializer1.unserialize(dumped1)).toEqual(data);
expect(serializer2.unserialize(dumped2)).toEqual(data);
});
});

View file

@ -1,21 +1,10 @@
{
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true,
"module": "esnext",
"target": "ESNext",
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"preserveConstEnums": true,
"declaration": true,
"target": "es6",
"lib": [
"dom",
"es6"
]
},
"exclude": [
"node_modules",
"dist",
"src/**/*.test.ts"
]
"preserveConstEnums": true
}
}