core/validators/schema.validator.ts

core/validators/schema.validator.ts is a file in GovLab Patterns. 67 lines of code and 11 definitions.

import { DataLoadError, isBlank, kindOf } from "#core/classifiers/schema.classifier";
import type { FieldSchema, Primitive } from "#types/schema.types";
import {
    NULL_PRIMITIVE,
    kindMismatch,
    notAnObject,
    primitiveMismatch,
    requiredField,
    unknownMappingFields,
    unknownRecordFields,
} from "#configuration/strings/schema.strings";
import { isRecord } from "#core/predicates/record.predicate";

const ACCEPTED: Readonly<Record<Primitive, ReadonlySet<string>>> = {
    boolean: new Set(["boolean"]),
    float: new Set(["number"]),
    integer: new Set(["number"]),
    string: new Set(["string"]),
};

const scalarMatches = function scalarMatches(value: unknown, primitive: Primitive | null): boolean {
    return primitive === null || ACCEPTED[primitive].has(typeof value);
};

const sortedList = function sortedList(names: readonly string[]): string {
    return names.toSorted((a, b) => a.localeCompare(b)).join(", ");
};

const validateField = function validateField(field: FieldSchema, record: Record<string, unknown>): void {
    const value = record[field.name];
    if (isBlank(value)) {
        if (!field.nullable) {
            throw new DataLoadError(requiredField(field.name));
        }
        return;
    }
    const kind = kindOf(value);
    if (kind !== field.kind) {
        throw new DataLoadError(kindMismatch(field.name, field.kind, kind));
    }
    if (kind === "scalar" && !scalarMatches(value, field.primitive)) {
        throw new DataLoadError(primitiveMismatch(field.name, field.primitive ?? NULL_PRIMITIVE));
    }
};

export const validateRecord = function validateRecord(
    record: unknown,
    schema: readonly FieldSchema[],
): Record<string, unknown> {
    if (!isRecord(record)) {
        throw new DataLoadError(notAnObject(JSON.stringify(record)));
    }
    const allowed = new Set(schema.map((field) => field.name));
    const unknown = Object.keys(record).filter((key) => !allowed.has(key));
    if (unknown.length > 0) {
        throw new DataLoadError(unknownRecordFields(sortedList(unknown)));
    }
    for (const field of schema) {
        validateField(field, record);
    }
    return record;
};

export const validateMapping = function validateMapping(
    mapping: ReadonlyMap<string, readonly string[]>,
    schema: readonly FieldSchema[],
): void {
    const fields = new Set(schema.map((field) => field.name));
    const unknown = [...mapping.keys()].filter((name) => !fields.has(name));
    if (unknown.length > 0) {
        throw new DataLoadError(unknownMappingFields(sortedList(unknown)));
    }
};