Skip to content

Commit 0de7ce0

Browse files
committed
feat(api): code-first OpenAPI generation + typed frontend client
#47 shipped the projects routes (mock manager/store) that the #24 route-shell PR was building, leaving #24 conflicting on nearly every file. The route shell itself is now redundant, but two pieces of #24 are genuinely net-new and absent from main — salvage them here, rebuilt on top of #47's merged code: - Code-first OpenAPI: apispec/specgen reflects the controllers' request/response types and project DTOs (the same types the handlers use at runtime) into openapi.yaml via swaggest. `cmd/genspec` + `go:generate` regenerate the committed, embedded spec; a drift test (TestBuild_MatchesEmbedded) and a route parity test (TestRouteSpecParity) fail CI if the spec and the code disagree. This replaces main's hand-maintained openapi.yaml so the "single source of truth" claim is actually enforced, not aspirational. - Typed frontend client: frontend/src/api/schema.d.ts is generated from that spec via openapi-typescript (`npm run gen:api`), consumed by a small openapi-fetch client. The frontend now gets its types from the daemon contract instead of hand-maintaining them. specgen lives outside apispec (which controllers import for the 501 stub) to avoid an import cycle. Handlers now encode named response DTOs (controllers/dto.go) instead of map[string]any so the generator reflects the real wire shapes. A gen-verify CI job regenerates both artifacts and fails on a stale commit. Tradeoff: the generated spec drops the hand-authored examples / x-rest-audit notes from #47's openapi.yaml; those can be re-added as operation metadata in specgen if wanted. Behaviour-only patch (no handler logic changes). Supersedes the codegen + frontend parts of #24. Refs #20, #47.
1 parent 83d1ea1 commit 0de7ce0

15 files changed

Lines changed: 1734 additions & 335 deletions

File tree

.github/workflows/go.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66
pull_request:
77
paths:
88
- "backend/**"
9+
- "frontend/**"
910
- ".github/workflows/go.yml"
1011

1112
permissions:
@@ -42,3 +43,40 @@ jobs:
4243

4344
- name: Test
4445
run: go test -race ./...
46+
47+
# gen-verify regenerates the code-first artifacts (openapi.yaml from Go, then
48+
# the frontend TS types from that spec) and fails if the committed copies are
49+
# stale — i.e. someone changed a Go contract type without running
50+
# `go generate ./...` + `npm run gen:api`.
51+
gen-verify:
52+
runs-on: ubuntu-latest
53+
steps:
54+
- uses: actions/checkout@v4
55+
56+
- uses: actions/setup-go@v5
57+
with:
58+
go-version: "1.22"
59+
cache: false
60+
61+
- uses: actions/setup-node@v4
62+
with:
63+
node-version: "20"
64+
cache: npm
65+
cache-dependency-path: frontend/package-lock.json
66+
67+
- name: Generate OpenAPI from Go
68+
working-directory: backend
69+
run: go generate ./...
70+
71+
- name: Generate TypeScript from OpenAPI
72+
working-directory: frontend
73+
run: |
74+
npm ci
75+
npm run gen:api
76+
77+
- name: Fail on stale generated files
78+
run: |
79+
if ! git diff --exit-code; then
80+
echo "::error::Generated files are stale. Run 'go generate ./...' in backend and 'npm run gen:api' in frontend, then commit."
81+
exit 1
82+
fi

backend/cmd/genspec/main.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Command genspec writes the code-first OpenAPI document produced by
2+
// apispec.Build() to disk. It is invoked via `go generate` (see
3+
// internal/httpd/apispec/gen.go); the output openapi.yaml is committed and
4+
// embedded by the apispec package.
5+
package main
6+
7+
import (
8+
"flag"
9+
"log"
10+
"os"
11+
12+
"github.com/aoagents/agent-orchestrator/backend/internal/httpd/apispec/specgen"
13+
)
14+
15+
func main() {
16+
out := flag.String("out", "openapi.yaml", "output path for the generated OpenAPI document")
17+
flag.Parse()
18+
19+
doc, err := specgen.Build()
20+
if err != nil {
21+
log.Fatalf("genspec: build openapi: %v", err)
22+
}
23+
if err := os.WriteFile(*out, doc, 0o644); err != nil {
24+
log.Fatalf("genspec: write %s: %v", *out, err)
25+
}
26+
}

backend/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ require (
77
github.com/creack/pty v1.1.24
88
github.com/go-chi/chi/v5 v5.1.0
99
github.com/pressly/goose/v3 v3.27.1
10+
github.com/swaggest/jsonschema-go v0.3.79
11+
github.com/swaggest/openapi-go v0.2.61
1012
gopkg.in/yaml.v3 v3.0.1
1113
modernc.org/sqlite v1.51.0
1214
)
@@ -19,9 +21,11 @@ require (
1921
github.com/ncruces/go-strftime v1.0.0 // indirect
2022
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
2123
github.com/sethvargo/go-retry v0.3.0 // indirect
24+
github.com/swaggest/refl v1.4.0 // indirect
2225
go.uber.org/multierr v1.11.0 // indirect
2326
golang.org/x/sync v0.20.0 // indirect
2427
golang.org/x/sys v0.43.0 // indirect
28+
gopkg.in/yaml.v2 v2.4.0 // indirect
2529
modernc.org/libc v1.72.3 // indirect
2630
modernc.org/mathutil v1.7.1 // indirect
2731
modernc.org/memory v1.11.0 // indirect

backend/go.sum

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
github.com/bool64/dev v0.2.43 h1:yQ7qiZVef6WtCl2vDYU0Y+qSq+0aBrQzY8KXkklk9cQ=
2+
github.com/bool64/dev v0.2.43/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
3+
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
4+
github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
15
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
26
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
37
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
@@ -14,6 +18,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
1418
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
1519
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
1620
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
21+
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
22+
github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
1723
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
1824
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
1925
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
@@ -26,10 +32,24 @@ github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5s
2632
github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM=
2733
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
2834
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
35+
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
36+
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
2937
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
3038
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
3139
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
3240
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
41+
github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ=
42+
github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
43+
github.com/swaggest/jsonschema-go v0.3.79 h1:0TOShCbAJ9Xjt1e2W83l+QtMQSG2pbun2EkiYTyafCs=
44+
github.com/swaggest/jsonschema-go v0.3.79/go.mod h1:GqVmJ+XNLeUHhFIhHNKc+C68euxfrl3a3aoZH4vTRl0=
45+
github.com/swaggest/openapi-go v0.2.61 h1:psc+LE7pWhEjmJpmkti9tUmBPkkobdUNflBf5Ps6JSc=
46+
github.com/swaggest/openapi-go v0.2.61/go.mod h1:786CwSwleh1IorB0nfwYGESWf83JgQh6fBc1PeJe4Iw=
47+
github.com/swaggest/refl v1.4.0 h1:CftOSdTqRqs100xpFOT/Rifss5xBV/CT0S/FN60Xe9k=
48+
github.com/swaggest/refl v1.4.0/go.mod h1:4uUVFVfPJ0NSX9FPwMPspeHos9wPFlCMGoPRllUbpvA=
49+
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
50+
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
51+
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
52+
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
3353
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
3454
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
3555
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
@@ -42,6 +62,8 @@ golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
4262
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
4363
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
4464
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
65+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
66+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
4567
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
4668
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
4769
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package apispec
2+
3+
// openapi.yaml is generated from Go (see build.go) — do not edit it by hand.
4+
// Regenerate with `go generate ./...` from the backend module root.
5+
//
6+
//go:generate go run ../../../cmd/genspec -out openapi.yaml

0 commit comments

Comments
 (0)