rescribet/link-lib

View on GitHub
src/ComponentStore/ComponentStore.ts

Summary

Maintainability
A
2 hrs
Test Coverage
C
79%
import { NamedNode } from "@ontologies/core";
import * as rdfs from "@ontologies/rdfs";

import { Id } from "../datastrucures/DataSlice";
import ll from "../ontology/ll";
import { Schema } from "../Schema";
import { ComponentMapping, ComponentRegistration } from "../types";
import { DEFAULT_TOPOLOGY } from "../utilities/constants";

import { ComponentCache } from "./ComponentCache";

const MSG_TYPE_ERR = "Non-optimized NamedNode instance given. Please memoize your namespace correctly.";

/** Constant used to determine that a component is used to render a type rather than a property. */
export const RENDER_CLASS_NAME: NamedNode = ll.typeRenderClass;

function convertToCacheKey(types: Id[], props: Id[], topology: Id): string {
    return `${types.join()}[${props.join()}][${topology}]`;
}

const assert = (obj: any): void => {
    if (obj === undefined) {
        throw new TypeError(MSG_TYPE_ERR);
    }
};

/**
 * Handles registration and querying for view components.
 */
export class ComponentStore<T> {
    /**
     * Generate registration description objects for later registration use.
     * @see LinkedRenderStore#registerAll
     */
    public static registerRenderer<T>(
        component: T,
        types: Id[],
        fields: Id[],
        topologies: Id[],
    ): Array<ComponentRegistration<T>> {
        if (typeof component === "undefined") {
            throw new Error(`Undefined component was given for (${types}, ${fields}, ${topologies}).`);
        }
        const registrations: Array<ComponentRegistration<T>> = [];

        for (const type of types) {
            assert(type);
            for (const field of fields) {
                assert(field);
                for (const topology of topologies) {
                    assert(topology);

                    registrations.push({
                        component,
                        property: field,
                        topology,
                        type,
                    });
                }
            }
        }

        return registrations;
    }

    private lookupCache: ComponentCache<T> = new ComponentCache<T>();
    /**
     * Lookup map ordered with the following hierarchy;
     * [propertyType][resourceType][topology]
     */
    private registrations: ComponentMapping<T> = {};
    private schema: Schema;

    public constructor(schema: Schema) {
        this.schema = schema;
        this.registrations[RENDER_CLASS_NAME.value] = {};
    }

    /**
     * TODO: remove defaultType - Basically a bug. We default the type if no matches were found, rather than using
     *   inheritance to associate unknown types the RDF way (using rdfs:Resource).
     */
    public getRenderComponent(
        types: Id[],
        fields: Id[],
        topology: Id,
        defaultType: Id,
    ): T | null {
        const oTypes = this.schema.expand(types);
        const key = convertToCacheKey(oTypes, fields, topology);
        const cached = this.lookupCache.get(key);
        if (cached !== undefined) {
            return cached;
        }

        const match = this.findMatch(oTypes, fields, topology, defaultType);

        return this.lookupCache.add(match, key);
    }

    /**
     * Register a renderer for a type/property.
     * @param component The component to return for the rendering of the object.
     * @param type The type's SomeNode of the object which the {component} can render.
     * @param [field] The property's SomeNode if the {component} is a subject renderer.
     * @param [topology] An alternate topology this {component} should render.
     */
    public registerRenderer(
        component: T,
        type: Id,
        field: Id = RENDER_CLASS_NAME.value,
        topology: Id = DEFAULT_TOPOLOGY.value,
    ): void {
        if (!field || !type) {
            return;
        }

        this.store(component, field, type, topology);
        this.lookupCache.clear();
    }

    /**
     * Find a component from a cache.
     * @param field The property (or {RENDER_CLASS_NAME})
     * @param klass Either the resource type or resource IRI
     * @param topology The topology
     * @param cache The cache to look into (defaults to the mapping)
     * @returns The appropriate component if any
     */
    protected lookup(
        field: Id,
        klass: Id,
        topology: Id,
        cache: ComponentMapping<T> = this.registrations,
    ): T | undefined {
        const predMap = cache[field];
        if (!predMap || !predMap[klass]) {
            return undefined;
        }

        return predMap[klass][topology];
    }

    /** Store a component to a cache. */
    protected store(
        component: T,
        field: Id,
        klass: Id,
        topology: Id,
        cache: ComponentMapping<T> = this.registrations,
    ): void {
        if (typeof cache[field] === "undefined") {
            cache[field] = {};
        }
        if (typeof cache[field][klass] === "undefined") {
            cache[field][klass] = {};
        }
        cache[field][klass][topology] = component;
    }

    /**
     * Expands the given types and returns the best class to render it with.
     * @param components The set of components to choose from.
     * @param [types] The types to expand on.
     * @returns The best match for the given components and types.
     */
    private bestClass(components: Id[], types: Id[]): Id | undefined {
        if (types.length > 0) {
            const direct = this.schema.sort(types).find((c) => components.indexOf(c) >= 0);
            if (direct) {
                return direct;
            }
        }

        const chain = this.schema.expand(types ?? []);

        return components.find((c) => chain.indexOf(c) > 0);
    }

    private classMatch(possibleClasses: Id[], types: Id[], fields: Id[], topology: Id): T | undefined {
        for (const field of fields) {
            const bestClass = this.bestClass(possibleClasses, types);
            const component = bestClass && this.lookup(
              field,
              bestClass,
              topology,
            );

            if (component) {
                return component;
            }
        }

        return undefined;
    }

    private defaultMatch(fields: Id[], topology: Id, defaultType: Id): T | undefined {
        for (const field of fields) {
            const component = this.lookup(field, defaultType, topology);
            if (component) {
                return component;
            }
        }

        return undefined;
    }

    private exactMatch(types: Id[], fields: Id[], topology: Id): T | undefined {
        for (const field of fields) {
            for (const type of types) {
                const exact = this.lookup(field, type, topology);
                if (exact !== undefined) {
                    return exact;
                }
            }
        }

        return undefined;
    }

    private findMatch(types: Id[], fields: Id[], topology: Id, defaultType: Id): T | null {
        let match: T | null | undefined = this.exactMatch(types, fields, topology);
        if (match !== undefined) {
            return match;
        }

        const possibleClasses = this.registeredClasses(fields, topology);

        if (possibleClasses.length === 0) {
            return this.noClassesMatch(types, fields, topology, defaultType);
        }

        match = this.classMatch(possibleClasses, types, fields, topology);
        if (match !== undefined) {
            return match;
        }

        match = this.defaultMatch(fields, topology, defaultType);
        if (match !== undefined) {
            return match;
        }

        return null;
    }

    private noClassesMatch(types: Id[], fields: Id[], topology: Id, defaultType: Id): T | null {
        if (topology === DEFAULT_TOPOLOGY.value) {
            return null;
        }

        const foundComponent = this.getRenderComponent(
          types,
          fields,
          DEFAULT_TOPOLOGY.value,
          defaultType,
        );

        if (foundComponent) {
            return foundComponent;
        }

        return null;
    }

    /**
     * Returns a list of classes which have registrations for a combination of {fields} and {topology}.
     * @private
     */
    private registeredClasses(fields: Id[], topology: Id): Id[] {
        const classes = [rdfs.Resource.value];

        for (const field of fields) {
            if (typeof this.registrations[field] === "undefined") {
                continue;
            }

            const types = Object.keys(this.registrations[field]);
            for (const type of types) {
                const compType = this.lookup(field, type, topology);
                if (compType !== undefined) {
                    classes.push(type);
                }
            }
        }

        return classes;
    }
}