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

5.0 KiB

emitter

(no submodules)

Purpose

createOpenapiEmitter is the aggregator the spec calls for: it ingests every plugin's manifest, every registered route (with its Zod-converted schemas), the active auth adapter's security scheme, and the host config, then produces a single OpenAPI 3.1 document covering the entire mounted plugin set. The emitter is the single source of truth served at /openapi.json and consumed by the docs UI sibling packages.

By centralising emission, the host eliminates drift: every plugin's routes flow through the same namespacing rules, the same strictness policy, and the same security inheritance. Plugins cannot hand-author OpenAPI fragments that disagree with what their Hono runtime actually serves.

Features

  • Manifest-driven tags + components — displayName becomes the tag, <pluginName>.<TypeName> becomes the component schema prefix.
  • Deterministic operationIds — every route receives <plugin>.<method>.<oasPath>. Collisions raise the moment they appear.
  • Security inheritance — every operation lists the active auth adapter's scheme except for routes listed in any plugin's publicRoutes, which receive security: [].
  • Strictness gate — routes missing request + response schemas warn (with an OTel counter) or refuse install, per host.openapi.strictness.
  • Per-plugin filtered view — getDocumentForPlugin(name) returns only operations tagged with that plugin; misses raise OPENAPI.UNKNOWN_PLUGIN_IN_FILTER.
  • JSON + YAML serialisation — getJson() / getYaml() produce serialised bodies for the HTTP endpoints.
  • OTel emission + conversion-error counters — visibility on document production and Zod→OAS conversion failures.
  • Lifecycle dedup — the constructed document is memoised; repeated getDocument() calls return the same reference.

Usage: Basic

import { createOpenapiEmitter } from "@suaveplan/api-openapi/emitter";

const emitter = createOpenapiEmitter({
    manifests: [
        { name: "billing", displayName: "Billing", description: "Invoices" },
    ],
    routes: [
        {
            pluginName: "billing",
            route: { method: "GET", path: "/invoices/:id", tags: [] },
            responseSchema: { type: "object" },
        },
    ],
    authAdapter: {
        name: "Bearer",
        scheme: { type: "http", scheme: "bearer" },
    },
    hostConfig: {
        info: { title: "host", version: "1.0.0" },
    },
});
emitter.getJson();

Usage: Advanced

import { createOpenapiEmitter } from "@suaveplan/api-openapi/emitter";
import { z } from "zod";

const Invoice = z.object({ id: z.string() });
const emitter = createOpenapiEmitter({
    manifests: [{ name: "billing", displayName: "Billing" }],
    routes: [
        {
            pluginName: "billing",
            route: { method: "GET", path: "/invoices/:id", tags: [] },
            // Thunk → emitter wraps a thrown converter error in
            // OPENAPI.SCHEMA_CONVERSION_FAILED and increments the
            // conversion counter.
            responseSchema: () => ({ $ref: "#/components/schemas/billing.Invoice" }),
            components: { Invoice: { type: "object" } },
        },
    ],
    hostConfig: {
        info: { title: "host", version: "1.0.0" },
        environment: "production",
        openapi: { strictness: "refuse-install" },
    },
});
emitter.getDocumentForPlugin("billing");

API Reference

  • createOpenapiEmitter(input) → OpenapiEmitter.
  • OpenapiEmitter.getDocument() → OpenAPIDocument — memoised.
  • OpenapiEmitter.getJson() / getYaml() → string.
  • OpenapiEmitter.getDocumentForPlugin(name) → OpenAPIDocument.
  • OpenapiEmitter.getPluginNames() → readonly string[].
  • OpenapiEmitter.getStrictness() → StrictnessMode.
  • EmitterPluginManifest, EmitterRouteInput, EmitterAuthAdapter, EmitterHostConfig — input bag types.
  • __resetEmitterCounters() — test-only seam.
  • Constants: EMISSION_COUNTER_NAME, CONVERSION_ERROR_COUNTER_NAME.

Implementation Notes

  • Schemas may be supplied as a pre-converted object or as a thunk; the thunk form gives the emitter a chance to catch + wrap conversion errors with OPENAPI.SCHEMA_CONVERSION_FAILED. Pre-converted objects bypass the catch (the caller has already succeeded).
  • Strictness application is per route and bound to whichever slot is primary for the method — request for non-GET, response for GET — so the failure mode mirrors what API clients actually rely on.
  • Per-plugin filtered views are cached on first request. The filter matches by the resolved tag (displayName or name), not by the raw plugin name, so two plugins sharing a displayName are merged into a single filtered view (and the host's manifest validator should reject such collisions upstream).
  • Memoisation: getDocument() returns the same object reference on every call; mutations to the returned document leak across callers by design — callers MUST structuredClone if they need to mutate.