Files
skills/docs/api-stack/api/api-openapi/src/merger/merger.md
2026-05-29 08:20:56 +01:00

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 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

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): 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).