├── .benchmark
├── README.md
├── output.svg
└── serializer.bench.ts
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── deno.json
├── deserialize.test.ts
├── deserialize.ts
├── mod.ts
├── parse.test.ts
├── parse.ts
├── scripts
├── benchmark.ts
└── build_npm.ts
├── serialize.test.ts
├── serialize.ts
├── serializer.test.ts
├── serializer.ts
├── symbol.ts
└── types.ts
/.benchmark/README.md:
--------------------------------------------------------------------------------
1 | # Superserial Benchmark
2 |
3 | 
4 |
--------------------------------------------------------------------------------
/.benchmark/output.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.benchmark/serializer.bench.ts:
--------------------------------------------------------------------------------
1 | import { Serializer } from "../serializer.ts";
2 | import * as devalue from "npm:devalue@4.2.2";
3 | import * as flatted from "npm:flatted@3.2.7";
4 | import * as superjson from "npm:superjson@1.12.2";
5 |
6 | const origin = {
7 | string: "String",
8 | number: 1000.0,
9 | true: true,
10 | false: false,
11 | null: null,
12 | array: [1, 2, 3, 4, 5],
13 | object: {
14 | items: [{ name: 1 }, { name: 2 }, { name: 3 }],
15 | },
16 | };
17 |
18 | const serializer = new Serializer();
19 |
20 | Deno.bench({
21 | name: "serialize #Serializer.serialize",
22 | baseline: true,
23 | group: "serialize",
24 | fn: () => {
25 | serializer.serialize(origin);
26 | },
27 | });
28 |
29 | Deno.bench({
30 | name: "serialize #JSON.stringify",
31 | group: "serialize",
32 | fn: () => {
33 | JSON.stringify(origin);
34 | },
35 | });
36 |
37 | Deno.bench({
38 | name: "serialize #devalue.stringify",
39 | group: "serialize",
40 | fn: () => {
41 | devalue.stringify(origin);
42 | },
43 | });
44 |
45 | Deno.bench({
46 | name: "serialize #flatted.stringify",
47 | group: "serialize",
48 | fn: () => {
49 | flatted.stringify(origin);
50 | },
51 | });
52 |
53 | Deno.bench({
54 | name: "serialize #superjson.stringify",
55 | group: "serialize",
56 | fn: () => {
57 | superjson.stringify(origin);
58 | },
59 | });
60 |
61 | const serializedSuperserial = serializer.serialize(origin);
62 | Deno.bench({
63 | name: "serialize #Serializer.deserialize",
64 | baseline: true,
65 | group: "deserialize",
66 | fn: () => {
67 | serializer.deserialize(serializedSuperserial);
68 | },
69 | });
70 |
71 | const serializedJson = JSON.stringify(origin);
72 | Deno.bench({
73 | name: "serialize #JSON.parse",
74 | group: "deserialize",
75 | fn: () => {
76 | JSON.parse(serializedJson);
77 | },
78 | });
79 |
80 | const serializedDevalue = devalue.stringify(origin);
81 | Deno.bench({
82 | name: "serialize #devalue.parse",
83 | group: "deserialize",
84 | fn: () => {
85 | devalue.parse(serializedDevalue);
86 | },
87 | });
88 |
89 | const serializedFlatted = flatted.stringify(origin);
90 | Deno.bench({
91 | name: "serialize #flatted.parse",
92 | group: "deserialize",
93 | fn: () => {
94 | flatted.parse(serializedFlatted);
95 | },
96 | });
97 |
98 | const serializedSuperjson = superjson.stringify(origin);
99 | Deno.bench({
100 | name: "serialize #superjson.parse",
101 | group: "deserialize",
102 | fn: () => {
103 | superjson.parse(serializedSuperjson);
104 | },
105 | });
106 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | deno-version: [v1.x]
15 | steps:
16 | - name: Git Checkout Deno Module
17 | uses: actions/checkout@v2
18 | - name: Use Deno Version ${{ matrix.deno-version }}
19 | uses: denoland/setup-deno@v1
20 | with:
21 | deno-version: ${{ matrix.deno-version }}
22 | - name: Format
23 | run: deno fmt --check
24 | - name: Lint
25 | run: deno lint
26 | - name: Unit Test
27 | run: deno test --coverage=coverage
28 | - name: Create coverage report
29 | run: deno coverage ./coverage --lcov > coverage.lcov
30 | - name: Collect coverage
31 | uses: codecov/codecov-action@v1.0.10
32 | with:
33 | file: ./coverage.lcov
34 | - name: Build Module
35 | run: deno task build:npm
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .npm
2 | deno.lock
3 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "denoland.vscode-deno",
4 | "streetsidesoftware.code-spell-checker",
5 | "github.vscode-github-actions"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.lint": true,
4 | "deno.unstable": true,
5 | "editor.formatOnSave": true,
6 | "editor.defaultFormatter": "denoland.vscode-deno",
7 | "cSpell.words": [
8 | "deno",
9 | "denostack",
10 | "superserial"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022-2022 Changwan Jun
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # superserial
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | A comprehensive Serializer/Deserializer that can handle any data type.
16 |
17 | ## Usage
18 |
19 | ### with Deno
20 |
21 | ```bash
22 | deno add @denostack/superserial
23 | ```
24 |
25 | ```ts
26 | import { Serializer } from "@denostack/superserial";
27 |
28 | const serializer = new Serializer();
29 |
30 | const nodes = [{ self: null as any, siblings: [] as any[] }, {
31 | self: null as any,
32 | siblings: [] as any[],
33 | }];
34 | nodes[0].self = nodes[0];
35 | nodes[0].siblings = nodes;
36 | nodes[1].self = nodes[1];
37 | nodes[1].siblings = nodes;
38 |
39 | const serialized = serializer.serialize(nodes);
40 |
41 | console.log(serialized);
42 | // [$1,$2];{"self":$1,"siblings":$0};{"self":$2,"siblings":$0}
43 | ```
44 |
45 | ### with Node.js & Browser
46 |
47 | **Install**
48 |
49 | ```bash
50 | npm install superserial
51 | ```
52 |
53 | ```ts
54 | import { Serializer } from "superserial";
55 |
56 | // Usage is as above :-)
57 | ```
58 |
59 | ## Index
60 |
61 | - [Built-in Objects](#built-in-objects)
62 | - [Circular Reference](#circular-reference)
63 | - [Class Support](#class-support)
64 |
65 | ### Built-in Objects
66 |
67 | **Value Properties**
68 |
69 | - `NaN`
70 | - `Infinity`, `-Infinity`
71 | - `undefined`
72 |
73 | ```ts
74 | serializer.serialize({
75 | und: undefined,
76 | nan: NaN,
77 | inf: Infinity,
78 | ninf: -Infinity,
79 | }); // {"und":undefined,"nan":NaN,"inf":Infinity,"ninf":-Infinity}
80 | ```
81 |
82 | **Fundamental Objects**
83 |
84 | - `Symbol`
85 |
86 | **ETC**
87 |
88 | - `BigInt`
89 | - `Date`
90 | - `RegExp`
91 | - `Map`
92 | - `Set`
93 |
94 | ```ts
95 | const symbol = Symbol();
96 | serializer.serialize({
97 | sym: symbol,
98 | bigint: 100n,
99 | date: new Date(),
100 | regex: /abc/gmi,
101 | map: new Map([["key1", "value1"], ["key2", "value2"]]),
102 | set: new Set([1, 2, 3, 4]),
103 | });
104 | // {"sym":$1,"bigint":100n,"date":$2,"regex":$3,"map":$4,"set":$5};Symbol();Date(1648740167514);/abc/gim;Map("key1"=>"value1","key2"=>"value2");Set(1,2,3,4)
105 | ```
106 |
107 | ### Circular Reference
108 |
109 | Existing JSON functions do not support circular references, but **superserial**
110 | has solved this problem.
111 |
112 | ```ts
113 | const nodes = [{ self: null as any, siblings: [] as any[] }, {
114 | self: null as any,
115 | siblings: [] as any[],
116 | }];
117 | nodes[0].self = nodes[0];
118 | nodes[0].siblings = nodes;
119 | nodes[1].self = nodes[1];
120 | nodes[1].siblings = nodes;
121 |
122 | const serialized = serializer.serialize(nodes);
123 |
124 | console.log(serialized);
125 | // [$1,$2];{"self":$1,"siblings":$0};{"self":$2,"siblings":$0}
126 |
127 | const deserialized = serializer.deserialize(serialized) as typeof nodes;
128 |
129 | console.log(deserialized === deserialized[0].siblings); // true
130 | console.log(deserialized[0] === deserialized[0].self); // true
131 | console.log(deserialized === deserialized[1].siblings); // true
132 | console.log(deserialized[1] === deserialized[1].self); // true
133 | ```
134 |
135 | **Circular Set & Map**
136 |
137 | ```ts
138 | const set = new Set();
139 | set.add(set);
140 |
141 | serializer.serialize(set); // Set($0)
142 |
143 | const map = new Map();
144 | map.set(map, map);
145 |
146 | serializer.serialize(map); // Map($0=>$0)
147 | ```
148 |
149 | Deserialization also works perfectly!
150 |
151 | ```ts
152 | const set = serializer.deserialize("Set($0)");
153 |
154 | console.log(set === [...set][0]); // true
155 |
156 | const map = serializer.deserialize("Map($0=>$0)");
157 |
158 | console.log(map === [...map.keys()][0]); // true
159 | console.log(map === map.get([...map.keys()][0])); // true
160 | ```
161 |
162 | ### Class Support
163 |
164 | Classes contain methods, getters, etc., but JSON doesn't fully support them.
165 | **superserial** includes features that make it easy to use.
166 |
167 | The class to be used for `deserialize` is defined when the Serializer is
168 | created.
169 |
170 | ```ts
171 | class TestUser {
172 | constructor(
173 | public name?: string,
174 | public age?: number,
175 | ) {
176 | }
177 | }
178 |
179 | const serializer = new Serializer({ classes: { TestUser } });
180 | ```
181 |
182 | Serializes the object and then deserializes it again. Since the original class
183 | object is converted as it is, all getters and methods can be used as they are.
184 |
185 | ```ts
186 | const serialized = serializer.serialize(new TestUser("wan2land", 20));
187 | console.log(serialized);
188 | // TestUser{"name":"wan2land","age":20}
189 |
190 | const user = serializer.deserialize(serialized);
191 | console.log(user); // TestUser { name: "wan2land", age: 20 }
192 | ```
193 |
194 | #### Alias
195 |
196 | If you want to serialize a class with a different name, you can use the
197 | `classes` option.
198 |
199 | ```ts
200 | class TestUser {
201 | constructor(
202 | public name?: string,
203 | public age?: number,
204 | ) {
205 | }
206 | }
207 |
208 | const serializer = new Serializer({
209 | classes: {
210 | AliasTestUser: TestUser,
211 | },
212 | });
213 | ```
214 |
215 | ```ts
216 | const serialized = serializer.serialize(new TestUser("wan2land", 20));
217 | console.log(serialized);
218 | // AliasTestUser{"name":"wan2land","age":20} <--- AliasTestUser
219 |
220 | const user = serializer.deserialize(serialized);
221 | console.log(user); // TestUser { name: "wan2land", age: 20 }
222 | ```
223 |
224 | #### toSerialize / toDeserialize
225 |
226 | Private variables can be converted using two special symbols (`toSerialize`,
227 | `toDeserialize`).
228 |
229 | When serializing(`serialize`), the object's data is created based on the
230 | `toSerialize` method. You can check the result of `toSerialize` by looking at
231 | the serialized string.
232 |
233 | When deserializing(`deserialize`), it is impossible to create an object without
234 | a constructor call. (ref.
235 | [No backdoor to access private](https://github.com/tc39/proposal-class-fields#no-backdoor-to-access-private))
236 | If the `toDeserialize` method is included, a value can be injected through
237 | `toDeserialize` after calling the constructor.
238 |
239 | ```ts
240 | import {
241 | Serializer,
242 | toDeserialize,
243 | toSerialize,
244 | } from "https://deno.land/x/superserial/mod.ts";
245 |
246 | class TestUser {
247 | #_age = 0;
248 | constructor(public name: string) {
249 | this.#_age = 0;
250 | }
251 |
252 | setAge(age: number) {
253 | this.#_age = age;
254 | }
255 |
256 | getAge() {
257 | return this.#_age;
258 | }
259 |
260 | [toSerialize]() {
261 | return {
262 | name: this.name,
263 | age: this.#_age,
264 | };
265 | }
266 |
267 | [toDeserialize](
268 | value: {
269 | name: string;
270 | age: number;
271 | },
272 | ) {
273 | this.name = value.name;
274 | this.#_age = value.age;
275 | }
276 | }
277 |
278 | const serializer = new Serializer({ classes: { TestUser } });
279 |
280 | {
281 | const user = new TestUser("wan2land");
282 | user.setAge(20);
283 |
284 | console.log(serializer.serialize(user)); // TestUser{"name":"wan2land","age":20}
285 | }
286 | {
287 | const user = serializer.deserialize(
288 | 'TestUser{"name":"wan2land","age":20}',
289 | );
290 | console.log(user); // TestUser { name: "wan2land" }
291 | console.log(user.getAge()); // 20
292 | }
293 | ```
294 |
295 | ## Benchmark
296 |
297 | Please see [benchmark results](.benchmark).
298 |
299 | ## See also
300 |
301 | - [Creating Superserial](https://wan2.land/posts/2022/09/14/superserial/) - My
302 | blog post about superserial. (Korean)
303 | - [SuperClosure](https://github.com/jeremeamia/super_closure) PHP Serialize
304 | Library, superserial was inspired by this.
305 | - [flatted](https://github.com/WebReflection/flatted)
306 | - [lave](https://github.com/jed/lave)
307 | - [arson](https://github.com/benjamn/arson)
308 | - [devalue](https://github.com/Rich-Harris/devalue)
309 | - [superjson](https://github.com/blitz-js/superjson)
310 |
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@denostack/superserial",
3 | "version": "0.3.5",
4 | "tasks": {
5 | "bench": "deno run --allow-run=deno --allow-write=.benchmark scripts/benchmark.ts",
6 | "test": "deno task test:unit && deno task test:lint && deno task test:format && deno task test:types",
7 | "test:format": "deno fmt --check",
8 | "test:lint": "deno lint",
9 | "test:unit": "deno test -A",
10 | "test:types": "deno check mod.ts",
11 | "build:npm": "deno run --allow-sys --allow-env --allow-read --allow-write --allow-net --allow-run scripts/build_npm.ts"
12 | },
13 | "imports": {
14 | "@deno/dnt": "jsr:@deno/dnt@^0.41.1",
15 | "@std/assert": "jsr:@std/assert@^0.222.1",
16 | "@std/fmt": "jsr:@std/fmt@^0.222.1",
17 | "@std/testing": "jsr:@std/testing@^0.222.1",
18 | "ansi-to-svg": "npm:ansi-to-svg@1.4.3"
19 | },
20 | "exports": {
21 | ".": "./mod.ts",
22 | "./deserialize": "./deserialize.ts",
23 | "./serialize": "./serialize.ts",
24 | "./serializer": "./serializer.ts"
25 | },
26 | "lint": {
27 | "exclude": [".npm"]
28 | },
29 | "fmt": {
30 | "exclude": [".npm"]
31 | },
32 | "lock": false
33 | }
34 |
--------------------------------------------------------------------------------
/deserialize.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertEquals,
3 | assertInstanceOf,
4 | assertNotStrictEquals,
5 | assertStrictEquals,
6 | } from "@std/assert";
7 | import { assertSpyCall, spy } from "@std/testing/mock";
8 |
9 | import { deserialize } from "./deserialize.ts";
10 | import { toDeserialize } from "./symbol.ts";
11 |
12 | Deno.test("deserialize scalar", () => {
13 | assertEquals(deserialize("null"), null);
14 | assertEquals(deserialize("undefined"), undefined);
15 |
16 | assertEquals(deserialize("true"), true);
17 | assertEquals(deserialize("false"), false);
18 |
19 | assertEquals(deserialize("30"), 30);
20 | assertEquals(deserialize("30.1"), 30.1);
21 |
22 | assertEquals(deserialize("30n"), 30n);
23 | assertEquals(deserialize("-30n"), -30n);
24 | assertEquals(deserialize("9007199254740991000000n"), 9007199254740991000000n);
25 | assertEquals(
26 | deserialize("-9007199254740991000000n"),
27 | -9007199254740991000000n,
28 | );
29 |
30 | assertEquals(deserialize('"string"'), "string");
31 | });
32 |
33 | Deno.test("deserialize string with escape", () => {
34 | assertEquals(deserialize('"\\\\"'), "\\");
35 | assertEquals(deserialize('"\\\\x00"'), "\\x00");
36 | assertEquals(deserialize('"\\u0000"'), "\x00");
37 | });
38 |
39 | Deno.test("deserialize extend scalar", () => {
40 | assertEquals(deserialize("NaN"), NaN);
41 | assertEquals(deserialize("Infinity"), Infinity);
42 | assertEquals(deserialize("-Infinity"), -Infinity);
43 | });
44 |
45 | Deno.test("deserialize symbol", () => {
46 | const symbol1 = deserialize("Symbol()");
47 | assertEquals(typeof symbol1, "symbol");
48 | assertEquals(symbol1.description, undefined);
49 |
50 | const symbol2 = deserialize('Symbol("desc1")');
51 | assertEquals(typeof symbol2, "symbol");
52 | assertEquals(symbol2.description, "desc1");
53 |
54 | const deserialized = deserialize<[symbol, symbol, symbol, [symbol]]>(
55 | '[$1,$2,$3,$4];Symbol("sym1");Symbol("sym2");Symbol("sym1");[$2]',
56 | );
57 | assertEquals(deserialized[0].description, "sym1");
58 | assertEquals(deserialized[1].description, "sym2");
59 | assertEquals(deserialized[2].description, "sym1");
60 |
61 | assertNotStrictEquals(deserialized[0], deserialized[2]);
62 | assertStrictEquals(
63 | deserialized[1],
64 | deserialized[3][0],
65 | );
66 | });
67 |
68 | Deno.test("deserialize built-in Set", () => {
69 | assertEquals(
70 | deserialize("Set(1,2,3,4,5)"),
71 | new Set([1, 2, 3, 4, 5]),
72 | );
73 | });
74 |
75 | Deno.test("deserialize built-in Set circular", () => {
76 | const deserialized = deserialize>("Set($0)");
77 | assertStrictEquals(
78 | deserialized,
79 | [...deserialized][0],
80 | );
81 | });
82 |
83 | Deno.test("deserialize built-in Map", () => {
84 | assertEquals(
85 | deserialize(
86 | 'Map("string"=>"this is string",true=>"boolean",null=>"null",$1=>"object");{}',
87 | ),
88 | new Map([
89 | ["string", "this is string"],
90 | [true, "boolean"],
91 | [null, "null"],
92 | [{}, "object"],
93 | ]),
94 | );
95 | });
96 |
97 | Deno.test("deserialize build-in Map deep", () => {
98 | const map1 = new Map([["key1_1", "value1_1"], ["key1_2", "value1_2"]]);
99 | const map2 = new Map([["key2_1", "value2_1"], ["key2_2", "value2_2"]]);
100 |
101 | assertEquals(
102 | deserialize(
103 | 'Map("key1"=>$1,$2=>"val2");Map("key1_1"=>"value1_1","key1_2"=>"value1_2");Map("key2_1"=>"value2_1","key2_2"=>"value2_2")',
104 | ),
105 | new Map([
106 | ["key1", map1] as const,
107 | [map2, "val2"] as const,
108 | ]),
109 | );
110 | });
111 |
112 | Deno.test("deserialize build-in Map circular", () => {
113 | const map1 = deserialize("Map($0=>$0)") as Map;
114 | const keys1 = [...map1.keys()];
115 | assertEquals(keys1.length, 1);
116 |
117 | assertStrictEquals(keys1[0], map1);
118 | assertStrictEquals(map1.get(map1), map1);
119 |
120 | const map2 = deserialize('Map($0=>"val","key"=>$0)') as Map;
121 | const keys2 = [...map2.keys()];
122 | assertEquals(keys2.length, 2);
123 | assertStrictEquals(map2.get(map2), "val");
124 | assertStrictEquals(map2.get("key"), map2);
125 | });
126 |
127 | Deno.test("deserialize array", () => {
128 | assertEquals(deserialize("[]"), []);
129 |
130 | assertEquals(
131 | deserialize(
132 | '[$1,$2,$3];[$4,$5];[1,2];[1];[$6,2,"",false,$7];[$8];{};[];[]',
133 | ),
134 | [[[{}, 2, "", false, []], [[]]], [1, 2], [1]],
135 | );
136 |
137 | assertEquals(
138 | deserialize('[$1,$2];{"name":"wan2land"};{"name":"wan3land"}'),
139 | [{ name: "wan2land" }, { name: "wan3land" }],
140 | );
141 |
142 | assertEquals(
143 | deserialize('{"users":$1};[$2,$3];{"name":"wan2land"};{"name":"wan3land"}'),
144 | { users: [{ name: "wan2land" }, { name: "wan3land" }] },
145 | );
146 |
147 | // also support json
148 | assertEquals(
149 | deserialize('[{"name":"wan2land"},{"name":"wan3land"}]'),
150 | [{ name: "wan2land" }, { name: "wan3land" }],
151 | );
152 | });
153 |
154 | Deno.test("deserialize object", () => {
155 | assertEquals(deserialize("{}"), {});
156 |
157 | assertEquals(deserialize('{"foo":"foo string","und":undefined}'), {
158 | foo: "foo string",
159 | und: undefined,
160 | });
161 |
162 | assertEquals(
163 | deserialize('{"string":$1,"true":$2,"false":$3};"string";true;false'),
164 | { string: "string", true: true, false: false },
165 | );
166 | });
167 |
168 | Deno.test("deserialize object self circular", () => {
169 | const result = deserialize<{ boolean: false; self: unknown }>(
170 | '{"boolean":false,"self":$0}',
171 | );
172 |
173 | assertEquals(result.boolean, false);
174 | assertStrictEquals(result.self, result);
175 | });
176 |
177 | Deno.test("deserialize object circular", () => {
178 | const result = deserialize<
179 | { children: { parent: unknown; sibling: unknown }[] }
180 | >(
181 | '{"children":[$1,$2]};{"parent":$0,"sibling":$2};{"parent":$0,"sibling":$1}',
182 | );
183 |
184 | assertStrictEquals(result.children[0].parent, result);
185 | assertStrictEquals(result.children[1].parent, result);
186 |
187 | assertStrictEquals(result.children[0].sibling, result.children[1]);
188 | assertStrictEquals(result.children[1].sibling, result.children[0]);
189 | });
190 |
191 | Deno.test("deserialize class object", () => {
192 | class TestUser {
193 | age = 0;
194 | constructor(public name: string) {
195 | }
196 | }
197 |
198 | const spyConsole = spy(console, "warn");
199 |
200 | try {
201 | const deserialized = deserialize(
202 | 'TestUser{"name":"wan2land","age":20}',
203 | { classes: { TestUser } },
204 | ) as TestUser;
205 |
206 | assertInstanceOf(deserialized, TestUser);
207 |
208 | assertEquals(deserialized.name, "wan2land");
209 | assertEquals(deserialized.age, 20);
210 |
211 | assertEquals(spyConsole.calls.length, 0);
212 | } finally {
213 | spyConsole.restore();
214 | }
215 | });
216 |
217 | Deno.test("deserialize class object without constructor", () => {
218 | class TestUser {
219 | name?: string;
220 | age?: number;
221 | }
222 |
223 | const spyConsole = spy(console, "warn");
224 |
225 | try {
226 | const deserialized = deserialize(
227 | 'TestUser{"name":"wan2land","age":20}',
228 | { classes: { TestUser } },
229 | ) as TestUser;
230 |
231 | assertInstanceOf(deserialized, TestUser);
232 |
233 | assertEquals(deserialized.name, "wan2land");
234 | assertEquals(deserialized.age, 20);
235 |
236 | assertEquals(spyConsole.calls.length, 0);
237 | } finally {
238 | spyConsole.restore();
239 | }
240 | });
241 |
242 | Deno.test("deserialize class object with loadClass", () => {
243 | class TestUser {
244 | age = 0;
245 | constructor(public name: string) {
246 | }
247 | }
248 |
249 | const loadClass = spy((name: string) => {
250 | if (name === "TestUser") {
251 | return TestUser;
252 | }
253 | return null;
254 | });
255 |
256 | const deserialized = deserialize(
257 | 'TestUser{"name":"wan2land","age":20}',
258 | {
259 | loadClass,
260 | },
261 | ) as TestUser;
262 |
263 | assertInstanceOf(deserialized, TestUser);
264 |
265 | assertEquals(deserialized.name, "wan2land");
266 | assertEquals(deserialized.age, 20);
267 |
268 | assertEquals(loadClass.calls.length, 1);
269 | });
270 |
271 | Deno.test("deserialize class object undefined", () => {
272 | const spyConsole = spy(console, "warn");
273 |
274 | try {
275 | assertEquals(
276 | deserialize(
277 | 'TestUser{"name":"wan2land","age":20}',
278 | ),
279 | {
280 | name: "wan2land",
281 | age: 20,
282 | },
283 | );
284 |
285 | assertSpyCall(
286 | spyConsole,
287 | 0,
288 | {
289 | args: [
290 | "Class TestUser is not defined. It will be ignored.",
291 | ],
292 | },
293 | );
294 | } finally {
295 | spyConsole.restore();
296 | }
297 | });
298 |
299 | Deno.test("deserialize class with private", () => {
300 | class TestUser {
301 | #_age = 0;
302 | constructor(public name: string) {
303 | this.#_age = 0;
304 | }
305 |
306 | setAge(age: number) {
307 | this.#_age = age;
308 | }
309 |
310 | getAge() {
311 | return this.#_age;
312 | }
313 |
314 | [toDeserialize](
315 | value: {
316 | name: string;
317 | age: number;
318 | },
319 | ) {
320 | this.name = value.name;
321 | this.#_age = value.age;
322 | }
323 | }
324 |
325 | const spyConsole = spy(console, "warn");
326 |
327 | try {
328 | const deserialized = deserialize(
329 | 'TestUser{"name":"wan2land","age":20}',
330 | { classes: { TestUser } },
331 | ) as TestUser;
332 |
333 | assertInstanceOf(deserialized, TestUser);
334 |
335 | assertEquals(deserialized.name, "wan2land");
336 | assertEquals(deserialized.getAge(), 20);
337 |
338 | assertEquals(spyConsole.calls.length, 0);
339 | } finally {
340 | spyConsole.restore();
341 | }
342 | });
343 |
344 | Deno.test("deserialize class with private", () => {
345 | class User {
346 | #comments: Comment[] = [];
347 |
348 | constructor(public name: string) {
349 | }
350 |
351 | [toDeserialize](
352 | value: {
353 | name: string;
354 | comments: Comment[];
355 | },
356 | ) {
357 | this.name = value.name;
358 | this.#comments = value.comments;
359 | }
360 |
361 | writeComment(text: string) {
362 | this.#comments.push(new Comment(text));
363 | }
364 |
365 | getComments() {
366 | return this.#comments;
367 | }
368 | }
369 |
370 | class Comment {
371 | constructor(public text: string) {}
372 | }
373 |
374 | const deserialized = deserialize(
375 | 'User{"name":"wan2land","comments":$1};[$2,$3];Comment{"text":"hello world 1"};Comment{"text":"hello world 2"}',
376 | {
377 | classes: {
378 | User,
379 | Comment,
380 | },
381 | },
382 | ) as User;
383 |
384 | assertInstanceOf(deserialized, User);
385 |
386 | assertEquals(deserialized.name, "wan2land");
387 |
388 | const comments = deserialized.getComments();
389 | assertEquals(comments.length, 2);
390 |
391 | assertInstanceOf(comments[0], Comment);
392 | assertInstanceOf(comments[1], Comment);
393 | assertEquals(comments[0].text, "hello world 1");
394 | assertEquals(comments[1].text, "hello world 2");
395 | });
396 |
--------------------------------------------------------------------------------
/deserialize.ts:
--------------------------------------------------------------------------------
1 | import { type AstAny, type AstRoot, parse } from "./parse.ts";
2 | import { toDeserialize } from "./symbol.ts";
3 | import type { ConstructType } from "./types.ts";
4 |
5 | export type ClassLoadHandler = (
6 | name: string,
7 | ) => ConstructType | null | undefined;
8 |
9 | export interface DeserializeOptions {
10 | classes?: { [className: string]: ConstructType };
11 | loadClass?: ClassLoadHandler;
12 | }
13 |
14 | export function deserialize(
15 | ctx: string,
16 | options: DeserializeOptions = {},
17 | ): T {
18 | const mapClasses = options.classes ?? {};
19 | const loadClass: ClassLoadHandler = options.loadClass ?? ((name) => {
20 | const foundClass = name ? mapClasses[name] ?? null : null;
21 | if (name && !foundClass) {
22 | console.warn(`Class ${name} is not defined. It will be ignored.`);
23 | }
24 | return foundClass;
25 | });
26 |
27 | const refs = [] as unknown[];
28 | const valueMap = new Map();
29 | const resolvers = [] as (() => void)[];
30 |
31 | function transformAstAny(ast: AstAny) {
32 | if (ast[0] === 64) {
33 | const index = ast[1];
34 | if (index in refs) {
35 | return refs[index];
36 | }
37 | throw new Error(`not found ref $${index}`);
38 | }
39 | return transformAstRoot(ast);
40 | }
41 |
42 | function transformAstRoot(ast: AstRoot) {
43 | const value = valueMap.get(ast);
44 | if (value) {
45 | return value;
46 | }
47 | switch (ast[0]) {
48 | case 0:
49 | return undefined;
50 | case 1:
51 | return null;
52 | case 2: // boolean
53 | case 3: // number
54 | case 4: // bigint
55 | case 5: // string
56 | return ast[1];
57 | case 6: {
58 | const value = typeof ast[1] === "string" ? Symbol(ast[1]) : Symbol();
59 | valueMap.set(ast, value);
60 | return value;
61 | }
62 | case 16: {
63 | const value = [] as unknown[];
64 | valueMap.set(ast, value);
65 | const items = ast[1];
66 | resolvers.push(() => {
67 | value.push(...items.map(transformAstAny));
68 | });
69 | return value;
70 | }
71 | case 17: {
72 | const name = ast[1];
73 | const entries = ast[2];
74 |
75 | const baseClass = name ? loadClass(name) ?? null : null;
76 | const value = baseClass ? Reflect.construct(baseClass, []) : {};
77 | valueMap.set(ast, value);
78 | resolvers.push(() => {
79 | const merged = Object.fromEntries(
80 | entries.map(([k, v]) => [k[1], transformAstAny(v)]),
81 | );
82 | if (typeof value[toDeserialize] === "function") {
83 | value[toDeserialize](merged);
84 | } else {
85 | Object.assign(value, merged);
86 | }
87 | });
88 | return value;
89 | }
90 | case 32: {
91 | const value = ast[2] ? new RegExp(ast[1], ast[2]) : new RegExp(ast[1]);
92 | valueMap.set(ast, value);
93 | return value;
94 | }
95 | case 33: {
96 | const value = new Date(ast[1]);
97 | valueMap.set(ast, value);
98 | return value;
99 | }
100 | case 34: {
101 | const value = new Set();
102 | valueMap.set(ast, value);
103 | const items = ast[1];
104 | resolvers.push(() => {
105 | for (const item of items) {
106 | value.add(transformAstAny(item));
107 | }
108 | });
109 | return value;
110 | }
111 | case 35: {
112 | const value = new Map();
113 | valueMap.set(ast, value);
114 | const entries = ast[1];
115 | resolvers.push(() => {
116 | for (const [k, v] of entries) {
117 | value.set(transformAstAny(k), transformAstAny(v));
118 | }
119 | });
120 | return value;
121 | }
122 | }
123 | throw new Error(`wrong ast type(${ast[0]})`);
124 | }
125 |
126 | for (const [rootIndex, root] of parse(ctx).entries()) {
127 | refs[rootIndex] = transformAstRoot(root);
128 | }
129 |
130 | let resolver: (() => void) | undefined;
131 | while ((resolver = resolvers.shift())) {
132 | resolver();
133 | }
134 |
135 | return refs[0] as T;
136 | }
137 |
--------------------------------------------------------------------------------
/mod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Serializer/Deserializer that supports everything you can imagine.
3 | *
4 | * ### Example
5 | *
6 | * ```ts
7 | * import { Serializer } from "https://deno.land/x/superserial/mod.ts";
8 | *
9 | * const serializer = new Serializer();
10 | *
11 | * const data = new Set();
12 | * data.add(data); // set referencing itself
13 | *
14 | * const serialized = serializer.serialize(data);
15 | *
16 | * console.log(serialized); // Set($0)
17 | * ```
18 | *
19 | * @module
20 | */
21 |
22 | export type { ConstructType } from "./types.ts";
23 |
24 | export { Serializer, type SerializerOptions } from "./serializer.ts";
25 |
26 | export { serialize, type SerializeOptions } from "./serialize.ts";
27 | export {
28 | type ClassLoadHandler,
29 | deserialize,
30 | type DeserializeOptions,
31 | } from "./deserialize.ts";
32 |
33 | export { toDeserialize, toSerialize } from "./symbol.ts";
34 |
--------------------------------------------------------------------------------
/parse.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertThrows } from "@std/assert";
2 |
3 | import { parse } from "./parse.ts";
4 |
5 | Deno.test("parse, ignore whitespace", () => {
6 | assertEquals(parse(" null "), [[1]]);
7 | assertEquals(parse("\n\r\t\v\f \u00A0\uFEFF\u2028\u2029null "), [[1]]);
8 | });
9 |
10 | Deno.test("parse, undefined", () => {
11 | assertEquals(parse("undefined"), [[0]]);
12 | assertEquals(parse(" undefined "), [[0]]);
13 | });
14 |
15 | Deno.test("parse, null", () => {
16 | assertEquals(parse("null"), [[1]]);
17 | assertEquals(parse(" null "), [[1]]);
18 | });
19 |
20 | Deno.test("parse, boolean", () => {
21 | assertEquals(parse("true"), [[2, true]]);
22 | assertEquals(parse("false"), [[2, false]]);
23 |
24 | assertEquals(parse(" true "), [[2, true]]);
25 | assertEquals(parse(" false "), [[2, false]]);
26 | });
27 |
28 | Deno.test("parse, number", () => {
29 | assertEquals(parse("30"), [[3, 30]]);
30 | assertEquals(parse("30."), [[3, 30]]);
31 | assertEquals(parse("30.1"), [[3, 30.1]]);
32 | assertEquals(parse("30.1e+5"), [[3, 3010000]]);
33 | assertEquals(parse("30.1E-5"), [[3, 0.000301]]);
34 | assertEquals(parse("-30"), [[3, -30]]);
35 | assertEquals(parse("-30."), [[3, -30]]);
36 | assertEquals(parse("-30.1"), [[3, -30.1]]);
37 | assertEquals(parse("-30.1E+5"), [[3, -3010000]]);
38 | assertEquals(parse("-30.1e-5"), [[3, -0.000301]]);
39 |
40 | assertEquals(parse(" 30 "), [[3, 30]]);
41 | assertEquals(parse(" 30. "), [[3, 30]]);
42 | assertEquals(parse(" 30.1 "), [[3, 30.1]]);
43 | assertEquals(parse(" 30.1E+5 "), [[3, 3010000]]);
44 | assertEquals(parse(" 30.1e-5 "), [[3, 0.000301]]);
45 | assertEquals(parse(" -30 "), [[3, -30]]);
46 | assertEquals(parse(" -30. "), [[3, -30]]);
47 | assertEquals(parse(" -30.1 "), [[3, -30.1]]);
48 | assertEquals(parse(" -30.1e+5 "), [[3, -3010000]]);
49 | assertEquals(parse(" -30.1E-5 "), [[3, -0.000301]]);
50 | });
51 |
52 | Deno.test("parse, number error", () => {
53 | assertThrows(
54 | () => parse("3a"),
55 | SyntaxError,
56 | "Unexpected token 'a' in SuperSerial at position 2",
57 | );
58 |
59 | assertThrows(
60 | () => parse("30.1e"),
61 | SyntaxError,
62 | "Unexpected end of SuperSerial input",
63 | );
64 |
65 | assertThrows(
66 | () => parse("30.1e+a"),
67 | SyntaxError,
68 | "Unexpected token 'a' in SuperSerial at position 7",
69 | );
70 | });
71 |
72 | Deno.test("parse, bigint", () => {
73 | assertEquals(parse("30n"), [[4, 30n]]);
74 | assertEquals(parse("-30n"), [[4, -30n]]);
75 | assertEquals(parse("9007199254740991000000n"), [[
76 | 4,
77 | 9007199254740991000000n,
78 | ]]);
79 | assertEquals(parse("-9007199254740991000000n"), [[
80 | 4,
81 | -9007199254740991000000n,
82 | ]]);
83 |
84 | assertEquals(parse(" 30n "), [[4, 30n]]);
85 | assertEquals(parse(" -30n "), [[4, -30n]]);
86 | assertEquals(parse(" 9007199254740991000000n "), [[
87 | 4,
88 | 9007199254740991000000n,
89 | ]]);
90 | assertEquals(parse(" -9007199254740991000000n "), [[
91 | 4,
92 | -9007199254740991000000n,
93 | ]]);
94 | });
95 |
96 | Deno.test("parse, NaN & Infinity", () => {
97 | assertEquals(parse("NaN"), [[3, NaN]]);
98 | assertEquals(parse("Infinity"), [[3, Infinity]]);
99 | assertEquals(parse("-Infinity"), [[3, -Infinity]]);
100 | });
101 |
102 | Deno.test("parse, string", () => {
103 | assertEquals(parse('"string"'), [[5, "string"]]);
104 | assertEquals(parse('"str\\nin\\\\g"'), [[5, "str\nin\\g"]]);
105 |
106 | assertEquals(parse(' "string" '), [[5, "string"]]);
107 | assertEquals(parse(' "str\\nin\\\\g" '), [[5, "str\nin\\g"]]);
108 |
109 | // special characters
110 | assertEquals(parse(' "\\u1234" '), [[5, "\u1234"]]);
111 | assertEquals(parse(' "\\"" '), [[5, '"']]);
112 | assertEquals(parse(' "\\b" '), [[5, "\b"]]);
113 | assertEquals(parse(' "\\f" '), [[5, "\f"]]);
114 | assertEquals(parse(' "\\r" '), [[5, "\r"]]);
115 | assertEquals(parse(' "\\t" '), [[5, "\t"]]);
116 | });
117 |
118 | Deno.test("parse, string error", () => {
119 | assertThrows(
120 | () => parse('"s'),
121 | SyntaxError,
122 | "Unexpected end of SuperSerial input",
123 | );
124 | });
125 |
126 | Deno.test("parse, array", () => {
127 | assertEquals(parse("[]"), [[16, []]]);
128 | assertEquals(parse('[null,true,false,1,10n,"..."]'), [[16, [
129 | [1],
130 | [2, true],
131 | [2, false],
132 | [3, 1],
133 | [4, 10n],
134 | [5, "..."],
135 | ]]]);
136 |
137 | assertEquals(parse(" [ ] "), [[16, []]]);
138 | assertEquals(
139 | parse(' [ null , true , false , 1 , 10n , "..." ] '),
140 | [[
141 | 16,
142 | [
143 | [1],
144 | [2, true],
145 | [2, false],
146 | [3, 1],
147 | [4, 10n],
148 | [5, "..."],
149 | ],
150 | ]],
151 | );
152 | });
153 |
154 | Deno.test("parse, array error", () => {
155 | assertThrows(
156 | () => parse("[1,2,3,]"),
157 | SyntaxError,
158 | "Unexpected token ']' in SuperSerial at position 8",
159 | );
160 | });
161 |
162 | Deno.test("parse, object", () => {
163 | assertEquals(parse("{}"), [[17, null, []]]);
164 | assertEquals(parse('{"name":"wan2land","age":20}'), [[17, null, [
165 | [[5, "name"], [5, "wan2land"]],
166 | [[5, "age"], [3, 20]],
167 | ]]]);
168 | assertEquals(parse("Something{}"), [[17, "Something", []]]);
169 | assertEquals(parse("something{}"), [[17, "something", []]]);
170 |
171 | assertEquals(parse(" { } "), [[17, null, []]]);
172 | assertEquals(parse(' { "name" : "wan2land" , "age" : 20 } '), [[
173 | 17,
174 | null,
175 | [
176 | [[5, "name"], [5, "wan2land"]],
177 | [[5, "age"], [3, 20]],
178 | ],
179 | ]]);
180 | assertEquals(parse(" Something { } "), [[17, "Something", []]]);
181 | assertEquals(parse(" something { } "), [[17, "something", []]]);
182 | });
183 |
184 | Deno.test("parse, object error", () => {
185 | assertThrows(
186 | () => parse('{"name"}'),
187 | SyntaxError,
188 | "Unexpected token '}' in SuperSerial at position 8",
189 | );
190 | assertThrows(
191 | () => parse('{"name":1%'),
192 | SyntaxError,
193 | "Unexpected token '%' in SuperSerial at position 10",
194 | );
195 | });
196 |
197 | Deno.test("parse, regexp", () => {
198 | assertEquals(parse("/a/"), [[32, "a", null]]);
199 | assertEquals(parse("/a\\\\/gmi"), [[32, "a\\\\", "gmi"]]);
200 | assertEquals(parse("/a/img"), [[32, "a", "img"]]);
201 |
202 | assertEquals(parse(" /a/ "), [[32, "a", null]]);
203 | assertEquals(parse(" /a\\\\/gmi "), [[32, "a\\\\", "gmi"]]);
204 | assertEquals(parse(" /a/img "), [[32, "a", "img"]]);
205 | });
206 |
207 | Deno.test("parse, symbol", () => {
208 | assertEquals(parse("Symbol()"), [[6, null]]);
209 | assertEquals(parse('Symbol("description")'), [[6, "description"]]);
210 |
211 | assertEquals(parse(" Symbol ( ) "), [[6, null]]);
212 | assertEquals(parse(' Symbol ( "description" ) '), [[6, "description"]]);
213 | });
214 |
215 | Deno.test("parse, built-in Date", () => {
216 | assertEquals(parse("Date(123456)"), [[33, 123456]]);
217 | assertEquals(parse("Date(-123456)"), [[33, -123456]]);
218 |
219 | assertEquals(parse(" Date ( 123456 ) "), [[33, 123456]]);
220 | assertEquals(parse(" Date ( -123456 ) "), [[33, -123456]]);
221 | });
222 |
223 | Deno.test("parse, built-in Set", () => {
224 | assertEquals(
225 | parse("Set()"),
226 | [[34, []]],
227 | );
228 |
229 | assertEquals(
230 | parse("Set(1,2,3,4,5)"),
231 | [[34, [[3, 1], [3, 2], [3, 3], [3, 4], [3, 5]]]],
232 | );
233 |
234 | assertEquals(
235 | parse(" Set ( 1 , 2 , 3 , 4 , 5 ) "),
236 | [[34, [[3, 1], [3, 2], [3, 3], [3, 4], [3, 5]]]],
237 | );
238 | });
239 |
240 | Deno.test("parse, built-in Map", () => {
241 | assertEquals(
242 | parse(
243 | "Map()",
244 | ),
245 | [[35, []]],
246 | );
247 |
248 | assertEquals(
249 | parse(
250 | 'Map("string"=>"this is string",true=>"boolean",null=>"null",$1=>"object")',
251 | ),
252 | [[35, [
253 | [
254 | [5, "string"],
255 | [5, "this is string"],
256 | ],
257 | [
258 | [2, true],
259 | [5, "boolean"],
260 | ],
261 | [
262 | [1],
263 | [5, "null"],
264 | ],
265 | [
266 | [64, 1],
267 | [5, "object"],
268 | ],
269 | ]]],
270 | );
271 |
272 | assertEquals(
273 | parse(
274 | ' Map ( "string" => "this is string" , true => "boolean" , null => "null" , $1 => "object" ) ',
275 | ),
276 | [[35, [
277 | [
278 | [5, "string"],
279 | [5, "this is string"],
280 | ],
281 | [
282 | [2, true],
283 | [5, "boolean"],
284 | ],
285 | [
286 | [1],
287 | [5, "null"],
288 | ],
289 | [
290 | [64, 1],
291 | [5, "object"],
292 | ],
293 | ]]],
294 | );
295 | });
296 |
297 | Deno.test("parse, ref", () => {
298 | assertEquals(parse("[$0]"), [[16, [[64, 0]]]]);
299 | assertEquals(parse('{"a":$2}'), [[17, null, [[[5, "a"], [64, 2]]]]]);
300 |
301 | assertEquals(parse(" [ $0 ] "), [[16, [[64, 0]]]]);
302 | assertEquals(parse(' { "a" : $2 } '), [[17, null, [[[5, "a"], [
303 | 64,
304 | 2,
305 | ]]]]]);
306 | });
307 |
308 | Deno.test("parse, multiple roots", () => {
309 | assertEquals(parse("1;2;3"), [[3, 1], [3, 2], [3, 3]]);
310 | assertEquals(parse(" 1 ; 2 ; 3 "), [[3, 1], [3, 2], [3, 3]]);
311 | });
312 |
--------------------------------------------------------------------------------
/parse.ts:
--------------------------------------------------------------------------------
1 | const NUM_CHARS = new Set(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]);
2 |
3 | export type AstRoot =
4 | | AstUndefined
5 | | AstNull
6 | | AstBool
7 | | AstNumber
8 | | AstBigInt
9 | | AstString
10 | | AstSymbol
11 | | AstArray
12 | | AstObject
13 | | AstRegExp
14 | | AstDate
15 | | AstSet
16 | | AstMap;
17 |
18 | export type AstAny = AstRoot | AstRef;
19 |
20 | export type AstUndefined = [type: 0];
21 | export type AstNull = [type: 1];
22 | export type AstBool = [type: 2, value: boolean];
23 | export type AstNumber = [type: 3, value: number];
24 | export type AstBigInt = [type: 4, value: bigint];
25 | export type AstString = [type: 5, value: string];
26 | export type AstSymbol = [type: 6, description: string | null];
27 |
28 | export type AstArray = [type: 16, items: AstAny[]];
29 | export type AstObject = [
30 | type: 17,
31 | name: string | null,
32 | entries: [AstString, AstAny][],
33 | ];
34 |
35 | export type AstRegExp = [type: 32, pattern: string, flags: string | null];
36 | export type AstDate = [type: 33, timestamp: number];
37 | export type AstSet = [type: 34, items: AstAny[]];
38 | export type AstMap = [type: 35, entries: [AstAny, AstAny][]];
39 |
40 | export type AstRef = [type: 64, index: number];
41 |
42 | let buf = "";
43 | let pos = 0;
44 |
45 | function consume(s: string) {
46 | for (const c of s) {
47 | if (buf[pos] !== c) {
48 | throw error();
49 | }
50 | pos++;
51 | }
52 | }
53 |
54 | function white() {
55 | while (1) {
56 | switch (buf[pos]) {
57 | case "\t":
58 | case "\v":
59 | case "\f":
60 | case " ":
61 | case "\u00A0":
62 | case "\uFEFF":
63 | case "\n":
64 | case "\r":
65 | case "\u2028":
66 | case "\u2029":
67 | pos++;
68 | break;
69 | default:
70 | return;
71 | }
72 | }
73 | }
74 |
75 | function error() {
76 | return new SyntaxError(
77 | buf[pos]
78 | ? `Unexpected token '${buf[pos]}' in SuperSerial at position ${pos + 1}`
79 | : "Unexpected end of SuperSerial input",
80 | );
81 | }
82 |
83 | function parseAny(): AstAny {
84 | white();
85 | if (buf[pos] === "$") {
86 | pos++;
87 | let result = "";
88 | while (NUM_CHARS.has(buf[pos])) {
89 | result += buf[pos++];
90 | }
91 | return [64, +result];
92 | }
93 | return parseRoot();
94 | }
95 |
96 | function parseRoot(): AstRoot {
97 | white();
98 | switch (buf[pos]) {
99 | case "-":
100 | case "0":
101 | case "1":
102 | case "2":
103 | case "3":
104 | case "4":
105 | case "5":
106 | case "6":
107 | case "7":
108 | case "8":
109 | case "9": {
110 | return parseNumber();
111 | }
112 | case '"':
113 | return parseString();
114 | case "[":
115 | return parseArray();
116 | case "/":
117 | return parseRegExp();
118 | default: {
119 | const name = keyword();
120 | switch (name) {
121 | case "null":
122 | return [1];
123 | case "true":
124 | return [2, true];
125 | case "false":
126 | return [2, false];
127 | }
128 | if (buf[pos] === "{") {
129 | return parseObject(name);
130 | }
131 | switch (name) {
132 | case "undefined":
133 | return [0];
134 | case "NaN":
135 | return [3, NaN];
136 | case "Infinity":
137 | return [3, Infinity];
138 | }
139 | if (buf[pos] === "(") {
140 | switch (name) {
141 | case "Map":
142 | return parseMap();
143 | case "Set":
144 | return parseSet();
145 | case "Date":
146 | return parseDate();
147 | case "Symbol":
148 | return parseSymbol();
149 | default:
150 | throw error();
151 | }
152 | }
153 | }
154 | }
155 |
156 | throw error();
157 | }
158 |
159 | function parseNumber(): AstNumber | AstBigInt {
160 | let result = "";
161 | let mult = 1;
162 |
163 | if (buf[pos] === "-") {
164 | pos++;
165 | mult = -1;
166 | }
167 | if (buf[pos] === "I") {
168 | pos++;
169 | consume("nfinity");
170 | return [3, mult * Infinity];
171 | }
172 | if (NUM_CHARS.has(buf[pos])) {
173 | result += buf[pos++];
174 | } else {
175 | throw error();
176 | }
177 | while (NUM_CHARS.has(buf[pos])) {
178 | result += buf[pos++];
179 | }
180 | if (buf[pos] === "n") {
181 | pos++;
182 | return [4, BigInt(result) * BigInt(mult)];
183 | } else {
184 | if (buf[pos] === ".") {
185 | result += buf[pos++];
186 | while (NUM_CHARS.has(buf[pos])) {
187 | result += buf[pos++];
188 | }
189 | }
190 | if (buf[pos] === "e" || buf[pos] === "E") {
191 | result += buf[pos++];
192 | if (buf[pos] === "-" || buf[pos] === "+") {
193 | result += buf[pos++];
194 | }
195 | if (NUM_CHARS.has(buf[pos])) {
196 | result += buf[pos++];
197 | } else {
198 | throw error();
199 | }
200 | while (NUM_CHARS.has(buf[pos])) {
201 | result += buf[pos++];
202 | }
203 | }
204 | }
205 | return [3, +result * mult];
206 | }
207 |
208 | function parseString(): AstString {
209 | let result = "";
210 | pos++;
211 | while (1) {
212 | if (pos >= buf.length) {
213 | break;
214 | }
215 | switch (buf[pos]) {
216 | case '"':
217 | pos++;
218 | return [5, result];
219 | case "\\":
220 | pos++;
221 | switch (buf[pos]) {
222 | case "u": {
223 | pos++;
224 | let uffff = 0;
225 | for (let i = 0; i < 4; i++) {
226 | const hex = parseInt(buf[pos], 16);
227 | if (!isFinite(hex)) {
228 | throw error();
229 | }
230 | pos++;
231 | uffff = uffff * 16 + hex;
232 | }
233 | result += String.fromCharCode(uffff);
234 | continue;
235 | }
236 | case '"':
237 | pos++;
238 | result += '"';
239 | continue;
240 | case "\\":
241 | pos++;
242 | result += "\\";
243 | continue;
244 | case "b":
245 | pos++;
246 | result += "\b";
247 | continue;
248 | case "f":
249 | pos++;
250 | result += "\f";
251 | continue;
252 | case "n":
253 | pos++;
254 | result += "\n";
255 | continue;
256 | case "r":
257 | pos++;
258 | result += "\r";
259 | continue;
260 | case "t":
261 | pos++;
262 | result += "\t";
263 | continue;
264 | }
265 | break;
266 | default:
267 | result += buf[pos++];
268 | continue;
269 | }
270 | break;
271 | }
272 | throw error();
273 | }
274 |
275 | function parseArray(): AstArray {
276 | pos++;
277 | white();
278 | if (buf[pos] === "]") {
279 | pos++;
280 | return [16, []];
281 | }
282 |
283 | const result = [] as AstAny[];
284 | result.push(parseAny());
285 |
286 | white();
287 | while (buf[pos] === ",") {
288 | pos++;
289 | result.push(parseAny());
290 | white();
291 | }
292 | if (buf[pos] === "]") {
293 | pos++;
294 | return [16, result];
295 | }
296 | throw error();
297 | }
298 |
299 | function parseObject(name: string | null = null): AstObject {
300 | pos++;
301 | white();
302 | if (buf[pos] === "}") {
303 | pos++;
304 | return [17, name, []];
305 | }
306 | const result = [] as [AstString, AstAny][];
307 | while (1) {
308 | const key = parseString(); // TODO Symbol
309 | white();
310 | if (buf[pos] !== ":") {
311 | throw error();
312 | }
313 | pos++;
314 | result.push([key, parseAny()]);
315 | white();
316 | if (buf[pos] === ",") {
317 | pos++;
318 | white();
319 | continue;
320 | }
321 | if (buf[pos] === "}") {
322 | pos++;
323 | return [17, name, result];
324 | }
325 | break;
326 | }
327 | throw error();
328 | }
329 |
330 | function parseRegExp(): AstRegExp {
331 | pos++;
332 | let pattern = "";
333 | if (buf[pos] === "/") {
334 | throw error();
335 | }
336 | while (buf[pos]) {
337 | if (buf[pos] === "/") {
338 | pos++;
339 | switch (buf[pos]) {
340 | case "i": {
341 | pos++;
342 | switch (buf[pos]) {
343 | case "m": {
344 | pos++;
345 | if (buf[pos] === "g") {
346 | pos++;
347 | return [32, pattern, "img"];
348 | } else {
349 | return [32, pattern, "im"];
350 | }
351 | }
352 | case "g": {
353 | pos++;
354 | if (buf[pos] === "m") {
355 | pos++;
356 | return [32, pattern, "igm"];
357 | } else {
358 | return [32, pattern, "ig"];
359 | }
360 | }
361 | default: {
362 | return [32, pattern, "i"];
363 | }
364 | }
365 | }
366 | case "m": {
367 | pos++;
368 | switch (buf[pos]) {
369 | case "i": {
370 | pos++;
371 | if (buf[pos] === "g") {
372 | pos++;
373 | return [32, pattern, "mig"];
374 | } else {
375 | return [32, pattern, "mi"];
376 | }
377 | }
378 | case "g": {
379 | pos++;
380 | if (buf[pos] === "i") {
381 | pos++;
382 | return [32, pattern, "mgi"];
383 | } else {
384 | return [32, pattern, "mg"];
385 | }
386 | }
387 | default: {
388 | return [32, pattern, "m"];
389 | }
390 | }
391 | }
392 | case "g": {
393 | pos++;
394 | switch (buf[pos]) {
395 | case "m": {
396 | pos++;
397 | if (buf[pos] === "i") {
398 | pos++;
399 | return [32, pattern, "gmi"];
400 | } else {
401 | return [32, pattern, "gm"];
402 | }
403 | }
404 | case "i": {
405 | pos++;
406 | if (buf[pos] === "m") {
407 | pos++;
408 | return [32, pattern, "gim"];
409 | } else {
410 | return [32, pattern, "gi"];
411 | }
412 | }
413 | default: {
414 | return [32, pattern, "g"];
415 | }
416 | }
417 | }
418 | }
419 | return [32, pattern, null];
420 | } else if (buf[pos] === "\\") {
421 | pattern += buf[pos++];
422 | pattern += buf[pos++];
423 | } else {
424 | pattern += buf[pos++];
425 | }
426 | }
427 | throw error();
428 | }
429 |
430 | function parseSet(): AstSet {
431 | pos++;
432 | white();
433 | if (buf[pos] === ")") {
434 | pos++;
435 | return [34, []];
436 | }
437 |
438 | const items = [] as AstAny[];
439 | items.push(parseAny());
440 |
441 | white();
442 | while (buf[pos] === ",") {
443 | pos++;
444 | items.push(parseAny());
445 | white();
446 | }
447 | if (buf[pos] === ")") {
448 | pos++;
449 | return [34, items];
450 | }
451 | throw error();
452 | }
453 |
454 | function parseMap(): AstMap {
455 | pos++;
456 | white();
457 | if (buf[pos] === ")") {
458 | pos++;
459 | return [35, []];
460 | }
461 | const entries = [] as [AstAny, AstAny][];
462 | while (1) {
463 | const key = parseAny();
464 | white();
465 | consume("=>");
466 | white();
467 | const value = parseAny();
468 | entries.push([key, value]);
469 | white();
470 | if (buf[pos] === ",") {
471 | pos++;
472 | white();
473 | continue;
474 | }
475 | if (buf[pos] === ")") {
476 | pos++;
477 | break;
478 | }
479 | throw error();
480 | }
481 | return [35, entries];
482 | }
483 |
484 | function parseSymbol(): AstSymbol {
485 | pos++;
486 | white();
487 | if (buf[pos] === ")") {
488 | pos++;
489 | return [6, null];
490 | }
491 | if (buf[pos] === '"') {
492 | const valueString = parseString();
493 | white();
494 | if (buf[pos] === ")") {
495 | pos++;
496 | return [6, valueString[1]];
497 | }
498 | }
499 | throw error();
500 | }
501 |
502 | function parseDate(): AstDate {
503 | pos++;
504 | white();
505 | let value = "";
506 | let mult = 1;
507 | if (buf[pos] === "-") {
508 | pos++;
509 | mult = -1;
510 | }
511 | if (NUM_CHARS.has(buf[pos])) {
512 | value += buf[pos++];
513 | } else {
514 | throw error();
515 | }
516 | while (NUM_CHARS.has(buf[pos])) {
517 | value += buf[pos++];
518 | }
519 | if (buf[pos] === ".") {
520 | value += buf[pos++];
521 | while (NUM_CHARS.has(buf[pos])) {
522 | value += buf[pos++];
523 | }
524 | }
525 | if (buf[pos] === "e" || buf[pos] === "E") {
526 | value += buf[pos++];
527 | if (buf[pos] === "-" || buf[pos] === "+") {
528 | value += buf[pos++];
529 | }
530 | if (NUM_CHARS.has(buf[pos])) {
531 | value += buf[pos++];
532 | } else {
533 | throw error();
534 | }
535 | while (NUM_CHARS.has(buf[pos])) {
536 | value += buf[pos++];
537 | }
538 | }
539 | white();
540 | if (buf[pos] === ")") {
541 | pos++;
542 | return [33, +value * mult];
543 | }
544 | throw error();
545 | }
546 |
547 | function keyword(): string | null {
548 | let chartCode = buf.charCodeAt(pos);
549 | let result = "";
550 | if (
551 | chartCode >= 65 && chartCode <= 90 || // UPPERCASE
552 | chartCode >= 97 && chartCode <= 122 || // lowercase
553 | chartCode === 95 // _
554 | ) {
555 | result += buf[pos++];
556 | } else {
557 | return null;
558 | }
559 | while ((chartCode = buf.charCodeAt(pos))) {
560 | if (
561 | chartCode >= 65 && chartCode <= 90 || // UPPERCASE
562 | chartCode >= 97 && chartCode <= 122 || // lowercase
563 | chartCode >= 48 && chartCode <= 57 || // number
564 | chartCode === 95 // _
565 | ) {
566 | result += buf[pos++];
567 | } else {
568 | break;
569 | }
570 | }
571 | white();
572 | return result;
573 | }
574 |
575 | export function parse(
576 | ctx: string,
577 | ): AstRoot[] {
578 | buf = ctx;
579 | pos = 0;
580 |
581 | const roots = [] as AstRoot[];
582 | roots.push(parseRoot());
583 | white();
584 | while (buf[pos] === ";") {
585 | pos++;
586 | roots.push(parseRoot());
587 | white();
588 | }
589 |
590 | if (buf.length !== pos) {
591 | throw error();
592 | }
593 |
594 | return roots;
595 | }
596 |
--------------------------------------------------------------------------------
/scripts/benchmark.ts:
--------------------------------------------------------------------------------
1 | import ansiToSvg from "ansi-to-svg";
2 |
3 | const cmd = new Deno.Command("deno", {
4 | args: ["bench"],
5 | stdout: "piped",
6 | stderr: "piped",
7 | });
8 |
9 | const commandOutput = await cmd.spawn().output();
10 |
11 | const decoder = new TextDecoder();
12 | const output = decoder.decode(commandOutput.stdout);
13 |
14 | const svg: string = ansiToSvg(output, {
15 | paddingTop: 4,
16 | paddingBottom: 4,
17 | paddingLeft: 8,
18 | paddingRight: 8,
19 | });
20 |
21 | await Deno.writeTextFile("./.benchmark/output.svg", svg);
22 |
--------------------------------------------------------------------------------
/scripts/build_npm.ts:
--------------------------------------------------------------------------------
1 | import { build, emptyDir } from "@deno/dnt";
2 | import { bgGreen } from "@std/fmt/colors";
3 |
4 | const denoInfo = JSON.parse(
5 | Deno.readTextFileSync(new URL("../deno.json", import.meta.url)),
6 | );
7 | const version = denoInfo.version;
8 |
9 | console.log(bgGreen(`version: ${version}`));
10 |
11 | await emptyDir("./.npm");
12 |
13 | await build({
14 | entryPoints: ["./mod.ts"],
15 | outDir: "./.npm",
16 | shims: {
17 | deno: false,
18 | },
19 | test: false,
20 | compilerOptions: {
21 | lib: ["ES2021", "DOM"],
22 | },
23 | package: {
24 | name: "superserial",
25 | version,
26 | description:
27 | "A comprehensive Serializer/Deserializer that can handle any data type.",
28 | keywords: [
29 | "serialize",
30 | "serializer",
31 | "serialization",
32 | "JSON",
33 | "flatted",
34 | "circular",
35 | ],
36 | license: "MIT",
37 | repository: {
38 | type: "git",
39 | url: "git+https://github.com/denostack/superserial.git",
40 | },
41 | bugs: {
42 | url: "https://github.com/denostack/superserial/issues",
43 | },
44 | },
45 | });
46 |
47 | // post build steps
48 | Deno.copyFileSync("README.md", ".npm/README.md");
49 |
--------------------------------------------------------------------------------
/serialize.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals } from "@std/assert";
2 | import { serialize } from "./serialize.ts";
3 | import { toSerialize } from "./symbol.ts";
4 |
5 | Deno.test("serialize scalar", () => {
6 | assertEquals(serialize(null), "null");
7 | assertEquals(serialize(undefined), "undefined");
8 |
9 | assertEquals(serialize(true), "true");
10 | assertEquals(serialize(false), "false");
11 |
12 | assertEquals(serialize(30), "30");
13 | assertEquals(serialize(30.1), "30.1");
14 |
15 | assertEquals(serialize(30n), "30n");
16 | assertEquals(serialize(-30n), "-30n");
17 | assertEquals(
18 | serialize(9007199254740991000000n),
19 | "9007199254740991000000n",
20 | );
21 | assertEquals(
22 | serialize(-9007199254740991000000n),
23 | "-9007199254740991000000n",
24 | );
25 |
26 | assertEquals(serialize("string"), '"string"');
27 | });
28 |
29 | Deno.test("serialize string with escape", () => {
30 | assertEquals(serialize("\\"), '"\\\\"');
31 | assertEquals(serialize("\\x00"), '"\\\\x00"');
32 | assertEquals(serialize("\x00"), '"\\u0000"');
33 | });
34 |
35 | Deno.test("serialize extend scalar", () => {
36 | assertEquals(serialize(NaN), "NaN");
37 | assertEquals(serialize(Infinity), "Infinity");
38 | assertEquals(serialize(-Infinity), "-Infinity");
39 | });
40 |
41 | Deno.test("serialize symbol", () => {
42 | assertEquals(serialize(Symbol()), "Symbol()");
43 | assertEquals(serialize(Symbol("desc1")), 'Symbol("desc1")');
44 |
45 | const symbol1 = Symbol("sym1");
46 | const symbol2 = Symbol("sym2");
47 | assertEquals(
48 | serialize([symbol1, symbol2, Symbol("sym1"), [symbol2]]),
49 | '[$1,$2,$3,$4];Symbol("sym1");Symbol("sym2");Symbol("sym1");[$2]',
50 | );
51 | });
52 |
53 | Deno.test("serialize built-in Set", () => {
54 | assertEquals(serialize(new Set([1, 2, 3, 4, 5])), "Set(1,2,3,4,5)");
55 | });
56 |
57 | Deno.test("serialize built-in Set circular", () => {
58 | const set = new Set();
59 | set.add(set);
60 | assertEquals(serialize(set), "Set($0)");
61 | });
62 |
63 | Deno.test("serialize built-in Map", () => {
64 | assertEquals(
65 | serialize(
66 | new Map([
67 | ["string", "this is string"],
68 | [true, "boolean"],
69 | [null, "null"],
70 | [{}, "object"],
71 | ]),
72 | ),
73 | 'Map("string"=>"this is string",true=>"boolean",null=>"null",$1=>"object");{}',
74 | );
75 | });
76 |
77 | Deno.test("serialize build-in Map deep", () => {
78 | const map1 = new Map([["key1_1", "value1_1"], ["key1_2", "value1_2"]]);
79 | const map2 = new Map([["key2_1", "value2_1"], ["key2_2", "value2_2"]]);
80 |
81 | assertEquals(
82 | serialize(
83 | new Map([
84 | ["key1", map1] as const,
85 | [map2, "val2"] as const,
86 | ]),
87 | ),
88 | 'Map("key1"=>$1,$2=>"val2");Map("key1_1"=>"value1_1","key1_2"=>"value1_2");Map("key2_1"=>"value2_1","key2_2"=>"value2_2")',
89 | );
90 | });
91 |
92 | Deno.test("serialize build-in Map circular", () => {
93 | const map1 = new Map();
94 | map1.set(map1, map1);
95 |
96 | assertEquals(
97 | serialize(map1),
98 | "Map($0=>$0)",
99 | );
100 |
101 | const map2 = new Map();
102 | map2.set(map2, "val");
103 | map2.set("key", map2);
104 |
105 | assertEquals(
106 | serialize(map2),
107 | 'Map($0=>"val","key"=>$0)',
108 | );
109 | });
110 |
111 | Deno.test("serialize built-in Date", () => {
112 | assertEquals(serialize(new Date(1640962800000)), "Date(1640962800000)");
113 | });
114 |
115 | Deno.test("serialize regex", () => {
116 | assertEquals(serialize(/abc/), "/abc/");
117 |
118 | assertEquals(serialize(/abc/gmi), "/abc/gim");
119 | });
120 |
121 | Deno.test("serialize array", () => {
122 | assertEquals(serialize([]), "[]");
123 |
124 | assertEquals(
125 | serialize([[[{}, 2, "", false, []], [[]]], [1, 2], [1]]),
126 | '[$1,$2,$3];[$4,$5];[1,2];[1];[$6,2,"",false,$7];[$8];{};[];[]',
127 | );
128 |
129 | assertEquals(
130 | serialize([{ name: "wan2land" }, { name: "wan3land" }]),
131 | '[$1,$2];{"name":"wan2land"};{"name":"wan3land"}',
132 | );
133 |
134 | assertEquals(
135 | serialize({ users: [{ name: "wan2land" }, { name: "wan3land" }] }),
136 | '{"users":$1};[$2,$3];{"name":"wan2land"};{"name":"wan3land"}',
137 | );
138 | });
139 |
140 | Deno.test("serialize object", () => {
141 | assertEquals(serialize({}), "{}");
142 |
143 | assertEquals(
144 | serialize({ foo: "foo string", und: undefined }),
145 | '{"foo":"foo string","und":undefined}',
146 | );
147 | assertEquals(
148 | serialize({ foo: { bar: "bar string" } }),
149 | '{"foo":$1};{"bar":"bar string"}',
150 | );
151 | });
152 |
153 | Deno.test("serialize object self circular", () => {
154 | const selfCircular = {} as { selfCircular: unknown };
155 | selfCircular.selfCircular = selfCircular;
156 | assertEquals(serialize(selfCircular), '{"selfCircular":$0}');
157 | });
158 |
159 | Deno.test("serialize object circular", () => {
160 | const parent = {} as { children: unknown[] };
161 | const child1 = { parent } as {
162 | parent: unknown;
163 | next: unknown;
164 | siblings: unknown[];
165 | };
166 | const child2 = { parent } as {
167 | parent: unknown;
168 | next: unknown;
169 | siblings: unknown[];
170 | };
171 | const children = [child1, child2];
172 | child1.next = child2;
173 | child1.siblings = children;
174 | child2.next = child1;
175 | child2.siblings = children;
176 | parent.children = children;
177 |
178 | assertEquals(
179 | serialize(parent),
180 | '{"children":$1};[$2,$3];{"parent":$0,"next":$3,"siblings":$1};{"parent":$0,"next":$2,"siblings":$1}',
181 | );
182 | });
183 |
184 | Deno.test("serialize function (not support)", () => {
185 | assertEquals(serialize(function () {}), "{}");
186 | });
187 |
188 | Deno.test("serialize object prototype null", () => {
189 | assertEquals(serialize(Object.create(null)), "{}");
190 | });
191 |
192 | Deno.test("serialize class", () => {
193 | class TestUser {
194 | #_privateSomething = 1;
195 | publicSomething = 2;
196 | constructor(public name: string, public age: number) {
197 | }
198 | }
199 |
200 | const user = new TestUser("wan2land", 20);
201 |
202 | assertEquals(
203 | serialize(user),
204 | 'TestUser{"name":"wan2land","age":20,"publicSomething":2}',
205 | );
206 | });
207 |
208 | Deno.test("serialize class with alias", () => {
209 | class TestUser {
210 | #_privateSomething = 1;
211 | publicSomething = 2;
212 | constructor(public name: string, public age: number) {
213 | }
214 | }
215 |
216 | const user = new TestUser("wan2land", 20);
217 |
218 | assertEquals(
219 | serialize(user, { classNames: new Map([[TestUser, "AliasedTestUser"]]) }),
220 | 'AliasedTestUser{"name":"wan2land","age":20,"publicSomething":2}',
221 | );
222 | });
223 |
224 | Deno.test("serialize class with private", () => {
225 | class TestUser {
226 | #_privateSomething = 1;
227 | publicSomething = 2;
228 | constructor(public name: string, public age: number) {
229 | }
230 |
231 | [toSerialize]() {
232 | return {
233 | name: this.name,
234 | age: this.age,
235 | publicSomething: this.publicSomething,
236 | privateSomething: this.#_privateSomething,
237 | };
238 | }
239 | }
240 |
241 | const user = new TestUser("wan2land", 20);
242 |
243 | assertEquals(
244 | serialize(user),
245 | 'TestUser{"name":"wan2land","age":20,"publicSomething":2,"privateSomething":1}',
246 | );
247 | });
248 |
--------------------------------------------------------------------------------
/serialize.ts:
--------------------------------------------------------------------------------
1 | import { toSerialize } from "./symbol.ts";
2 | import type { ConstructType } from "./types.ts";
3 |
4 | export interface SerializeOptions {
5 | classNames?: Map, string>;
6 | prettify?: boolean;
7 | }
8 |
9 | export function serialize(
10 | value: unknown,
11 | { prettify, classNames }: SerializeOptions = {},
12 | ): string {
13 | let output = "";
14 | let depth = 0;
15 |
16 | const roots = [] as unknown[];
17 | const rootIndexMap = new Map();
18 |
19 | function _stringifyString(value: string) {
20 | output += JSON.stringify(value); // fastest way
21 | }
22 |
23 | function _stringifyScalar(
24 | value: unknown,
25 | ): value is object | symbol {
26 | if (value === null) {
27 | output += "null";
28 | return false;
29 | }
30 | switch (typeof value) {
31 | case "undefined": {
32 | output += "undefined";
33 | return false;
34 | }
35 | case "number": {
36 | if (Number.isNaN(value)) {
37 | output += "NaN";
38 | return false;
39 | }
40 | if (!Number.isFinite(value)) {
41 | output += value > 0 ? "Infinity" : "-Infinity";
42 | return false;
43 | }
44 | output += `${value}`;
45 | return false;
46 | }
47 | case "bigint": {
48 | output += `${value}n`;
49 | return false;
50 | }
51 | case "boolean": {
52 | output += value ? "true" : "false";
53 | return false;
54 | }
55 | case "string": {
56 | _stringifyString(value);
57 | return false;
58 | }
59 | }
60 |
61 | return true;
62 | }
63 |
64 | function _stringifyRoot(value: unknown) {
65 | if (_stringifyScalar(value)) {
66 | // simple :-)
67 | if (typeof value === "symbol") {
68 | output += "Symbol(";
69 | if (value.description) {
70 | _stringifyString(value.description);
71 | }
72 | output += ")";
73 | return;
74 | }
75 |
76 | if (value instanceof RegExp) {
77 | output += value.toString();
78 | return;
79 | }
80 |
81 | if (value instanceof Date) {
82 | output += "Date(";
83 | output += value.getTime().toString();
84 | output += ")";
85 | return;
86 | }
87 |
88 | // complex :D
89 | if (prettify) {
90 | output += " ".repeat(depth);
91 | }
92 |
93 | if (Array.isArray(value)) {
94 | _stringifyListStart("[");
95 | _stringifyList(value);
96 | _stringifyListEnd("]");
97 | return;
98 | }
99 |
100 | if (value instanceof Map) {
101 | _stringifyListStart("Map(");
102 | _stringifyMap([...value.entries()]);
103 | _stringifyListEnd(")");
104 | return;
105 | }
106 | if (value instanceof Set) {
107 | _stringifyListStart("Set(");
108 | _stringifyList([...value]);
109 | _stringifyListEnd(")");
110 | return;
111 | }
112 |
113 | const name = value.constructor && value.constructor !== Object &&
114 | value.constructor !== Function
115 | ? (classNames?.get(value.constructor) ?? value.constructor.name)
116 | : "";
117 |
118 | _stringifyListStart(prettify && name ? `${name} {` : `${name}{`);
119 | _stringifyKv(
120 | Object.entries(
121 | toSerialize in value && typeof value[toSerialize] === "function"
122 | ? value[toSerialize]()
123 | : value,
124 | ),
125 | );
126 | _stringifyListEnd("}");
127 | return;
128 | }
129 | }
130 |
131 | function _stringifyUnknown(value: unknown) {
132 | if (_stringifyScalar(value)) {
133 | let idx = rootIndexMap.get(value);
134 | if (typeof idx !== "number") {
135 | rootIndexMap.set(value, idx = roots.length);
136 | roots.push(value);
137 | }
138 | output += `$${idx}`;
139 | }
140 | }
141 |
142 | const _stringifyListStart = prettify
143 | ? (name: string) => {
144 | output += name;
145 | output += "\n";
146 | depth++;
147 | }
148 | : (name: string) => {
149 | output += name;
150 | depth++;
151 | };
152 |
153 | const _stringifyListEnd = prettify
154 | ? (name: string) => {
155 | depth--;
156 | output += "\n";
157 | output += " ".repeat(depth);
158 | output += name;
159 | }
160 | : (name: string) => {
161 | depth--;
162 | output += name;
163 | };
164 |
165 | const _stringifyList = prettify
166 | ? (value: unknown[]) => {
167 | for (let i = 0; i < value.length; i++) {
168 | if (i > 0) {
169 | output += ",\n";
170 | }
171 | output += " ".repeat(depth);
172 | _stringifyUnknown(value[i]);
173 | }
174 | }
175 | : (value: unknown[]) => {
176 | for (let i = 0; i < value.length; i++) {
177 | if (i > 0) {
178 | output += ",";
179 | }
180 | _stringifyUnknown(value[i]);
181 | }
182 | };
183 | const _stringifyMap = prettify
184 | ? (value: [string, unknown][]) => {
185 | for (let i = 0; i < value.length; i++) {
186 | if (i > 0) {
187 | output += ",\n";
188 | }
189 | output += " ".repeat(depth);
190 | _stringifyUnknown(value[i][0]);
191 | output += " => ";
192 | _stringifyUnknown(value[i][1]);
193 | }
194 | }
195 | : (value: [string, unknown][]) => {
196 | for (let i = 0; i < value.length; i++) {
197 | if (i > 0) {
198 | output += ",";
199 | }
200 | _stringifyUnknown(value[i][0]);
201 | output += "=>";
202 | _stringifyUnknown(value[i][1]);
203 | }
204 | };
205 | const _stringifyKv = prettify
206 | ? (value: [string, unknown][]) => {
207 | for (let i = 0; i < value.length; i++) {
208 | if (i > 0) {
209 | output += ",\n";
210 | }
211 | output += " ".repeat(depth);
212 | _stringifyString(value[i][0]);
213 | output += ": ";
214 | _stringifyUnknown(value[i][1]);
215 | }
216 | }
217 | : (value: [string, unknown][]) => {
218 | for (let i = 0; i < value.length; i++) {
219 | if (i > 0) {
220 | output += ",";
221 | }
222 | _stringifyString(value[i][0]);
223 | output += ":";
224 | _stringifyUnknown(value[i][1]);
225 | }
226 | };
227 |
228 | rootIndexMap.set(value, 0);
229 | roots.push(value);
230 |
231 | _stringifyRoot(value);
232 | const splitter = prettify ? ";\n" : ";";
233 | for (let i = 1; i < roots.length; i++) {
234 | output += splitter;
235 | _stringifyRoot(roots[i]);
236 | }
237 | return output;
238 | }
239 |
--------------------------------------------------------------------------------
/serializer.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertInstanceOf } from "@std/assert";
2 | import { Serializer, toDeserialize, toSerialize } from "./mod.ts";
3 |
4 | Deno.test("serializer, toSerialize, toDeserialize", () => {
5 | class TestUser {
6 | #_age = 0;
7 | constructor(public name: string) {
8 | this.#_age = 0;
9 | }
10 |
11 | setAge(age: number) {
12 | this.#_age = age;
13 | }
14 |
15 | getAge() {
16 | return this.#_age;
17 | }
18 |
19 | [toSerialize]() {
20 | return {
21 | name: this.name,
22 | age: this.#_age,
23 | };
24 | }
25 |
26 | [toDeserialize](
27 | value: {
28 | name: string;
29 | age: number;
30 | },
31 | ) {
32 | this.name = value.name;
33 | this.#_age = value.age;
34 | }
35 | }
36 |
37 | const serializer = new Serializer({ classes: { TestUser } });
38 | {
39 | const user = new TestUser("wan2land");
40 | user.setAge(20);
41 |
42 | assertEquals(
43 | serializer.serialize(user),
44 | 'TestUser{"name":"wan2land","age":20}',
45 | );
46 | }
47 | {
48 | const user = serializer.deserialize(
49 | 'TestUser{"name":"wan2land","age":20}',
50 | );
51 |
52 | assertInstanceOf(user, TestUser);
53 | assertEquals(user.name, "wan2land");
54 | assertEquals(user.getAge(), 20);
55 | }
56 | });
57 |
58 | Deno.test("serializer, alias class names", () => {
59 | class TestUser {
60 | constructor(public name: string) {
61 | }
62 | }
63 |
64 | const serializer = new Serializer({ classes: { AliasedTestUser: TestUser } });
65 | {
66 | const user = new TestUser("wan2land");
67 |
68 | assertEquals(
69 | serializer.serialize(user),
70 | 'AliasedTestUser{"name":"wan2land"}',
71 | );
72 | }
73 | {
74 | const user = serializer.deserialize(
75 | 'AliasedTestUser{"name":"wan2land"}',
76 | );
77 |
78 | assertInstanceOf(user, TestUser);
79 | assertEquals(user.name, "wan2land");
80 | }
81 | });
82 |
--------------------------------------------------------------------------------
/serializer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type ClassLoadHandler,
3 | deserialize,
4 | type DeserializeOptions,
5 | } from "./deserialize.ts";
6 | import { serialize, type SerializeOptions } from "./serialize.ts";
7 | import type { ConstructType } from "./types.ts";
8 |
9 | export interface SerializerOptions {
10 | classes?: { [className: string]: ConstructType };
11 | loadClass?: ClassLoadHandler;
12 | }
13 |
14 | export class Serializer {
15 | #classNames?: Map, string>;
16 |
17 | constructor(public options?: SerializerOptions) {
18 | }
19 |
20 | serialize(value: unknown, options: SerializeOptions = {}): string {
21 | return serialize(value, {
22 | prettify: options.prettify,
23 | classNames: options.classNames ?? (
24 | this.#classNames ??= new Map(
25 | Object.entries(this.options?.classes ?? {}).map((
26 | [key, value],
27 | ) => [value, key]),
28 | )
29 | ),
30 | });
31 | }
32 |
33 | deserialize(code: string, options: DeserializeOptions = {}): T {
34 | return deserialize(code, {
35 | classes: options?.classes ?? this.options?.classes,
36 | loadClass: options?.loadClass ?? this.options?.loadClass,
37 | });
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/symbol.ts:
--------------------------------------------------------------------------------
1 | export const toSerialize = Symbol("serialize");
2 | export const toDeserialize = Symbol("deserialize");
3 |
--------------------------------------------------------------------------------
/types.ts:
--------------------------------------------------------------------------------
1 | // deno-lint-ignore ban-types
2 | export type ConstructType = (new (...args: unknown[]) => T) | Function;
3 |
--------------------------------------------------------------------------------