Skip to content

Multi-version µEd API support via versioned adapters - #34

Open
m-messer wants to merge 2 commits into
mainfrom
feature/muEd-versioning
Open

Multi-version µEd API support via versioned adapters#34
m-messer wants to merge 2 commits into
mainfrom
feature/muEd-versioning

Conversation

@m-messer

@m-messer m-messer commented Sep 3, 2026

Copy link
Copy Markdown
Member

Context

shimmy previously had only single-version negotiation scaffolding for the µEd API: it could advertise and gate on an X-Api-Version string, but there was exactly one version (0.1.0) and no code path behaved differently per requested version. This adds a generic, additive framework so a future version slots in as one adapter file + one spec file + one registration line, with 0.1.0 behaviour byte-for-byte unchanged.

Design

Adapter-per-version behind one interface + an ordered registry.

  • runtime/mued_adapter.goMuEdAdapter interface (decode + encode for evaluate/preview/health/chat) and MuEdRegistry (Register/Resolve/Adapter/Default/Latest, oldest-first order). Default() is the first registered version, pinned separately from Latest(), so registering a newer version never silently moves header-less clients onto new semantics.
  • runtime/mued_v0_1_0.gomuEdV010 delegates every method to the existing MuEdBuildLegacy* / MuEdTo* free functions; init() registers it.
  • runtime/version.goSupportedMuEdVersions is now a function backed by the registry; MuEdIsVersionSupported / MuEdResolveVersion delegate to it (behaviour and call signatures otherwise unchanged).
  • Handlers become version-agnostic: checkMuEdVersion resolves X-Api-Version to an adapter and ServeEvaluate / ServeHealth / ServeChat / ServeChatHealth drive it. MuEdHandler gained an optional injected registry (nil → process default).
  • OpenAPI specs are now per-version: //go:embed mued_v*.yml into a version-keyed map; OpenAPIMiddleware builds one router per version and selects per request via the same resolver the handlers use. Unknown-route pass-through and 400/500 semantics unchanged.

One shared handler chain on both deployments. The standalone server used to wrap the mux with NormalizePath + OpenAPI validation; the Lambda adapter wrapped it with NormalizePath only, leaving Lambda requests unvalidated and Lambda responses unchecked (a non-conforming response would 500 on standalone but ship as-is on Lambda). The wrapped chain is now server.NewMux behind server.HandlerModule, and both app/standalone and app/lambda serve the exact same *server.Mux. The only remaining deployment difference is transport.

Backward compatibility

Only 0.1.0 is registered, so SupportedMuEdVersions() == ["0.1.0"], version resolution behaves exactly as before, and all pre-existing runtime/*_test.go and handler/*_test.go assertions pass unmodified.

Tests

  • runtime/mued_adapter_test.go — registry order/resolution/dispatch, multi-version via a synthetic adapter.
  • runtime/mued_v0_1_0_test.go — byte-for-byte equivalence of every adapter method vs the free functions.
  • handler/mued_version_test.go — dispatch to a synthetic version adapter; "" vs 0.1.0 parity.
  • internal/server/openapi_test.go + testdata/mued_v0.2.0.yml — same body accepted under v0.1.0, rejected under a synthetic v0.2.0 requiring an extra field.
  • internal/server/mux_test.go — the shared chain applies validation + path normalisation.
  • app/standalone/module_test.go, app/lambda/module_test.gofx.ValidateApp over both graphs.

go build ./..., go vet ./..., and go test ./... all pass.

Follow-up (not in this PR)

Semver ordering of embedded specs is currently lexical — fine while version components stay single-digit.

🤖 Generated with Claude Code

m-messer and others added 2 commits September 3, 2026 12:20
The standalone server wrapped the route mux with NormalizePath + the
OpenAPI request/response validation middleware; the Lambda adapter wrapped
it with NormalizePath only. That left Lambda requests unvalidated against
the spec and, more importantly, Lambda responses unchecked — so a
non-conforming response would 500 on standalone but ship as-is on Lambda.

Extract the wrapped chain into server.NewMux (mux + NormalizePath +
per-version OpenAPI validation) and a server.HandlerModule fx module.
Both app/standalone and app/lambda now depend on it and serve the exact
same *server.Mux, so the two deployments validate identically. The only
remaining deployment difference is transport: standalone runs an
http.Server + listener (and optional h2c); Lambda hands the same handler
to httpadapter.

NewHttpServer / NewLifecycleServer no longer build the chain or return an
error. Added fx.ValidateApp coverage for the Lambda graph and a NewMux
test asserting validation + path normalisation are applied.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAusDUAMwEVN8hAV4N8qGk
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants