You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lib/src/support/bus/serial/Serialization.ts

159 lines
5.3 KiB

import {Container, Inject, Instantiable, Singleton} from '../../../di'
import {Awaitable, Collection, ErrorWithContext, JSONState} from '../../../util'
import {Serializer, SerialPayload} from '../types'
import {Validator} from '../../../validation/Validator'
/**
* Error thrown when attempting to (de-)serialize an object and a serializer cannot be found.
*/
export class NoSerializerError extends ErrorWithContext {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
constructor(object: any, context?: {[key: string]: any}) {
super('The object could not be (de-)serialized, as no compatible serializer has been registered.', {
object,
...(context || {}),
})
}
}
/**
* Encode a value to JSON using a registered serializer.
* @throws NoSerializerError
* @param value
*/
export function encode<T>(value: T): Promise<string> {
return Container.getContainer()
.make<Serialization>(Serialization)
.encodeJSON(value)
}
/**
* Decode a value from JSON using a registered serializer.
* @throws NoSerializerError
* @param payload
* @param validator
*/
export function decode<T>(payload: string, validator?: Validator<T>): Awaitable<T> {
return Container.getContainer()
.make<Serialization>(Serialization)
.decodeJSON(payload, validator)
}
interface RegisteredSerializer<T extends Serializer<unknown, JSONState>> {
key: Instantiable<T>,
instance?: T
}
/**
* Service that manages (de-)serialization of objects.
*/
@Singleton()
export class Serialization {
@Inject()
protected readonly injector!: Container
/**
* Serializers registered with the service.
* We store the DI keys and realize them as needed, rather than at register time
* since most registration is done via the @ObjectSerializer decorator and the
* ContainerBlueprint. Realizing them at that time can cause loops in the DI call
* to realizeContainer since getContainer() -> realizeContainer() -> make the serializer
* -> getContainer(). This largely defers the realization until after all the DI keys
* are registered with the global Container.
*/
protected serializers: Collection<RegisteredSerializer<Serializer<unknown, JSONState>>> = new Collection()
/** Register a new serializer with the service. */
public register(key: Instantiable<Serializer<unknown, JSONState>>): this {
// Prepend instead of push so that later-registered serializers are prioritized when matching
this.serializers.prepend({
key,
})
return this
}
/** Register an already-realized serializer instance with this service. */
public registerInstance(key: Instantiable<Serializer<unknown, JSONState>>, instance: Serializer<unknown, JSONState>): this {
// Prepend instead of push so that later-registered serializers are prioritized when matching
this.serializers.prepend({
key,
instance,
})
return this
}
protected matchActual<T>(actual: T): Serializer<T, JSONState> {
for ( const serializer of this.serializers ) {
if ( !serializer.instance ) {
serializer.instance = this.injector.make(serializer.key)
}
if ( serializer.instance?.matchActual(actual) ) {
return serializer.instance as Serializer<T, JSONState>
}
}
throw new NoSerializerError(actual)
}
protected matchSerial<TSerial extends JSONState>(serial: SerialPayload<unknown, TSerial>): Serializer<unknown, TSerial> {
for ( const serializer of this.serializers ) {
if ( !serializer.instance ) {
serializer.instance = this.injector.make(serializer.key)
}
if ( serializer.instance?.matchSerial(serial) ) {
return serializer.instance as Serializer<unknown, TSerial>
}
}
throw new NoSerializerError(serial)
}
/**
* Encode a value to its serial payload using a registered serializer, if one exists.
* @throws NoSerializerError
* @param value
*/
public encode<T>(value: T): Awaitable<SerialPayload<T, JSONState>> {
return this.matchActual(value).encode(value)
}
/**
* Encode a value to JSON using a registered serializer, if one exists.
* @throws NoSerializerError
* @param value
*/
public async encodeJSON<T>(value: T): Promise<string> {
return JSON.stringify(await this.encode(value))
}
/**
* Decode a serial payload to the original object using a registered serializer, if one exists.
* @throws NoSerializerError
* @param payload
* @param validator
*/
public decode<T>(payload: SerialPayload<unknown, JSONState>, validator?: Validator<T>): Awaitable<T> {
const matched = this.matchSerial(payload)
const decoded = matched.decode(payload) as Awaitable<T>
if ( validator ) {
return validator.parse(decoded)
}
return decoded
}
/**
* Decode a value from JSON using a registered serializer, if one exists.
* @throws NoSerializerError
* @param payload
* @param validator
*/
public async decodeJSON<T>(payload: string, validator?: Validator<T>): Promise<T> {
return this.decode(JSON.parse(payload), validator)
}
}