From f253e3fcb17055f79a8b201887b667c3cc512fb3 Mon Sep 17 00:00:00 2001 From: Jan Novak Date: Thu, 7 May 2026 17:36:46 +0200 Subject: [PATCH] =?UTF-8?q?feat(go):=20M5.1=20=E2=80=94=20hand-author=20/a?= =?UTF-8?q?pi/*=20wire=20types=20+=20JSON=20Schemas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add internal/web/api package with Go structs for every /api/X route: AdultsResponse, JuniorsResponse, PaymentsResponse, VersionResponse. All fields carry explicit json: tags matching the Python view-model keys. Key design choices: - member_data / month_labels / raw_payments are nested objects (not the pre-serialised JSON strings used in Jinja templates) - Expected{Value int; Unknown bool} with custom MarshalJSON emits int or the string "?" for junior single-attendance months - RawTransaction covers the full 11-column payments sheet row schemagen_test.go reflects all four response types via github.com/invopop/jsonschema and golden-compares against committed schemas in tests/fixtures/api-schema/. The JSONSchema() method on Expected lives in the test file so the prod binary has no jsonschema dependency. Closes M5.1 in docs/plans/2026-05-03-2349-go-backend-rewrite-progress.md. Co-Authored-By: Claude Opus 4.7 --- ...650-go-rewrite-m5-1-api-structs-schemas.md | 240 ++++++++++ go/go.mod | 5 + go/go.sum | 10 + go/internal/web/api/adults.go | 42 ++ go/internal/web/api/juniors.go | 41 ++ go/internal/web/api/payments.go | 9 + go/internal/web/api/schemagen_test.go | 81 ++++ go/internal/web/api/types.go | 122 ++++++ go/internal/web/api/version.go | 9 + .../fixtures/api-schema/adults.schema.json | 399 +++++++++++++++++ .../fixtures/api-schema/juniors.schema.json | 411 ++++++++++++++++++ .../fixtures/api-schema/payments.schema.json | 91 ++++ .../fixtures/api-schema/version.schema.json | 26 ++ 13 files changed, 1486 insertions(+) create mode 100644 docs/plans/2026-05-07-1650-go-rewrite-m5-1-api-structs-schemas.md create mode 100644 go/internal/web/api/adults.go create mode 100644 go/internal/web/api/juniors.go create mode 100644 go/internal/web/api/payments.go create mode 100644 go/internal/web/api/schemagen_test.go create mode 100644 go/internal/web/api/types.go create mode 100644 go/internal/web/api/version.go create mode 100644 go/tests/fixtures/api-schema/adults.schema.json create mode 100644 go/tests/fixtures/api-schema/juniors.schema.json create mode 100644 go/tests/fixtures/api-schema/payments.schema.json create mode 100644 go/tests/fixtures/api-schema/version.schema.json diff --git a/docs/plans/2026-05-07-1650-go-rewrite-m5-1-api-structs-schemas.md b/docs/plans/2026-05-07-1650-go-rewrite-m5-1-api-structs-schemas.md new file mode 100644 index 0000000..5c6b097 --- /dev/null +++ b/docs/plans/2026-05-07-1650-go-rewrite-m5-1-api-structs-schemas.md @@ -0,0 +1,240 @@ +# M5.1 — Hand-author Go API structs + emit JSON Schemas + +Companion to: +- [2026-05-03-2349-go-backend-rewrite.md](2026-05-03-2349-go-backend-rewrite.md) (master design) +- [2026-05-03-2349-go-backend-rewrite-progress.md](2026-05-03-2349-go-backend-rewrite-progress.md) (progress tracker — M5.1 row) +- [2026-05-07-1431-m5-json-api-parity.md](2026-05-07-1431-m5-json-api-parity.md) (Python view-model extraction prep — already merged as `b562ce3` / `32a16ff` / `59223c0`) + +## Context + +M4 (IO layer behind interfaces) just landed. M5 is the JSON-parity contract phase — byte-equal JSON between Python and Go for `/api/adults`, `/api/juniors`, `/api/payments`, `/api/version`. Within M5, the work splits four ways: + +- **M5.1 — this plan.** Define the wire contract: hand-authored Go structs with explicit `json:` tags matching Python keys, plus committed JSON Schemas generated by `github.com/invopop/jsonschema`. **Schemas only — no handlers, no Python `/api/X` routes, no parity tool.** +- M5.2 — Implement Go handlers that compose `services/*` results into these structs. +- M5.3 — Add Python `/api/X` shadow endpoints in [app.py](app.py). +- M5.4 — `cmd/parity/main.go` + `make parity` target. + +The recent Python view-model extraction (`scripts/views.py`) lays the groundwork: every Python builder now returns a plain dict that an `/api/X` shadow can `jsonify` (with one minor unwrap step — see decision #1 below). M5.1 is the matching Go side: types and schemas that pin down the contract before any code writes JSON to a wire. + +## Key design decisions + +1. **Wire format is nested objects, not strings-of-JSON.** The Python view-model dicts contain three template-only fields that are pre-serialized JSON strings: `member_data`, `month_labels_json`, `raw_payments_json`. Those exist purely to feed inline `