first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
# merger
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<OpenAPIDocument> = {
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).