# core/converters/schema.converter.ts

> 99 lines of code and 24 definitions.

Tree: Bane's Lab Build Scripts
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/build-scripts#file-build-scripts-core-converters-schema-converter-ts
Source text: https://banes-lab.com/source/build-scripts/core/converters/schema.converter.ts.txt

Listed in [core/converters](https://banes-lab.com/api/source/build-scripts/core/converters.md), after [core/converters/route.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/route.converter.ts.md) and before [core/converters/search.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/search.converter.ts.md).

## Definitions

- `isObjectValue` (lexical_declaration, line 48, exported)
- `add` (lexical_declaration, line 72, exported)
- `data` (lexical_declaration, line 98, exported)
- `inferSchema` (lexical_declaration, line 52, exported)
- `samplesOf` (lexical_declaration, line 70, exported)
- `objectSchema` (lexical_declaration, line 34)
- `typesOf` (lexical_declaration, line 25)
- `schemaLeaves` (lexical_declaration, line 93, exported)
- `SCHEMA_REF` (lexical_declaration, line 10)
- `typeOf` (lexical_declaration, line 12, exported)
- `types` (lexical_declaration, line 26)
- `sorted` (lexical_declaration, line 30)
- `keys` (lexical_declaration, line 35)
- `properties` (lexical_declaration, line 41)
- `required` (lexical_declaration, line 44)
- `objects` (lexical_declaration, line 53, exported)
- `arrays` (lexical_declaration, line 54, exported)
- `inferredKindOf` (lexical_declaration, line 62, exported)
- `samples` (lexical_declaration, line 71, exported)
- `kind` (lexical_declaration, line 73, exported)
- `held` (lexical_declaration, line 77, exported)
- `address` (lexical_declaration, line 95, exported)
- `ref` (lexical_declaration, line 96, exported)
- `title` (lexical_declaration, line 97, exported)

## Contained in

- [core/converters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-converters.md)

## Used by

- [core/coordinators/catalog.index.coordinator.ts](https://banes-lab.com/source/build-scripts/core/coordinators/catalog.index.coordinator.ts.md)
- [core/formatters/data.formatter.ts](https://banes-lab.com/source/build-scripts/core/formatters/data.formatter.ts.md)
- [core/validators/catalog.kind.validator.ts](https://banes-lab.com/source/build-scripts/core/validators/catalog.kind.validator.ts.md)
- [core/validators/schema.validator.ts](https://banes-lab.com/source/build-scripts/core/validators/schema.validator.ts.md)

## Linked from

- [core/converters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-converters.md)
- [core/coordinators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-coordinators.md)
- [core/formatters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-formatters.md)
- [core/validators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-validators.md)

## Source

```typescript
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 };
    });
};
```
