Ctrl K
ComunidadGuías…

scripts/read-json.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * 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<Missing = never> = <Schema extends v.GenericSchema>(
  path: string,
  schema: Schema,
) => Promise<v.InferOutput<Schema> | 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<null> = 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<void> => {
  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`);
};