# core/validators/field.validator.ts

> 78 lines of code and 11 definitions.

Tree: GovLab Context
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/context#file-context-core-validators-field-validator-ts
Source text: https://banes-lab.com/source/context/core/validators/field.validator.ts.txt

Listed in [core/validators](https://banes-lab.com/api/source/context/core/validators.md), after [core/validators/exemplar.validator.ts](https://banes-lab.com/source/context/core/validators/exemplar.validator.ts.md) and before [core/validators/grammar.reference.validator.ts](https://banes-lab.com/source/context/core/validators/grammar.reference.validator.ts.md).

## Definitions

- `refuseRecord` (lexical_declaration, line 53, exported)
- `emptyFields` (lexical_declaration, line 38, exported)
- `hasShape` (lexical_declaration, line 15)
- `danglingFields` (lexical_declaration, line 66, exported)
- `fieldDefect` (lexical_declaration, line 31)
- `recordDefects` (lexical_declaration, line 44, exported)
- `defect` (lexical_declaration, line 46, exported)
- `defects` (lexical_declaration, line 59, exported)
- `id` (lexical_declaration, line 61, exported)
- `{ target }` (lexical_declaration, line 73, exported)
- `known` (lexical_declaration, line 74, exported)

## Contained in

- [core/validators](https://banes-lab.com/anatomy/context/folder-context-core-validators.md)

## Uses

- [configuration/strings/field.strings.ts](https://banes-lab.com/source/context/configuration/strings/field.strings.ts.md)
- [core/predicates/field.predicate.ts](https://banes-lab.com/source/context/core/predicates/field.predicate.ts.md)
- [core/predicates/record.predicate.ts](https://banes-lab.com/source/context/core/predicates/record.predicate.ts.md)
- [core/selectors/field.selector.ts](https://banes-lab.com/source/context/core/selectors/field.selector.ts.md)

## Used by

- [core/normalizers/algorithm.normalizer.ts](https://banes-lab.com/source/context/core/normalizers/algorithm.normalizer.ts.md)
- [core/normalizers/architecture.normalizer.ts](https://banes-lab.com/source/context/core/normalizers/architecture.normalizer.ts.md)
- [core/normalizers/grammar.normalizer.ts](https://banes-lab.com/source/context/core/normalizers/grammar.normalizer.ts.md)
- [core/normalizers/lexicon.normalizer.ts](https://banes-lab.com/source/context/core/normalizers/lexicon.normalizer.ts.md)
- [core/normalizers/reason.normalizer.ts](https://banes-lab.com/source/context/core/normalizers/reason.normalizer.ts.md)
- [core/validators/ontology.validator.ts](https://banes-lab.com/source/context/core/validators/ontology.validator.ts.md)
- [core/validators/reason.validator.ts](https://banes-lab.com/source/context/core/validators/reason.validator.ts.md)

## Linked from

- [configuration/strings](https://banes-lab.com/anatomy/context/folder-context-configuration-strings.md)
- [core/normalizers](https://banes-lab.com/anatomy/context/folder-context-core-normalizers.md)
- [core/predicates](https://banes-lab.com/anatomy/context/folder-context-core-predicates.md)
- [core/selectors](https://banes-lab.com/anatomy/context/folder-context-core-selectors.md)
- [core/validators](https://banes-lab.com/anatomy/context/folder-context-core-validators.md)

## Source

```typescript
import type { DanglingField, FieldSpec, FieldType, KindSchema } from "#types/field.types";
import {
    NO_ID,
    SHAPE_NAMES,
    misshapenField,
    missingField,
    nonKebabId,
    refusedRecord,
} from "#configuration/strings/field.strings";
import { fieldValue, valuesAt } from "#core/selectors/field.selector";
import { isKebabId, lacksEntry } from "#core/predicates/field.predicate";
import { isPlainRecord, isStringList } from "#core/predicates/record.predicate";
import { ID_FIELD } from "#configuration/constants/field.constants";

const hasShape = function hasShape(type: FieldType, value: unknown): boolean {
    if (type === "flag") {
        return typeof value === "boolean";
    }
    if (type === "object") {
        return isPlainRecord(value);
    }
    if (type === "records") {
        return Array.isArray(value) && value.every(isPlainRecord);
    }
    if (type === "labels" || type === "refs") {
        return isStringList(value);
    }
    return typeof value === "string";
};

const fieldDefect = function fieldDefect(path: string, spec: FieldSpec, value: unknown): string | null {
    if (value === undefined) {
        return spec.required ? missingField(path) : null;
    }
    return hasShape(spec.type, value) ? null : misshapenField(path, SHAPE_NAMES[spec.type]);
};

export const emptyFields = function emptyFields(schema: KindSchema, record: unknown): string[] {
    return Object.entries(schema)
        .filter(([path, spec]) => spec.required && lacksEntry(fieldValue(record, path)))
        .map(([path]) => path);
};

export const recordDefects = function recordDefects(schema: KindSchema, raw: Record<string, unknown>): string[] {
    const defects = Object.entries(schema).flatMap(([path, spec]) => {
        const defect = fieldDefect(path, spec, fieldValue(raw, path));
        return defect === null ? [] : [defect];
    });
    const id = raw[ID_FIELD];
    return typeof id === "string" && !isKebabId(id) ? [...defects, nonKebabId(id)] : defects;
};

export const refuseRecord = function refuseRecord(
    subject: string,
    kind: string,
    schema: KindSchema,
    raw: Record<string, unknown>,
): void {
    const defects = recordDefects(schema, raw);
    if (defects.length > 0) {
        const id = typeof raw[ID_FIELD] === "string" ? raw[ID_FIELD] : NO_ID;
        throw new Error(refusedRecord(subject, kind, id, defects));
    }
};

export const danglingFields = function danglingFields(
    kind: string,
    schema: KindSchema,
    records: readonly { id: string }[],
    members: (target: string) => ReadonlySet<string> | undefined,
): DanglingField[] {
    return Object.entries(schema).flatMap(([field, spec]) => {
        const { target } = spec;
        const known = target === undefined ? undefined : members(target);
        if (known === undefined || target === undefined) {
            return [];
        }
        return records.flatMap((record) =>
            valuesAt(record, field)
                .filter((value) => !known.has(value))
                .map((value) => ({ field, id: record.id, kind, target, value })),
        );
    });
};
```
