commit 981e74c3ccc4b1282d5683816488defbb501d7bd Author: rowan Date: Mon May 19 13:31:22 2025 -0500 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a54a8cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +test.ts + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..68a7db6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,516 @@ +{ + "name": "serde-json-ts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "serde-json-ts", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "serde": "file:../serde-ts" + }, + "devDependencies": { + "esbuild": "^0.25.4", + "typescript": "^5.8.3" + } + }, + "../serde-ts": { + "name": "serde", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@tsmetadata/polyfill": "^1.1.3" + }, + "devDependencies": { + "esbuild": "^0.25.4", + "typescript": "^5.8.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" + } + }, + "node_modules/serde": { + "resolved": "../serde-ts", + "link": true + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5e61a85 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "serde-json-ts", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "esbuild src/index.ts --bundle --outfile=dist/index.js", + "build:test": "esbuild test.ts --platform=node --target=es2022 --bundle --outfile=dist/test.js --tsconfig=tsconfig.json", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "serde": "file:../serde-ts" + }, + "devDependencies": { + "esbuild": "^0.25.4", + "typescript": "^5.8.3" + } +} diff --git a/src/de.ts b/src/de.ts new file mode 100644 index 0000000..ba64325 --- /dev/null +++ b/src/de.ts @@ -0,0 +1,362 @@ +import { DefaultIterableAccessImpl, DefaultMapAccessImpl, Deserialize, Deserializer, GenericVisitor, IterableAccess, MapAccess, Visitor } from 'serde/de' +import { mixin, Nullable } from 'serde/utils' +import { unexpected } from './err' + +type Byte = number + +const clamp = (value: number, min: number, max: number): number => { + return Math.min(Math.max(value, min), max) +} + +const isNumeric = (value: any): value is number => { + return !isNaN(value) +} + +const isNumericToken = (value: Byte) => { + return value === Token.Period || value === Token.Hyphen || Token.Digit.includes(value) +} + +interface Predicate { + (value: T): boolean +} + +const encoder = new TextEncoder() +const b = (strings: TemplateStringsArray) => encoder.encode(strings[0]) +const char = (strings: TemplateStringsArray) => b(strings)[0] + +const Literal = Object.freeze({ + True: b`true`, + False: b`false` +} as const) + +const Token = Object.freeze({ + Space: char` `, + LeftCurly: char`{`, + RightCurly: char`}`, + LeftSquare: char`[`, + RightSquare: char`]`, + Quote: char`"`, + ForwardSlash: char`\\`, + Digit: b`0123456789`, + Hyphen: char`-`, + Period: char`.`, + Comma: char`,`, + Colon: char`:` +} as const) + +export interface CommaSeparated extends MapAccess, IterableAccess { } +@mixin(DefaultMapAccessImpl) +@mixin(DefaultIterableAccessImpl) +export class CommaSeparated implements MapAccess, IterableAccess { + private readonly de: JSONDeserializer + private first: boolean = true + + constructor(deserializer: JSONDeserializer) { + this.de = deserializer + } + + nextKeySeed>(seed: K): Nullable { + if (this.de.buffer.peek().next() === Token.RightCurly) { + return + } + + if (!this.first) { + const take = this.de.buffer.take() + if (take.next() !== Token.Comma) { + throw unexpected(',', take.toString(), this.de.buffer.position) + } + } + + this.first = false + return seed.deserialize(this.de) + } + + nextValueSeed>(seed: V): Nullable { + const next = this.de.buffer.next() + if (next !== Token.Colon) { + console.log(this.de.buffer.toString()) + throw unexpected(':', next.toString(), this.de.buffer.position) + } + + return seed.deserialize(this.de) + } + + nextElementSeed>(seed: I): Nullable { + if (this.de.buffer.peek().next() === Token.RightSquare) { + return + } + + if (!this.first) { + const take = this.de.buffer.take() + if (take.next() !== Token.Comma) { + throw unexpected(',', take.toString(), this.de.buffer.position) + } + } + + this.first = false + return seed.deserialize(this.de) + } +} + +class StringBuffer { + private readonly view: Uint8Array + private index: number = 0 + private readonly encoder: TextEncoder + private readonly decoder: TextDecoder + + get position() { + return this.index + } + + get length() { + return this.view.byteLength + } + + constructor(view: Uint8Array, encoder: TextEncoder = new TextEncoder(), decoder: TextDecoder = new TextDecoder()) { + this.view = view + this.encoder = encoder + this.decoder = decoder + } + + static fromArrayBuffer(value: ArrayBuffer, encoder?: TextEncoder, decoder?: TextDecoder): StringBuffer { + return new this(new Uint8Array(value), encoder, decoder) + } + + static fromString(value: string, encoder: TextEncoder = new TextEncoder(), decoder?: TextDecoder): StringBuffer { + return this.fromArrayBuffer( + encoder.encode(value), + encoder, + decoder + ) + } + + next() { + const value = this.view[this.index] + this.index += 1 + return value + } + + nextChar() { + return this.take().toString() + } + + done(): boolean { + return this.index >= this.view.byteLength + } + + toBytes() { + return this.view.slice(this.index) + } + + toString() { + return this.decoder.decode(this.toBytes()) + } + + take(limit: number = 1): StringBuffer { + const bytes = this.peek(limit) + this.index += limit + return bytes + } + + at(index: number) { + return this.view[this.index + index] + } + + takeWhile(fn: Predicate): StringBuffer { + let index = 0 + + while (!this.done() && fn(this.at(index))) { + index += 1 + } + + return this.take(index) + } + + drop(limit: number) { + this.index += limit + return this + } + + peek(limit: number = 1): StringBuffer { + const index = this.index + return this.slice(index, index + limit) + } + + startsWith(value: string | ArrayBufferLike): boolean { + if (typeof value === 'string') { + return this.startsWith(this.encoder.encode(value)) + } + + const length = value.byteLength + const bytes = new Uint8Array(value) + + return this.peek(length).toBytes().every((v, i) => v === bytes[i]) + } + + slice(start?: number, end?: number) { + return new StringBuffer( + this.view.subarray(start, end), + this.encoder, + this.decoder + ) + } + + indexOf(value: number | ArrayBufferLike, start: number = 0) { + const search = new Uint8Array(isNumeric(value) ? [value] : value) + start = clamp(start, this.index, this.length) + const bytes = this.slice(start) + + for (let i = 0, len = bytes.length; i < len; i++) { + if (bytes.at(i) === search[0] && bytes.slice(i).startsWith(search)) { + return i + } + } + + return -1 + } +} + +export class JSONDeserializer implements Deserializer { + readonly buffer: StringBuffer + + constructor(buffer: StringBuffer) { + this.buffer = buffer + } + + static fromString(value: string): JSONDeserializer { + return new this(StringBuffer.fromString(value)) + } + + deserializeAny>(visitor: V): T { + const peek = this.buffer.peek() + const nextByte = peek.take() + const byte = nextByte.next() + + switch (true) { + case b`n`.includes(byte): + return this.deserializeNull(visitor) + case b`tf`.includes(byte): + return this.deserializeBoolean(visitor) + case b`-0123456789`.includes(byte): + return this.deserializeNumber(visitor) + case Token.Quote === byte: + return this.deserializeString(visitor) + case Token.LeftSquare === byte: + return this.deserializeIterable(visitor) + case Token.LeftCurly === byte: + return this.deserializeObject(visitor) + default: + throw new SyntaxError(`Invalid syntax at position ${this.buffer.position}: "${nextByte.toString()}"`) + } + } + + deserializeNull>(visitor: V): T { + if (this.buffer.startsWith('null')) { + this.buffer.take(4) + } + + return visitor.visitNull() + } + + private _deserializeObject(fn: (value: CommaSeparated) => T): T { + let next = this.buffer.take() + if (next.next() === Token.LeftCurly) { + + const value = fn(new CommaSeparated(this)) + + next = this.buffer.take() + if (next.next() === Token.RightCurly) { + return value + } else { + throw unexpected('}', next.toString(), this.buffer.position) + } + } else { + throw unexpected('{', next.toString(), this.buffer.position) + } + } + + deserializeObject>(visitor: V): T { + return this._deserializeObject(visitor.visitObject.bind(visitor)) + } + + deserializeString>(visitor: V): T { + const next = this.buffer.take() + if (next.next() === Token.Quote) { + let index = -1 + + do { + index = this.buffer.indexOf(Token.Quote, index) + } while (index > -1 && this.buffer.at(index - 1) === Token.ForwardSlash) + + if (index === -1) { + throw new SyntaxError('Unterminated string literal') + } + + const bytes = this.buffer.take(index) + this.buffer.take() + return visitor.visitString(bytes.toString()) + } else { + throw unexpected('"', next.toString(), this.buffer.position) + } + } + + deserializeNumber>(visitor: V): T { + const next = this.buffer.peek().next() + + if (isNumericToken(next)) { + const digits = this.buffer.takeWhile(isNumericToken).toString() + if (digits.length >= 16) { + const number = BigInt(digits) + return visitor.visitBigInt(number) + } else if (digits.length > 0) { + let number = parseInt(digits.toString(), 10) + return visitor.visitNumber(number) + } + } + + throw unexpected('"-", ".", or 0..=9', next.toString(), this.buffer.position) + } + + deserializeBigInt>(visitor: V): T { + return this.deserializeNumber(visitor) + } + + deserializeBoolean>(visitor: V): T { + const next = this.buffer.next() + let length = 3 + + switch (next) { + case Literal.False[0]: + length = 4 + case Literal.True[0]: + break + default: + throw unexpected('"true" or "false"', this.buffer.next().toString(), this.buffer.position) + } + + this.buffer.take(length) + return visitor.visitBoolean(length === 3) + } + + deserializeSymbol>(_visitor: V): T { + throw new Error('Method not implemented.') + } + + deserializeIterable>(visitor: V): T { + let next = this.buffer.take() + if (next.next() === Token.LeftSquare) { + + const value = visitor.visitIterable!(new CommaSeparated(this)) + + next = this.buffer.take() + if (next.next() === Token.RightSquare) { + return value + } else { + throw unexpected(']', next.toString(), this.buffer.position) + } + } else { + throw unexpected('[', next.toString(), this.buffer.position) + } + } +} + diff --git a/src/err.ts b/src/err.ts new file mode 100644 index 0000000..f2d43b4 --- /dev/null +++ b/src/err.ts @@ -0,0 +1,2 @@ +export const unexpected = (expected: string, actual: string, position: number) => new SyntaxError(`Expected ${expected} at position ${position} (got '${actual}')`) + diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..b2240b9 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,16 @@ +import { serializeWith } from 'serde/ser' +import { Deserialize } from 'serde/de' +import { JSONSerializer } from './ser' +import { JSONDeserializer } from './de' + +export function toString(value: any): string { + const serializer = new JSONSerializer() + serializeWith(serializer, value) + return serializer.output +} + +export function fromString>(value: string, into: D): T { + const deserializer = JSONDeserializer.fromString(value) + return into.deserialize(deserializer) +} + diff --git a/src/ser.ts b/src/ser.ts new file mode 100644 index 0000000..922afc0 --- /dev/null +++ b/src/ser.ts @@ -0,0 +1,103 @@ +import { IterableSerializer, ObjectSerializer, Serializable, Serializer, serializeWith } from 'serde/ser' + +const Identifier = (value: string) => `\x02${value}\x04` + +class JSONObjectSerializer implements ObjectSerializer { + private ser: JSONSerializer + private first: boolean = true + + constructor(serializer: JSONSerializer) { + this.ser = serializer + serializer.write('{') + } + + serializeKey(key: U): void { + if (!this.first) { + this.ser.write(',') + } else { + this.first = false + } + + serializeWith(this.ser, key) + this.ser.write(':') + } + + serializeValue(value: U): void { + serializeWith(this.ser, value) + } + + end(): void { + this.ser.write('}') + } +} + +class JSONIterableSerializer implements IterableSerializer { + private ser: JSONSerializer + private first: boolean = true + + constructor(serializer: JSONSerializer) { + this.ser = serializer + serializer.write('[') + } + + serializeElement(element: U): void { + if (!this.first) { + this.ser.write(',') + } else { + this.first = false + } + + serializeWith(this.ser, element) + } + + end(): void { + this.ser.write(']') + } +} + +export class JSONSerializer implements Serializer { + output: string = '' + + write(value: string) { + this.output += value + } + + serializeString(value: string) { + this.write(`"${value}"`) + } + + serializeBoolean(value: boolean): void { + this.write(value.toString()) + } + + serializeSymbol(value: symbol): void { + const key = Symbol.keyFor(value) + if (key) { + this.write(key) + } else { + return this.serializeString(value.toString()) + } + } + + serializeObject(): ObjectSerializer { + return new JSONObjectSerializer(this) + } + + serializeNumber(value: number) { + this.write(value.toString()) + } + + serializeBigInt(value: bigint) { + this.write(value.toString()) + } + + serializeIterable(): IterableSerializer { + return new JSONIterableSerializer(this) + } + + serializeNull() { + return this.write('null') + } +} + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d77b881 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,113 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": ["es2020", "dom"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "libReplacement": true, /* Enable lib replacement. */ + //"experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "node16", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node16", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./out", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}