From 584f4f1bb71cd84221117d01f6cf995250631c1c Mon Sep 17 00:00:00 2001 From: rowan Date: Sat, 24 May 2025 21:35:06 -0500 Subject: [PATCH] refactor Serialize a bit, add to readme --- README.md | 91 +++++++++++++++++++++++++++++++++++++++++ dist/de/interface.d.ts | 8 ++-- dist/de/interface.js | 14 ++++++- dist/registry.d.ts | 10 ++--- dist/registry.js | 10 ++--- dist/ser/impl.d.ts | 3 +- dist/ser/impl.js | 13 +++++- dist/ser/interface.d.ts | 6 ++- dist/ser/interface.js | 3 ++ dist/utils.d.ts | 14 ++++++- dist/utils.js | 20 ++++++++- src/de/interface.ts | 21 +++++++--- src/registry.ts | 10 ++--- src/ser/impl.ts | 6 +-- src/ser/interface.ts | 9 +++- 15 files changed, 202 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 6f2c708..d9285b1 100644 --- a/README.md +++ b/README.md @@ -24,20 +24,111 @@ there are four interfaces which are relevant to `serde-ts` users. ## `Serializer` +```ts +interface ISerializer { + serializeAny?(value: any): T + serializeBoolean(value: boolean): T + serializeNumber(value: number): T + serializeBigInt(value: bigint): T + serializeString(value: string): T + serializeSymbol(value: symbol): T + serializeNull(): T + serializeObject(): ISerializeObject + serializeClass(name: string): ISerializeObject +} +``` + a `Serializer` is responsible for transforming the internal `serde-ts` data model into the target serialization format. this means that it will have a `serialize` function for each of `serde-ts`'s data types. ## `Deserializer` +```ts +interface IDeserializer { + deserializeAny(visitor: Partial>): T + deserializeBoolean(visitor: Partial>): T + deserializeNumber(visitor: Partial>): T + deserializeBigInt(visitor: Partial>): T + deserializeString(visitor: Partial>): T + deserializeSymbol(visitor: Partial>): T + deserializeNull(visitor: Partial>): T + deserializeObject(visitor: Partial>): T + deserializeIterable(visitor: Partial>): T + deserializeFunction(visitor: Partial>): T +} +``` a `Deserializer` is responsible for transforming the target serialization format into the internal `serde-ts` data model. it will have a `deserialize` for each of the `serde-ts`'s data types. ## `Serialize` +```ts +interface Serialize { + (serializer: ISerializer, value: U): T +} +``` `Serialize` is responsible for transforming a value *to* the internal data model. for example, a `Vector2` may be internally serialized as an iterable of numbers. ## `Deserialize` +```ts +interface Deserialize { + (deserializer: IDeserializer): T +} +``` `Deserialize` is responsible for transforming a value *from* the internal data model. it is the inverse of `Serialize`. depending on the `Deserialize` target, it may accept an iterable of numbers and produce a `Vector2`. +# Example +in simple cases, a formatter for `serde-ts` will expose a pair of functions like `toString` and `fromString` which will handle serializing and deserializing for you. + +in more complex cases, you still won't be implementing `Serializer`/`Deserializer`, only consuming them with `Serialize`/`Deserialize`. assuming we're using a format which reads/writes JSON + +```ts +import { registerSerialize, registerDeserialize } from 'serde' +import { ISerializer } from 'serde/ser' +import { IDeserializer, IIterableAccess } from 'serde/de' +import { fromString, toString } from 'serde-json-ts' + +class Vector2 { + x: number + y: number + + constructor(x: number, y: number) { + this.x = x + this.y = y + } +} + +// we're registering to the global serde registry +registerSerialize(Vector2, (serializer: ISerializer, value: Vector2) => { + const iter = serializer.serializeIterable() // returns an ISerializeIterable + iter.serializeElement(value.x) + iter.serializeElement(value.y) + return iter.end() +}) + +registerDeserialize(Vector2, (deserializer: IDeserializer) => deserializer.deserializeIterable({ + // we could implement visitNumber here, but we'll let the default + // deserializer handle it + visitIterable(access: IIterableAccess) { + const elements = [] + + for (const item of access) { + elements.push(item as number) + } + + return new Vector2(elements[0], elements[1]) + } +}) +) + +const one = new Vector2(1, 1) +const serialized = toString(one) +console.log(serialized) +// "[1, 1]" + +const deserializedOne = fromString(serialized, Vector2) +console.log(deserializedOne) +// Vector2 { x: 1, y: 1 } +``` diff --git a/dist/de/interface.d.ts b/dist/de/interface.d.ts index ea715c9..b39a496 100644 --- a/dist/de/interface.d.ts +++ b/dist/de/interface.d.ts @@ -16,8 +16,8 @@ export declare abstract class MapAccess { abstract nextKeySeed>(seed: K): IteratorResult; abstract nextValueSeed>(seed: V): IteratorResult; nextEntrySeed, V extends Deserialize>(kseed: K, vseed: V): IteratorResult<[TK, TV]>; - abstract nextKey(): IteratorResult; - abstract nextValue(): IteratorResult; + nextKey(): IteratorResult; + nextValue(): IteratorResult; nextEntry(): IteratorResult<[K, V]>; private generate; keys>(seed?: K): Iterator; @@ -26,12 +26,14 @@ export declare abstract class MapAccess { [Symbol.iterator](): Iterator; } export interface IIterableAccess { + nextElementSeed>(seed: D): IteratorResult; nextElement(): IteratorResult; sizeHint?(): Nullable; [Symbol.iterator](): Iterator; } export declare abstract class IterableAccess implements IIterableAccess { - abstract nextElement(): IteratorResult; + abstract nextElementSeed>(seed: D): IteratorResult; + nextElement(): IteratorResult; [Symbol.iterator](): Iterator; } export declare function isVisitor(visitor: any): visitor is IVisitor; diff --git a/dist/de/interface.js b/dist/de/interface.js index 0b95303..b473fc9 100644 --- a/dist/de/interface.js +++ b/dist/de/interface.js @@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.IterableAccess = exports.MapAccess = void 0; exports.isVisitor = isVisitor; const utils_1 = require("../utils"); +const generic_1 = require("./generic"); class MapAccess { nextEntrySeed(kseed, vseed) { const key = this.nextKeySeed(kseed); @@ -14,6 +15,12 @@ class MapAccess { } return utils_1.IterResult.Done(); } + nextKey() { + return this.nextKeySeed(generic_1.GenericSeed.deserialize); + } + nextValue() { + return this.nextValueSeed(generic_1.GenericSeed.deserialize); + } nextEntry() { const key = this.nextKey(); if (!key.done) { @@ -51,9 +58,12 @@ class MapAccess { } exports.MapAccess = MapAccess; class IterableAccess { - *[Symbol.iterator]() { + nextElement() { + return this.nextElementSeed(generic_1.GenericSeed.deserialize); + } + [Symbol.iterator]() { return { - next: this.nextElement + next: this.nextElement.bind(this) }; } } diff --git a/dist/registry.d.ts b/dist/registry.d.ts index f8643b7..1cbd5c7 100644 --- a/dist/registry.d.ts +++ b/dist/registry.d.ts @@ -1,11 +1,11 @@ import { Deserialize } from './de'; import { Serialize } from './ser'; export declare class Registry { - serializers: Map>; + serializers: Map>; deserializers: Map>; - registerSerializer(ctor: Function, serialize: Serialize): void; - registerDeserializer(ctor: Function, deserialize: Deserialize): void; + registerSerialize(ctor: Function, serialize: Serialize): void; + registerDeserialize(ctor: Function, deserialize: Deserialize): void; } export declare const GlobalRegistry: Registry; -export declare const registerSerializer: (ctor: Function, serialize: Serialize) => void; -export declare const registerDeserializer: (ctor: Function, deserialize: Deserialize) => void; +export declare const registerSerialize: (ctor: Function, serialize: Serialize) => void; +export declare const registerDeserialize: (ctor: Function, deserialize: Deserialize) => void; diff --git a/dist/registry.js b/dist/registry.js index 90eff1e..d7a4879 100644 --- a/dist/registry.js +++ b/dist/registry.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.registerDeserializer = exports.registerSerializer = exports.GlobalRegistry = exports.Registry = void 0; +exports.registerDeserialize = exports.registerSerialize = exports.GlobalRegistry = exports.Registry = void 0; class Registry { constructor() { Object.defineProperty(this, "serializers", { @@ -16,14 +16,14 @@ class Registry { value: new Map() }); } - registerSerializer(ctor, serialize) { + registerSerialize(ctor, serialize) { this.serializers.set(ctor, serialize); } - registerDeserializer(ctor, deserialize) { + registerDeserialize(ctor, deserialize) { this.deserializers.set(ctor, deserialize); } } exports.Registry = Registry; exports.GlobalRegistry = new Registry(); -exports.registerSerializer = exports.GlobalRegistry.registerSerializer.bind(exports.GlobalRegistry); -exports.registerDeserializer = exports.GlobalRegistry.registerDeserializer.bind(exports.GlobalRegistry); +exports.registerSerialize = exports.GlobalRegistry.registerSerialize.bind(exports.GlobalRegistry); +exports.registerDeserialize = exports.GlobalRegistry.registerDeserialize.bind(exports.GlobalRegistry); diff --git a/dist/ser/impl.d.ts b/dist/ser/impl.d.ts index 75539f5..b99d4d4 100644 --- a/dist/ser/impl.d.ts +++ b/dist/ser/impl.d.ts @@ -1,2 +1,3 @@ import { ISerializer } from './interface'; -export declare function serialize>(serializer: S, value: V): T; +import { Registry } from '../registry'; +export declare function serialize>(serializer: S, value: U, registry?: Registry): T; diff --git a/dist/ser/impl.js b/dist/ser/impl.js index 0c90628..53f2a02 100644 --- a/dist/ser/impl.js +++ b/dist/ser/impl.js @@ -2,6 +2,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.serialize = serialize; const utils_1 = require("../utils"); +const registry_1 = require("../registry"); class UnhandledTypeError extends TypeError { constructor(serializer, value) { super(`unhandled type: '${typeof value}' for serializer ${serializer.constructor.name}`); @@ -19,7 +20,13 @@ function serializeClass(serializer, value) { const ser = serializer.serializeClass(name); return serializeObject(ser, value); } -function serialize(serializer, value) { +function getSerialize(value, registry) { + if ((0, utils_1.isObject)(value)) { + return registry.serializers.get(value.constructor); + } + return registry.serializers.get(utils_1.PrimitivePrototype[typeof value]) || defaultSerialize; +} +function defaultSerialize(serializer, value) { switch (typeof value) { case 'string': return serializer.serializeString(value); case 'number': return serializer.serializeNumber(value); @@ -36,3 +43,7 @@ function serialize(serializer, value) { default: throw new UnhandledTypeError(serializer, value); } } +function serialize(serializer, value, registry = registry_1.GlobalRegistry) { + const ser = getSerialize(value, registry); + return ser(serializer, value); +} diff --git a/dist/ser/interface.d.ts b/dist/ser/interface.d.ts index 03c74c6..139dfeb 100644 --- a/dist/ser/interface.d.ts +++ b/dist/ser/interface.d.ts @@ -27,9 +27,11 @@ export interface ISerializer { serializeSymbol(value: symbol): T; serializeNull(): T; serializeObject(): ISerializeObject; + serializeIterable(): ISerializeIterable; serializeClass(name: string): ISerializeObject; } export declare class Serializer implements ISerializer { + serializeIterable(): ISerializeIterable; serializeAny(_value: any): T; serializeBoolean(_value: boolean): T; serializeNumber(_value: number): T; @@ -40,6 +42,6 @@ export declare class Serializer implements ISerializer { serializeObject(): ISerializeObject; serializeClass(_name: string): ISerializeObject; } -export interface Serialize { - >(serializer: S, value: T): U; +export interface Serialize { + (serializer: ISerializer, value: U): T; } diff --git a/dist/ser/interface.js b/dist/ser/interface.js index 0665d89..7e836d0 100644 --- a/dist/ser/interface.js +++ b/dist/ser/interface.js @@ -12,6 +12,9 @@ class SerializeIterable { } exports.SerializeIterable = SerializeIterable; class Serializer { + serializeIterable() { + throw new Error("Method not implemented."); + } serializeAny(_value) { throw new Error("Method not implemented."); } diff --git a/dist/utils.d.ts b/dist/utils.d.ts index 4e4591e..da362b0 100644 --- a/dist/utils.d.ts +++ b/dist/utils.d.ts @@ -6,7 +6,8 @@ export type Primitive = string | number | boolean | symbol | bigint | null | und export interface ToString { toString(): string; } -export declare function isPlainObject(value: any): boolean; +export declare function isObject(value: any): value is object; +export declare function isPlainObject(value: any): value is object; export declare function isFunction(value: any): value is Function; export declare function isIterable(value: any): value is Iterable; export declare function isString(value: any): value is string; @@ -16,3 +17,14 @@ export declare class IterResult { static Next(value: T): IteratorResult; static Done(): IteratorResult; } +export declare function Null(...args: any): null; +export declare const PrimitivePrototype: Readonly<{ + readonly undefined: typeof Null; + readonly boolean: BooleanConstructor; + readonly number: NumberConstructor; + readonly bigint: BigIntConstructor; + readonly string: StringConstructor; + readonly symbol: SymbolConstructor; + readonly object: ObjectConstructor; + readonly function: FunctionConstructor; +}>; diff --git a/dist/utils.js b/dist/utils.js index ed4dbbc..fcb3628 100644 --- a/dist/utils.js +++ b/dist/utils.js @@ -1,11 +1,16 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.IterResult = void 0; +exports.PrimitivePrototype = exports.IterResult = void 0; +exports.isObject = isObject; exports.isPlainObject = isPlainObject; exports.isFunction = isFunction; exports.isIterable = isIterable; exports.isString = isString; exports.isNumber = isNumber; +exports.Null = Null; +function isObject(value) { + return typeof value === 'object'; +} function isPlainObject(value) { return Object.getPrototypeOf(value) === Object.prototype; } @@ -30,3 +35,16 @@ class IterResult { } } exports.IterResult = IterResult; +function Null(...args) { + return null; +} +exports.PrimitivePrototype = Object.freeze({ + undefined: Null, + boolean: Boolean, + number: Number, + bigint: BigInt, + string: String, + symbol: Symbol, + object: Object, + function: Function +}); diff --git a/src/de/interface.ts b/src/de/interface.ts index ef437ec..9a9aee8 100644 --- a/src/de/interface.ts +++ b/src/de/interface.ts @@ -1,4 +1,5 @@ import { IterResult, Nullable } from '../utils' +import { GenericSeed } from './generic' export interface IMapAccess { nextKeySeed>(seed: K): IteratorResult @@ -32,8 +33,13 @@ export abstract class MapAccess { return IterResult.Done() } - abstract nextKey(): IteratorResult - abstract nextValue(): IteratorResult + nextKey(): IteratorResult { + return this.nextKeySeed(GenericSeed.deserialize) + } + + nextValue(): IteratorResult { + return this.nextValueSeed(GenericSeed.deserialize) + } nextEntry(): IteratorResult<[K, V]> { const key = this.nextKey() as IteratorResult @@ -87,17 +93,22 @@ export abstract class MapAccess { } export interface IIterableAccess { + nextElementSeed>(seed: D): IteratorResult nextElement(): IteratorResult sizeHint?(): Nullable [Symbol.iterator](): Iterator } export abstract class IterableAccess implements IIterableAccess { - abstract nextElement(): IteratorResult + abstract nextElementSeed>(seed: D): IteratorResult - *[Symbol.iterator](): Iterator { + nextElement(): IteratorResult { + return this.nextElementSeed(GenericSeed.deserialize) + } + + [Symbol.iterator](): Iterator { return { - next: this.nextElement + next: this.nextElement.bind(this) } } } diff --git a/src/registry.ts b/src/registry.ts index fd99e3d..6c69392 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -2,19 +2,19 @@ import { Deserialize } from './de' import { Serialize } from './ser' export class Registry { - serializers: Map> = new Map() + serializers: Map> = new Map() deserializers: Map> = new Map() - registerSerializer(ctor: Function, serialize: Serialize) { + registerSerialize(ctor: Function, serialize: Serialize) { this.serializers.set(ctor, serialize) } - registerDeserializer(ctor: Function, deserialize: Deserialize) { + registerDeserialize(ctor: Function, deserialize: Deserialize) { this.deserializers.set(ctor, deserialize) } } export const GlobalRegistry = new Registry() -export const registerSerializer = GlobalRegistry.registerSerializer.bind(GlobalRegistry) -export const registerDeserializer = GlobalRegistry.registerDeserializer.bind(GlobalRegistry) +export const registerSerialize = GlobalRegistry.registerSerialize.bind(GlobalRegistry) +export const registerDeserialize = GlobalRegistry.registerDeserialize.bind(GlobalRegistry) diff --git a/src/ser/impl.ts b/src/ser/impl.ts index 74f1e26..d62f24e 100644 --- a/src/ser/impl.ts +++ b/src/ser/impl.ts @@ -23,12 +23,12 @@ function serializeClass>(serialize return serializeObject(ser, value) } -function getSerialize(value: U, registry: Registry): Serialize { +function getSerialize(value: U, registry: Registry): Serialize { if (isObject(value)) { - return registry.serializers.get(value.constructor) as Serialize + return registry.serializers.get(value.constructor) as Serialize } - return registry.serializers.get(PrimitivePrototype[typeof value]) || defaultSerialize as Serialize + return registry.serializers.get(PrimitivePrototype[typeof value]) || defaultSerialize as Serialize } function defaultSerialize>(serializer: S, value: U): T { switch (typeof value) { diff --git a/src/ser/interface.ts b/src/ser/interface.ts index 332c24d..3d4b33b 100644 --- a/src/ser/interface.ts +++ b/src/ser/interface.ts @@ -35,10 +35,15 @@ export interface ISerializer { serializeSymbol(value: symbol): T serializeNull(): T serializeObject(): ISerializeObject + serializeIterable(): ISerializeIterable serializeClass(name: string): ISerializeObject } export class Serializer implements ISerializer { + serializeIterable(): ISerializeIterable { + throw new Error("Method not implemented.") + } + serializeAny(_value: any): T { throw new Error("Method not implemented.") } @@ -76,7 +81,7 @@ export class Serializer implements ISerializer { } } -export interface Serialize { - (serializer: ISerializer, value: U): T +export interface Serialize { + (serializer: ISerializer, value: U): T }