core/converters/schema.converter.ts

core/converters/schema.converter.ts is a file in Bane's Lab Build Scripts. 99 lines of code and 24 definitions.

import { GRAMMAR_LEVELS, SCHEMA_DESCRIPTION } from "#configuration/strings/catalog.strings";
import type { GrammarKind, Leaf } from "#types/catalog.types";
import { INDEX_KIND, MAP_KEY_THRESHOLD, SCHEMA_DIALECT, SCHEMA_KIND } from "#configuration/constants/catalog.constants";
import type { JsonSchema, KindSamples } from "#types/schema.types";
import { JSON_ROUTE } from "#configuration/constants/site.constants";
import { isRecord } from "#core/selectors/base.selector";
import { kindOfAddress } from "#core/converters/catalog.grammar.converter";
import { schemaLeaf } from "#core/resolvers/catalog.resolver";

const SCHEMA_REF = "api:schema/";

export const typeOf = function typeOf(value: unknown): string {
    if (value === null) {
        return "null";
    }
    if (Array.isArray(value)) {
        return "array";
    }
    if (typeof value === "number") {
        return Number.isInteger(value) ? "integer" : "number";
    }
    return typeof value;
};

const typesOf = function typesOf(values: readonly unknown[]): string | readonly string[] {
    const types = new Set(values.map(typeOf));
    if (types.has("number")) {
        types.delete("integer");
    }
    const sorted = [...types].toSorted((left, right) => left.localeCompare(right));
    return sorted.length === 1 ? (sorted[0] ?? "") : sorted;
};

const objectSchema = function objectSchema(objects: readonly Record<string, unknown>[]): JsonSchema {
    const keys = [...new Set(objects.flatMap((object) => Object.keys(object)))].toSorted((left, right) =>
        left.localeCompare(right),
    );
    if (keys.length > MAP_KEY_THRESHOLD) {
        return { additionalProperties: inferSchema(objects.flatMap((object) => Object.values(object))) };
    }
    const properties = Object.fromEntries(
        keys.map((key) => [key, inferSchema(objects.filter((object) => key in object).map((object) => object[key]))]),
    );
    const required = keys.filter((key) => objects.every((object) => key in object));
    return { properties, required };
};

export const isObjectValue = function isObjectValue(value: unknown): value is Record<string, unknown> {
    return isRecord(value) && !Array.isArray(value);
};

export const inferSchema = function inferSchema(values: readonly unknown[]): JsonSchema {
    const objects = values.filter(isObjectValue);
    const arrays = values.filter((value): value is readonly unknown[] => Array.isArray(value));
    return {
        type: typesOf(values),
        ...(objects.length === 0 ? {} : objectSchema(objects)),
        ...(arrays.length === 0 ? {} : { items: inferSchema(arrays.flat()) }),
    };
};

export const inferredKindOf = function inferredKindOf(address: string): GrammarKind | null {
    if (!address.startsWith(JSON_ROUTE)) {
        return null;
    }
    const kind = kindOfAddress(address);
    return kind === SCHEMA_KIND ? null : kind;
};

export const samplesOf = function samplesOf(files: ReadonlyMap<string, string>, drafts: readonly Leaf[]): KindSamples {
    const samples = new Map<GrammarKind, unknown[]>();
    const add = (address: string, data: () => unknown): void => {
        const kind = inferredKindOf(address);
        if (kind === null) {
            return;
        }
        const held = samples.get(kind);
        if (held === undefined) {
            samples.set(kind, [data()]);
        } else {
            held.push(data());
        }
    };
    for (const [address, body] of files) {
        add(address, () => JSON.parse(body));
    }
    for (const leaf of drafts) {
        add(leaf.identity.address.json, () => leaf.data);
    }
    return samples;
};

export const schemaLeaves = function schemaLeaves(samples: KindSamples, site: string): readonly Leaf[] {
    return [...samples.entries()].map(([kind, values]): Leaf => {
        const address = schemaLeaf(kind);
        const ref = SCHEMA_REF + kind;
        const title = GRAMMAR_LEVELS[kind];
        const data = {
            $id: site + address.json,
            $schema: SCHEMA_DIALECT,
            description: SCHEMA_DESCRIPTION,
            ref,
            title,
            ...inferSchema(values),
        };
        return { data, identity: { address, href: null, kind: INDEX_KIND, ref, summary: null, title }, markdown: null };
    });
};