commit 0b3dabddde89645a8de11a5d168687d89c609eb7 Author: rowan Date: Thu May 15 14:24:06 2025 -0500 initial commitl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed1bf77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +out/ +node_modules/ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..08e30e5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,505 @@ +{ + "name": "serde", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "serde", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@types/text-encoding": "^0.0.40", + "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/@types/text-encoding": { + "version": "0.0.40", + "resolved": "https://registry.npmjs.org/@types/text-encoding/-/text-encoding-0.0.40.tgz", + "integrity": "sha512-dHzoIdwBfY7jcSTTt6XBkaeiuFQAQD7r/7aJySKDdHkYBCDOvs9jPVt4NYXuwBMn89PP6gSd29WubIS19wTiXg==", + "dev": true, + "license": "MIT" + }, + "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/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..d4196c6 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "serde", + "version": "1.0.0", + "description": "", + "main": "src/index.ts", + "scripts": { + "build": "tsc", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "devDependencies": { + "esbuild": "^0.25.4", + "typescript": "^5.8.3" + } +} diff --git a/src/de/generic.ts b/src/de/generic.ts new file mode 100644 index 0000000..84115b1 --- /dev/null +++ b/src/de/generic.ts @@ -0,0 +1,81 @@ +import { staticImplements } from '../utils' +import { Deserialize, Deserializer, IterableAccess, MapAccess, Visitor } from './interface' + +@staticImplements>() +export class GenericSeed implements Deserialize { + static deserialize(deserializer: D): T { + return deserializer.deserializeAny(new GenericVisitor()) + } + + deserialize(deserializer: D): T { + return GenericSeed.deserialize(deserializer) + } +} + +export class GenericVisitor implements Visitor { + visitString(value: string): T { + return value as T + } + + visitNumber(value: number): T { + return value as T + } + + visitBigInt(value: bigint): T { + return value as T + } + + visitBoolean(value: boolean): T { + return value as T + } + + visitSymbol(value: symbol): T { + return value as T + } + + visitNull(): T { + return null as T + } + + visitObject(access: MapAccess): T { + const result: Record = {} + let entry + + while ((entry = access.nextEntry())) { + result[entry[0]] = entry[1] + } + + return result + } + + visitFunction?(value: Function): T { + return value as T + } + + visitMap?(access: MapAccess): T { + const result = new Map() + let entry + + while ((entry = access.nextEntry())) { + result.set(entry[0], entry[1]) + } + + return result as T + } + + visitIterable?(access: IterableAccess): T { + const result = new Array(access.sizeHint()) + let element + + while ((element = access.nextElement())) { + result.push(element) + } + + return result as T + } + + visitClass?(_name: string, _fields: string[], _value: any): T { + throw new Error('Method not implemented.') + } +} + diff --git a/src/de/index.ts b/src/de/index.ts new file mode 100644 index 0000000..62e48a6 --- /dev/null +++ b/src/de/index.ts @@ -0,0 +1,4 @@ +export * from './interface' +export * from './generic' +export * from './mixin' + diff --git a/src/de/interface.ts b/src/de/interface.ts new file mode 100644 index 0000000..165b538 --- /dev/null +++ b/src/de/interface.ts @@ -0,0 +1,88 @@ +import { Nullable } from '../utils' +import { GenericSeed } from './generic' + +export interface MapAccess { + nextKeySeed>(seed: K): Nullable + nextValueSeed>(seed: V): Nullable + nextEntrySeed, V extends Deserialize>(kseed: K, vseed: V): Nullable<[TK, TV]> + nextKey(): Nullable + nextValue(): Nullable + nextEntry(): Nullable<[K, V]> +} + +export abstract class DefaultMapAccessImpl implements MapAccess { + abstract nextKeySeed>(seed: K): Nullable + abstract nextValueSeed>(seed: V): Nullable + + nextEntrySeed, V extends Deserialize>(kseed: K, vseed: V): Nullable<[TK, TV]> { + const key = this.nextKeySeed(kseed) as Nullable + if (key) { + const value = this.nextValueSeed(vseed) as Nullable + + if (value) { + return [key, value] + } + } + } + + nextKey(): Nullable { + return this.nextValueSeed(GenericSeed) + } + + nextValue(): Nullable { + return this.nextValueSeed(GenericSeed) + } + + nextEntry(): Nullable<[K, V]> { + return this.nextEntrySeed(GenericSeed, GenericSeed) + } +} + +export interface IterableAccess { + nextElementSeed>(seed: I): Nullable + nextElement(): Nullable + sizeHint(): number +} + +export abstract class DefaultIterableAccessImpl implements IterableAccess { + abstract nextElementSeed>(seed: I): Nullable + + nextElement(): Nullable { + return this.nextElementSeed(GenericSeed) + } + + sizeHint(): number { return 0 } +} + +export interface Visitor { + visitString(value: string): T + visitNumber(value: number): T + visitBigInt(value: bigint): T + visitBoolean(value: boolean): T + visitSymbol(value: symbol): T + visitNull(): T + visitObject(value: MapAccess): T + visitFunction?(value: Function): T + visitMap?(value: MapAccess): T + visitIterable?(value: IterableAccess): T + visitClass?(name: string, fields: string[], value: any): T +} + +export interface Deserializer { + deserializeAny>(visitor: V): T + deserializeString>(visitor: V): T + deserializeNumber>(visitor: V): T + deserializeBigInt>(visitor: V): T + deserializeBoolean>(visitor: V): T + deserializeSymbol>(visitor: V): T + deserializeNull>(visitor: V): T + deserializeObject>(visitor: V): T + deserializeFunction?>(visitor: V): T + deserializeMap?>(visitor: V): T + deserializeIterable?>(visitor: V): T + deserializeClass?>(name: string, fields: string[], visitor: V): T +} + +export interface Deserialize { + deserialize(deserializer: D): T +} diff --git a/src/de/mixin.ts b/src/de/mixin.ts new file mode 100644 index 0000000..2c7cc9f --- /dev/null +++ b/src/de/mixin.ts @@ -0,0 +1,30 @@ +import { CaseConvention, Constructor, staticImplements } from '../utils' +import { GenericVisitor } from './generic' +import { Deserialize, Deserializer } from './interface' + +export interface DeserializationOptions { + rename?: string + renameAll?: CaseConvention +} + +const DefaultDeserializationOptions = {} + +export function deserialize(options?: DeserializationOptions) { + options = { + ...DefaultDeserializationOptions, + ...options + } + + return function (constructor: C) { + @staticImplements>() + class Deserializable extends constructor { + static deserialize(deserializer: D): T { + const visitor = new GenericVisitor() + return deserializer.deserializeAny(visitor) + } + } + + return Deserializable + } +} + diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..acf717a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,400 @@ +//export type Nullable = T | undefined +// +//function staticImplements() { +// return (constructor: U) => { constructor } +//} +// +//@staticImplements>() +//class GenericSeed implements Deserialize { +// static deserialize(deserializer: D): T { +// return deserializer.deserializeAny(new GenericVisitor()) +// } +// +// deserialize(deserializer: D): T { +// return GenericSeed.deserialize(deserializer) +// } +//} +// +//class GenericVisitor implements Visitor { +// visitString(value: string): T { +// return value as T +// } +// +// visitNumber(value: number): T { +// return value as T +// } +// +// visitBigInt(value: bigint): T { +// return value as T +// } +// +// visitBoolean(value: boolean): T { +// return value as T +// } +// +// visitSymbol(value: symbol): T { +// return value as T +// } +// +// visitNull(): T { +// return null as T +// } +// +// visitObject(access: MapAccess): T { +// const result: Record = {} +// let entry +// +// while ((entry = access.nextEntry())) { +// result[entry[0]] = entry[1] +// } +// +// return result +// } +// +// visitFunction?(value: Function): T { +// return value as T +// } +// +// visitMap?(access: MapAccess): T { +// const result = new Map() +// let entry +// +// while ((entry = access.nextEntry())) { +// result.set(entry[0], entry[1]) +// } +// +// return result as T +// } +// +// visitIterable?(access: IterableAccess): T { +// const result = new Array(access.sizeHint()) +// let element +// +// while ((element = access.nextElement())) { +// result.push(element) +// } +// +// return result as T +// } +// +// visitClass?(_name: string, _fields: string[], _value: any): T { +// throw new Error("Method not implemented.") +// } +//} +// +//export abstract class MapAccess { +// abstract nextKeySeed>(seed: K): Nullable +// abstract nextValueSeed>(seed: V): Nullable +// +// nextEntrySeed, V extends Deserialize>(kseed: K, vseed: V): Nullable<[TK, TV]> { +// const key = this.nextKeySeed(kseed) as Nullable +// if (key) { +// const value = this.nextValueSeed(vseed) as Nullable +// +// if (value) { +// return [key, value] +// } +// } +// } +// +// nextKey(): Nullable { +// return this.nextValueSeed(GenericSeed) +// } +// +// nextValue(): Nullable { +// return this.nextValueSeed(GenericSeed) +// } +// +// nextEntry(): Nullable<[K, V]> { +// return this.nextEntrySeed(GenericSeed, GenericSeed) +// } +//} +// +//export abstract class IterableAccess { +// abstract nextElementSeed>(seed: I): Nullable +// +// nextElement(): Nullable { +// return this.nextElementSeed(GenericSeed) +// } +// +// sizeHint(): number { return 0 } +//} +// +//export interface ObjectSerializer { +// serializeKey(key: U): T +// serializeValue(value: U): T +// end(): T +//} +// +//export interface IterableSerializer { +// serializeElement(element: U): T +// end(): T +//} +// +//export interface ClassSerializer { +// serializeField(name: PropertyKey, value: U): T +// end(): T +//} +// +//const TypeSerializerMethods = [ +// 'serializeString', +// 'serializeNumber', +// 'serializeBigInt', +// 'serializeBoolean', +// 'serializeSymbol', +// 'serializeMap', +// 'serializeIterable', +// 'serializeNull', +// 'serializeObject', +// 'serializeInstance', +// 'serializeFunction' +//] as const +// +//interface TypeSerializer { +// serializeString(value: string): T +// serializeNumber(value: number): T +// serializeBigInt(value: bigint): T +// serializeBoolean(value: boolean): T +// serializeSymbol(value: Symbol): T +// serializeNull(): T +// serializeObject(): ObjectSerializer +// serializeFunction?(value: Function): T +// serializeMap?(): ObjectSerializer +// serializeIterable?(): IterableSerializer +// serializeClass?(name: PropertyKey): ClassSerializer +//} +// +//const AnySerializerMethods = ['serializeAny'] +// +//interface AnySerializer { +// serializeAny?(value?: any): T +//} +// +//function isGenericSerializer(value: any): boolean { +// return AnySerializerMethods.every(k => isFunction(value[k])) && +// TypeSerializerMethods.every(k => !isFunction(value[k])) +//} +// +//function isPlainObject(value: any): boolean { +// return Object.getPrototypeOf(value) === Object.prototype +//} +// +//function isFunction(value: any): value is Function { +// return value != null && typeof value === 'function' +//} +// +//function isIterable(value: any): value is Iterable { +// return isFunction(value[Symbol.iterator]) +//} +// +//export type Serializer = Partial> & Partial> +// +//export interface Visitor { +// visitString(value: string): T +// visitNumber(value: number): T +// visitBigInt(value: bigint): T +// visitBoolean(value: boolean): T +// visitSymbol(value: symbol): T +// visitNull(): T +// visitObject(value: MapAccess): T +// visitFunction?(value: Function): T +// visitMap?(value: MapAccess): T +// visitIterable?(value: IterableAccess): T +// visitClass?(name: string, fields: string[], value: any): T +//} +// +//export interface Deserializer { +// deserializeAny>(visitor: V): T +// deserializeString>(visitor: V): T +// deserializeNumber>(visitor: V): T +// deserializeBigInt>(visitor: V): T +// deserializeBoolean>(visitor: V): T +// deserializeSymbol>(visitor: V): T +// deserializeNull>(visitor: V): T +// deserializeObject>(visitor: V): T +// deserializeFunction?>(visitor: V): T +// deserializeMap?>(visitor: V): T +// deserializeIterable?>(visitor: V): T +// deserializeClass?>(name: string, fields: string[], visitor: V): T +//} +// +//export type Primitive = string | number | boolean | symbol | bigint | null | undefined +//export interface ToString { +// toString(): string +//} +// +//export interface Serialize { +// serialize>(serializer: S): T +//} +// +//export type Serializable = Primitive | ToString | Serialize +// +//export interface Deserialize { +// deserialize(deserializer: D): T +//} +// +//type Constructor = new (...args: any[]) => object +// +//export const CaseConvention = Object.freeze({ +// Lowercase: 0, +// Uppercase: 1, +// PascalCase: 2, +// CamelCase: 3, +// SnakeCase: 4, +// ScreamingSnakeCase: 5, +// KebabCase: 6, +// ScreamingKebabCase: 7 +//} as const) +// +//export type CaseConvention = typeof CaseConvention[keyof typeof CaseConvention] +// +//function orElse(thisArg: any, a: Nullable, b: Function) { +// return function(...args: any) { +// const fn = a != null ? a : b +// return fn.apply(thisArg, args) +// } +//} +// +//// helper for better ergonomics +//// allows us to capture this, the fallback method, and the args in a closure +//function ifNull(thisArg: any, b: Function, ...args: any) { +// return function(a: Nullable) { +// return orElse(thisArg, a, b).call(thisArg, args) +// } +//} +// +//const unhandledType = (serializer: any, value: any) => new TypeError(`"${serializer.constructor.name}" has no method for value type "${typeof value}"`) +// +//function serializeEntries>(serializer: ObjectSerializer, value: E) { +// let state +// +// for (const [key, val] of value) { +// state = serializer.serializeKey(key) +// state = serializer.serializeValue(val) +// } +// +// return serializer.end() +//} +// +//function serializeObject>(serializer: ObjectSerializer, value: R) { +// return serializeEntries(serializer, Object.entries(value) as Iterable<[K, V]>) +//} +// +//function getClassName(value: any): Nullable { +// return value?.constructor.name +//} +// +//function serializeClass>(serializer: ClassSerializer, value: R) { +// for (const prop in value) { +// serializer.serializeField(prop, value[prop]) +// } +// +// return serializer.end() +//} +// +//function serializeIter>(serializer: IterableSerializer, value: V) { +// let state +// +// for (const val of value) { +// state = serializer.serializeElement(val) +// } +// +// return serializer.end() +//} +// +//// dispatches in the order of serialize -> serializeAny -> throw TypeError +//export function serializeWith(serializer: Serializer, value: Serializable): Nullable { +// // prepare fallback methods +// const serializeAny = orElse( +// serializer, +// serializer.serializeAny, +// (value: Serializable) => unhandledType(serializer, value) +// ) +// +// const serialize = ifNull(serializer, serializeAny, value) +// +// switch (typeof value) { +// case 'string': return serialize(serializer.serializeString) +// case 'number': return serialize(serializer.serializeNumber) +// case 'bigint': return serialize(serializer.serializeBigInt) +// case 'boolean': return serialize(serializer.serializeBoolean) +// case 'symbol': return serialize(serializer.serializeSymbol) +// case 'undefined': return serialize(serializer.serializeNull) +// case 'function': return serialize(serializer.serializeFunction) +// +// case 'object': +// if (value instanceof Map && isFunction(serializer.serializeMap)) { +// return serializeEntries(serializer.serializeMap(), value) +// } else if (isIterable(value) && isFunction(serializer.serializeIterable)) { +// return serializeIter(serializer.serializeIterable(), value) +// } else if (isFunction(serializer.serializeClass) && !isPlainObject(value)) { +// const name = getClassName(value) +// return serializeClass(serializer.serializeClass(name!), value as any) +// } else if (isFunction(serializer.serializeObject)) { +// return serializeObject(serializer.serializeObject!(), value as Record) +// } // deliberate fallthrough when the above fail +// +// default: return serializeAny(value) +// } +//} +// +//export interface SerializationOptions { +// default?: () => T +// rename?: string +// renameAll?: CaseConvention +// tag?: string +// untagged?: boolean +// withInherited?: boolean +//} +// +//const DefaultSerializationOptions: SerializationOptions = { +// withInherited: true +//} +// +//export function serialize(options?: SerializationOptions) { +// options = { +// ...DefaultSerializationOptions, +// ...options +// } +// +// return function (constructor: T) { +// return class Serializable extends constructor implements Serializable { +// static name = constructor.name +// serialize(serializer: Serializer): U { +// // shortcut for serializers with only the serializeAny method +// if (isGenericSerializer(serializer)) { +// return serializer.serializeAny!(this) as U +// } else { +// return serializeWith(serializer, this) as U +// } +// } +// } +// } +//} +// +//export interface DeserializationOptions { +// rename?: string +// renameAll?: CaseConvention +//} +// +//const DefaultDeserializationOptions = { +//} +// +//export function deserialize(options?: DeserializationOptions) { +// options = { +// ...DefaultDeserializationOptions, +// ...options +// } +// +// return function (constructor: C) { +// @staticImplements>() +// class Deserializable extends constructor { +// static deserialize(deserializer: D): T { +// const visitor = new GenericVisitor() +// return deserializer.deserializeAny(visitor) +// } +// } +// +// return Deserializable +// } +//} diff --git a/src/json.ts b/src/json.ts new file mode 100644 index 0000000..0f75479 --- /dev/null +++ b/src/json.ts @@ -0,0 +1,388 @@ +import { DefaultIterableAccessImpl, DefaultMapAccessImpl, Deserialize, Deserializer, IterableAccess, MapAccess, Visitor } from './de' +import { Serializer, serializeWith } from './ser' +import { mixin, Nullable } from './utils' + +export function toString(value: any): string { + return serializeWith(new JSONSerializer(), value)! +} + +export function fromString>(value: string, into: D): T { + const deserializer = JSONDeserializer.fromString(value) + return into.deserialize(deserializer) +} + +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) { + 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 JSONSerializer implements Serializer { + serializeAny(value?: any): string { + return JSON.stringify(value) + } +} + +const unexpected = (expected: string, actual: string, position: number) => new SyntaxError(`Expected ${expected} at position ${position} (got '${actual}')`) + +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() + } + + deserializeObject>(visitor: V): T { + let next = this.buffer.take() + if (next.next() === Token.LeftCurly) { + + const value = visitor.visitObject(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) + } + } + + 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.') + } + + deserializeFunction?>(_visitor: V): T { + throw new Error('Method not implemented.') + } + + deserializeMap?>(_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) + } + } + + deserializeClass?>(name: string, fields: string[], visitor: V): T { + throw new Error('Method not implemented.') + } +} + + diff --git a/src/ser/impl.ts b/src/ser/impl.ts new file mode 100644 index 0000000..68495c5 --- /dev/null +++ b/src/ser/impl.ts @@ -0,0 +1,78 @@ +import { ifNull, isFunction, isIterable, isPlainObject, Nullable, orElse } from '../utils' +import { ClassSerializer, IterableSerializer, ObjectSerializer, Serializable, Serializer } from './interface' + +const unhandledType = (serializer: any, value: any) => new TypeError(`'${serializer.constructor.name}' has no method for value type '${typeof value}'`) + +function serializeEntries>(serializer: ObjectSerializer, value: E) { + let state + + for (const [key, val] of value) { + state = serializer.serializeKey(key) + state = serializer.serializeValue(val) + } + + return serializer.end() +} + +function serializeObject>(serializer: ObjectSerializer, value: R) { + return serializeEntries(serializer, Object.entries(value) as Iterable<[K, V]>) +} + +function getClassName(value: any): Nullable { + return value?.constructor.name +} + +function serializeClass>(serializer: ClassSerializer, value: R) { + for (const prop in value) { + serializer.serializeField(prop, value[prop]) + } + + return serializer.end() +} + +function serializeIter>(serializer: IterableSerializer, value: V) { + let state + + for (const val of value) { + state = serializer.serializeElement(val) + } + + return serializer.end() +} + +// dispatches in the order of serialize -> serializeAny -> throw TypeError +export function serializeWith(serializer: Serializer, value: Serializable): Nullable { + // prepare fallback methods + const serializeAny = orElse( + serializer, + serializer.serializeAny, + (value: Serializable) => unhandledType(serializer, value) + ) + + const serialize = ifNull(serializer, serializeAny, value) + + switch (typeof value) { + case 'string': return serialize(serializer.serializeString) + case 'number': return serialize(serializer.serializeNumber) + case 'bigint': return serialize(serializer.serializeBigInt) + case 'boolean': return serialize(serializer.serializeBoolean) + case 'symbol': return serialize(serializer.serializeSymbol) + case 'undefined': return serialize(serializer.serializeNull) + case 'function': return serialize(serializer.serializeFunction) + + case 'object': + if (value instanceof Map && isFunction(serializer.serializeMap)) { + return serializeEntries(serializer.serializeMap(), value) + } else if (isIterable(value) && isFunction(serializer.serializeIterable)) { + return serializeIter(serializer.serializeIterable(), value) + } else if (isFunction(serializer.serializeClass) && !isPlainObject(value)) { + const name = getClassName(value) + return serializeClass(serializer.serializeClass(name!), value as any) + } else if (isFunction(serializer.serializeObject)) { + return serializeObject(serializer.serializeObject!(), value as Record) + } // deliberate fallthrough when the above fail + + default: return serializeAny(value) + } +} + diff --git a/src/ser/index.ts b/src/ser/index.ts new file mode 100644 index 0000000..31ca20c --- /dev/null +++ b/src/ser/index.ts @@ -0,0 +1,4 @@ +export * from './interface' +export * from './mixin' +export * from './impl' + diff --git a/src/ser/interface.ts b/src/ser/interface.ts new file mode 100644 index 0000000..3eac4b9 --- /dev/null +++ b/src/ser/interface.ts @@ -0,0 +1,65 @@ +import { isFunction, Primitive, ToString } from "../utils" + +export interface ObjectSerializer { + serializeKey(key: U): T + serializeValue(value: U): T + end(): T +} + +export interface IterableSerializer { + serializeElement(element: U): T + end(): T +} + +export interface ClassSerializer { + serializeField(name: PropertyKey, value: U): T + end(): T +} + +const TypeSerializerMethods = [ + 'serializeString', + 'serializeNumber', + 'serializeBigInt', + 'serializeBoolean', + 'serializeSymbol', + 'serializeMap', + 'serializeIterable', + 'serializeNull', + 'serializeObject', + 'serializeInstance', + 'serializeFunction' +] as const + +interface TypeSerializer { + serializeString(value: string): T + serializeNumber(value: number): T + serializeBigInt(value: bigint): T + serializeBoolean(value: boolean): T + serializeSymbol(value: Symbol): T + serializeNull(): T + serializeObject(): ObjectSerializer + serializeFunction?(value: Function): T + serializeMap?(): ObjectSerializer + serializeIterable?(): IterableSerializer + serializeClass?(name: PropertyKey): ClassSerializer +} + +const AnySerializerMethods = ['serializeAny'] + +interface AnySerializer { + serializeAny?(value?: any): T +} + +export function isGenericSerializer(value: any): boolean { + return AnySerializerMethods.every(k => isFunction(value[k])) && + TypeSerializerMethods.every(k => !isFunction(value[k])) +} + +export type Serializer = Partial> & Partial> + +export type Serializable = Primitive | ToString | Serialize + +export interface Serialize { + serialize>(serializer: S): T +} + diff --git a/src/ser/mixin.ts b/src/ser/mixin.ts new file mode 100644 index 0000000..b0cf375 --- /dev/null +++ b/src/ser/mixin.ts @@ -0,0 +1,37 @@ +import { CaseConvention, Constructor } from '../utils' +import { serializeWith } from './impl' +import { isGenericSerializer, Serializer } from './interface' + +export interface SerializationOptions { + default?: () => T + rename?: string + renameAll?: CaseConvention + tag?: string + untagged?: boolean + withInherited?: boolean +} + +const DefaultSerializationOptions: SerializationOptions = { + withInherited: true +} + +export function serialize(options?: SerializationOptions) { + options = { + ...DefaultSerializationOptions, + ...options + } + + return function (constructor: T) { + return class Serializable extends constructor implements Serializable { + static name = constructor.name + serialize(serializer: Serializer): U { + // shortcut for serializers with only the serializeAny method + if (isGenericSerializer(serializer)) { + return serializer.serializeAny!(this) as U + } else { + return serializeWith(serializer, this) as U + } + } + } + } +} diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 0000000..53ae095 --- /dev/null +++ b/src/test.ts @@ -0,0 +1,33 @@ +import { deserialize } from './de' +import { fromString, toString } from './json' +import { serialize } from './ser' + +const InnerStruct = deserialize()( + @serialize() + class { + c = 'awawa' + }) + +const TestStruct = deserialize()( + @serialize() + class { + a = 1 + b + inner = new InnerStruct() + d = true + e = Math.pow(2, 53) + f = Symbol('test') + g = [1, 'a', [3]] + + constructor() { + this.b = new Map() + this.b.set('test key', 2) + } + }) + +const test = new TestStruct() +const value = toString(test) +console.log(value) +const test2 = fromString(value, TestStruct) +console.log(test2) + diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..51aeb72 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,72 @@ +export type Nullable = T | undefined + +export type Primitive = string | number | boolean | symbol | bigint | null | undefined + +export interface ToString { + toString(): string +} + +export function staticImplements() { + return (constructor: U) => { constructor } +} + +export function isPlainObject(value: any): boolean { + return Object.getPrototypeOf(value) === Object.prototype +} + +export function isFunction(value: any): value is Function { + return value != null && typeof value === 'function' +} + +export function isIterable(value: any): value is Iterable { + return isFunction(value[Symbol.iterator]) +} + +export type Constructor = new (...args: any[]) => T + +export const CaseConvention = Object.freeze({ + Lowercase: 0, + Uppercase: 1, + PascalCase: 2, + CamelCase: 3, + SnakeCase: 4, + ScreamingSnakeCase: 5, + KebabCase: 6, + ScreamingKebabCase: 7 +} as const) + +export type CaseConvention = typeof CaseConvention[keyof typeof CaseConvention] + +export function orElse(thisArg: any, a: Nullable, b: Function) { + return function(...args: any) { + const fn = a != null ? a : b + return fn.apply(thisArg, args) + } +} + +export function ifNull(thisArg: any, b: Function, ...args: any) { + return function(a: Nullable) { + return orElse(thisArg, a, b).call(thisArg, args) + } +} + +function applyMixins(derivedCtor: any, constructors: any[]) { + constructors.forEach((baseCtor) => { + Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { + Object.defineProperty( + derivedCtor.prototype, + name, + Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || + Object.create(null) + ) + }) + }) +} + +export function mixin(impl: Function) { + return function >(constructor: TBase) { + applyMixins(constructor, [impl]) + return constructor + } +} + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..fd0def4 --- /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": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* 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. */ + } +}