improvements of OpenAPI generation - #20
Open
esmin wants to merge 1 commit into
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Feature details
A — Safety & spec validity — ✅
generateSchema: thread aseen map[reflect.Type]bool; on revisit return&Schema{}(or a$ref) instead of recursing. Prevents stack overflow for recursive/mutually-recursive types.GenerateOpenAPI: afterconvertPathToOpenAPI, scan{name}tokens and synthesize missingParameter{Name, In: "path", Required: true, Schema: string}. User-supplied parameters override.400/500inRouteOptions.Responses(assign defaults only when absent)./users/{id}includes a required path parameter; explicit400survives.router_generic.go(GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS): moveDeprecated:into its own paragraph (blank comment line before it) sostaticcheckSA1019 /gopls/ pkg.go.dev recognize it. Scope: this is the only planned change to these 7 functions. They remain deprecated shims that delegate tor.Handle(...); no functional/metadata changes to them.B — Wire generic types (core) — ✅
genericBodyType[T]()returningnilfor theEmptysentinel, elsereflect.TypeFor[T]()(mirrors the check inBox.Bind,box.go:231-234).HandleWithOptionsto delegate to a privateregisterRoute(..., reqType, resType reflect.Type)that setsRouteInfo.RequestType/ResponseType.registerGeneric[Req, Res](method, path, handler, opts)and route the 7*Routermethods (GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS) through it, with variadicopts ...*RouteOptions. Scope:*Routermethods only — the deprecated package-level functions inrouter_generic.goare out of scope (see A5).Emptyyieldsnil; generated spec contains request/response schemas.C — Completeness — ✅
RouteOptions.SuccessStatus int(default 200;204emits no body) and thread it into generation. This is what makes POST/DELETE accurate.RouteOptions.OptionalRequestBody bool(request bodies default torequired: true).handleWithGroupMiddlewareand variadic options onRouteGroupgeneric methods.SuccessStatus: 201/204; explicitResponsesoverride inference; group generics documented.Phase D — Docs & example — ✅
WithInfo(...)+RouteOptions(e.g.Summary,Tags,SuccessStatus) in a runnable example — delivered asexamples/03-rest-api-with-openapi/r.OpenAPI(fursy.OpenAPIConfig{...})API; the real API isWithInfo(Info{...})+ServeOpenAPI(path)/GenerateOpenAPI(Info{}).CHANGELOG.mdandllms.md.Phase E — Enhancements (E1–E2 done; E3–E4 pending)
$refintocomponents.schemas(replaces inline duplication, e.g. a shared type referenced from several operations). — ✅ DONEoperationIdauto-generation from method + path. — ✅ DONEapplication/json, XML, form).Every generic route method accepts optional
*RouteOptionsas a trailing variadic argument, on both*Routerand*RouteGroup:Plain handlers get the same metadata via
Router.HandleWithOptionsandRouteGroup.HandleWithOptions.RouteOptionsfieldsBehaviour
Req/ResonBox[Req, Res]are recorded automatically; named types becomecomponents.schemasentries referenced by$ref(Emptymeans "no body").SuccessStatussets the inferred success response (0 ⇒ 200);204emits no content.Responsestake precedence over inference and are never overwritten by the default 400/500.:paramtemplates are auto-declared as requiredin: pathparameters; explicitParametersoverride.OperationIDis preserved; otherwise a unique id is derived from method + path (E2).Type capture
Registration core
Generic registration
adaptGenericHandleris untouched — types are pure metadata; runtime binding andmaxBodySizelimits are unaffected.Generator adjustments (
openapi.go)GenerateOpenAPIconsumesRouteInfo.RequestType/ResponseTypeand:SuccessStatus(default 200; 204 ⇒ no content) —openapi.go:719-731.Responseswhen provided; never overwrites user400/500with defaults.components.schemasand references them with$refvia aschemaRegistry(Phase E1) —openapi.go:401-473.operationId(explicit, or derived from method + path) (Phase E2).The
RequestTypecheck is atopenapi.go:692.Semantics
ReqResEmptyT200+$ref/schema forTapplication/jsonschema forReq(required unlessOptionalRequestBody)T200+$ref/schema forTEmpty200, no bodySuccessStatusoverride wins;204⇒ no bodyNamed package types are emitted as
$reftocomponents.schemas(E1); unnamed types are inlined.Precedence: explicit
Responses> inferred (RequestType/ResponseType+SuccessStatus) > bare200 Success.Test coverage
Implemented in
openapi_test.go:genericBodyType:Empty→ nil; struct/pointer/slice/string→ non-nil.POST[Req,Res]records both types onRouteInfo; variadic*RouteOptionsare recorded.$refs);SuccessStatus: 201/204(204 ⇒ no content); explicitResponsesoverride inference and are not clobbered; group generics are documented./users/{id}declaresidasin: path, required: true; explicitParametersoverride auto-declaration.operationId: deterministic, unique ids.Not done (optional):
testUser.Verification:
go test -race ./...,go vet ./...,golangci-lint run ./...,gofmt.5. Backward compatibility
opts ...*RouteOptions— existing generic 2-arg calls compile unchanged.RouteOptionsgains fields — additive;HandleWithOptionssignature unchanged.RouteInfo.RequestType/ResponseTypealready exported.$refs.6. Decisions & open questions
Decided
200; useSuccessStatusfor201/204.required— defaults totrue;OptionalRequestBodyopts outcomponents.schemasand referenced by$ref.Open
application/jsononly, while binding also accepts XML/form (E3).Res == EmptywithoutSuccessStatusstill emits200 Success; onlySuccessStatus: 204removes the body.ServeOpenAPIregenerates per request with no caching (E4) and documents itself (E5) (/openapi.json) plus anyGET /docs route.References
fursy/openapi.go— generation,generateSchema,convertPathToOpenAPI,ServeOpenAPI.fursy/router.go— generic methods,Handle/HandleWithOptions, route metadata.fursy/group.go,fursy/route_info.go,fursy/handler_generic.go,fursy/box.go.fursy/openapi_test.go— current coverage.examples/03-rest-api-with-openapi/— example showcasing the generated OpenAPI.Appendix — Deferred static lint findings (pre-existing, not OpenAPI-related)
Surfaced on 2026-09-11 after rebuilding the linters with the local Go 1.27.1 toolchain (
staticcheckv0.8.1,golangci-lintv2.13.2,goplsv0.23.0). Deliberately not fixed yet — recorded here so they aren't lost.internal/binding/binding_test.go:497U1000(unused)hiddenfield in thewithUnexportedtest struct; kept only to verify form binding skips unexported fields.//lint:ignore U1000 <reason>(standalonestaticcheckdoes not honor//nolint:unused).middleware/circuitbreaker.go:318S1040(gosimple)originalResponse.(http.ResponseWriter)—Context.Responseis already declared ashttp.ResponseWriter(context.go:95-105), sookis always true. The adjacent comment claimingc.Response is of type any(line 315) is stale/wrong.!= nilguard) and deleting the misleading comment, over suppression.Why they were invisible before
staticcheck(v0.6.1, built with Go 1.25.3) could not analyze this Go 1.27 module at all (export data version 4 > 2;module requires at least go1.27, but Staticcheck was built with go1.25.3), so it reported nothing..github/workflows/test.yml) runsgolangci-lint, and.golangci.ymlenablesstaticcheck(SA* only) but notgosimple(S1040) orunused(U1000); hencegolangci-lint run ./...reports 0 issues.circuitbreaker.go:317-318dates to 2025-11-16;binding_test.go:497dates to 2026-03-05.To enable detection in CI (optional)
Add
gosimpleandunusedtolinters.enablein.golangci.yml, or add a standalonestaticcheck ./...CI step. Doing so would also make the deprecation markers actionable in CI.