Codegen pipeline
A gpsystem projektek API-kontraktusa spec-first: a source of truth a TypeSpec, abból fordul OpenAPI, abból pedig szigorú ("strict") szerver-interfész. Így egy spec-eltérés nem futásidejű meglepetés, hanem fordítási hiba:
spec/typespec/**.tsp ──tsp compile──▶ api/openapi/<Namespace>.yaml
│ │
│ source of truth │ oapi-codegen v2.8.x
│ (kézzel + generátorokkal) │ + engine-specifikus template-ek
▼ ▼
gpsystem CLI internal/modules/<m>/surfaces/<s>/http/gen/api.gen.go
A mise run generate futtatja a teljes láncot: tsp compile → go generate ./... → go mod tidy → go build ./.... A go:generate direktíva a surface http/handler.go-jában él, és az api/oapi-codegen/<modul>-<surface>.yaml konfigot hajtja (a surfaces/ wrapper miatt a benne hivatkozott relatív útvonalak hat szinttel lépnek feljebb: ../../../../../../). Ez a fájl csak a Handler structot, a New-t és az interfész-assertiont tartalmazza; maguk a handler-metódusok kategóriánként egy fájlban élnek (<modul>_handler.go, vagy <modul>_<csoport>_handler.go egy olyan kategóriának, mint notifications), így az add handler futtatása műveletek szerint csoportosít, nem szór szét egy fájlt függvényenként (lásd add handler).
TypeSpec konvenciók
- Minden surface saját
@service@server("/api/v1<base>")-zel. Az emitter a kimeneti fájlt a namespace-ről nevezi el, ezért a namespace-ek determinisztikusak:Project.Moduleazapisurface-re,Project.Module<Surface>minden másra (a shopban:Shop.ShopésShop.ShopAdmin). - A
shared/errors.tspahttperr.Problemtükre (Hibamodell); ashared/paginator.tspapaginate.Page[T]/paginate.CursorPage[T]tükre (Lapozás). Tartsd őket szinkronban a Go-oldallal. - Az OpenAPI kimenet 3.0-ra van rögzítve (az oapi-codegen nem olvas 3.1-et).
@service namespace-t egy másikba (pl. Shop.Shop.Admin a Shop.Shop alá). A TypeSpec összefésüli a route-jaikat, és duplicate-operation hibával áll le. A generátorok mindig testvér namespace-eket emittálnak.Egy művelet útja: a shop terméklistája
1. A kontraktus. A shop api surface-ének .tsp-jében: az add handler a // gpsystem:operations anchorhoz illeszt, de kézzel is ugyanide írsz:
@tag("shop")
interface ShopApi {
// gpsystem:operations
@get
@route("/products")
@operationId("ListProducts")
@summary("List products")
listProducts(...CursorQuery): CursorPage<Product>;
}
A CursorQuery / CursorPage<T> a scaffoldolt shared/paginator.tsp-ből jön: cursor-lapozott lista egyetlen sorban.
2. A generált interfész. A tsp compile után az oapi-codegen egy strict interfészt emittál a gen csomagba: a request/response típusnevekben nincs Object utótag, és a szignatúra semmi mástól nem függ, csak a context.Context-től és sima Go típusoktól:
type StrictServerInterface interface {
// (GET /products)
ListProducts(ctx context.Context, request ListProductsRequest) (ListProductsResponse, error)
// ...
}
3. A handler. Ezt már te írod (a vázat az add handler adja):
func (h *Handler) ListProducts(ctx context.Context, req gen.ListProductsRequest) (gen.ListProductsResponse, error) {
page, err := h.svc.ListProducts(ctx, req.Params.Cursor, req.Params.Limit)
if err != nil {
return nil, service.ErrListProducts.Wrap(err, "api: list products")
}
return gen.ListProducts200JSONResponse(page), nil
}
4. A bekötés. A generált drótozás a register.go-ban:
func RegisterApi(router chi.Router, deps Dependencies) {
apiSvc := apiservice.New()
apiHandler := apihttp.New(apiSvc)
apiPolicies := apipolicy.New()
router.Route("/shop", func(r chi.Router) {
shopapigen.HandlerWithOptions(shopapigen.NewStrictHandlerWithOptions(
apiHandler,
[]shopapigen.StrictMiddlewareFunc{
server.StrictValidator[shopapigen.StrictHandlerFunc](nil),
// Az utolsó a legkülső: az authorizáció a validáció előtt fut.
policy.Enforcer[shopapigen.StrictHandlerFunc](apiPolicies, shopapigen.PermissionByOperation, shopapigen.PolicyByOperation),
},
shopapigen.StrictHTTPServerOptions{
RequestErrorHandlerFunc: httperr.WriteBadRequest,
ResponseErrorHandlerFunc: server.WriteError,
},
), shopapigen.ChiServerOptions{
BaseRouter: r,
ErrorHandlerFunc: httperr.WriteBadRequest,
})
})
}
A kérésvalidáció a server.StrictValidator strict middleware-ként fut, a lánc legbelső elemeként; a hibarenderelés (mind a validációból/dekódolásból, mind egy visszaadott üzleti hibából) a server.WriteError-on és a httperr.WriteBadRequest-en megy át, a generált kód hibahookjaiba kötve. A server.WriteError az auth/rbac/policy sentineleket (lásd Policy-k) Problemre képezi, mielőtt mindent mást a httperr.WriteError-ra delegálna. Az üzleti kódodat egyik sem érinti.
A template-készlet: upstream routing + minimális strict override
A projektek az oapi-codegen beépített chi-server routing template-jeit használják (ezek naprakészek upstreamben), plusz egy minimális kit-override-ot (internal/templates/oapichi/): a két strict template-et (strict/strict-interface.tmpl, strict/strict-http.tmpl) egyetlen érdemi eltéréssel: a generált request/response típusnevekről lekerül az Object utótag (ListProductsRequest, nem ListProductsRequestObject), így az általad írt üzleti handler-kód mindenhol mentes marad ettől az utótagtól.
Hogyan marad ez biztonságos
Az override a kit repóban van pinelve és tesztelve, egy kontraktus-teszttel, ami a register-snippet által használt azonosítókat, a StrictHandlerFunc alakját és az Object-mentes neveket pineli, és lefordítja a generált csomagot. Az oapi-codegen verzióemelése először ezt a tesztet töri, sosem egy fogyasztó projektet.
Deklaratív authorizáció: @permission és @policy
A projekt-scaffold egy kis decorator-libet szállít (spec/typespec/lib/policy.tsp + policy.js), így minden surface .tsp-ben elérhető a @permission("...") és a @policy("..."). A lánc végig gépi:
@permission("orders.place") / @policy("orders.owner") a műveleten
→ x-permission / x-policy extension az emittált OpenAPI-ban (setExtension)
→ a strict template PermissionByOperation / PolicyByOperation mapeket generál a gen csomagba
→ a register.go-ba kötött policy.Enforcer middleware
RBAC-permissiont ellenőriz, és a surface policy/ registry-jéből futtatja a policy-t
A generált mapek így néznek ki:
var PermissionByOperation = map[string]string{
"PlaceOrder": "orders.place",
}
var PolicyByOperation = map[string]string{
"GetOrder": "orders.owner",
}
Az enforcer a strict-middleware lánc legkülső eleme: már a teljesen bekötött, típusos requestet látja, a belépett usert a contextből olvassa, és 401/403-mal rövidre zár, mielőtt az üzleti handler futna. Fail-closed: a @policy-val megjelölt, de a registry-ben nem regisztrált policy nem átenged, hanem hibázik. A szemantikát és a policy-írást a Policy-k oldal írja le.
A gpsystem.yaml manifest
A projekt gyökerében élő gpsystem.yaml jelöli ki a projekt-gyökeret a CLI-nek, és rögzíti, mit generált eddig:
version: 1
name: shop
module: github.com/acme/shop
kit: github.com/gp-system/gpsystem
db: pgx # vagy bun
templatesSHA: 3f2a… # a bemásolt oapi template-készlet checksumja
modules:
shop:
surfaces: [api, admin]
events: [orderPlaced]
- A
dbdönti el, melyik template-fát renderelik a generátorok (add surface,add handler, ...); a választás projekt-létrehozáskor történik. - A
templatesSHAalapján ismeri fel azupgrade templates, hogy módosítottad-e lokálisan a bemásolt template-eket: módosítottat csak--force-szal ír felül. - A
modulesbejegyzésekből tudja azadd surface, hogy mi létezik már, és azadd listener, hogy létező eventre iratkozol-e fel.
A // gpsystem:* anchorok
A generátorok meglévő fájlt soha nem írnak felül, kizárólag megjelölt anchor-kommenteknél illesztenek be. A fontosabbak:
| Anchor | Fájl | Ki illeszt ide |
|---|---|---|
gpsystem:modules | spec/typespec/main.tsp | new module / add surface |
gpsystem:operations | <surface>.tsp | add handler |
gpsystem:surfaces, gpsystem:imports | register.go | new module / add surface |
gpsystem:registrations | cmd/<modul>/main.go | add surface |
gpsystem:dependencies, gpsystem:dependencies-init | register.go / entrypointok | add event (dispatcher-bekötés) |
gpsystem:listeners, gpsystem:jobs | listeners/register.go / jobs/register.go | add listener / add job |
gpsystem:config | internal/platform/config/config.go | jövőbeli konfig-bővítések |
Az illesztések idempotensek (kétszer futtatva nem duplikálnak), és a fájl goimports-szal formázódik utánuk. Ha egy anchort kitörölsz, az érintett generátor hangosan hibázik, nem próbál tippelni: a hibaüzenet előtt kiírja a beillesztendő snippetet és a visszaállítandó anchor-sort, hogy kézzel be tudd illeszteni a megfelelő helyre. Az anchorokat hagyd a helyükön, ahogy a mintaalkalmazás is teszi.
Használt patternek
- Spec-first codegen (TypeSpec → OpenAPI 3.0 → oapi-codegen strict szerver): Design patternek. A TypeSpec nyelv hivatalos dokumentációja: typespec.io/docs; az oapi-codegen projekt: github.com/oapi-codegen/oapi-codegen.
- Anchor-comment kód-injektálás (
// gpsystem:*, idempotens, whitespace-normalizált): Design patternek. - Fail-closed policy enforcement a
@permission/@policyenforcer láncban: Design patternek.
Kapcsolódó oldalak
- add handler: művelet scaffoldolása a
.tsp-be és a handlerbe. - Validáció: hogyan validál a generált binding.
- Policy-k: az enforcer és a policy-registry.