OpenHPS/openhps-core

View on GitHub
src/data/decorators/utils.ts

Summary

Maintainability
B
6 hrs
Test Coverage
import { AnyT, Constructor, IndexedObject, JsonObjectMetadata, Serializable } from 'typedjson';
import { SerializableMemberOptions, SerializableObjectOptions } from './options';
import { DataSerializer } from '../DataSerializer';
import { DataSerializerUtils, ConcreteTypeDescriptor } from '../DataSerializerUtils';

const cloneDeep = require('lodash.clonedeep'); // eslint-disable-line

/**
 * Inject member options into object
 * @param {any} target Prototype
 * @param {PropertyKey} propertyKey Property key
 * @param {any} options Options to inject
 */
export function updateSerializableMember(
    target: unknown,
    propertyKey: string,
    options: SerializableMemberOptions | IndexedObject,
) {
    const reflectPropCtor: Constructor<any> = Reflect.getMetadata('design:type', target, propertyKey);

    // Inject additional options if available
    if (options) {
        const ownMeta = JsonObjectMetadata.ensurePresentInPrototype(target);
        const rootMeta = DataSerializerUtils.getRootMetadata(target.constructor);
        const ownMemberMetadata = ownMeta.dataMembers.get(propertyKey) || ownMeta.dataMembers.get(options.name);
        const rootMemberMetadata = rootMeta
            ? rootMeta.dataMembers.get(propertyKey) || rootMeta.dataMembers.get(options.name)
            : undefined;
        if (!ownMemberMetadata) {
            throw new Error(`Unable to get member metadata for ${target} on property ${propertyKey}!`);
        }
        ownMemberMetadata.options = mergeDeep(ownMemberMetadata.options ?? {}, options);
        if (rootMemberMetadata) {
            ownMemberMetadata.options = mergeDeep(rootMemberMetadata.options ?? {}, ownMemberMetadata.options);
        }
        // Merge known sub types as well
        rootMeta.knownTypes.forEach((otherType) => {
            if (otherType === target || target instanceof otherType) {
                return;
            }
            const otherMeta =
                DataSerializerUtils.getMetadata(otherType) ?? JsonObjectMetadata.ensurePresentInPrototype(otherType);
            const otherMemberMetadata =
                otherMeta.dataMembers.get(propertyKey) || otherMeta.dataMembers.get(options.name);
            if (otherMemberMetadata) {
                otherMemberMetadata.options = mergeDeep(ownMemberMetadata.options ?? {}, otherMemberMetadata.options);
            } else {
                otherMeta.dataMembers.set(options.name ?? propertyKey, cloneDeep(ownMemberMetadata));
            }
        });
        // TODO: Possibly need to sync super types as well
    }

    // Detect generic types that have no deserialization or constructor specified
    const meta = JsonObjectMetadata.ensurePresentInPrototype(target);
    const existingOptions = meta.dataMembers.get(options ? options.name || propertyKey : propertyKey);
    if (
        reflectPropCtor === Object &&
        (!options || (!options.deserializer && !Object.keys(options).includes('constructor')))
    ) {
        existingOptions.serializer = (object) => DataSerializer.serialize(object);
        existingOptions.deserializer = (objectJson) => DataSerializer.deserialize(objectJson);
        existingOptions.type = () => AnyT;
    } else if (
        existingOptions &&
        typeof options !== 'object' &&
        existingOptions.type() instanceof ConcreteTypeDescriptor
    ) {
        existingOptions.type = () => new ConcreteTypeDescriptor(reflectPropCtor);
    }
}

/**
 * Inject object members
 * @param {Serializable} target Target to update
 * @param {SerializableObjectOptions} options Options to inject
 */
export function updateSerializableObject<T>(target: Serializable<T>, options: SerializableObjectOptions<T>): void {
    const ownMeta = DataSerializerUtils.getMetadata(target);
    const rootMeta = DataSerializerUtils.getRootMetadata(target.prototype);
    rootMeta.knownTypes.add(target);
    if (rootMeta.initializerCallback && !ownMeta.initializerCallback) {
        ownMeta.initializerCallback = rootMeta.initializerCallback;
    }

    // Merge options
    if (options) {
        ownMeta.options = mergeDeep(
            ownMeta === rootMeta ? ownMeta.options ?? {} : ownMeta.options ?? rootMeta.options ?? {},
            options,
        );

        // Merge known sub types as well
        rootMeta.knownTypes.forEach((otherType) => {
            if (otherType === target || !(otherType.prototype instanceof target)) {
                return;
            }

            const otherMeta = DataSerializerUtils.getMetadata(otherType);
            otherMeta.options = mergeDeep(ownMeta.options ?? {}, otherMeta.options);

            if (!otherMeta.initializerCallback && ownMeta.initializerCallback) {
                otherMeta.initializerCallback = ownMeta.initializerCallback;
            }
        });
    }

    // Sync settings from super types
    rootMeta.knownTypes.forEach((otherType) => {
        if (otherType === target || !(target.prototype instanceof otherType)) {
            return;
        }

        const otherMeta = DataSerializerUtils.getMetadata(otherType);
        if (otherMeta && otherMeta.initializerCallback && !ownMeta.initializerCallback) {
            ownMeta.initializerCallback = otherMeta.initializerCallback;
        }
    });

    // (Re)register type
    DataSerializer.registerType(target);
}

/**
 * Check if something is an object
 * @param {any} item Item to check for object
 * @returns {boolean} Is an object
 */
function isObject(item: any): boolean {
    return item && typeof item === 'object' && !Array.isArray(item);
}

/**
 * Deep merge member options
 * @param {unknown} target Target object
 * @param {string} propertyKey Property key in target
 * @param {any} options Member options
 * @returns {any} Merged objects
 */
export function mergeMemberOptions(target: unknown, propertyKey: string, options: any): any {
    if (typeof options === 'function') {
        return options;
    }
    const memberOptions = DataSerializerUtils.getMemberOptions(target.constructor as Constructor<any>, propertyKey);
    if (!memberOptions) {
        return options;
    }
    return mergeDeep(memberOptions, options);
}

/**
 * Deep merge objects
 * @param {any} target Target object
 * @param {any} source Source object
 * @returns {any} Merged object
 */
function mergeDeep(target: any, source: any): any {
    const output = cloneDeep(target);
    if (isObject(target) && isObject(source)) {
        Object.keys(source).forEach((key) => {
            if (Array.isArray(source[key])) {
                output[key] = source[key];
                const targetProperty =
                    target[key] !== undefined ? (Array.isArray(target[key]) ? target[key] : [target[key]]) : [];
                output[key].push(...targetProperty.filter((val: any) => !source[key].includes(val)));
            } else if (isObject(source[key])) {
                if (!(key in target)) Object.assign(output, { [key]: source[key] });
                else output[key] = mergeDeep(target[key], source[key]);
            } else {
                Object.assign(output, { [key]: source[key] });
            }
        });
    }
    return output;
}

export const SerializationUtils = {
    cloneDeep,
    mergeDeep,
};