muse/src/resource.ts

105 lines
2.5 KiB
TypeScript

import {
type AnyResource,
AnyResourceSchema,
ResourceMap,
} from "@proscenium/playbill";
import YAML, { YAMLParseError } from "yaml";
import { ZodError } from "zod";
import { ResourceFileError } from "./errors";
import { extractFrontmatter, extractMarkdown } from "./markdown";
type FileFormat = "yaml" | "markdown";
function determineFileFormat(filePath: string): FileFormat {
return filePath.split(".").pop() === "md" ? "markdown" : "yaml";
}
function loadYamlResourceFile(filePath: string, text: string): AnyResource[] {
const parsedDocs = YAML.parseAllDocuments(text);
if (parsedDocs.some((doc) => doc.toJS() === null)) {
throw new ResourceFileError("Encountered NULL resource", filePath);
}
const errors = parsedDocs.flatMap((doc) => doc.errors);
if (errors.length > 0) {
throw new ResourceFileError(
"Error parsing YAML resource",
filePath,
errors.map((e) => e.message).join(", "),
);
}
const collection: AnyResource[] = [];
for (const doc of parsedDocs) {
const raw = doc.toJS();
const parsed = AnyResourceSchema.parse(raw);
const type = parsed.$define;
const schemaToUse = ResourceMap[type];
const validated = schemaToUse.parse(parsed);
collection.push(validated);
}
return collection;
}
function loadMarkdownResourceFile(
filePath: string,
text: string,
): AnyResource[] {
try {
const frontmatter = extractFrontmatter(text);
const markdown = extractMarkdown(text);
const together = {
...frontmatter,
description: markdown,
};
const parsed = AnyResourceSchema.parse(together);
return [parsed];
} catch (e) {
if (e instanceof YAMLParseError) {
throw new ResourceFileError(
"Error parsing Markdown frontmatter",
filePath,
e.message,
);
}
if (e instanceof ZodError) {
throw new ResourceFileError(
"Error parsing resource file",
filePath,
e.message,
);
}
throw e;
}
}
/**
* Load and process all documents in a yaml file at the given path
* @param filePath - The path to the yaml file
* @returns An array of validated resources
*
* @throws ResourceFileError
* @throws NullResourceError
*/
export async function loadResourceFile(
filePath: string,
): Promise<AnyResource[]> {
const file = Bun.file(filePath);
const text = await file.text();
const format = determineFileFormat(filePath);
switch (format) {
case "yaml":
return loadYamlResourceFile(filePath, text);
case "markdown":
return loadMarkdownResourceFile(filePath, text);
}
}