Skip to content

OpenAPI export for from http services (bynk openapi) — depends on #855 #856

Description

@accuser

Depends on #855 (wire-contract peek). This consumes #855's shared boundary-shape derivation (its DECISION C) to generate a standard OpenAPI document for a from http service. The export is the pivot artifact: it serves API consumers, client codegen, CI publishing, and a future in-editor service explorer + invoker (noted as a dependent follow-up).

Summary

  • Scope: Tooling / compiler output. A bynk openapi command (and library entry) that emits an OpenAPI 3.1 document for each from http service, derived from routes, handler params, boundary types, and refinements. No grammar / AST / checker-semantics / runtime changes; the emitter gains an alternate, opt-in output.
  • Addresses: A Bynk from http service is an HTTP API, but there is no machine-readable contract for consumers — no way to codegen a client, publish docs, or diff the API in CI. The boundary is fully specified in the source, yet unusable outside the toolchain.
  • Realises: bynk openapi produces a valid OpenAPI spec: paths/methods from the routes, request schemas from param types, response schemas keyed by the exact HTTP status codes the handlers return, and refinements expressed as JSON Schema constraints — no hand-maintained annotations.

Framing (why this is the language's to fix)

The spec is a lossless re-projection of facts the compiler already holds — and only it holds them consistently (the response status set comes from the HttpResult variant table plus a body walk; refinement constraints come from the same predicates the boundary re-validates). A hand-written OpenAPI file drifts from the handlers the moment either changes; a generated one cannot. This is the compiler publishing the contract it already enforces.

What exists today (grounded)

  • Wire-contract peek — surface boundary shape (contract form/hash, refinements-as-revalidation) #855's shared boundary-shape function (its DECISION C) turns a Bynk type + its refinements into a structural shape — the direct input to JSON Schema. This issue depends on that being factored.
  • Routes and params are in the AST. HandlerKind::Http { method, path } (bynk-syntax/src/ast.rs:1144); handler params with the :segment path bindings; return type HttpResult[T].
  • The status-code table is static and complete. HttpVariant { name, status: u16, … } enumerates every HttpResult variant with its code (200/201/202/204/400/401/404/429/…, ast.rs:1226,1239). A handler's responses = the success (T on its 2xx) + the error variants its body constructs (a body walk) — so the responses object is exact, not annotated.
  • Refinements carry the constraints. String where NonEmptyminLength: 1; numeric bounds → minimum/maximum; patterns → pattern — from the same Refinement/PredKind the codec re-validates (serialisation.rs).
  • The emitter already has an alternate-output structure (bynk-emit produces .ts, source maps, bynk-contracts.json, bynk-secrets.json); an OpenAPI document is one more derived output, opt-in.

The surface

  • CLI: bynk openapi [--service NAME] [--out FILE] writes an OpenAPI 3.1 document (JSON or YAML) for the project's from http service(s). Defaults to stdout so it composes in CI.
  • The document:
    • paths ← routes; path templating from :segment{segment}.
    • parameters ← path/query params, with schema from the param type + refinement constraints.
    • requestBody ← body param types → JSON Schema (records → objects, sums → oneOf).
    • responses ← the exact status set (success T + body-walked error variants), each with its body schema; error variant bodies (e.g. TooManyRequests's message) included.
    • components/schemas ← named boundary types, shared by $ref.
  • CI use: a --check mode (later) can diff the generated spec against a committed one to gate breaking API changes.

Decisions

[DECISION A] Fidelity of rich refinements (Recommended: standard JSON Schema core + x-bynk-* extensions + descriptions). Simple refinements map cleanly (minLength, minimum, maximum, pattern). Cross-field / implication predicates (e.g. status == Paid implies paymentRef.isSome(), ast.rs:931) have no JSON Schema equivalent — carry them as x-bynk-* extension keys plus a human-readable description, keeping the document valid OpenAPI while losing nothing. State the mapping explicitly.

[DECISION B] Responses: declared-only vs body-walked exact set (Recommended: body-walked exact set). List the precise responses a handler can produce — the 2xx from HttpResult[T] plus the error variants actually constructed in its body — rather than the full HttpResult vocabulary. This makes the spec a true description of behaviour, and it is derivable (the body walk #855 already needs for its response set).

[DECISION C] Output surface (Recommended: CLI + library, spec is the product; editor "export" command as a thin wrapper). The primary surface is bynk openapi (CI, codegen, publishing). The editor gets a "Bynk: Export OpenAPI" command that calls the same path. The spec — not an editor view — is the deliverable; the service explorer/invoker is a separate dependent proposal that consumes this output.

The deltas (concretely)

  • Grammar / AST (bynk-syntax). None.
  • Checker (bynk-check). None beyond Wire-contract peek — surface boundary shape (contract form/hash, refinements-as-revalidation) #855's shared boundary-shape function.
  • Emitter (bynk-emit). New OpenAPI generation module: routes/params/responses/schemas from the AST + the shared boundary-shape function; JSON and YAML rendering. Opt-in — never part of the normal .ts build.
  • CLI (bynkc/bynk). New bynk openapi subcommand (--service, --out, format flag; later --check).
  • Runtime. None.
  • VS Code extension (vscode-bynk). A thin bynk.exportOpenapi command wrapping the CLI path (optional; the CLI is the real surface).

Risks & mitigations

  • Generated spec drifting from actual handler behaviour. → Derive responses from the body walk (DECISION B) and schemas from the shared boundary-shape function (Wire-contract peek — surface boundary shape (contract form/hash, refinements-as-revalidation) #855 DECISION C); a fixture asserts the emitted TS boundary and the spec agree on shape.
  • Invalid OpenAPI from un-mappable predicates. → DECISION A: rich predicates go to x-bynk-* + description; validate generated output against an OpenAPI schema in tests.
  • Path-templating mismatch (:seg vs {seg}) or param location errors. → Fixtures cover path + query params and multi-segment routes; validate against the OpenAPI schema.

Docs delta

  • Reference / Guide / Spec: Document bynk openapi in the CLI reference; a recipe "publish your service's OpenAPI" and the refinement→JSON-Schema mapping table (incl. the x-bynk-* extensions). Note it in docs/docs/tooling.
  • Changelog + version history: advance the currency banner + appendix version history to the version this ships as; changelog entry under tooling.
  • Roadmap: record OpenAPI export shipped; note --check API-diff gating and the service explorer + invoker as follow-ups.

Tooling delta (ADR 0156 — silence is an oversight)

(The new surface is the bynk openapi output + CLI; the four LSP tooling surfaces are untouched.)

Done when

  • bynk openapi emits a valid OpenAPI 3.1 document for a project's from http service(s): paths/methods, path+query params with refinement-derived constraints, request bodies, and responses keyed by the exact status codes the handlers return.
  • Named boundary types appear once in components/schemas and are $ref'd.
  • Rich predicates that JSON Schema cannot express are carried as x-bynk-* + description; the document validates against the OpenAPI schema (DECISION A).
  • Responses reflect the body-walked exact set (DECISION B); path templating :seg{seg} is correct.
  • Schemas derive from Wire-contract peek — surface boundary shape (contract form/hash, refinements-as-revalidation) #855's shared boundary-shape function (no drift from the emitted codec).
  • Fixtures (next free indices) cover: the rate-limiter service (GET /check/{client}client path param constrained minLength: 1; RateView 200 schema + 429 error body); a service with a request body (record → object) and a sum response (oneOf); a rich predicate rendered as x-bynk-* + description. Generated specs validated against the OpenAPI 3.1 schema.
  • Docs current per the delta above; tooling surfaces stated (LSP surfaces unchanged).
  • Version bump (scripts/bump-version.sh — assigned at implementation, per the stamp workflow) for a tooling increment.
  • A new ADR records DECISIONS A–C; its number is assigned when the implementing PR lands. The implementing PR closes this issue (Closes #<this>).

Follow-ups (noted, not in scope)

  • Service explorer + invoker — an in-editor Swagger-UI-style view (reusing the webview substrate) that renders this spec and can invoke endpoints against bynk dev, with refinement-aware pre-flight validation and response conformance checking. Its own proposal; the invoker's dev-vs-deployed target and effectful-method (non-GET) gating are first-class design decisions there.
  • bynk openapi --check — diff the generated spec against a committed baseline to gate breaking API changes in CI.

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedAn accepted proposal — approved to buildproposalAn increment proposal — the sign-off artefact; accepted = approval to buildtooling

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions