import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { fixtureAdded, fixtureContended, fixtureModuleMissing, halfMissing, notAPair, setMissing, } from "../strings/fixture.strings.ts"; const CLOSER = "];"; const FIRES_LEAD = "FIRES "; const ACCEPTS_LEAD = "ACCEPTS "; const PAIR_SEPARATOR = "/"; export interface FixtureSample { readonly path: string; readonly text: string; } export interface ParsedSamples { readonly fires: FixtureSample | null; readonly accepts: FixtureSample | null; } export interface FixtureRequest { readonly target: string; readonly absolute: string; readonly pair: string; readonly body: string; readonly witness: string; } export interface FixtureOutcome { readonly code: number; readonly message: string; } export const parseSamples = function parseSamples(body: string): ParsedSamples { let fires: FixtureSample | null = null; let accepts: FixtureSample | null = null; let openPath = ""; let openLines: string[] = []; let into: "" | "accepts" | "fires" = ""; const close = (): void => { if (into === "" || openPath.length === 0) { return; } const sample = { path: openPath, text: `${openLines.join("\n")}\n` }; if (into === "fires") { fires = sample; } else { accepts = sample; } }; for (const line of body.split("\n")) { if (line.startsWith(FIRES_LEAD)) { close(); into = "fires"; openPath = line.slice(FIRES_LEAD.length).trim(); openLines = []; continue; } if (line.startsWith(ACCEPTS_LEAD)) { close(); into = "accepts"; openPath = line.slice(ACCEPTS_LEAD.length).trim(); openLines = []; continue; } if (into === "") { continue; } openLines.push(line); } close(); return { accepts, fires }; }; const quoted = function quoted(text: string): string { let out = ""; for (const character of text) { if (character === "\\") { out += String.raw`\\`; } else if (character === '"') { out += String.raw`\"`; } else if (character === "\n") { out += String.raw`\n`; } else { out += character; } } return `"${out}"`; }; export const fixtureEntry = function fixtureEntry(rule: string, kind: string, samples: ParsedSamples): string { const { fires } = samples; const { accepts } = samples; return [ " {", ` rule: ${quoted(rule)},`, ` kind: ${quoted(kind)},`, " fires: [", ` { path: ${quoted(fires?.path ?? "")}, text: ${quoted(fires?.text ?? "")} },`, " ],", " passes: [", ` { path: ${quoted(accepts?.path ?? "")}, text: ${quoted(accepts?.text ?? "")} },`, " ],", " },", ].join("\n"); }; export const runFixture = function runFixture(request: FixtureRequest): FixtureOutcome { if (!existsSync(request.absolute)) { return { code: 2, message: fixtureModuleMissing(request.target) }; } const separator = request.pair.indexOf(PAIR_SEPARATOR); if (separator <= 0 || separator === request.pair.length - 1) { return { code: 2, message: notAPair(request.pair) }; } const samples = parseSamples(request.body); if (samples.fires === null || samples.accepts === null) { return { code: 2, message: halfMissing(samples.fires === null ? "fired" : "accepted") }; } const before = readFileSync(request.absolute, "utf8"); if (before !== request.witness) { return { code: 2, message: fixtureContended(request.target) }; } const lines = before.split("\n"); let at = -1; for (let index = lines.length - 1; index >= 0; index -= 1) { if ((lines[index] ?? "").trim() !== CLOSER) { continue; } at = index; break; } if (at === -1) { return { code: 2, message: setMissing(request.target) }; } const [rule, kind] = [request.pair.slice(0, separator), request.pair.slice(separator + 1)]; const written = [...lines.slice(0, at), fixtureEntry(rule, kind, samples), ...lines.slice(at)].join("\n"); writeFileSync(request.absolute, written, "utf8"); return { code: 0, message: fixtureAdded(request.pair, request.target) }; };