/** * Reads and writes the JSON files the repository's checks keep. */ import * as v from "valibot"; import { nullIfNotFound } from "#scripts/not-found.ts"; /** Parse JSON text, and let the caller decide what a torn text becomes. */ export const parseJsonWith = (onParseError: (error: unknown) => unknown) => (text: string): unknown => { try { return JSON.parse(text); } catch (error) { return onParseError(error); } }; /** * Reads the JSON at the file named by `path`, validated against `schema`. * `Missing` names what an unreadable or wrong-shaped file becomes. */ type SchemaRead = ( path: string, schema: Schema, ) => Promise | Missing>; /** What torn text reads as, distinct from any value a parse can produce. */ const torn = Symbol("torn text"); /** * What the file at `path` holds, checked against `schema`. `null` means there * is no file, or what is there is half written or the wrong shape. A disk that * cannot be read at all still throws: that is not the same as "nothing here". * * A file that holds valid JSON `null` for a schema that accepts it is * refused loudly: this API uses `null` as its unread marker, so a null value * reads as missing. A torn text is checked before the schema ever sees * it, so a half-written file still reads as `null` even for null-accepting * schemas. */ export const readJsonOrNull: SchemaRead = async (path, schema) => { const text = await nullIfNotFound(Deno.readTextFile(path)); if (text === null) return null; const json = parseJsonWith(() => torn)(text); if (json === torn) return null; const parsed = v.safeParse(schema, json); if (parsed.success && parsed.output === null) { throw new Error( `The JSON at ${path} holds null, which this API uses as its "nothing" marker. ` + "Wrap the value (for example in an object) so it can never read as missing.", ); } return parsed.success ? parsed.output : null; }; /** * What the file at `path` holds, checked against `schema`. A file that is * not there, or is half written, or is the wrong shape, fails loudly: a * required registry that reads as empty reads as a clean run. * * The file must not hold valid JSON `null` for a schema that accepts it: * this API uses `null` as its unread marker, so a null value reads as * missing. Schemas like `v.null()` are refused here rather than conflated. */ export const readJsonOrThrow: SchemaRead = async (path, schema) => { const read = await readJsonOrNull(path, schema); if (read === null) { throw new Error(`Cannot read the JSON at ${path}.`); } return read; }; /** * Write `value` at `path` as JSON a person reviews: indented, sorted by * insertion, and newline-terminated. A value that does not serialize — * `undefined`, a function, a symbol — fails here. It never writes invalid * JSON. */ export const writeJsonFile = async ( path: string, value: unknown, ): Promise => { const text = JSON.stringify(value, null, 2); if (text === undefined) { throw new Error(`Cannot write JSON to ${path}: the value has no JSON.`); } await Deno.writeTextFile(path, `${text}\n`); };