mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(scripts): add segment-anchored tool-path rewrite transform
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
export type IdToSection = Record<string, string>;
|
||||
|
||||
// A real toolId must be a full path segment: the char after it is end-of-string
|
||||
// or a boundary char (so "convert" never matches inside "convert-video", whose
|
||||
// next char is "-"). ":" lets YAML path keys match (".../resize:"); the backtick
|
||||
// lets backtick string literals match. ":toolId"/"{toolId}" placeholders never
|
||||
// appear as real ids in the map, so parametric routes are left untouched.
|
||||
const BOUNDARY = "(?=$|[/\"'`\\s?#:])";
|
||||
|
||||
function escapeRegExp(s: string): string {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
export function rewriteToolPaths(text: string, idToSection: IdToSection): string {
|
||||
const ids = Object.keys(idToSection).sort((a, b) => b.length - a.length); // longest first
|
||||
|
||||
// Guard: the transform is idempotent only because no section slug (a value)
|
||||
// is also a toolId (a key). Fail loudly if that invariant is ever violated.
|
||||
const sections = new Set(Object.values(idToSection));
|
||||
for (const id of Object.keys(idToSection)) {
|
||||
if (sections.has(id)) {
|
||||
throw new Error(
|
||||
`rewriteToolPaths: id "${id}" is also a section slug, which would break idempotency`,
|
||||
);
|
||||
}
|
||||
}
|
||||
let out = text;
|
||||
for (const id of ids) {
|
||||
const re = new RegExp(`/api/v1/tools/${escapeRegExp(id)}${BOUNDARY}`, "g");
|
||||
out = out.replace(re, `/api/v1/tools/${idToSection[id]}/${id}`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { rewriteToolPaths } from "../../../scripts/lib/rewrite-tool-paths.js";
|
||||
|
||||
const MAP: Record<string, string> = {
|
||||
crop: "image",
|
||||
"crop-video": "video",
|
||||
convert: "image",
|
||||
"convert-video": "video",
|
||||
ocr: "image",
|
||||
"ocr-pdf": "pdf",
|
||||
resize: "image",
|
||||
"passport-photo": "image",
|
||||
"strip-metadata": "image",
|
||||
};
|
||||
|
||||
describe("rewriteToolPaths", () => {
|
||||
it("inserts the section as a full segment", () => {
|
||||
expect(rewriteToolPaths(`fetch("/api/v1/tools/crop")`, MAP)).toBe(
|
||||
`fetch("/api/v1/tools/image/crop")`,
|
||||
);
|
||||
});
|
||||
|
||||
it("does NOT corrupt prefix-colliding ids", () => {
|
||||
expect(rewriteToolPaths("/api/v1/tools/convert-video", MAP)).toBe(
|
||||
"/api/v1/tools/video/convert-video",
|
||||
);
|
||||
expect(rewriteToolPaths("/api/v1/tools/ocr-pdf", MAP)).toBe("/api/v1/tools/pdf/ocr-pdf");
|
||||
});
|
||||
|
||||
it("handles sub-routes and YAML path keys", () => {
|
||||
expect(rewriteToolPaths("/api/v1/tools/passport-photo/analyze", MAP)).toBe(
|
||||
"/api/v1/tools/image/passport-photo/analyze",
|
||||
);
|
||||
expect(rewriteToolPaths(" /api/v1/tools/resize:", MAP)).toBe(" /api/v1/tools/image/resize:");
|
||||
expect(rewriteToolPaths("/api/v1/tools/strip-metadata/inspect", MAP)).toBe(
|
||||
"/api/v1/tools/image/strip-metadata/inspect",
|
||||
);
|
||||
});
|
||||
|
||||
it("matches inside backtick template literals", () => {
|
||||
expect(rewriteToolPaths("`/api/v1/tools/resize`", MAP)).toBe("`/api/v1/tools/image/resize`");
|
||||
});
|
||||
|
||||
it("leaves parametric placeholders and wildcards untouched", () => {
|
||||
expect(rewriteToolPaths("/api/v1/tools/:toolId/batch", MAP)).toBe(
|
||||
"/api/v1/tools/:toolId/batch",
|
||||
);
|
||||
expect(rewriteToolPaths("/api/v1/tools/{toolId}/batch", MAP)).toBe(
|
||||
"/api/v1/tools/{toolId}/batch",
|
||||
);
|
||||
expect(rewriteToolPaths("**/api/v1/tools/**", MAP)).toBe("**/api/v1/tools/**");
|
||||
});
|
||||
|
||||
it("rewrites the id inside a glob", () => {
|
||||
expect(rewriteToolPaths("**/api/v1/tools/resize", MAP)).toBe("**/api/v1/tools/image/resize");
|
||||
});
|
||||
|
||||
it("is idempotent", () => {
|
||||
const once = rewriteToolPaths("/api/v1/tools/convert", MAP);
|
||||
expect(rewriteToolPaths(once, MAP)).toBe(once);
|
||||
expect(once).toBe("/api/v1/tools/image/convert");
|
||||
});
|
||||
|
||||
it("rewrites multiple distinct ids on one line", () => {
|
||||
expect(rewriteToolPaths(`"/api/v1/tools/crop" then "/api/v1/tools/resize"`, MAP)).toBe(
|
||||
`"/api/v1/tools/image/crop" then "/api/v1/tools/image/resize"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("leaves unknown ids untouched", () => {
|
||||
expect(rewriteToolPaths("/api/v1/tools/nonexistent-tool", MAP)).toBe(
|
||||
"/api/v1/tools/nonexistent-tool",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if an id is also a section slug (idempotency invariant)", () => {
|
||||
expect(() => rewriteToolPaths("x", { image: "image" })).toThrow(/idempotency/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user