Files
SnapOtter/apps/api/src/routes/tools/json-xml.ts
T
SnapOtterandGitHub 7865339fc8 fix(lint): biome-format tool-factory and json-xml to green apps/api lint (#252)
Collapses the error_message line introduced in #251 plus the pre-existing
preValidate blocks and the json-xml wrapped assignment that were already
failing apps/api lint on main. Pure formatting, no logic change.
2026-06-16 16:07:15 +08:00

63 lines
2.3 KiB
TypeScript

import { XMLBuilder, XMLParser } from "fast-xml-parser";
import type { FastifyInstance } from "fastify";
import { z } from "zod";
import { InputValidationError } from "../../modality/contract.js";
import { createToolRoute } from "../tool-factory.js";
const settingsSchema = z.object({
pretty: z.boolean().default(true),
});
export function registerJsonXml(app: FastifyInstance) {
createToolRoute(app, {
toolId: "json-xml",
settingsSchema,
process: async () => {
throw new Error("json-xml is v2-only");
},
processV2: async (ctx) => {
const settings = settingsSchema.parse(ctx.settings);
const input = ctx.inputs[0];
const base = input.filename.replace(/\.[^.]+$/, "");
const lower = input.filename.toLowerCase();
const text = input.buffer.toString("utf8");
if (lower.endsWith(".xml")) {
// xml -> json
const parser = new XMLParser({ ignoreAttributes: false });
const parsed = parser.parse(text);
const json = JSON.stringify(parsed, null, settings.pretty ? 2 : 0);
return {
buffer: Buffer.from(json, "utf8"),
filename: `${base}.json`,
contentType: "application/json",
};
}
// json -> xml
let data: unknown;
try {
data = JSON.parse(text);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
throw new InputValidationError(`Not valid JSON: ${msg.split("\n")[0]}`);
}
// The builder needs an object/array root; primitives (null, string, number,
// boolean) crash builder.build or produce per-character garbage XML.
if (data === null || typeof data !== "object") {
throw new InputValidationError("JSON must be an object or array to convert to XML");
}
// Wrap in a root element when the top level is an array or has multiple
// keys, so the XML is well-formed with a single root.
const wrapped = Array.isArray(data) || Object.keys(data).length !== 1 ? { root: data } : data;
const builder = new XMLBuilder({ format: settings.pretty, ignoreAttributes: false });
const xml = builder.build(wrapped) as string;
return {
buffer: Buffer.from(xml, "utf8"),
filename: `${base}.xml`,
contentType: "application/xml",
};
},
});
}