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(value: T): Promise { return Container.getContainer() .make(Serialization) .encodeJSON(value) } /** * Decode a value from JSON using a registered serializer. * @throws NoSerializerError * @param payload * @param validator */ export function decode(payload: string, validator?: Validator): Awaitable { return Container.getContainer() .make(Serialization) .decodeJSON(payload, validator) } interface RegisteredSerializer> { key: Instantiable, 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>> = new Collection() /** Register a new serializer with the service. */ public register(key: Instantiable>): 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>, instance: Serializer): this { // Prepend instead of push so that later-registered serializers are prioritized when matching this.serializers.prepend({ key, instance, }) return this } protected matchActual(actual: T): Serializer { 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 } } throw new NoSerializerError(actual) } protected matchSerial(serial: SerialPayload): Serializer { 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 } } throw new NoSerializerError(serial) } /** * Encode a value to its serial payload using a registered serializer, if one exists. * @throws NoSerializerError * @param value */ public encode(value: T): Awaitable> { 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(value: T): Promise { 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(payload: SerialPayload, validator?: Validator): Awaitable { const matched = this.matchSerial(payload) const decoded = matched.decode(payload) as Awaitable 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(payload: string, validator?: Validator): Promise { return this.decode(JSON.parse(payload), validator) } }