# core/validators/metadata.validator.ts

> 96 lines of code and 23 definitions.

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

Listed in [core/validators](https://banes-lab.com/api/source/docs/core/validators.md), after [core/validators/manifest.validator.ts](https://banes-lab.com/source/docs/core/validators/manifest.validator.ts.md) and before [core/validators/package.validator.ts](https://banes-lab.com/source/docs/core/validators/package.validator.ts.md).

## Definitions

- `everyChar` (lexical_declaration, line 20)
- `frontmatterSchema` (lexical_declaration, line 85, exported)
- `itemsDefects` (lexical_declaration, line 49)
- `typeDefect` (lexical_declaration, line 36)
- `isKebabChar` (lexical_declaration, line 32)
- `fieldDefects` (lexical_declaration, line 72)
- `LIST_SEPARATOR` (lexical_declaration, line 15)
- `SAMPLE_SIZE` (lexical_declaration, line 16)
- `KEBAB_JOIN` (lexical_declaration, line 17)
- `BOOLEAN_VALUES` (lexical_declaration, line 18)
- `allowed` (lexical_declaration, line 51)
- `entries` (lexical_declaration, line 56)
- `sample` (lexical_declaration, line 58)
- `known` (lexical_declaration, line 61)
- `{ source, field, value, spec }` (lexical_declaration, line 73)
- `line` (lexical_declaration, line 74)
- `enumDefects` (lexical_declaration, line 75)
- `typeError` (lexical_declaration, line 79)
- `typeDefects` (lexical_declaration, line 80)
- `{ fields }` (lexical_declaration, line 89, exported)
- `missing` (lexical_declaration, line 90, exported)
- `invalid` (lexical_declaration, line 98, exported)
- `spec` (lexical_declaration, line 99, exported)

## Contained in

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

## Uses

- [configuration/strings/document.strings.ts](https://banes-lab.com/source/docs/configuration/strings/document.strings.ts.md)
- [core/predicates/character.predicate.ts](https://banes-lab.com/source/docs/core/predicates/character.predicate.ts.md)

## Used by

- [core/analyzers/document.analyzer.ts](https://banes-lab.com/source/docs/core/analyzers/document.analyzer.ts.md)

## Linked from

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

## Source

```typescript
import type { FieldCheck, FieldSpec, FrontmatterSchema, FrontmatterSchemaDefect } from "#types/metadata.types";
import { fieldLine, listValues, parseFrontmatter } from "#core/parsers/metadata.parser";
import {
    invalidBoolean,
    invalidEnum,
    invalidInteger,
    invalidKebab,
    invalidList,
    invalidListEntry,
    missingRequiredField,
} from "#configuration/strings/document.strings";
import { isAsciiLower } from "#core/predicates/character.predicate";
import { isDigit } from "@govlab/constants";

const LIST_SEPARATOR = ", ";
const SAMPLE_SIZE = 2;
const KEBAB_JOIN = "-";
const BOOLEAN_VALUES: ReadonlySet<string> = new Set(["true", "false"]);

const everyChar = function everyChar(value: string, accept: (char: string) => boolean): boolean {
    if (value.length === 0) {
        return false;
    }
    for (const char of value) {
        if (!accept(char)) {
            return false;
        }
    }
    return true;
};

const isKebabChar = function isKebabChar(char: string): boolean {
    return isAsciiLower(char) || isDigit(char) || char === KEBAB_JOIN;
};

const typeDefect = function typeDefect(field: string, value: string, kind: FieldSpec["kind"]): string | null {
    if (kind === "integer" && !everyChar(value, isDigit)) {
        return invalidInteger(field, value);
    }
    if (kind === "boolean" && !BOOLEAN_VALUES.has(value)) {
        return invalidBoolean(field, value);
    }
    if (kind === "kebab" && !everyChar(value, isKebabChar)) {
        return invalidKebab(field, value);
    }
    return null;
};

const itemsDefects = function itemsDefects(check: FieldCheck): FrontmatterSchemaDefect[] {
    const { source, field, value, spec } = check;
    const allowed = spec.items;
    if (!allowed) {
        return [];
    }
    const line = fieldLine(source, field);
    const entries = listValues(value);
    if (entries === null) {
        const sample = allowed.slice(0, SAMPLE_SIZE).join(LIST_SEPARATOR);
        return [{ code: "invalid-type", detail: invalidList(field, value, sample), field, line }];
    }
    const known = allowed.join(LIST_SEPARATOR);
    return entries
        .filter((entry) => !allowed.includes(entry))
        .map((entry): FrontmatterSchemaDefect => ({
            code: "invalid-enum",
            detail: invalidListEntry(field, entry, known),
            field,
            line,
        }));
};

const fieldDefects = function fieldDefects(check: FieldCheck): FrontmatterSchemaDefect[] {
    const { source, field, value, spec } = check;
    const line = fieldLine(source, field);
    const enumDefects: FrontmatterSchemaDefect[] =
        spec.enum && !spec.enum.includes(value)
            ? [{ code: "invalid-enum", detail: invalidEnum(field, value, spec.enum.join(LIST_SEPARATOR)), field, line }]
            : [];
    const typeError = typeDefect(field, value, spec.kind);
    const typeDefects: FrontmatterSchemaDefect[] =
        typeError === null ? [] : [{ code: "invalid-type", detail: typeError, field, line }];
    return [...itemsDefects(check), ...enumDefects, ...typeDefects];
};

export const frontmatterSchema = function frontmatterSchema(
    source: string,
    schema: FrontmatterSchema,
): FrontmatterSchemaDefect[] {
    const { fields } = parseFrontmatter(source);
    const missing: FrontmatterSchemaDefect[] = schema.required
        .filter((key) => (fields[key] ?? "") === "")
        .map((key): FrontmatterSchemaDefect => ({
            code: "missing-required",
            detail: missingRequiredField(key),
            field: key,
            line: 1,
        }));
    const invalid = Object.entries(fields).flatMap(([field, value]) => {
        const spec = schema.fields[field];
        return spec === undefined ? [] : fieldDefects({ field, source, spec, value });
    });
    return [...missing, ...invalid];
};
```
