diff --git a/openapi-3-migration-analysis.md b/openapi-3-migration-analysis.md new file mode 100644 index 000000000..69b0dd44e --- /dev/null +++ b/openapi-3-migration-analysis.md @@ -0,0 +1,217 @@ +# OpenAPI 3 Migration Analysis + +Investigation of options for producing an OpenAPI 3 spec for the Vikunja API, +currently generated as Swagger 2.0 via `swaggo/swag`. + +## Current state + +- Generator: `github.com/swaggo/swag` v1.16.6 (annotation-driven) +- Invocation: `mage generate:swagger-docs` → + `swag init -g ./pkg/routes/routes.go --parseDependency -d . -o ./pkg/swagger` +- Output: `pkg/swagger/swagger.json`, `"swagger": "2.0"` +- Web framework: `labstack/echo/v5` (community fork, not mainline v4) +- API surface: **119 paths, 161 operations, 87 model definitions** +- Spec-to-code consistency is **not enforced** — drift is possible and silent + +## Option A — Upgrade to swaggo/swag v2 + +swaggo has a `v2` branch that adds OpenAPI 3.1 generation via a `-v3.1` CLI flag. + +**Readiness verdict: experimental, not production-ready.** + +- Latest tag: `v2.0.0-rc5` (2026-01-08). No stable v2.0.0 — RC line started in 2023 + (~3 years in RC) +- Tracking issue #548 "Proposal swag 2.0 using OpenAPI 3.0" still open, no GA + timeline (178 reactions, 44 comments, last activity Dec 2025) +- PR #2156 (Mar 2026) explicitly states *"v2 is out of sync with master by a + large margin"* +- ~11 open PRs against `base:v2`, several sitting 6–12+ months +- Known unfixed correctness bugs in v3 output: formData (#1554, since 2023), + echo-swagger integration (#1588), oneOf/embedded-struct/binary attachment + fixes unmerged +- Stable production line is still v1.16.x (Swagger 2.0 only) + +**Recommendation:** Track, revisit at GA. Not viable today. + +## Option B — Convert (post-process Swagger 2.0 → OpenAPI 3) + +Keep the existing swag-driven pipeline, add a conversion step. + +| Tool | Output | Notes | +|---|---|---| +| `swagger2openapi` (Mike Ralphson, Node.js) | OAS 3.0.x | De facto standard, battle-tested | +| `api-spec-converter` (LucyBot) | OAS 3.0 | Older, less maintained | +| `gnostic` (Google) | OAS 3.0 protobuf model | Go-native, heavier | + +**Trade-offs** + +- Delivers OAS **3.0** only, not 3.1 +- Some 2.0 patterns translate awkwardly (`additionalProperties: true`, + `nullable`, file uploads, security schemes) +- Documentation-quality output is fine; client/server *generation* from + converted specs sometimes hits oddities +- Does **not** solve the spec-to-code drift problem + +**Effort:** ~half a day. Add Node toolchain (or Docker step) to CI. + +**When it wins:** If downstream consumers just need OAS 3.0 for docs/SDK +generation and spec fidelity isn't a correctness requirement. + +## Option C — Switch the code-first generator + +| Tool | Approach | Echo v5 fit | Notes | +|---|---|---|---| +| **Huma** (`danielgtaylor/huma` v2) | Typed handler funcs, spec derived from input/output structs | Adapter is v4; ~1-day fork for v5 | OAS **3.1** + JSON Schema 2020-12. Most active Go-native generator. | +| Fuego | Web framework + auto-spec | Own router | Would replace Echo entirely | +| go-swagger | Annotation-driven | n/a | Still 2.0, no real OAS 3 support | + +**Huma details** (source: `github.com/danielgtaylor/huma/v2`, v2.37.3 Mar 2026, +~4k stars, 1,371 commits, production-ready per README): + +- Runs on top of the existing router — existing Echo middleware (JWT, CORS, + rate-limit, Sentry, logger) keeps working unchanged +- Handler signature: + ```go + huma.Register(api, huma.Operation{ + OperationID: "get-task", Method: "GET", Path: "/tasks/{id}", + }, func(ctx context.Context, in *struct{ + ID int `path:"id"` + }) (*struct{ Body Task }, error) { ... }) + ``` +- Validation automatic from struct tags (`minLength`, `pattern`, `enum`, + `format`, `required`...); business-rule validation stays in models/services +- Security schemes declared in `huma.Config`, referenced per-operation; Huma + does **not** verify tokens (existing JWT middleware stays authoritative) +- Errors use RFC 9457 `application/problem+json`; customizable via + `huma.NewError` hook to bridge to existing `models.Err*` types +- Incremental migration supported: same `*echo.Echo`, two specs served in + parallel, migrate endpoint-by-endpoint + +## Option D — Spec-first / inverted + +Hand-write `openapi.yaml`, generate handlers from it. + +| Tool | What it generates | Server frameworks | +|---|---|---| +| `oapi-codegen` | Types + server interface + clients | Echo v4, Chi, Gin, Fiber, net/http (no v5) | +| `ogen` (ogen-go/ogen) | Strict types + own radix router + clients | None — replaces router | +| OpenAPI Generator (openapitools) | Stubs + clients (many languages) | Go template, not idiomatic | + +**Cost for vikunja** + +1. Author ~3000+ lines of hand-curated YAML for 161 ops × 87 models + (weeks of work to reach parity) +2. Wire generated interfaces into existing services (touches every route) +3. Lose "code is source of truth"; gain "spec is source of truth" *only if* + enforced by CI +4. Echo v5 friction for `oapi-codegen`; ogen drops Echo entirely + +**When it wins:** If you also want multi-language SDKs, server-side validation +as a primary feature, and can absorb several weeks of upfront investment. + +## Recommendation: Huma (Option C) + +Best balance of: +- OAS 3.1 output (not just 3.0) +- Compile-time guarantee that handlers match spec (solves drift) +- Preserves existing Echo + middleware + permissions investment +- Supports incremental migration with zero big-bang risk + +ogen is technically excellent but the wrong shape — it requires abandoning +Echo and writing the spec first, which conflicts with vikunja's existing +architecture. + +## Huma migration plan + +### Vikunja-side ground truth + +- **31 generic `WebHandler` instances** in `pkg/routes/routes.go` drive the + bulk of the 161 endpoints via a shared `Create/Read/ReadAll/Update/Delete` + codepath (`pkg/web/handler/*.go`) with a struct-factory and central + permissions/validation. Re-implementing this generic layer once in Huma + auto-covers most CRUD endpoints. +- **~30 custom handler files** in `pkg/routes/api/v1/` (login, OAuth, OIDC, + TOTP, attachments, exports, webhooks, CalDAV) — per-endpoint rewrite work. +- **Validation:** `govalidator` via `c.Validate(i)` + (`pkg/routes/validation.go:52`). +- **Errors:** centralized `CreateHTTPErrorHandler` + (`pkg/routes/error_handler.go`) mapping `models.Err*` and `httpCodeGetter` + to HTTP codes. +- **Web framework:** `labstack/echo/v5` with `*echo.Context` (pointer). + +### What the migration touches + +**1. Echo v5 adapter (~1 day)** +Huma ships `humaecho` for `labstack/echo/v4` only (~150 LOC wrapping +`echo.Context`). Fork it for v5: swap the import, adjust for the pointer +`Context` shape, write tests. Community precedents exist (`superstas/huma`, +`eugenepentland/huma`). + +**2. Generic `WebHandler` rewrite (~2–3 days)** +The leverage point. Re-express `pkg/web/handler/{create,read,readall,update,delete}.go` +using Huma generics so all 31 existing generic handlers migrate in one pass. + +**3. Custom handlers (~3–6 days)** +Each becomes one `huma.Register` call. Mechanical but per-file. + +**4. Validation bridge (~3–5 days)** +Two paths: +- **Migrate tags:** convert `valid:"required,email"` → + `validate:"required" format:"email"`. Tedious but a clean win — constraints + become visible in the spec. +- **Keep govalidator:** Huma middleware calls the existing validator after + tag validation. Cheaper, but constraints stay invisible in the spec. + +**5. Error mapping (~1 day)** +Wrap existing `models.Err*` types via `huma.NewError` so +`httpCodeGetter` + RFC 9457 `application/problem+json` produce matching +responses. + +**6. Auth declaration (~0.5 day)** +JWT middleware stays on Echo. Declare bearer scheme once in +`huma.Config.Components.SecuritySchemes`; reference per-operation via +`Operation.Security`. Permissions logic in models stays exactly as-is. + +### Incremental rollout + +Same `*echo.Echo`, two specs served in parallel during transition: + +``` +/api/v1/ ← swag-annotated handlers, /swagger.json (OAS 2.0) +/api/v1/ ← Huma handlers, /openapi.json (OAS 3.1) +``` + +Migrate one resource at a time, ship continuously. `check:got-swag` stays +valid for unmigrated routes; add `check:openapi` for the Huma portion. When +the last endpoint is moved, retire swag. + +### Budget + +| Phase | Effort | +|---|---| +| Adapter fork + generic CRUD shell | ~1 week | +| Custom handlers | ~1 week (parallelizable) | +| Validation tag migration | ~3–5 days | +| Error/auth bridge + spec wiring + tests | ~3 days | +| Frontend SDK regeneration + smoke testing | ~1 week | +| **Total** | **3–4 weeks focused work, shippable incrementally** | + +### Risks + +- **Echo v5 fork divergence** — if labstack/echo/v5 changes its `Context` + shape, the adapter needs updating. Surface is small, low risk. +- **Frontend impact** — JSON shapes stay compatible (same tags), but error + JSON changes to RFC 9457. Plan one round of frontend service updates. +- **Two-spec window** — needs documenting for API consumers. The only + realistic alternative is a multi-month freeze. + +## Suggested first step: spike + +Before committing to the full migration, run a 2–3 day spike: + +1. Fork `humaecho` for `echo/v5` +2. Port one self-contained resource (e.g. `Label` — small surface, full CRUD) +3. Stand up `/openapi.json` alongside the existing `/swagger.json` +4. Verify adapter, generic-CRUD bridge, auth, and error wiring end-to-end + +If the spike lands cleanly, the rest is repetition at known cost.