├── .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 | ![output](./output.svg) 4 | -------------------------------------------------------------------------------- /.benchmark/output.svg: -------------------------------------------------------------------------------- 1 | cpu: Apple M1runtime: deno 1.29.2 (aarch64-apple-darwin)file:///Users/wan2land/Workspace/@denostack/superserial/.benchmark/serializer.bench.tsbenchmark time (avg) (min … max) p75 p99 p995------------------------------------------------------------------------- -----------------------------serialize #Serializer.serialize 1.88 µs/iter (1.86 µs1.93 µs) 1.89 µs1.93 µs1.93 µsserialize #JSON.stringify 628.98 ns/iter (621.61 ns640.09 ns) 631.48 ns640.09 ns640.09 nsserialize #devalue.stringify 4.19 µs/iter (4.17 µs4.22 µs) 4.2 µs4.22 µs4.22 µsserialize #flatted.stringify 3.6 µs/iter (3.57 µs3.63 µs) 3.61 µs3.63 µs3.63 µsserialize #superjson.stringify 8.69 µs/iter (7.79 µs178.08 µs) 8.67 µs10.08 µs16.33 µssummaryserialize #Serializer.serialize2.99x slower than serialize #JSON.stringify1.91x faster than serialize #flatted.stringify2.23x faster than serialize #devalue.stringify4.62x faster than serialize #superjson.stringifyserialize #Serializer.deserialize 8.62 µs/iter (7.92 µs199.92 µs) 8.71 µs10.33 µs12.21 µsserialize #JSON.parse 994.16 ns/iter (976.21 ns1.01 µs) 998.7 ns1.01 µs1.01 µsserialize #devalue.parse 1.68 µs/iter (1.66 µs1.7 µs) 1.68 µs1.7 µs1.7 µsserialize #flatted.parse 7.25 µs/iter (7.15 µs7.38 µs) 7.3 µs7.38 µs7.38 µsserialize #superjson.parse 2.81 µs/iter (2.79 µs2.84 µs) 2.82 µs2.84 µs2.84 µssummaryserialize #Serializer.deserialize8.67x slower than serialize #JSON.parse5.14x slower than serialize #devalue.parse3.06x slower than serialize #superjson.parse1.19x slower than serialize #flatted.parse -------------------------------------------------------------------------------- /.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 | Build 5 | Coverage 6 | License 7 | Language Typescript 8 |
9 | JSR version 10 | Deno version 11 | NPM Version 12 | Downloads 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 | --------------------------------------------------------------------------------