refactor Serialize a bit, add to readme

This commit is contained in:
Rowan 2025-05-24 21:35:06 -05:00
parent 5d4591e63c
commit 584f4f1bb7
15 changed files with 202 additions and 36 deletions

View file

@ -24,20 +24,111 @@ there are four interfaces which are relevant to `serde-ts` users.
## `Serializer`
```ts
interface ISerializer<T> {
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<T>
serializeClass(name: string): ISerializeObject<T>
}
```
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<Type>` function for each of `serde-ts`'s data types.
## `Deserializer`
```ts
interface IDeserializer {
deserializeAny<T>(visitor: Partial<IVisitor<T>>): T
deserializeBoolean<T>(visitor: Partial<IVisitor<T>>): T
deserializeNumber<T>(visitor: Partial<IVisitor<T>>): T
deserializeBigInt<T>(visitor: Partial<IVisitor<T>>): T
deserializeString<T>(visitor: Partial<IVisitor<T>>): T
deserializeSymbol<T>(visitor: Partial<IVisitor<T>>): T
deserializeNull<T>(visitor: Partial<IVisitor<T>>): T
deserializeObject<T>(visitor: Partial<IVisitor<T>>): T
deserializeIterable<T>(visitor: Partial<IVisitor<T>>): T
deserializeFunction<T>(visitor: Partial<IVisitor<T>>): T
}
```
a `Deserializer` is responsible for transforming the target serialization format into the internal `serde-ts` data model. it will have a `deserialize<Type>` for each of the `serde-ts`'s data types.
## `Serialize`
```ts
interface Serialize<T> {
<U>(serializer: ISerializer<T>, 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<T> {
(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<void>, value: Vector2) => {
const iter = serializer.serializeIterable() // returns an ISerializeIterable<void>
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 }
```

View file

@ -16,8 +16,8 @@ export declare abstract class MapAccess {
abstract nextKeySeed<T, K extends Deserialize<T>>(seed: K): IteratorResult<T>;
abstract nextValueSeed<T, V extends Deserialize<T>>(seed: V): IteratorResult<T>;
nextEntrySeed<TK, TV, K extends Deserialize<TK>, V extends Deserialize<TV>>(kseed: K, vseed: V): IteratorResult<[TK, TV]>;
abstract nextKey<T>(): IteratorResult<T>;
abstract nextValue<V>(): IteratorResult<V>;
nextKey<T>(): IteratorResult<T>;
nextValue<V>(): IteratorResult<V>;
nextEntry<K, V>(): IteratorResult<[K, V]>;
private generate;
keys<T, K extends Deserialize<T>>(seed?: K): Iterator<T>;
@ -26,12 +26,14 @@ export declare abstract class MapAccess {
[Symbol.iterator]<K, V>(): Iterator<K, V>;
}
export interface IIterableAccess {
nextElementSeed<T, D extends Deserialize<T>>(seed: D): IteratorResult<T>;
nextElement<T>(): IteratorResult<T>;
sizeHint?(): Nullable<number>;
[Symbol.iterator]<T>(): Iterator<T>;
}
export declare abstract class IterableAccess implements IIterableAccess {
abstract nextElement<T>(): IteratorResult<T>;
abstract nextElementSeed<T, D extends Deserialize<T>>(seed: D): IteratorResult<T>;
nextElement<T>(): IteratorResult<T>;
[Symbol.iterator]<T>(): Iterator<T>;
}
export declare function isVisitor<T>(visitor: any): visitor is IVisitor<T>;

14
dist/de/interface.js vendored
View file

@ -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)
};
}
}

10
dist/registry.d.ts vendored
View file

@ -1,11 +1,11 @@
import { Deserialize } from './de';
import { Serialize } from './ser';
export declare class Registry {
serializers: Map<Function, Serialize<any>>;
serializers: Map<Function, Serialize<any, any>>;
deserializers: Map<Function, Deserialize<any>>;
registerSerializer<T>(ctor: Function, serialize: Serialize<T>): void;
registerDeserializer<T>(ctor: Function, deserialize: Deserialize<T>): void;
registerSerialize<T, U>(ctor: Function, serialize: Serialize<T, U>): void;
registerDeserialize<T>(ctor: Function, deserialize: Deserialize<T>): void;
}
export declare const GlobalRegistry: Registry;
export declare const registerSerializer: <T>(ctor: Function, serialize: Serialize<T>) => void;
export declare const registerDeserializer: <T>(ctor: Function, deserialize: Deserialize<T>) => void;
export declare const registerSerialize: <T, U>(ctor: Function, serialize: Serialize<T, U>) => void;
export declare const registerDeserialize: <T>(ctor: Function, deserialize: Deserialize<T>) => void;

10
dist/registry.js vendored
View file

@ -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);

3
dist/ser/impl.d.ts vendored
View file

@ -1,2 +1,3 @@
import { ISerializer } from './interface';
export declare function serialize<T, V, S extends ISerializer<T>>(serializer: S, value: V): T;
import { Registry } from '../registry';
export declare function serialize<T, U, S extends ISerializer<T>>(serializer: S, value: U, registry?: Registry): T;

13
dist/ser/impl.js vendored
View file

@ -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);
}

View file

@ -27,9 +27,11 @@ export interface ISerializer<T> {
serializeSymbol(value: symbol): T;
serializeNull(): T;
serializeObject(): ISerializeObject<T>;
serializeIterable(): ISerializeIterable<T>;
serializeClass(name: string): ISerializeObject<T>;
}
export declare class Serializer<T> implements ISerializer<T> {
serializeIterable(): ISerializeIterable<T>;
serializeAny(_value: any): T;
serializeBoolean(_value: boolean): T;
serializeNumber(_value: number): T;
@ -40,6 +42,6 @@ export declare class Serializer<T> implements ISerializer<T> {
serializeObject(): ISerializeObject<T>;
serializeClass(_name: string): ISerializeObject<T>;
}
export interface Serialize<T> {
<U, S extends Serializer<U>>(serializer: S, value: T): U;
export interface Serialize<T, U> {
(serializer: ISerializer<T>, value: U): T;
}

View file

@ -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.");
}

14
dist/utils.d.ts vendored
View file

@ -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<any>;
export declare function isString(value: any): value is string;
@ -16,3 +17,14 @@ export declare class IterResult {
static Next<T>(value: T): IteratorResult<T>;
static Done<T>(): IteratorResult<T>;
}
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;
}>;

20
dist/utils.js vendored
View file

@ -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
});

View file

@ -1,4 +1,5 @@
import { IterResult, Nullable } from '../utils'
import { GenericSeed } from './generic'
export interface IMapAccess {
nextKeySeed<T, K extends Deserialize<T>>(seed: K): IteratorResult<T>
@ -32,8 +33,13 @@ export abstract class MapAccess {
return IterResult.Done()
}
abstract nextKey<T>(): IteratorResult<T>
abstract nextValue<V>(): IteratorResult<V>
nextKey<T>(): IteratorResult<T> {
return this.nextKeySeed(GenericSeed.deserialize)
}
nextValue<V>(): IteratorResult<V> {
return this.nextValueSeed(GenericSeed.deserialize)
}
nextEntry<K, V>(): IteratorResult<[K, V]> {
const key = this.nextKey() as IteratorResult<K>
@ -87,17 +93,22 @@ export abstract class MapAccess {
}
export interface IIterableAccess {
nextElementSeed<T, D extends Deserialize<T>>(seed: D): IteratorResult<T>
nextElement<T>(): IteratorResult<T>
sizeHint?(): Nullable<number>
[Symbol.iterator]<T>(): Iterator<T>
}
export abstract class IterableAccess implements IIterableAccess {
abstract nextElement<T>(): IteratorResult<T>
abstract nextElementSeed<T, D extends Deserialize<T>>(seed: D): IteratorResult<T>
*[Symbol.iterator]<T>(): Iterator<T> {
nextElement<T>(): IteratorResult<T> {
return this.nextElementSeed(GenericSeed.deserialize)
}
[Symbol.iterator]<T>(): Iterator<T> {
return {
next: this.nextElement
next: this.nextElement.bind(this)
}
}
}

View file

@ -2,19 +2,19 @@ import { Deserialize } from './de'
import { Serialize } from './ser'
export class Registry {
serializers: Map<Function, Serialize<any>> = new Map()
serializers: Map<Function, Serialize<any, any>> = new Map()
deserializers: Map<Function, Deserialize<any>> = new Map()
registerSerializer<T>(ctor: Function, serialize: Serialize<T>) {
registerSerialize<T, U>(ctor: Function, serialize: Serialize<T, U>) {
this.serializers.set(ctor, serialize)
}
registerDeserializer<T>(ctor: Function, deserialize: Deserialize<T>) {
registerDeserialize<T>(ctor: Function, deserialize: Deserialize<T>) {
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)

View file

@ -23,12 +23,12 @@ function serializeClass<T, U extends object, S extends ISerializer<T>>(serialize
return serializeObject(ser, value)
}
function getSerialize<T, U>(value: U, registry: Registry): Serialize<T> {
function getSerialize<T, U>(value: U, registry: Registry): Serialize<T, U> {
if (isObject(value)) {
return registry.serializers.get(value.constructor) as Serialize<T>
return registry.serializers.get(value.constructor) as Serialize<T, U>
}
return registry.serializers.get(PrimitivePrototype[typeof value]) || defaultSerialize as Serialize<T>
return registry.serializers.get(PrimitivePrototype[typeof value]) || defaultSerialize as Serialize<T, U>
}
function defaultSerialize<T, U, S extends ISerializer<T>>(serializer: S, value: U): T {
switch (typeof value) {

View file

@ -35,10 +35,15 @@ export interface ISerializer<T> {
serializeSymbol(value: symbol): T
serializeNull(): T
serializeObject(): ISerializeObject<T>
serializeIterable(): ISerializeIterable<T>
serializeClass(name: string): ISerializeObject<T>
}
export class Serializer<T> implements ISerializer<T> {
serializeIterable(): ISerializeIterable<T> {
throw new Error("Method not implemented.")
}
serializeAny(_value: any): T {
throw new Error("Method not implemented.")
}
@ -76,7 +81,7 @@ export class Serializer<T> implements ISerializer<T> {
}
}
export interface Serialize<T> {
<U>(serializer: ISerializer<T>, value: U): T
export interface Serialize<T, U> {
(serializer: ISerializer<T>, value: U): T
}