UNPKG

@openhps/core

Version:

Open Hybrid Positioning System - Core component

425 lines (389 loc) 18.2 kB
import { Serializer as JSONSerializer } from 'typedjson/lib/cjs/serializer'; import type { ConcreteTypeDescriptor, TypeDescriptor } from 'typedjson/lib/types/type-descriptor'; import { ensureTypeDescriptor, ArrayTypeDescriptor, MapTypeDescriptor, SetTypeDescriptor, } from 'typedjson/lib/cjs/type-descriptor'; import { AnyT, IndexedObject, JsonObjectMetadata, Serializable, TypeHintEmitter } from 'typedjson'; import { MemberOptionsBase } from './decorators/options'; import { ObjectMemberMetadata } from './decorators/metadata'; import { isInstanceOf, isValueDefined, nameof } from 'typedjson/lib/cjs/helpers'; import { mergeOptions } from 'typedjson/lib/cjs/options-base'; import type { OptionsBase } from 'typedjson/lib/types/options-base'; import { BufferUtils } from '../utils/BufferUtils'; export class Serializer extends JSONSerializer { protected declare options?: OptionsBase; protected declare typeHintEmitter: TypeHintEmitter; protected declare serializationStrategy: Map<Serializable<any>, SerializerFn<any, TypeDescriptor, any>>; protected errorHandler: (error: Error) => void = (e: Error) => { e.message = e.message.replace('@jsonObject', '@SerializableObject()'); e.message = e.message.replace('@jsonMember', '@SerializableMember()'); e.message = e.message.replace('@jsonSetMember', '@SerializableSetMember()'); e.message = e.message.replace('@jsonMapMember', '@SerializableMapMember()'); e.message = e.message.replace('@jsonArrayMember', '@SerializableArrayMember()'); throw e; }; declare setSerializationStrategy: ( type: Serializable<any>, serializer: SerializerFn<any, TypeDescriptor, any>, ) => void; declare setTypeHintEmitter: (typeEmitterCallback: TypeHintEmitter) => void; declare getTypeHintEmitter: () => TypeHintEmitter; declare setErrorHandler: (errorHandlerCallback: (error: Error) => void) => void; declare getErrorHandler: () => (error: Error) => void; declare retrievePreserveNull: (memberOptions?: MemberOptionsBase) => boolean; constructor() { super(); this.setSerializationStrategy(Map, this.convertAsMap.bind(this)); this.setSerializationStrategy(Array, this.convertAsArray.bind(this)); this.setSerializationStrategy(Set, this.convertAsSet.bind(this)); this.setSerializationStrategy(Uint8Array, BufferUtils.toHexString); } convertSingleValue( sourceObject: any, typeDescriptor: TypeDescriptor, memberName?: string, memberOptions?: ObjectMemberMetadata, serializerOptions?: any, ): any { const targetObject = this._convertSingleValue.bind(this)( sourceObject, typeDescriptor, memberName, memberOptions, serializerOptions, ); if (memberName === undefined && typeof targetObject === 'object') { targetObject.__type = typeDescriptor.ctor.name; } return targetObject; } private _convertSingleValue( sourceObject: any, typeDescriptor: TypeDescriptor, memberName?: string, memberOptions?: ObjectMemberMetadata, serializerOptions?: any, ): any { if (this.retrievePreserveNull(memberOptions) && sourceObject === null) { return null; } if (!isValueDefined(sourceObject)) { return; } if (!isInstanceOf(sourceObject, typeDescriptor.ctor)) { const expectedName = nameof(typeDescriptor.ctor); const actualName = nameof(sourceObject.constructor); this.errorHandler( new TypeError( `Could not serialize '${memberName}': expected '${expectedName}',` + ` got '${actualName}'.`, ), ); return; } const serializer = this.serializationStrategy.get(typeDescriptor.ctor); if (serializer !== undefined) { return serializer(sourceObject, typeDescriptor, memberName, this, memberOptions, serializerOptions); } // if not present in the strategy do property by property serialization if (typeof sourceObject === 'object') { return this.convertAsObject( sourceObject, typeDescriptor, memberName, this, memberOptions, serializerOptions, ); } let error = `Could not serialize '${memberName}'; don't know how to serialize type`; if (typeDescriptor.hasFriendlyName()) { error += ` '${typeDescriptor.ctor.name}'`; } this.errorHandler(new TypeError(`${error}.`)); } convertAsObject( sourceObject: IndexedObject, typeDescriptor: ConcreteTypeDescriptor, memberName: string, serializer: Serializer, memberOptions?: ObjectMemberMetadata, serializerOptions?: any, ) { let sourceTypeMetadata: JsonObjectMetadata | undefined; let targetObject: IndexedObject; let typeHintEmitter = serializer.getTypeHintEmitter(); if (sourceObject.constructor !== typeDescriptor.ctor && sourceObject instanceof typeDescriptor.ctor) { // The source object is not of the expected type, but it is a valid subtype. // This is OK, and we'll proceed to gather object metadata from the subtype instead. sourceTypeMetadata = JsonObjectMetadata.getFromConstructor(sourceObject.constructor); } else { sourceTypeMetadata = JsonObjectMetadata.getFromConstructor(typeDescriptor.ctor); } if (sourceTypeMetadata === undefined) { // Untyped serialization, "as-is", we'll just pass the object on. // We'll clone the source object, because type hints are added to the object itself, and we // don't want to modify // to the original object. targetObject = { ...sourceObject }; } else { const beforeSerializationMethodName = sourceTypeMetadata.beforeSerializationMethodName; if (beforeSerializationMethodName != null) { if (typeof (sourceObject as any)[beforeSerializationMethodName] === 'function') { // check for member first (sourceObject as any)[beforeSerializationMethodName](); } else if (typeof (sourceObject.constructor as any)[beforeSerializationMethodName] === 'function') { // check for static (sourceObject.constructor as any)[beforeSerializationMethodName](); } else { serializer.getErrorHandler()( new TypeError( `beforeSerialization callback '` + `${nameof(sourceTypeMetadata.classType)}.${beforeSerializationMethodName}` + `' is not a method.`, ), ); } } const sourceMeta = sourceTypeMetadata; // Strong-typed serialization available. // We'll serialize by members that have been marked with @jsonMember (including // array/set/map members), and perform recursive conversion on each of them. The converted // objects are put on the 'targetObject', which is what will be put into 'JSON.stringify' // finally. targetObject = {}; const classOptions = mergeOptions(serializer.options, sourceMeta.options); if (sourceMeta.typeHintEmitter != null) { typeHintEmitter = sourceMeta.typeHintEmitter; } sourceMeta.dataMembers.forEach((objMemberMetadata) => { const objMemberOptions = mergeOptions(classOptions, objMemberMetadata.options); let serialized; const value = sourceObject[objMemberMetadata.key]; if (objMemberMetadata.serializer != null) { serialized = objMemberMetadata.serializer(value, { fallback: (so, td) => serializer.convertSingleValue(so, ensureTypeDescriptor(td)), }); } else if (objMemberMetadata.type == null) { throw new TypeError( `Could not serialize ${objMemberMetadata.name}, there is` + ` no constructor nor serialization function to use.`, ); } else if (objMemberMetadata.type() === AnyT && value !== null && value !== undefined) { // Any type, serialize as an unknown object const globalDataType = Object.getPrototypeOf(value).constructor; serialized = serializer.convertSingleValue( value, globalDataType ? ensureTypeDescriptor(globalDataType) : objMemberMetadata.type(), `${nameof(sourceMeta.classType)}.${objMemberMetadata.key}`, objMemberOptions, serializerOptions, ); if (typeof serialized === 'object' && serialized !== null) { serialized.__type = globalDataType ? globalDataType.name : 'Object'; } } else { serialized = serializer.convertSingleValue( value, objMemberMetadata.type(), `${nameof(sourceMeta.classType)}.${objMemberMetadata.key}`, objMemberOptions, serializerOptions, ); } if ( (serializer.retrievePreserveNull(objMemberOptions) && serialized === null) || isValueDefined(serialized) ) { targetObject[objMemberMetadata.name] = serialized; } }); } // Add type-hint. typeHintEmitter(targetObject, sourceObject, typeDescriptor.ctor, sourceTypeMetadata); return targetObject; } /** * Performs the conversion of an array of typed objects (or primitive values) to an array of simple * javascript objects * (or primitive values) for serialization. * @param sourceObject Source object to convert * @param typeDescriptor Type descriptor of source object * @param memberName Member name to convert * @param serializer Serializer * @param memberOptions Member options of memberName * @param serializerOptions Custom serializer options */ convertAsArray( sourceObject: Array<any>, typeDescriptor: ArrayTypeDescriptor, memberName: string, serializer: Serializer, memberOptions?: ObjectMemberMetadata, serializerOptions?: any, ): Array<any> { if (!(typeDescriptor instanceof ArrayTypeDescriptor)) { throw new TypeError( `Could not serialize ${memberName} as Array: incorrect TypeDescriptor detected, please` + ' use proper annotation or function for this type', ); } if ((typeDescriptor.elementType as any) == null) { throw new TypeError(`Could not serialize ${memberName} as Array: missing element type definition.`); } // Check the type of each element, individually. // If at least one array element type is incorrect, we return undefined, which results in no // value emitted during serialization. This is so that invalid element types don't unexpectedly // alter the ordering of other, valid elements, and that no unexpected undefined values are in // the emitted array. sourceObject.forEach((element, i) => { if ( !(serializer.retrievePreserveNull(memberOptions) && element === null) && !isInstanceOf(element, typeDescriptor.elementType.ctor) ) { const expectedTypeName = nameof(typeDescriptor.elementType.ctor); const actualTypeName = element && nameof(element.constructor); throw new TypeError( `Could not serialize ${memberName}[${i}]:` + ` expected '${expectedTypeName}', got '${actualTypeName}'.`, ); } }); return sourceObject.map((element, i) => { return serializer.convertSingleValue( element, typeDescriptor.elementType, `${memberName}[${i}]`, memberOptions, serializerOptions, ); }); } /** * Performs the conversion of a set of typed objects (or primitive values) into an array * of simple javascript objects. * @param sourceObject * @param typeDescriptor * @param memberName * @param serializer * @param memberOptions * @param serializerOptions * @returns */ convertAsSet( sourceObject: Set<any>, typeDescriptor: SetTypeDescriptor, memberName: string, serializer: Serializer, memberOptions?: ObjectMemberMetadata, serializerOptions?: any, ): Array<any> { if (!(typeDescriptor instanceof SetTypeDescriptor)) { throw new TypeError( `Could not serialize ${memberName} as Set: incorrect TypeDescriptor detected, please` + ' use proper annotation or function for this type', ); } if ((typeDescriptor.elementType as any) == null) { throw new TypeError(`Could not serialize ${memberName} as Set: missing element type definition.`); } memberName += '[]'; const resultArray: Array<any> = []; // Convert each element of the set, and put it into an output array. // The output array is the one serialized, as JSON.stringify does not support Set serialization. // (TODO: clarification needed) sourceObject.forEach((element) => { const resultElement = serializer.convertSingleValue( element, typeDescriptor.elementType, memberName, memberOptions, serializerOptions, ); // Add to output if the source element was undefined, OR the converted element is defined. // This will add intentionally undefined values to output, but not values that became // undefined DURING serializing (usually because of a type-error). if (!isValueDefined(element) || isValueDefined(resultElement)) { resultArray.push(resultElement); } }); return resultArray; } /** * Performs the conversion of a map of typed objects (or primitive values) into an array * of simple javascript objects with `key` and `value` properties. * @param sourceObject * @param typeDescriptor * @param memberName * @param serializer * @param memberOptions * @param serializerOptions */ convertAsMap( sourceObject: Map<any, any>, typeDescriptor: MapTypeDescriptor, memberName: string, serializer: Serializer, memberOptions?: ObjectMemberMetadata, serializerOptions?: any, ): IndexedObject | Array<{ key: any; value: any }> { if (!(typeDescriptor instanceof MapTypeDescriptor)) { // Serialize as object return serializer.convertAsObject(sourceObject, typeDescriptor, memberName, serializer, memberOptions); } if ((typeDescriptor.valueType as any) == null) { // @todo Check type throw new TypeError(`Could not serialize ${memberName} as Map: missing value type definition.`); } if ((typeDescriptor.keyType as any) == null) { // @todo Check type throw new TypeError(`Could not serialize ${memberName} as Map: missing key type definition.`); } const keyMemberName = `${memberName}[].key`; const valueMemberName = `${memberName}[].value`; const resultShape = typeDescriptor.getCompleteOptions().shape; const result = resultShape === 1 ? ({} as IndexedObject) : []; const preserveNull = serializer.retrievePreserveNull(memberOptions); // Convert each *entry* in the map to a simple javascript object with key and value properties. sourceObject.forEach((value, key) => { const resultKeyValuePairObj = { key: serializer.convertSingleValue( key, typeDescriptor.keyType, keyMemberName, memberOptions, serializerOptions, ), value: serializer.convertSingleValue( value, typeDescriptor.valueType, valueMemberName, memberOptions, serializerOptions, ), }; // We are not going to emit entries with undefined keys OR undefined values. const keyDefined = isValueDefined(resultKeyValuePairObj.key); const valueDefined = (resultKeyValuePairObj.value === null && preserveNull) || isValueDefined(resultKeyValuePairObj.value); if (keyDefined && valueDefined) { if (resultShape === 1) { result[resultKeyValuePairObj.key] = resultKeyValuePairObj.value; } else { result.push(resultKeyValuePairObj); } } }); return result; } } export type SerializerFn<T, TD extends TypeDescriptor, Raw> = ( sourceObject: T, typeDescriptor: TD, memberName: string, serializer: Serializer, memberOptions?: ObjectMemberMetadata, serializerOptions?: any, ) => Raw;