All checks were successful
Deploy to K8s / deploy (push) Successful in 7s
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 <noreply@anthropic.com>
82 lines
2.1 KiB
Go
82 lines
2.1 KiB
Go
package api
|
|
|
|
// schemagen_test.go generates and golden-compares JSON Schema files for every
|
|
// /api/X response type.
|
|
//
|
|
// Normal run (CI): go test ./internal/web/api/... — asserts schemas match committed files.
|
|
// Regenerate: go test -run TestGenerateSchemas -update ./internal/web/api/...
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/invopop/jsonschema"
|
|
)
|
|
|
|
var updateFlag = flag.Bool("update", false, "overwrite api-schema fixture files with freshly generated schemas")
|
|
|
|
// JSONSchema makes Expected self-describing for the reflector at test time.
|
|
// The method is in a test file and is not compiled into production binaries.
|
|
// It emits oneOf [integer, "?"] to match the custom MarshalJSON behaviour.
|
|
func (Expected) JSONSchema() *jsonschema.Schema {
|
|
return &jsonschema.Schema{
|
|
OneOf: []*jsonschema.Schema{
|
|
{Type: "integer"},
|
|
{Enum: []any{"?"}},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestGenerateSchemas(t *testing.T) {
|
|
r := &jsonschema.Reflector{
|
|
AllowAdditionalProperties: false,
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
val any
|
|
}{
|
|
{"adults", &AdultsResponse{}},
|
|
{"juniors", &JuniorsResponse{}},
|
|
{"payments", &PaymentsResponse{}},
|
|
{"version", &VersionResponse{}},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
schema := r.Reflect(tc.val)
|
|
|
|
got, err := json.MarshalIndent(schema, "", " ")
|
|
if err != nil {
|
|
t.Fatalf("marshal schema: %v", err)
|
|
}
|
|
got = append(got, '\n')
|
|
|
|
// Path: go/internal/web/api/ → ../../.. → go/ → tests/fixtures/api-schema/
|
|
path := filepath.Join("..", "..", "..", "tests", "fixtures", "api-schema", tc.name+".schema.json")
|
|
|
|
if *updateFlag {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
if err := os.WriteFile(path, got, 0o644); err != nil {
|
|
t.Fatalf("write schema: %v", err)
|
|
}
|
|
t.Logf("wrote %s", path)
|
|
return
|
|
}
|
|
|
|
want, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read fixture %s: %v (re-run with -update to generate)", path, err)
|
|
}
|
|
if string(got) != string(want) {
|
|
t.Errorf("schema mismatch for %s; re-run with -update to regenerate", tc.name)
|
|
}
|
|
})
|
|
}
|
|
}
|