core/validators/metadata.validator.ts
core/validators/metadata.validator.ts is a file in GovLab Docs. 96 lines of code and 23 definitions.
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];
};