4.2 KiB
merger
(no submodules)
Purpose
OpenAPI document transforms — merge, prefix, filter, redact internal
routes. Multi-plugin servers compose the final published spec from
multiple sources (the registry's buildDocument(), the Hono
getOpenAPI31Document() output, hand-crafted addon docs), and the
merger is the deterministic operation that combines them without
losing information or producing collisions silently.
Every transform returns a new document — the inputs are never mutated —
so consumers can pipeline prefixPaths(removeInternalRoutes(merge(...)))
without worrying about aliasing.
Features
mergeOpenAPIDocuments(base, ...docs)— per-path-item path merge, spread-merge of components, dedup tags by name, concat servers.prefixPaths(doc, prefix)— prepends a prefix to every path, collapsing duplicate slashes soprefix="/api/"+path="/users"yields/api/users.filterPathsByTag(doc, tags[])— keeps only operations whosetagsarray intersects the supplied list.removeInternalRoutes(doc)— drops operations markedx-internal: true.extractOpenAPIDoc(app, info)— pulls a fully-rendered document out of a HonoOpenAPIHonofor inspection or merging.
Usage: Basic
import {
mergeOpenAPIDocuments,
} from "@suaveplan/api-openapi/merger";
const base: OpenAPIDocument = {
openapi: "3.1.0",
info: { title: "Combined", version: "1.0.0" },
paths: { "/a": { get: { responses: {} } } },
};
const addon: Partial<OpenAPIDocument> = {
paths: { "/b": { post: { responses: {} } } },
};
const merged = mergeOpenAPIDocuments(base, addon);
console.log(Object.keys(merged.paths)); // ["/a", "/b"]
Usage: Advanced
import {
extractOpenAPIDoc,
filterPathsByTag,
mergeOpenAPIDocuments,
prefixPaths,
removeInternalRoutes,
} from "@suaveplan/api-openapi/merger";
import { OpenAPIHono } from "@hono/zod-openapi";
const usersApp = new OpenAPIHono();
const projectsApp = new OpenAPIHono();
// … register routes …
const usersDoc = extractOpenAPIDoc(usersApp, {
title: "Users",
version: "1.0.0",
});
const projectsDoc = extractOpenAPIDoc(projectsApp, {
title: "Projects",
version: "1.0.0",
});
const merged = mergeOpenAPIDocuments(
{ openapi: "3.1.0", info: { title: "Public API", version: "1.0.0" }, paths: {} },
prefixPaths(usersDoc, "/v1"),
prefixPaths(projectsDoc, "/v1")
);
// Hide internal endpoints + restrict to public-facing tags.
const publicDoc = filterPathsByTag(
removeInternalRoutes(merged),
["users", "projects"]
);
API Reference
function mergeOpenAPIDocuments(base, ...docs): OpenAPIDocumentfunction prefixPaths(doc, prefix): OpenAPIDocumentfunction extractOpenAPIDoc(app, info): OpenAPIDocumentfunction filterPathsByTag(doc, tags[]): OpenAPIDocumentfunction removeInternalRoutes(doc): OpenAPIDocumentinterface MergeOptions { pathPrefix?: string; overridePaths?: boolean }— reserved for future use.
Implementation Notes
mergeOpenAPIDocuments is left-associative — merging (a, b, c) is the
same as merge(merge(a, b), c). This property is asserted under
fast-check at 200 runs in tests/property.test.ts, ensuring the merger
remains a monoid even as future spread-rules land.
mergeOpenAPIDocuments does not detect cross-document collisions
on the same (method, path). The last value wins. That is the intended
upstream behaviour and the only reasonable default — strict-detection
mode is reserved for a follow-up.
prefixPaths uses String.replace(/\/+/g, "/") to collapse duplicate
slashes. This is the only place double-slashes can be introduced (the
prefix /api/ plus path /users would yield /api//users without
collapse). The transform is idempotent: applying it twice with the same
prefix yields the same paths.
filterPathsByTag drops paths whose every operation is removed,
preventing empty PathItem shells in the result. removeInternalRoutes
applies the same rule for x-internal.
The transforms intentionally do not validate the input documents — they treat the input as best-effort structural data. Validation belongs to a separate spec-validator package (out of scope here).