# 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 so `prefix="/api/"` + `path="/users"` yields `/api/users`. - `filterPathsByTag(doc, tags[])` — keeps only operations whose `tags` array intersects the supplied list. - `removeInternalRoutes(doc)` — drops operations marked `x-internal: true`. - `extractOpenAPIDoc(app, info)` — pulls a fully-rendered document out of a Hono `OpenAPIHono` for inspection or merging. ## Usage: Basic ```ts 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 = { paths: { "/b": { post: { responses: {} } } }, }; const merged = mergeOpenAPIDocuments(base, addon); console.log(Object.keys(merged.paths)); // ["/a", "/b"] ``` ## Usage: Advanced ```ts 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): OpenAPIDocument` - `function prefixPaths(doc, prefix): OpenAPIDocument` - `function extractOpenAPIDoc(app, info): OpenAPIDocument` - `function filterPathsByTag(doc, tags[]): OpenAPIDocument` - `function removeInternalRoutes(doc): OpenAPIDocument` - `interface 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).