rescribet/link-lib

View on GitHub
src/Schema.ts

Summary

Maintainability
A
3 hrs
Test Coverage
A
94%
import * as rdfx from "@ontologies/rdf";
import * as rdfs from "@ontologies/rdfs";

import { DataRecord, Id } from "./datastrucures/DataSlice";
import { OWL } from "./schema/owl";
import { RDFS } from "./schema/rdfs";
import { normalizeType } from "./utilities";
import { DisjointSet } from "./utilities/DisjointSet";

import { RDFStore } from "./RDFStore";
import {
    VocabularyProcessingContext,
    VocabularyProcessor,
} from "./types";

/**
 * Implements some RDF/OWL logic to enhance the functionality of the property lookups.
 *
 * Basically duplicates some functionality already present in {IndexedFormula} IIRC, but this API should be more
 * optimized so it can be used in real-time by low-power devices as well.
 */
export class Schema {
    private static vocabularies: VocabularyProcessor[] = [OWL, RDFS];

    private equivalenceSet: DisjointSet<string> = new DisjointSet();
    // Typescript can't handle generic index types, so it is set to string.
    private expansionCache: { [k: string]: string[] };
    private liveStore: RDFStore;
    private superMap: Map<string, Set<string>> = new Map();
    private processedTypes: string[] = [];

    public constructor(liveStore: RDFStore) {
        this.liveStore = liveStore;
        this.liveStore.getInternalStore().addRecordCallback((recordId: Id): void => {
            const record = this.liveStore.getInternalStore().store.getRecord(recordId);

            if (record === undefined) {
                return;
            }
            this.process.call(this, record);
        });
        this.expansionCache = {};

        for (const vocab of Schema.vocabularies) {
            this.liveStore.addQuads(vocab.axioms);
        }

        const preexisting = liveStore.getInternalStore().store.allRecords();
        for (const record of preexisting) {
            this.process(record);
        }
    }

    public allEquals(recordId: Id, grade = 1.0): Id[] {
        if (grade >= 0) {
            return this.equivalenceSet.allValues(recordId);
        }

        return [recordId];
    }

    /** @private */
    public isInstanceOf(recordId: Id, klass: Id): boolean {
        const type = this.liveStore.getInternalStore().store.getField(recordId, rdfx.type.value);

        if (type === undefined) {
            return false;
        }

        const allCheckTypes = this.expand([klass]);
        const allRecordTypes = this.expand(Array.isArray(type) ? type.map((t) => t.value) : [type.value]);

        return allRecordTypes.some((t) => allCheckTypes.includes(t));
    }

    public expand(types: Id[]): Id[] {
        if (types.length === 1) {
            const existing = this.expansionCache[types[0] as unknown as string];
            this.expansionCache[types[0] as unknown as string] = existing
                ? existing
                : this.sort(this.mineForTypes(types));

            return this.expansionCache[types[0] as unknown as string];
        }

        return this.sort(this.mineForTypes(types));
    }

    public getProcessingCtx(): VocabularyProcessingContext<string> {
        return {
            dataStore: this.liveStore,
            equivalenceSet: this.equivalenceSet,
            store: this,
            superMap: this.superMap,
        };
    }

    /**
     * Expands the given lookupTypes to include all their equivalent and subclasses.
     * This is done in multiple iterations until no new types are found.
     * @param lookupTypes The types to look up. Once given, these are assumed to be classes.
     */
    public mineForTypes(lookupTypes: string[]): string[] {
        if (lookupTypes.length === 0) {
            return [rdfs.Resource.value];
        }

        const canonicalTypes: string[] = [];
        const lookupTypesExpanded = [];
        for (const lookupType of lookupTypes) {
            lookupTypesExpanded.push(...this.allEquals(lookupType));
        }
        for (const lookupType of lookupTypesExpanded) {
            const canon = this.liveStore.getInternalStore().store.primary(lookupType);

            if (!this.processedTypes.includes(canon)) {
                for (const vocab of Schema.vocabularies) {
                    vocab.processType(
                        canon,
                        this.getProcessingCtx(),
                    );
                }
                this.processedTypes.push(canon);
            }

            if (!canonicalTypes.includes(canon)) {
                canonicalTypes.push(canon);
            }
        }

        const allTypes = canonicalTypes
            .reduce(
                (a, b) => {
                    const superSet = this.superMap.get(b);
                    if (typeof superSet === "undefined") {
                        return a;
                    }

                    superSet.forEach((s) => {
                        if (!a.includes(s)) {
                            a.push(s);
                        }
                    });

                    return a;
                },
                [...lookupTypes],
            );

        return this.sort(allTypes);
    }

    public sort(types: string[]): string[] {
        return types.sort((a, b) => {
            if (this.isSubclassOf(a, b)) {
                return -1;
            } else if (this.isSubclassOf(b, a)) {
                return 1;
            }

            const aDepth = this.superTypeDepth(a);
            const bDepth = this.superTypeDepth(b);
            if (aDepth < bDepth) {
                return 1;
            } else if (aDepth > bDepth) {
                return -1;
            }

            return 0;
        });
    }

    /**
     * Returns the hierarchical depth of the type, or -1 if unknown.
     * @param type the type to check
     */
    private superTypeDepth(type: string): number {
        const superMap = this.superMap.get(type);

        return superMap ? superMap.size : -1;
    }

    private isSubclassOf(resource: string, superClass: string): boolean {
        const resourceMap = this.superMap.get(resource);

        if (resourceMap) {
            return resourceMap.has(superClass);
        }
        return false;
    }

    private process(record: DataRecord): void {
        for (const vocab of Schema.vocabularies) {
            for (const [field, values] of Object.entries(record)) {
                for (const value of normalizeType(values)) {
                    vocab.processStatement(
                        record._id.value,
                        field,
                        value,
                        this.getProcessingCtx(),
                    );
                }
            }
        }
    }
}