159 lines
5.3 KiB
TypeScript
159 lines
5.3 KiB
TypeScript
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)
|
|
}
|
|
}
|