203 lines
6.5 KiB
Markdown
203 lines
6.5 KiB
Markdown
# serde-ts
|
|
|
|
a library for serializing and deserializing javascript objects
|
|
|
|
# Usage
|
|
|
|
this library makes no assumptions about formats and loosely follows the architecture of Rust's [serde](https://serde.rs/) crate. the major difference are the particular data types. serde-ts's internal data model contains the following types
|
|
|
|
## `serde-ts` data types
|
|
* null/undefined
|
|
* boolean
|
|
* number
|
|
* bigint
|
|
* string
|
|
* symbol
|
|
* function
|
|
* object
|
|
* iterable
|
|
* class
|
|
|
|
with the exception of iterables and classes, these types were chosen as they are the ones provided by Javascript's native `typeof` operator which is used internally for type checking.
|
|
|
|
there are five 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
|
|
serializeIterable(len?: number): ISerializeIterable<T>
|
|
serializeObject(len?: number): ISerializeObject<T>
|
|
serializeClass(name: string, len?: number): 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`.
|
|
|
|
## `Visitor`
|
|
```ts
|
|
interface IVisitor<T> {
|
|
visitBoolean(value: boolean): T
|
|
visitNumber(value: number): T
|
|
visitBigInt(value: bigint): T
|
|
visitString(value: string): T
|
|
visitSymbol(value: symbol): T
|
|
visitNull(): T
|
|
visitObject(access: IMapAccess): T
|
|
visitIterable(access: IIterableAccess): T
|
|
}
|
|
```
|
|
|
|
a `Visitor` is part of the deserialization process and is how each data type is mapped from the internal data model to whatever the end structure is. `Deserialize` is responsible for creating a `Visitor` and giving it to a `Deserializer`. that `Visitor` will then be given every value once it has been deserialized into `serde-ts`'s internal model.
|
|
|
|
# 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 { forward, IDeserializer, IIterableAccess, IMapAccess } 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
|
|
}
|
|
|
|
static serialize(serializer: ISerializer<string>, value: Vector2) {
|
|
const iter = serializer.serializeIterable() // returns an ISerializeIterable<void>
|
|
iter.serializeElement(value.x)
|
|
iter.serializeElement(value.y)
|
|
return iter.end()
|
|
}
|
|
|
|
static deserialize(deserializer: IDeserializer) {
|
|
return 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])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
class Entity {
|
|
name: string
|
|
position: Vector2
|
|
|
|
constructor(name: string, position: Vector2) {
|
|
this.name = name
|
|
this.position = position
|
|
}
|
|
|
|
static serialize(serializer: ISerializer<string>, value: Entity) {
|
|
const ser = serializer.serializeObject()
|
|
ser.serializeEntry('name', value.name)
|
|
ser.serializeEntry('position', value.position)
|
|
return ser.end()
|
|
}
|
|
|
|
static deserialize(deserializer: IDeserializer) {
|
|
return deserializer.deserializeObject({
|
|
visitObject(access: IMapAccess) {
|
|
let name, position
|
|
for (const [key, value] of access) {
|
|
switch (key) {
|
|
case 'name':
|
|
name = value
|
|
break
|
|
case 'position':
|
|
// forward the deserialization to Vector2
|
|
position = forward(value as string, Vector2)
|
|
break
|
|
}
|
|
}
|
|
|
|
return new Entity(name as string, position as Vector2)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// we're registering to the global serde registry
|
|
registerSerialize(Vector2, Vector2.serialize)
|
|
registerDeserialize(Vector2, Vector2.deserialize)
|
|
registerSerialize(Entity, Entity.serialize)
|
|
registerDeserialize(Entity, Entity.deserialize)
|
|
|
|
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 }
|
|
|
|
const player = new Entity('Player', one)
|
|
const serializedPlayer = toString(player)
|
|
console.log(serializedPlayer)
|
|
// {"name":"Player","position":[1,1]}
|
|
|
|
const deserializedPlayer = fromString(serializedPlayer, Entity)
|
|
console.log(deserializedPlayer)
|
|
// Entity { name: 'Player', position: Vector2 { x: 1, y: 1 } }
|
|
```
|
|
|