diff --git a/examples/import-mapping/multiplepackages/admin/server.gen.go b/examples/import-mapping/multiplepackages/admin/server.gen.go index d7867e21d3..056a5e8dc4 100644 --- a/examples/import-mapping/multiplepackages/admin/server.gen.go +++ b/examples/import-mapping/multiplepackages/admin/server.gen.go @@ -171,6 +171,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/examples/import-mapping/samepackage/server.gen.go b/examples/import-mapping/samepackage/server.gen.go index 979b2415d0..b7bcb7b8f5 100644 --- a/examples/import-mapping/samepackage/server.gen.go +++ b/examples/import-mapping/samepackage/server.gen.go @@ -174,6 +174,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/examples/minimal-server/chi/api/ping.gen.go b/examples/minimal-server/chi/api/ping.gen.go index f54fdd3358..3d27d0428c 100644 --- a/examples/minimal-server/chi/api/ping.gen.go +++ b/examples/minimal-server/chi/api/ping.gen.go @@ -162,6 +162,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/examples/minimal-server/gorillamux/api/ping.gen.go b/examples/minimal-server/gorillamux/api/ping.gen.go index ee1a35215a..8236199fd1 100644 --- a/examples/minimal-server/gorillamux/api/ping.gen.go +++ b/examples/minimal-server/gorillamux/api/ping.gen.go @@ -153,6 +153,7 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/examples/overlay/api/ping.gen.go b/examples/overlay/api/ping.gen.go index 8cfd5525ab..8c2cfb4abc 100644 --- a/examples/overlay/api/ping.gen.go +++ b/examples/overlay/api/ping.gen.go @@ -160,6 +160,7 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/examples/petstore-expanded/chi/api/petstore-server.gen.go b/examples/petstore-expanded/chi/api/petstore-server.gen.go index 988314c0e1..406a8a1ee7 100644 --- a/examples/petstore-expanded/chi/api/petstore-server.gen.go +++ b/examples/petstore-expanded/chi/api/petstore-server.gen.go @@ -333,6 +333,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/examples/petstore-expanded/gorilla/api/petstore-server.gen.go b/examples/petstore-expanded/gorilla/api/petstore-server.gen.go index 9cf3a9f0f8..ef855a5844 100644 --- a/examples/petstore-expanded/gorilla/api/petstore-server.gen.go +++ b/examples/petstore-expanded/gorilla/api/petstore-server.gen.go @@ -305,6 +305,7 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/examples/petstore-expanded/strict/api/petstore-server.gen.go b/examples/petstore-expanded/strict/api/petstore-server.gen.go index fc6f040db7..783e1417e6 100644 --- a/examples/petstore-expanded/strict/api/petstore-server.gen.go +++ b/examples/petstore-expanded/strict/api/petstore-server.gen.go @@ -335,6 +335,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/bodies/responses/headers/headers.gen.go b/internal/test/bodies/responses/headers/headers.gen.go index eab62e0480..89435a7bc6 100644 --- a/internal/test/bodies/responses/headers/headers.gen.go +++ b/internal/test/bodies/responses/headers/headers.gen.go @@ -148,6 +148,7 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/events/webhooks/chi/doc.go b/internal/test/events/webhooks/chi/doc.go index e2c1a7bcf7..344e0ce592 100644 --- a/internal/test/events/webhooks/chi/doc.go +++ b/internal/test/events/webhooks/chi/doc.go @@ -4,8 +4,8 @@ // receiver shape is structurally identical to // internal/test/events/webhooks/stdhttp (which already round-trip-tests // the runtime behavior). This package -// is a compile-time assertion that the chi/chi-receiver.tmpl renders -// valid Go. +// is a compile-time assertion that the shared receiver-stdlib.tmpl +// renders valid Go. package chi //go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml ../spec.yaml diff --git a/internal/test/options/name_normalizer/to_camel_case/name_normalizer.gen.go b/internal/test/options/name_normalizer/to_camel_case/name_normalizer.gen.go index 433e7c16c0..d05668796e 100644 --- a/internal/test/options/name_normalizer/to_camel_case/name_normalizer.gen.go +++ b/internal/test/options/name_normalizer/to_camel_case/name_normalizer.gen.go @@ -522,6 +522,7 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/options/name_normalizer/to_camel_case_with_additional_initialisms/name_normalizer.gen.go b/internal/test/options/name_normalizer/to_camel_case_with_additional_initialisms/name_normalizer.gen.go index c450383cf8..52d61df509 100644 --- a/internal/test/options/name_normalizer/to_camel_case_with_additional_initialisms/name_normalizer.gen.go +++ b/internal/test/options/name_normalizer/to_camel_case_with_additional_initialisms/name_normalizer.gen.go @@ -522,6 +522,7 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/options/name_normalizer/to_camel_case_with_digits/name_normalizer.gen.go b/internal/test/options/name_normalizer/to_camel_case_with_digits/name_normalizer.gen.go index a6aaf9408a..dce8443582 100644 --- a/internal/test/options/name_normalizer/to_camel_case_with_digits/name_normalizer.gen.go +++ b/internal/test/options/name_normalizer/to_camel_case_with_digits/name_normalizer.gen.go @@ -522,6 +522,7 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/options/name_normalizer/to_camel_case_with_initialisms/name_normalizer.gen.go b/internal/test/options/name_normalizer/to_camel_case_with_initialisms/name_normalizer.gen.go index a8768876bb..406dcfe6de 100644 --- a/internal/test/options/name_normalizer/to_camel_case_with_initialisms/name_normalizer.gen.go +++ b/internal/test/options/name_normalizer/to_camel_case_with_initialisms/name_normalizer.gen.go @@ -522,6 +522,7 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/options/name_normalizer/unset/name_normalizer.gen.go b/internal/test/options/name_normalizer/unset/name_normalizer.gen.go index c5d9298637..b47d15ec53 100644 --- a/internal/test/options/name_normalizer/unset/name_normalizer.gen.go +++ b/internal/test/options/name_normalizer/unset/name_normalizer.gen.go @@ -522,6 +522,7 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/parameters/roundtrip/chi/gen/server.gen.go b/internal/test/parameters/roundtrip/chi/gen/server.gen.go index d174406489..024995a527 100644 --- a/internal/test/parameters/roundtrip/chi/gen/server.gen.go +++ b/internal/test/parameters/roundtrip/chi/gen/server.gen.go @@ -1521,6 +1521,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/parameters/roundtrip/gorilla/gen/server.gen.go b/internal/test/parameters/roundtrip/gorilla/gen/server.gen.go index a241d6282a..d7a935336f 100644 --- a/internal/test/parameters/roundtrip/gorilla/gen/server.gen.go +++ b/internal/test/parameters/roundtrip/gorilla/gen/server.gen.go @@ -1377,6 +1377,7 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/references/external/path_item_refs/bionicle/bionicle.gen.go b/internal/test/references/external/path_item_refs/bionicle/bionicle.gen.go index bdd7320097..0917d55859 100644 --- a/internal/test/references/external/path_item_refs/bionicle/bionicle.gen.go +++ b/internal/test/references/external/path_item_refs/bionicle/bionicle.gen.go @@ -219,6 +219,7 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/references/external/path_item_refs/fooservice/fooservice.gen.go b/internal/test/references/external/path_item_refs/fooservice/fooservice.gen.go index 1a4ee2868d..732be616c4 100644 --- a/internal/test/references/external/path_item_refs/fooservice/fooservice.gen.go +++ b/internal/test/references/external/path_item_refs/fooservice/fooservice.gen.go @@ -171,6 +171,7 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/references/external/path_item_response_qualification/gen/spec_base/issue.gen.go b/internal/test/references/external/path_item_response_qualification/gen/spec_base/issue.gen.go index f3c56a9c1b..fc379a3a19 100644 --- a/internal/test/references/external/path_item_response_qualification/gen/spec_base/issue.gen.go +++ b/internal/test/references/external/path_item_response_qualification/gen/spec_base/issue.gen.go @@ -557,6 +557,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/references/external/pathalias/server.gen.go b/internal/test/references/external/pathalias/server.gen.go index d5c6d2af4c..d996546b1e 100644 --- a/internal/test/references/external/pathalias/server.gen.go +++ b/internal/test/references/external/pathalias/server.gen.go @@ -162,6 +162,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/references/external/removed_ref/gen/spec_base/issue.gen.go b/internal/test/references/external/removed_ref/gen/spec_base/issue.gen.go index 816e6914a0..a8ea9791f3 100644 --- a/internal/test/references/external/removed_ref/gen/spec_base/issue.gen.go +++ b/internal/test/references/external/removed_ref/gen/spec_base/issue.gen.go @@ -192,6 +192,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/references/multipackage/default_response/gen/api/api.gen.go b/internal/test/references/multipackage/default_response/gen/api/api.gen.go index 3807ae73cb..a23972bb84 100644 --- a/internal/test/references/multipackage/default_response/gen/api/api.gen.go +++ b/internal/test/references/multipackage/default_response/gen/api/api.gen.go @@ -160,6 +160,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/references/multipackage/header_ref/gen/api/api.gen.go b/internal/test/references/multipackage/header_ref/gen/api/api.gen.go index 9a8df76e50..8050e7aaef 100644 --- a/internal/test/references/multipackage/header_ref/gen/api/api.gen.go +++ b/internal/test/references/multipackage/header_ref/gen/api/api.gen.go @@ -449,6 +449,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/references/multipackage/pruned_deps/api.gen.go b/internal/test/references/multipackage/pruned_deps/api.gen.go index 96a0e17c82..31e287b168 100644 --- a/internal/test/references/multipackage/pruned_deps/api.gen.go +++ b/internal/test/references/multipackage/pruned_deps/api.gen.go @@ -536,6 +536,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/references/multipackage/response_cast/gen/spec_base/issue.gen.go b/internal/test/references/multipackage/response_cast/gen/spec_base/issue.gen.go index 94baaa8c59..1839ca1b19 100644 --- a/internal/test/references/multipackage/response_cast/gen/spec_base/issue.gen.go +++ b/internal/test/references/multipackage/response_cast/gen/spec_base/issue.gen.go @@ -164,6 +164,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/references/multipackage/response_cast/gen/spec_other/issue.gen.go b/internal/test/references/multipackage/response_cast/gen/spec_other/issue.gen.go index 1840d9f1c9..e341162358 100644 --- a/internal/test/references/multipackage/response_cast/gen/spec_other/issue.gen.go +++ b/internal/test/references/multipackage/response_cast/gen/spec_other/issue.gen.go @@ -160,6 +160,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/schemas/nullable/nullable.gen.go b/internal/test/schemas/nullable/nullable.gen.go index 9a45381fcd..5007d74073 100644 --- a/internal/test/schemas/nullable/nullable.gen.go +++ b/internal/test/schemas/nullable/nullable.gen.go @@ -500,6 +500,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/schemas/objects/objects.gen.go b/internal/test/schemas/objects/objects.gen.go index dea233f151..4bc980413f 100644 --- a/internal/test/schemas/objects/objects.gen.go +++ b/internal/test/schemas/objects/objects.gen.go @@ -874,6 +874,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/servers/routers/basic/basic.gen.go b/internal/test/servers/routers/basic/basic.gen.go index 4f495dfb83..9b51b23cf8 100644 --- a/internal/test/servers/routers/basic/basic.gen.go +++ b/internal/test/servers/routers/basic/basic.gen.go @@ -634,6 +634,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/servers/routers/smoke/chi/out.gen.go b/internal/test/servers/routers/smoke/chi/out.gen.go index 8310254b3a..053365105a 100644 --- a/internal/test/servers/routers/smoke/chi/out.gen.go +++ b/internal/test/servers/routers/smoke/chi/out.gen.go @@ -246,6 +246,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/servers/routers/smoke/gorilla/out.gen.go b/internal/test/servers/routers/smoke/gorilla/out.gen.go index 3c0b47b8b0..4f3fd1bc8f 100644 --- a/internal/test/servers/routers/smoke/gorilla/out.gen.go +++ b/internal/test/servers/routers/smoke/gorilla/out.gen.go @@ -222,6 +222,7 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/servers/strict/chi/server.gen.go b/internal/test/servers/strict/chi/server.gen.go index b8e5589b8f..c8415db254 100644 --- a/internal/test/servers/strict/chi/server.gen.go +++ b/internal/test/servers/strict/chi/server.gen.go @@ -574,6 +574,7 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/internal/test/servers/strict/echo5/server.gen.go b/internal/test/servers/strict/echo5/server.gen.go index 6c594a135f..28034b0fb8 100644 --- a/internal/test/servers/strict/echo5/server.gen.go +++ b/internal/test/servers/strict/echo5/server.gen.go @@ -1256,8 +1256,10 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(ctx *echo.Context) erro if err != nil { return err } - body := MultipleRequestAndResponseTypesTextRequestBody(data) - request.TextBody = &body + if len(data) > 0 { + body := MultipleRequestAndResponseTypesTextRequestBody(data) + request.TextBody = &body + } } handler := func(ctx *echo.Context, request interface{}) (interface{}, error) { @@ -1490,8 +1492,10 @@ func (sh *strictHandler) TextExample(ctx *echo.Context) error { if err != nil { return err } - body := TextExampleTextRequestBody(data) - request.Body = &body + if len(data) > 0 { + body := TextExampleTextRequestBody(data) + request.Body = &body + } handler := func(ctx *echo.Context, request interface{}) (interface{}, error) { return sh.ssi.TextExample(ctx.Request().Context(), request.(TextExampleRequestObject)) diff --git a/internal/test/servers/strict/gorilla/server.gen.go b/internal/test/servers/strict/gorilla/server.gen.go index 2bd91c8dec..cb5333e458 100644 --- a/internal/test/servers/strict/gorilla/server.gen.go +++ b/internal/test/servers/strict/gorilla/server.gen.go @@ -334,7 +334,7 @@ func (siw *ServerInterfaceWrapper) HeadersExample(w http.ResponseWriter, r *http params.Header1 = Header1 } else { - err = fmt.Errorf("Header parameter header1 is required, but not found") + err := fmt.Errorf("Header parameter header1 is required, but not found") siw.ErrorHandlerFunc(w, r, &RequiredHeaderError{ParamName: "header1", Err: err}) return } @@ -490,6 +490,7 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H http.Error(w, err.Error(), http.StatusBadRequest) } } + wrapper := ServerInterfaceWrapper{ Handler: si, HandlerMiddlewares: options.Middlewares, diff --git a/pkg/codegen/client_view.go b/pkg/codegen/client_view.go new file mode 100644 index 0000000000..28ab90b867 --- /dev/null +++ b/pkg/codegen/client_view.go @@ -0,0 +1,130 @@ +package codegen + +import "fmt" + +// ClientMethodVariant is a precomputed view of one generated client method. +// Every operation yields a generic variant (the bodyless method, or the +// "WithBody" method taking an io.Reader when the operation has a request body) +// followed by one typed-body variant per request body that +// RequestBodyDefinition.IsSupportedByClient reports. +// +// All naming, signature, and comment decisions are made here in Go so that +// client.tmpl and client-with-responses.tmpl can render linearly, without the +// repeated `{{if .HasBody}}WithBody{{end}}` suffix dance and four-fragment +// signature assembly the templates previously carried at every declaration, +// implementation, and call site. +type ClientMethodVariant struct { + // Suffix is appended to the OperationId to form the method name and its + // request-builder name: "" or "WithBody" for the generic variant, + // "WithJSONBody" etc. for a typed-body variant. + Suffix string + + // ArgsDecl is the parameter-declaration fragment that follows the leading + // ctx/server parameter, with a leading comma when non-empty, e.g. + // ", id string, params *FooParams, contentType string, body io.Reader". + ArgsDecl string + + // CallArgs is the argument fragment passed when a method forwards to + // another function (the request builder, or the wrapped client method), + // with a leading comma when non-empty, e.g. ", id, params, contentType, body". + CallArgs string + + // InterfaceComment / MethodComment are the fully rendered Godoc comments + // (including any deprecation notice) for the ClientInterface declaration and + // the *Client method implementation respectively. The interface form places + // a blank "//" line before the deprecation notice; the implementation form + // does not -- matching the historical output of both sites. + InterfaceComment string + MethodComment string + + // WithResponseInterfaceComment / WithResponseMethodComment are the analogous + // comments for the ClientWithResponsesInterface declaration and the + // *ClientWithResponses method implementation. + WithResponseInterfaceComment string + WithResponseMethodComment string +} + +// clientMethodComment assembles a rendered method comment from the base Godoc +// comment and an optional deprecation notice. When interfaceStyle is true a +// blank "//" comment line separates the two, mirroring the ClientInterface +// declarations; the method-implementation sites omit it. +func clientMethodComment(base, deprecation string, interfaceStyle bool) string { + if deprecation == "" { + return base + } + if interfaceStyle { + return base + "\n//\n" + deprecation + } + return base + "\n" + deprecation +} + +// ClientMethodVariants returns the precomputed client method variants for this +// operation: the generic variant first, then one per client-supported request +// body. It is consumed by client.tmpl and client-with-responses.tmpl. +func (o OperationDefinition) ClientMethodVariants() []ClientMethodVariant { + pathDecl := genParamArgs(o.PathParams) + pathCall := genParamNames(o.PathParams) + + var paramsDecl, paramsCall string + if o.RequiresParamObject() { + paramsDecl = fmt.Sprintf(", params *%sParams", o.OperationId) + paramsCall = ", params" + } + + deprecation := o.DeprecationComment() + + variants := make([]ClientMethodVariant, 0, 1+len(o.Bodies)) + + // Generic variant: bodyless, or "WithBody" taking a raw io.Reader. + var genericSuffix, genericBodyDecl, genericBodyCall string + if o.HasBody() { + genericSuffix = "WithBody" + genericBodyDecl = ", contentType string, body io.Reader" + genericBodyCall = ", contentType, body" + } + genericClientBase := o.GenerateFunctionComment(o.OperationId, genericSuffix, false) + genericRespBase := o.GenerateFunctionComment(o.OperationId, genericSuffix+"WithResponse", true) + variants = append(variants, ClientMethodVariant{ + Suffix: genericSuffix, + ArgsDecl: pathDecl + paramsDecl + genericBodyDecl, + CallArgs: pathCall + paramsCall + genericBodyCall, + InterfaceComment: clientMethodComment(genericClientBase, deprecation, true), + MethodComment: clientMethodComment(genericClientBase, deprecation, false), + WithResponseInterfaceComment: clientMethodComment(genericRespBase, deprecation, true), + // The generic ClientWithResponses method implementation historically + // emits the blank "//" separator before the deprecation notice, unlike + // the plain-Client method implementation and the typed-body variants + // below. Preserved verbatim to keep generated output byte-identical. + WithResponseMethodComment: clientMethodComment(genericRespBase, deprecation, true), + }) + + // Typed-body variants, one per client-supported request body. + for _, body := range o.Bodies { + if !body.IsSupportedByClient() { + continue + } + suffix := body.Suffix() + bodyDecl := fmt.Sprintf(", body %s%sRequestBody", o.OperationId, body.NameTag) + clientBase := body.GenerateFunctionComment(o.OperationId, o, suffix, false) + respBase := body.GenerateFunctionComment(o.OperationId, o, suffix+"WithResponse", true) + variants = append(variants, ClientMethodVariant{ + Suffix: suffix, + ArgsDecl: pathDecl + paramsDecl + bodyDecl, + CallArgs: pathCall + paramsCall + ", body", + InterfaceComment: clientMethodComment(clientBase, deprecation, true), + MethodComment: clientMethodComment(clientBase, deprecation, false), + WithResponseInterfaceComment: clientMethodComment(respBase, deprecation, true), + WithResponseMethodComment: clientMethodComment(respBase, deprecation, false), + }) + } + + return variants +} + +// GenericClientVariant returns the generic (bodyless or "WithBody") client +// method variant, which every operation always has as its first variant. The +// request-builder template uses it for the New{OperationId}Request{Suffix} +// signature. +func (o OperationDefinition) GenericClientVariant() ClientMethodVariant { + return o.ClientMethodVariants()[0] +} diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go index 90ced55a36..20247c590a 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -256,6 +256,16 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { } } + // Build per-framework clones of the base tree, layering each framework's + // hook overrides over the shared server-*.tmpl skeletons. Must happen after + // user templates are parsed (so user overrides propagate into the clones) + // and before any template is executed (Clone forbids cloning an executed + // tree). + serverTemplates, err := buildServerTemplates(templates, t) + if err != nil { + return "", fmt.Errorf("error building per-framework server templates: %w", err) + } + ops, err := OperationDefinitions(spec) if err != nil { return "", fmt.Errorf("error creating operation definitions: %w", err) @@ -371,7 +381,7 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { var irisServerOut string if opts.Generate.IrisServer { - irisServerOut, err = GenerateIrisServer(t, ops) + irisServerOut, err = GenerateIrisServer(serverTemplates["iris"], ops) if err != nil { return "", fmt.Errorf("error generating Go handlers for Paths: %w", err) } @@ -379,7 +389,7 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { var echoServerOut string if opts.Generate.EchoServer { - echoServerOut, err = GenerateEchoServer(t, ops) + echoServerOut, err = GenerateEchoServer(serverTemplates["echo"], ops) if err != nil { return "", fmt.Errorf("error generating Go handlers for Paths: %w", err) } @@ -387,7 +397,7 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { var echo5ServerOut string if opts.Generate.Echo5Server { - echo5ServerOut, err = GenerateEcho5Server(t, ops) + echo5ServerOut, err = GenerateEcho5Server(serverTemplates["echo5"], ops) if err != nil { return "", fmt.Errorf("error generating Go handlers for Paths: %w", err) } @@ -395,7 +405,7 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { var chiServerOut string if opts.Generate.ChiServer { - chiServerOut, err = GenerateChiServer(t, ops) + chiServerOut, err = GenerateChiServer(serverTemplates["chi"], ops) if err != nil { return "", fmt.Errorf("error generating Go handlers for Paths: %w", err) } @@ -403,7 +413,7 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { var fiberServerOut string if opts.Generate.FiberServer { - fiberServerOut, err = GenerateFiberServer(t, ops) + fiberServerOut, err = GenerateFiberServer(serverTemplates["fiber"], ops) if err != nil { return "", fmt.Errorf("error generating Go handlers for Paths: %w", err) } @@ -411,7 +421,7 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { var fiberV3ServerOut string if opts.Generate.FiberV3Server { - fiberV3ServerOut, err = GenerateFiberV3Server(t, ops) + fiberV3ServerOut, err = GenerateFiberV3Server(serverTemplates["fiberv3"], ops) if err != nil { return "", fmt.Errorf("error generating Go handlers for Paths: %w", err) } @@ -419,7 +429,7 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { var ginServerOut string if opts.Generate.GinServer { - ginServerOut, err = GenerateGinServer(t, ops) + ginServerOut, err = GenerateGinServer(serverTemplates["gin"], ops) if err != nil { return "", fmt.Errorf("error generating Go handlers for Paths: %w", err) } @@ -427,7 +437,7 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { var gorillaServerOut string if opts.Generate.GorillaServer { - gorillaServerOut, err = GenerateGorillaServer(t, ops) + gorillaServerOut, err = GenerateGorillaServer(serverTemplates["gorilla"], ops) if err != nil { return "", fmt.Errorf("error generating Go handlers for Paths: %w", err) } @@ -454,7 +464,7 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { if err != nil { return "", fmt.Errorf("error generation response definitions for schema: %w", err) } - strictServerOut, err = GenerateStrictServer(t, ops, opts) + strictServerOut, err = GenerateStrictServer(t, serverTemplates, ops, opts) if err != nil { return "", fmt.Errorf("error generating Go handlers for Paths: %w", err) } @@ -1753,6 +1763,15 @@ func LoadTemplates(src embed.FS, t *template.Template) error { if d.IsDir() { return nil } + // hooks.tmpl files hold a framework's {{define}} overrides for the + // shared server-*.tmpl skeletons. They are intentionally NOT loaded + // into the shared base tree: buildServerTemplates parses each one into + // a per-framework clone instead. Loading them here would let one + // framework's overrides clobber the skeleton defaults that other + // frameworks (and unmigrated ones) rely on. + if d.Name() == "hooks.tmpl" { + return nil + } buf, err := src.ReadFile(path) if err != nil { @@ -1769,6 +1788,50 @@ func LoadTemplates(src embed.FS, t *template.Template) error { }) } +// serverTemplateHooks maps a server framework key to the embedded path of its +// hooks file. A hooks file is a set of {{define "..."}} blocks that override the +// {{block}} defaults in the shared server-*.tmpl skeletons. +// +// Only frameworks whose generated code deviates from the skeleton defaults need +// an entry. The skeleton defaults ARE the stdhttp shape, so stdhttp (and the +// frameworks not yet migrated to the layering: echo, gin, fiber, iris) run the +// templates straight from the base tree with no clone. +var serverTemplateHooks = map[string]string{ + "chi": "templates/chi/hooks.tmpl", + "gorilla": "templates/gorilla/hooks.tmpl", + "echo": "templates/echo/hooks.tmpl", + "echo5": "templates/echo/v5/hooks.tmpl", + "fiber": "templates/fiber/hooks.tmpl", + "fiberv3": "templates/fiber-v3/hooks.tmpl", + "gin": "templates/gin/hooks.tmpl", + "iris": "templates/iris/hooks.tmpl", +} + +// buildServerTemplates clones the base template tree once per framework in +// serverTemplateHooks and parses that framework's hooks file into the clone. +// text/template permits redefining a template that has not yet been executed, +// so parsing the {{define}} overrides replaces the skeleton {{block}} defaults +// in the clone (last definition wins). It must be called before the base tree +// is executed, because a template cannot be cloned once it has run. +func buildServerTemplates(src embed.FS, base *template.Template) (map[string]*template.Template, error) { + result := make(map[string]*template.Template, len(serverTemplateHooks)) + for framework, path := range serverTemplateHooks { + clone, err := base.Clone() + if err != nil { + return nil, fmt.Errorf("cloning base templates for %s: %w", framework, err) + } + buf, err := src.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading hooks file '%s': %w", path, err) + } + if _, err := clone.Parse(string(buf)); err != nil { + return nil, fmt.Errorf("parsing hooks file '%s': %w", path, err) + } + result[framework] = clone + } + return result, nil +} + func OperationSchemaImports(s *Schema) (map[string]goImport, error) { res := map[string]goImport{} diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index 6e0447a5d9..48141c59fc 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -605,11 +605,11 @@ type OperationDefinition struct { // appear in the spec rather than sorted (issue #1887). Zero when the // source location is unavailable (e.g. a programmatically-built spec), // in which case route registration falls back to the default order. - SpecOrder int - Spec *openapi3.Operation - IsAlias bool // True when this path is a $ref alias of another path item - AliasTarget string // When IsAlias is true, this is the OperationId of the canonical operation (for route registration to reference the correct wrapper) - PathItemRef string // The path item's $ref (if any); used to qualify externally-loaded schemas referenced from this operation's responses + SpecOrder int + Spec *openapi3.Operation + IsAlias bool // True when this path is a $ref alias of another path item + AliasTarget string // When IsAlias is true, this is the OperationId of the canonical operation (for route registration to reference the correct wrapper) + PathItemRef string // The path item's $ref (if any); used to qualify externally-loaded schemas referenced from this operation's responses // IsWebhook is true when this OperationDefinition was sourced from // spec.Webhooks (OpenAPI 3.1+). Webhook operations have no path @@ -672,7 +672,13 @@ func (o OperationDefinition) SourceName() string { type ReceiverTemplateData struct { Prefix string // "Webhook" or "Callback" PrefixLower string // lowercase form of Prefix, for prose - Operations []OperationDefinition + // CtxType is the framework context type as it appears in the handler + // signature: "echo.Context" vs "*echo.Context" for echo v4/v5, and + // "*fiber.Ctx" vs "fiber.Ctx" for fiber v2/v3. Only the echo and fiber + // receiver templates read it; the other frameworks' templates embed + // their fixed context type directly and leave this empty. + CtxType string + Operations []OperationDefinition } // NewReceiverTemplateData builds the template input for the given @@ -685,6 +691,50 @@ func NewReceiverTemplateData(prefix string, ops []OperationDefinition) ReceiverT } } +// InitiatorTemplateData is the input to the shared webhook / callback +// initiator template. Prefix selects between "Webhook" and "Callback" +// (and the lowercase form for prose), so a single template handles both +// the OpenAPI 3.1 webhook and OpenAPI callback client-side initiators. +type InitiatorTemplateData struct { + Prefix string // "Webhook" or "Callback" + PrefixLower string // lowercase form of Prefix, for prose + Operations []OperationDefinition +} + +// NewInitiatorTemplateData builds the template input for the given +// prefix ("Webhook" or "Callback") and operation list. +// +// Initiator operations must not carry path parameters: the initiator +// takes an opaque targetURL per call, so there is no route template to +// substitute into, and the generated request builders accept no path +// arguments while the initiator methods would forward them (producing +// uncompilable code). Webhook/CallbackOperationDefinitions reject +// in:path parameters at extraction time; this check is defense in depth +// for direct callers of the exported API. +func NewInitiatorTemplateData(prefix string, ops []OperationDefinition) (InitiatorTemplateData, error) { + for _, op := range ops { + if len(op.PathParams) > 0 { + return InitiatorTemplateData{}, fmt.Errorf("%s operation %s has path parameters, which are not supported for %s initiators", + strings.ToLower(prefix), op.OperationId, strings.ToLower(prefix)) + } + } + return InitiatorTemplateData{ + Prefix: prefix, + PrefixLower: strings.ToLower(prefix), + Operations: ops, + }, nil +} + +// EchoRegisterTemplateData is the input to the shared echo route- +// registration template. RouteReturnType selects between echo v4's +// "*echo.Route" and echo v5's "echo.RouteInfo" in the EchoRouter +// interface method signatures -- the only difference between the two +// echo versions' registration code. +type EchoRegisterTemplateData struct { + RouteReturnType string + Operations []OperationDefinition +} + // Params returns the list of all parameters except Path parameters. Path parameters // are handled differently from the rest, since they're mandatory. func (o *OperationDefinition) Params() []ParameterDefinition { @@ -1591,6 +1641,15 @@ func WebhookOperationDefinitions(swagger *openapi3.T) ([]OperationDefinition, er return nil, err } + // The initiator sends to an opaque caller-supplied target URL, so + // there is no route template to substitute a path parameter into. + // Rejecting it here beats silently dropping it, which would send + // requests to a URL that still contains the placeholder. + if pathParams := FilterParameterDefinitionByType(allParams, "path"); len(pathParams) > 0 { + return nil, fmt.Errorf("webhook %q operation %s declares path parameter %q: path parameters are not supported for webhooks", + webhookName, opName, pathParams[0].ParamName) + } + bodyDefinitions, typeDefinitions, err := GenerateBodyDefinitions(operationId, op.RequestBody, pathItem.Ref) if err != nil { return nil, fmt.Errorf("error generating body definitions for webhook %q: %w", webhookName, err) @@ -1733,6 +1792,15 @@ func CallbackOperationDefinitions(swagger *openapi3.T) ([]OperationDefinition, e return nil, err } + // See the matching check in WebhookOperationDefinitions: + // the initiator's target URL is opaque, so a path + // parameter has nowhere to go and must not be dropped + // silently. + if pathParams := FilterParameterDefinitionByType(allParams, "path"); len(pathParams) > 0 { + return nil, fmt.Errorf("callback %q operation %s declares path parameter %q: path parameters are not supported for callbacks", + callbackName, opName, pathParams[0].ParamName) + } + bodyDefinitions, typeDefinitions, err := GenerateBodyDefinitions(operationId, op.RequestBody, cbPathItem.Ref) if err != nil { return nil, fmt.Errorf("error generating body definitions for callback %q: %w", callbackName, err) @@ -2249,7 +2317,7 @@ func operationsInRegistrationOrder(ops []OperationDefinition) []OperationDefinit // all the wrapper functions around our handlers. func GenerateIrisServer(t *template.Template, operations []OperationDefinition) (string, error) { var buf bytes.Buffer - if err := GenerateTemplatesIntoBuffer(&buf, []string{"iris/iris-interface.tmpl", "iris/iris-middleware.tmpl"}, t, operations); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"server-interface.tmpl", "iris/iris-middleware.tmpl"}, t, operations); err != nil { return "", err } // Route registration follows spec-declaration order (issue #1887). @@ -2263,11 +2331,11 @@ func GenerateIrisServer(t *template.Template, operations []OperationDefinition) // all the wrapper functions around our handlers. func GenerateChiServer(t *template.Template, operations []OperationDefinition) (string, error) { var buf bytes.Buffer - if err := GenerateTemplatesIntoBuffer(&buf, []string{"chi/chi-interface.tmpl", "chi/chi-middleware.tmpl"}, t, operations); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"server-interface.tmpl", "server-middleware.tmpl"}, t, operations); err != nil { return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"chi/chi-handler.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"server-handler.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { return "", err } return buf.String(), nil @@ -2277,11 +2345,11 @@ func GenerateChiServer(t *template.Template, operations []OperationDefinition) ( // all the wrapper functions around our handlers. func GenerateFiberServer(t *template.Template, operations []OperationDefinition) (string, error) { var buf bytes.Buffer - if err := GenerateTemplatesIntoBuffer(&buf, []string{"fiber/fiber-interface.tmpl", "fiber/fiber-middleware.tmpl"}, t, operations); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"server-interface.tmpl", "fiber/fiber-middleware.tmpl"}, t, operations); err != nil { return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"fiber/fiber-handler.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"fiber-handler.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { return "", err } return buf.String(), nil @@ -2291,11 +2359,11 @@ func GenerateFiberServer(t *template.Template, operations []OperationDefinition) // all the wrapper functions around our handlers. func GenerateFiberV3Server(t *template.Template, operations []OperationDefinition) (string, error) { var buf bytes.Buffer - if err := GenerateTemplatesIntoBuffer(&buf, []string{"fiber-v3/fiber-interface.tmpl", "fiber-v3/fiber-middleware.tmpl"}, t, operations); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"server-interface.tmpl", "fiber/fiber-middleware.tmpl"}, t, operations); err != nil { return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"fiber-v3/fiber-handler.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"fiber-handler.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { return "", err } return buf.String(), nil @@ -2305,11 +2373,12 @@ func GenerateFiberV3Server(t *template.Template, operations []OperationDefinitio // all the wrapper functions around our handlers. func GenerateEchoServer(t *template.Template, operations []OperationDefinition) (string, error) { var buf bytes.Buffer - if err := GenerateTemplatesIntoBuffer(&buf, []string{"echo/echo-interface.tmpl", "echo/echo-wrappers.tmpl"}, t, operations); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"server-interface.tmpl", "echo/echo-wrappers.tmpl"}, t, operations); err != nil { return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"echo/echo-register.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { + registerData := EchoRegisterTemplateData{RouteReturnType: "*echo.Route", Operations: operationsInRegistrationOrder(operations)} + if err := GenerateTemplatesIntoBuffer(&buf, []string{"echo/echo-register.tmpl"}, t, registerData); err != nil { return "", err } return buf.String(), nil @@ -2319,11 +2388,15 @@ func GenerateEchoServer(t *template.Template, operations []OperationDefinition) // all the wrapper functions around our handlers. func GenerateEcho5Server(t *template.Template, operations []OperationDefinition) (string, error) { var buf bytes.Buffer - if err := GenerateTemplatesIntoBuffer(&buf, []string{"echo/v5/echo-interface.tmpl", "echo/v5/echo-wrappers.tmpl"}, t, operations); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"server-interface.tmpl", "echo/echo-wrappers.tmpl"}, t, operations); err != nil { return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"echo/v5/echo-register.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { + // Echo v5's EchoRouter methods return echo.RouteInfo instead of v4's + // *echo.Route; the shared echo/echo-register.tmpl is parameterized on + // that return type. + registerData := EchoRegisterTemplateData{RouteReturnType: "echo.RouteInfo", Operations: operationsInRegistrationOrder(operations)} + if err := GenerateTemplatesIntoBuffer(&buf, []string{"echo/echo-register.tmpl"}, t, registerData); err != nil { return "", err } return buf.String(), nil @@ -2333,7 +2406,7 @@ func GenerateEcho5Server(t *template.Template, operations []OperationDefinition) // all the wrapper functions around our handlers. func GenerateGinServer(t *template.Template, operations []OperationDefinition) (string, error) { var buf bytes.Buffer - if err := GenerateTemplatesIntoBuffer(&buf, []string{"gin/gin-interface.tmpl", "gin/gin-wrappers.tmpl"}, t, operations); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"server-interface.tmpl", "gin/gin-wrappers.tmpl"}, t, operations); err != nil { return "", err } // Route registration follows spec-declaration order (issue #1887). @@ -2347,11 +2420,11 @@ func GenerateGinServer(t *template.Template, operations []OperationDefinition) ( // all the wrapper functions around our handlers. func GenerateGorillaServer(t *template.Template, operations []OperationDefinition) (string, error) { var buf bytes.Buffer - if err := GenerateTemplatesIntoBuffer(&buf, []string{"gorilla/gorilla-interface.tmpl", "gorilla/gorilla-middleware.tmpl"}, t, operations); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"server-interface.tmpl", "server-middleware.tmpl"}, t, operations); err != nil { return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"gorilla/gorilla-register.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"server-handler.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { return "", err } return buf.String(), nil @@ -2361,43 +2434,63 @@ func GenerateGorillaServer(t *template.Template, operations []OperationDefinitio // all the wrapper functions around our handlers. func GenerateStdHTTPServer(t *template.Template, operations []OperationDefinition) (string, error) { var buf bytes.Buffer - if err := GenerateTemplatesIntoBuffer(&buf, []string{"stdhttp/std-http-interface.tmpl", "stdhttp/std-http-middleware.tmpl"}, t, operations); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"server-interface.tmpl", "server-middleware.tmpl"}, t, operations); err != nil { return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"stdhttp/std-http-handler.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"server-handler.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { return "", err } return buf.String(), nil } -func GenerateStrictServer(t *template.Template, operations []OperationDefinition, opts Configuration) (string, error) { - - var templates []string - - if opts.Generate.ChiServer || opts.Generate.GorillaServer || opts.Generate.StdHTTPServer { - templates = append(templates, "strict/strict-interface.tmpl", "strict/strict-http.tmpl") - } - if opts.Generate.EchoServer { - templates = append(templates, "strict/strict-interface.tmpl", "strict/strict-echo.tmpl") - } - if opts.Generate.GinServer { - templates = append(templates, "strict/strict-interface.tmpl", "strict/strict-gin.tmpl") - } - if opts.Generate.FiberServer { - templates = append(templates, "strict/strict-fiber-interface.tmpl", "strict/strict-fiber.tmpl") - } - if opts.Generate.FiberV3Server { - templates = append(templates, "strict/strict-fiber-v3-interface.tmpl", "strict/strict-fiber-v3.tmpl") - } - if opts.Generate.IrisServer { - templates = append(templates, "strict/strict-iris-interface.tmpl", "strict/strict-iris.tmpl") +func GenerateStrictServer(t *template.Template, serverTemplates map[string]*template.Template, operations []OperationDefinition, opts Configuration) (string, error) { + + // Each strict framework renders its interface + glue templates against a + // specific template tree: the base tree for frameworks that use the + // skeleton/base defaults, or a per-framework clone (from serverTemplates) + // for those that override strict hooks -- fiber v3 swaps the fiber context + // type from *fiber.Ctx to fiber.Ctx via the fiber.ctxType hook, so its + // interface template must render against the fiber v3 clone. + type strictTarget struct { + enabled bool + tree *template.Template + interfaceTmpl string + glueTmpl string + } + + targets := []strictTarget{ + {opts.Generate.ChiServer || opts.Generate.GorillaServer || opts.Generate.StdHTTPServer, t, "strict/strict-interface.tmpl", "strict/strict-http.tmpl"}, + {opts.Generate.EchoServer, t, "strict/strict-interface.tmpl", "strict/strict-echo.tmpl"}, + {opts.Generate.GinServer, t, "strict/strict-interface.tmpl", "strict/strict-gin.tmpl"}, + {opts.Generate.FiberServer, t, "strict/strict-fiber-interface.tmpl", "strict/strict-fiber.tmpl"}, + {opts.Generate.FiberV3Server, serverTemplates["fiberv3"], "strict/strict-fiber-interface.tmpl", "strict/strict-fiber.tmpl"}, + {opts.Generate.IrisServer, t, "strict/strict-iris-interface.tmpl", "strict/strict-iris.tmpl"}, + {opts.Generate.Echo5Server, serverTemplates["echo5"], "strict/strict-interface.tmpl", "strict/strict-echo.tmpl"}, + } + + // Configuration.Validate() enforces that at most one server type is + // enabled, so at most one target can be enabled here. Enforce the same + // invariant for direct callers of this exported function: every interface + // template declares the same package-level names (StrictServerInterface, + // RequestObject, ResponseObject), with visitor signatures that + // differ per framework, so emitting more than one target can never + // compile. (chi/gorilla/stdhttp form a single target: they share both + // templates, so any one of them yields identical output.) + var chosen *strictTarget + for i := range targets { + if !targets[i].enabled { + continue + } + if chosen != nil { + return "", fmt.Errorf("only one server type is supported at a time for strict server generation") + } + chosen = &targets[i] } - if opts.Generate.Echo5Server { - templates = append(templates, "strict/strict-interface.tmpl", "strict/strict-echo5.tmpl") + if chosen == nil { + return "", nil } - - return GenerateTemplates(templates, t, operations) + return GenerateTemplates([]string{chosen.interfaceTmpl, chosen.glueTmpl}, chosen.tree, operations) } func GenerateStrictResponses(t *template.Template, responses []ResponseDefinition) (string, error) { @@ -2424,7 +2517,11 @@ func GenerateClientWithResponses(t *template.Template, ops []OperationDefinition // WebhookOperationDefinitions); path operations are emitted by the // regular Client templates. func GenerateWebhookInitiator(t *template.Template, webhookOps []OperationDefinition) (string, error) { - return GenerateTemplates([]string{"webhook-initiator.tmpl"}, t, webhookOps) + data, err := NewInitiatorTemplateData("Webhook", webhookOps) + if err != nil { + return "", err + } + return GenerateTemplates([]string{"initiator.tmpl"}, t, data) } // GenerateCallbackInitiator generates the CallbackInitiator -- the @@ -2433,7 +2530,11 @@ func GenerateWebhookInitiator(t *template.Template, webhookOps []OperationDefini // gathered via CallbackOperationDefinitions, which walk paths/operations/ // callbacks rather than spec.Webhooks. func GenerateCallbackInitiator(t *template.Template, callbackOps []OperationDefinition) (string, error) { - return GenerateTemplates([]string{"callback-initiator.tmpl"}, t, callbackOps) + data, err := NewInitiatorTemplateData("Callback", callbackOps) + if err != nil { + return "", err + } + return GenerateTemplates([]string{"initiator.tmpl"}, t, data) } // GenerateStdHTTPReceiver renders the merged stdhttp receiver template @@ -2444,22 +2545,21 @@ func GenerateCallbackInitiator(t *template.Template, callbackOps []OperationDefi // query/header parameter binding inline (matching the param-binding // machinery used by the path-server middleware). func GenerateStdHTTPReceiver(t *template.Template, prefix string, ops []OperationDefinition) (string, error) { - return GenerateTemplates([]string{"stdhttp/std-http-receiver.tmpl"}, t, NewReceiverTemplateData(prefix, ops)) + return GenerateTemplates([]string{"receiver-stdlib.tmpl"}, t, NewReceiverTemplateData(prefix, ops)) } // GenerateChiReceiver renders the chi receiver template. Chi shares -// stdhttp's (w, r) handler signature, so the template is structurally -// identical -- only the file path and (in the future, if needed) -// framework-specific helpers differ. +// stdhttp's (w, r) handler signature, so it renders the same shared +// receiver-stdlib.tmpl template. func GenerateChiReceiver(t *template.Template, prefix string, ops []OperationDefinition) (string, error) { - return GenerateTemplates([]string{"chi/chi-receiver.tmpl"}, t, NewReceiverTemplateData(prefix, ops)) + return GenerateTemplates([]string{"receiver-stdlib.tmpl"}, t, NewReceiverTemplateData(prefix, ops)) } // GenerateGorillaReceiver renders the gorilla/mux receiver template. -// Gorilla shares stdhttp's (w, r) handler signature, so the template -// is structurally identical to stdhttp's. +// Gorilla shares stdhttp's (w, r) handler signature, so it renders the +// same shared receiver-stdlib.tmpl template. func GenerateGorillaReceiver(t *template.Template, prefix string, ops []OperationDefinition) (string, error) { - return GenerateTemplates([]string{"gorilla/gorilla-receiver.tmpl"}, t, NewReceiverTemplateData(prefix, ops)) + return GenerateTemplates([]string{"receiver-stdlib.tmpl"}, t, NewReceiverTemplateData(prefix, ops)) } // GenerateEchoReceiver renders the echo (v4) receiver template. Echo's @@ -2468,14 +2568,18 @@ func GenerateGorillaReceiver(t *template.Template, prefix string, ops []Operatio // reports them as 400 -- there's no errHandler argument like the // stdhttp receiver factory has. func GenerateEchoReceiver(t *template.Template, prefix string, ops []OperationDefinition) (string, error) { - return GenerateTemplates([]string{"echo/echo-receiver.tmpl"}, t, NewReceiverTemplateData(prefix, ops)) + data := NewReceiverTemplateData(prefix, ops) + data.CtxType = "echo.Context" + return GenerateTemplates([]string{"echo/echo-receiver.tmpl"}, t, data) } -// GenerateEcho5Receiver renders the echo (v5) receiver template. Same -// shape as v4 but with `*echo.Context` (pointer) -- the only API +// GenerateEcho5Receiver renders the shared echo receiver template with +// echo v5's `*echo.Context` (pointer) context type -- the only API // difference between echo v4 and v5 that affects the receiver. func GenerateEcho5Receiver(t *template.Template, prefix string, ops []OperationDefinition) (string, error) { - return GenerateTemplates([]string{"echo/v5/echo-receiver.tmpl"}, t, NewReceiverTemplateData(prefix, ops)) + data := NewReceiverTemplateData(prefix, ops) + data.CtxType = "*echo.Context" + return GenerateTemplates([]string{"echo/echo-receiver.tmpl"}, t, data) } // GenerateGinReceiver renders the gin receiver template. Gin's handler @@ -2491,15 +2595,18 @@ func GenerateGinReceiver(t *template.Template, prefix string, ops []OperationDef // returned via fiber.NewError so fiber's error chain reports them as // 400. Per-handler middleware is not generated; use fiber.App.Use(). func GenerateFiberReceiver(t *template.Template, prefix string, ops []OperationDefinition) (string, error) { - return GenerateTemplates([]string{"fiber/fiber-receiver.tmpl"}, t, NewReceiverTemplateData(prefix, ops)) + data := NewReceiverTemplateData(prefix, ops) + data.CtxType = "*fiber.Ctx" + return GenerateTemplates([]string{"fiber/fiber-receiver.tmpl"}, t, data) } -// GenerateFiberV3Receiver renders the fiber (v3) receiver template. -// Same shape as v2 but with `fiber.Ctx` (interface, by value) -- the -// only API difference between fiber v2 and v3 that affects the -// receiver. +// GenerateFiberV3Receiver renders the shared fiber receiver template with +// fiber v3's `fiber.Ctx` (interface, by value) context type -- the only +// API difference between fiber v2 and v3 that affects the receiver. func GenerateFiberV3Receiver(t *template.Template, prefix string, ops []OperationDefinition) (string, error) { - return GenerateTemplates([]string{"fiber-v3/fiber-receiver.tmpl"}, t, NewReceiverTemplateData(prefix, ops)) + data := NewReceiverTemplateData(prefix, ops) + data.CtxType = "fiber.Ctx" + return GenerateTemplates([]string{"fiber/fiber-receiver.tmpl"}, t, data) } // GenerateIrisReceiver renders the iris receiver template. Iris's diff --git a/pkg/codegen/operations_test.go b/pkg/codegen/operations_test.go index 89f3b102a0..0880c36aef 100644 --- a/pkg/codegen/operations_test.go +++ b/pkg/codegen/operations_test.go @@ -16,7 +16,9 @@ package codegen import ( "go/format" "net/http" + "strings" "testing" + "text/template" "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/assert" @@ -917,3 +919,110 @@ func TestGenerateFunctionComment_GofmtHeading(t *testing.T) { }) } } + +func TestNewInitiatorTemplateDataRejectsPathParams(t *testing.T) { + _, err := NewInitiatorTemplateData("Webhook", []OperationDefinition{ + { + OperationId: "PetUpdated", + PathParams: []ParameterDefinition{{ParamName: "petId"}}, + }, + }) + require.ErrorContains(t, err, "path parameters") + + data, err := NewInitiatorTemplateData("Callback", []OperationDefinition{ + {OperationId: "PetUpdated"}, + }) + require.NoError(t, err) + assert.Equal(t, "Callback", data.Prefix) + assert.Equal(t, "callback", data.PrefixLower) +} + +// buildStrictTestTrees loads the real embedded templates and per-framework +// clones the same way Generate() does, for exercising GenerateStrictServer. +func buildStrictTestTrees(t *testing.T) (*template.Template, map[string]*template.Template) { + t.Helper() + funcs := make(template.FuncMap, len(TemplateFunctions)+1) + for k, v := range TemplateFunctions { + funcs[k] = v + } + // Generate() injects "opts" before loading templates; stub it here. + funcs["opts"] = func() Configuration { return Configuration{} } + base := template.New("codegen").Funcs(funcs) + require.NoError(t, LoadTemplates(templates, base)) + clones, err := buildServerTemplates(templates, base) + require.NoError(t, err) + return base, clones +} + +func TestGenerateStrictServerInterfaceDedup(t *testing.T) { + base, clones := buildStrictTestTrees(t) + ops := []OperationDefinition{{OperationId: "Ping"}} + + // A single enabled server emits the interface exactly once. + out, err := GenerateStrictServer(base, clones, ops, Configuration{ + Generate: GenerateOptions{ChiServer: true}, + }) + require.NoError(t, err) + assert.Equal(t, 1, strings.Count(out, "type StrictServerInterface interface")) + + // Configuration.Validate() permits only one server type; the strict + // generator enforces the same invariant for direct API callers, since + // every interface template declares the same package-level type names. + for name, generate := range map[string]GenerateOptions{ + "fiber v2+v3": {FiberServer: true, FiberV3Server: true}, + "echo+fiber": {EchoServer: true, FiberServer: true}, + "chi+gin": {ChiServer: true, GinServer: true}, + } { + _, err = GenerateStrictServer(base, clones, ops, Configuration{Generate: generate}) + require.ErrorContains(t, err, "only one server type", name) + } +} + +func TestInitiatorPathParamsRejectedAtExtraction(t *testing.T) { + webhookSpec := ` +openapi: "3.1.0" +info: {title: t, version: "1"} +webhooks: + petUpdated: + post: + parameters: + - name: petId + in: path + required: true + schema: {type: string} + responses: + '200': {description: OK} +paths: {} +` + callbackSpec := ` +openapi: "3.0.0" +info: {title: t, version: "1"} +paths: + /subscribe: + post: + callbacks: + petUpdated: + '{$request.body#/callbackUrl}': + post: + parameters: + - name: petId + in: path + required: true + schema: {type: string} + responses: + '200': {description: OK} + responses: + '200': {description: OK} +` + for name, spec := range map[string]string{"webhook": webhookSpec, "callback": callbackSpec} { + t.Run(name, func(t *testing.T) { + swagger, err := openapi3.NewLoader().LoadFromData([]byte(spec)) + require.NoError(t, err) + _, err = Generate(swagger, Configuration{ + PackageName: "api", + Generate: GenerateOptions{Models: true}, + }) + require.ErrorContains(t, err, `path parameter "petId"`) + }) + } +} diff --git a/pkg/codegen/template_helpers.go b/pkg/codegen/template_helpers.go index 3a91cfdeb7..80c61da844 100644 --- a/pkg/codegen/template_helpers.go +++ b/pkg/codegen/template_helpers.go @@ -424,9 +424,29 @@ func httpMethodConstant(method string) string { } } +// dict builds a map[string]any from an even-length list of alternating +// key/value arguments. It lets a template pass more than one named value to a +// {{template}} invocation (text/template only accepts a single data argument), +// e.g. {{template "partial" (dict "Headers" $headers "Setter" "w.Header().Set")}}. +func dict(values ...any) (map[string]any, error) { + if len(values)%2 != 0 { + return nil, fmt.Errorf("dict requires an even number of arguments, got %d", len(values)) + } + m := make(map[string]any, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, ok := values[i].(string) + if !ok { + return nil, fmt.Errorf("dict keys must be strings, got %T", values[i]) + } + m[key] = values[i+1] + } + return m, nil +} + // TemplateFunctions is passed to the template engine, and we can call each // function here by keyName from the template code. var TemplateFunctions = template.FuncMap{ + "dict": dict, "genParamArgs": genParamArgs, "genParamTypes": genParamTypes, "genParamNames": genParamNames, diff --git a/pkg/codegen/templates/callback-initiator.tmpl b/pkg/codegen/templates/callback-initiator.tmpl deleted file mode 100644 index 908695bf37..0000000000 --- a/pkg/codegen/templates/callback-initiator.tmpl +++ /dev/null @@ -1,236 +0,0 @@ -// CallbackInitiator sends OpenAPI callback requests to target URLs. -// Modeled on the generated Client, but with no stored Server -- the full -// target URL is provided per-call by the caller (typically discovered -// from a callback expression on the parent operation's request body, -// e.g. `{$request.body#/callbackUrl}`). -type CallbackInitiator struct { - // Doer for performing requests, typically a *http.Client with any - // customized settings, such as certificate chains. - Client HttpRequestDoer - - // A list of callbacks for modifying requests which are generated before sending over - // the network. - RequestEditors []RequestEditorFn -} - -// CallbackInitiatorOption allows setting custom parameters during construction. -type CallbackInitiatorOption func(*CallbackInitiator) error - -// NewCallbackInitiator creates a new CallbackInitiator with reasonable defaults. -func NewCallbackInitiator(opts ...CallbackInitiatorOption) (*CallbackInitiator, error) { - initiator := CallbackInitiator{} - for _, o := range opts { - if err := o(&initiator); err != nil { - return nil, err - } - } - if initiator.Client == nil { - initiator.Client = &http.Client{} - } - return &initiator, nil -} - -// WithCallbackHTTPClient allows overriding the default Doer, which is -// automatically created using http.Client. This is useful for tests. -func WithCallbackHTTPClient(doer HttpRequestDoer) CallbackInitiatorOption { - return func(p *CallbackInitiator) error { - p.Client = doer - return nil - } -} - -// WithCallbackRequestEditorFn allows setting up a callback function, which -// will be called right before sending the callback request. This can be -// used to mutate the request, e.g. to add signature headers. -func WithCallbackRequestEditorFn(fn RequestEditorFn) CallbackInitiatorOption { - return func(p *CallbackInitiator) error { - p.RequestEditors = append(p.RequestEditors, fn) - return nil - } -} - -func (p *CallbackInitiator) applyCallbackEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { - for _, r := range p.RequestEditors { - if err := r(ctx, req); err != nil { - return err - } - } - for _, r := range additionalEditors { - if err := r(ctx, req); err != nil { - return err - } - } - return nil -} - -// CallbackInitiatorInterface is the interface specification for the callback initiator. -type CallbackInitiatorInterface interface { -{{range . -}} -{{$hasParams := .RequiresParamObject -}} -{{$opid := .OperationId -}} - // {{$opid}}{{if .HasBody}}WithBody{{end}} fires the {{.CallbackName}} callback{{if .HasBody}} with any body{{end}} - {{$opid}}{{if .HasBody}}WithBody{{end}}(ctx context.Context, targetURL string{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}, reqEditors... RequestEditorFn) (*http.Response, error) -{{range .Bodies}} - {{if .IsSupportedByClient -}} - {{$opid}}{{.Suffix}}(ctx context.Context, targetURL string{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody, reqEditors... RequestEditorFn) (*http.Response, error) - {{end -}} -{{end}}{{/* range .Bodies */}} -{{end}}{{/* range . */}} -} - - -{{/* Generate callback initiator methods */}} -{{range . -}} -{{$hasParams := .RequiresParamObject -}} -{{$opid := .OperationId -}} - -func (p *CallbackInitiator) {{$opid}}{{if .HasBody}}WithBody{{end}}(ctx context.Context, targetURL string{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}, reqEditors... RequestEditorFn) (*http.Response, error) { - req, err := New{{$opid}}CallbackRequest{{if .HasBody}}WithBody{{end}}(targetURL{{if $hasParams}}, params{{end}}{{if .HasBody}}, contentType, body{{end}}) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := p.applyCallbackEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return p.Client.Do(req) -} - -{{range .Bodies}} -{{if .IsSupportedByClient -}} -func (p *CallbackInitiator) {{$opid}}{{.Suffix}}(ctx context.Context, targetURL string{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody, reqEditors... RequestEditorFn) (*http.Response, error) { - req, err := New{{$opid}}CallbackRequest{{.Suffix}}(targetURL{{if $hasParams}}, params{{end}}, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := p.applyCallbackEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return p.Client.Do(req) -} -{{end -}} -{{end}}{{/* range .Bodies */}} -{{end}}{{/* range . */}} - -{{/* Generate callback request builders */}} -{{range . -}} -{{$hasParams := .RequiresParamObject -}} -{{$opid := .OperationId -}} -{{$method := .Method -}} -{{$callbackName := .CallbackName -}} - -{{range .Bodies}} -{{if .IsSupportedByClient -}} -// New{{$opid}}CallbackRequest{{.Suffix}} builds a {{.ContentType}} {{$method}} request for the {{$callbackName}} callback -func New{{$opid}}CallbackRequest{{.Suffix}}(targetURL string{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody) (*http.Request, error) { - var bodyReader io.Reader - {{if .IsJSON -}} - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - {{else if eq .NameTag "Formdata" -}} - bodyStr, err := runtime.MarshalForm(body, nil) - if err != nil { - return nil, err - } - bodyReader = strings.NewReader(bodyStr.Encode()) - {{else if eq .NameTag "Text" -}} - if stringer, ok := interface{}(body).(fmt.Stringer); ok { - bodyReader = strings.NewReader(stringer.String()) - } else { - {{if .Schema.IsPrimitive -}} - bodyReader = strings.NewReader(fmt.Sprint(body)) - {{else -}} - return nil, fmt.Errorf("text/plain is not supported for complex types, define a String() method on {{.Schema.TypeDecl}} to marshal it as text") - {{end -}} - } - {{end -}} - return New{{$opid}}CallbackRequestWithBody(targetURL{{if $hasParams}}, params{{end}}, "{{.ContentType}}", bodyReader) -} -{{end -}} -{{end}} - -// New{{$opid}}CallbackRequest{{if .HasBody}}WithBody{{end}} builds a {{.Method}} request for the {{.CallbackName}} callback{{if .HasBody}} with any body{{end}} -func New{{$opid}}CallbackRequest{{if .HasBody}}WithBody{{end}}(targetURL string{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}) (*http.Request, error) { - var err error - _ = err - - reqURL, err := url.Parse(targetURL) - if err != nil { - return nil, err - } - -{{if .QueryParams}} - if params != nil { - queryValues := reqURL.Query() - var rawQueryFragments []string - {{range $paramIdx, $param := .QueryParams}} - {{if .RequiresNilCheck}} if params.{{.GoName}} != nil { {{end}} - {{if .IsPassThrough}} - queryValues.Add("{{.ParamName}}", {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}) - {{end}} - {{if .IsJson}} - if queryParamBuf, err := json.Marshal({{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}); err != nil { - return nil, err - } else { - queryValues.Add("{{.ParamName}}", string(queryParamBuf)) - } - {{end}} - {{if .IsStyled}} - if queryFrag, err := runtime.StyleParamWithOptions("{{.Style}}", {{.Explode}}, "{{.ParamName}}", {{if and .RequiresNilCheck .HasOptionalPointer}}*{{end}}params.{{.GoName}}, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - {{end}} - {{if .RequiresNilCheck}}}{{end}} - {{end}} - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - reqURL.RawQuery = strings.Join(rawQueryFragments, "&") - } -{{end}}{{/* if .QueryParams */}} - - req, err := http.NewRequest({{.Method | httpMethodConstant}}, reqURL.String(), {{if .HasBody}}body{{else}}nil{{end}}) - if err != nil { - return nil, err - } - - {{if .HasBody}}req.Header.Add("Content-Type", contentType){{end}} -{{ if .HeaderParams }} - if params != nil { - {{range $paramIdx, $param := .HeaderParams}} - {{if .RequiresNilCheck}} if params.{{.GoName}} != nil { {{end}} - var headerParam{{$paramIdx}} string - {{if .IsPassThrough}} - headerParam{{$paramIdx}} = {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}} - {{end}} - {{if .IsJson}} - var headerParamBuf{{$paramIdx}} []byte - headerParamBuf{{$paramIdx}}, err = json.Marshal({{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}) - if err != nil { - return nil, err - } - headerParam{{$paramIdx}} = string(headerParamBuf{{$paramIdx}}) - {{end}} - {{if .IsStyled}} - headerParam{{$paramIdx}}, err = runtime.StyleParamWithOptions("{{.Style}}", {{.Explode}}, "{{.ParamName}}", {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationHeader, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - return nil, err - } - {{end}} - req.Header.Set("{{.ParamName}}", headerParam{{$paramIdx}}) - {{if .RequiresNilCheck}}}{{end}} - {{end}} - } -{{- end }}{{/* if .HeaderParams */}} - return req, nil -} - -{{end}}{{/* Range */}} diff --git a/pkg/codegen/templates/chi/chi-handler.tmpl b/pkg/codegen/templates/chi/chi-handler.tmpl deleted file mode 100644 index cc07375f92..0000000000 --- a/pkg/codegen/templates/chi/chi-handler.tmpl +++ /dev/null @@ -1,50 +0,0 @@ -// Handler creates http.Handler with routing matching OpenAPI spec. -func Handler(si ServerInterface) http.Handler { - return HandlerWithOptions(si, ChiServerOptions{}) -} - -type ChiServerOptions struct { - BaseURL string - BaseRouter chi.Router - Middlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. -func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler { - return HandlerWithOptions(si, ChiServerOptions { - BaseRouter: r, - }) -} - -func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler { - return HandlerWithOptions(si, ChiServerOptions { - BaseURL: baseURL, - BaseRouter: r, - }) -} - -// HandlerWithOptions creates http.Handler with additional options -func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler { -r := options.BaseRouter - -if r == nil { -r = chi.NewRouter() -} -if options.ErrorHandlerFunc == nil { - options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { - http.Error(w, err.Error(), http.StatusBadRequest) - } -} -{{if .}}wrapper := ServerInterfaceWrapper{ -Handler: si, -HandlerMiddlewares: options.Middlewares, -ErrorHandlerFunc: options.ErrorHandlerFunc, -} -{{end}} -{{range .}}r.Group(func(r chi.Router) { -r.{{.Method | lower | title }}(options.BaseURL+{{.Path | swaggerUriToChiUri | toGoString}}, wrapper.{{.HandlerName}}) -}) -{{end}} -return r -} diff --git a/pkg/codegen/templates/chi/chi-interface.tmpl b/pkg/codegen/templates/chi/chi-interface.tmpl deleted file mode 100644 index ba7e5e5fa1..0000000000 --- a/pkg/codegen/templates/chi/chi-interface.tmpl +++ /dev/null @@ -1,21 +0,0 @@ -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{range .}}{{if not .IsAlias}}{{.SummaryAsComment .OperationId }} -// ({{.Method}} {{.Path}}) -{{with .DeprecationComment}}// -{{.}} -{{end}}{{.OperationId}}(w http.ResponseWriter, r *http.Request{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) -{{end}}{{end}} -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. - -type Unimplemented struct {} - {{range .}}{{if not .IsAlias}}{{.SummaryAsComment .OperationId }} - // ({{.Method}} {{.Path}}) -{{with .DeprecationComment}}// -{{.}} -{{end}} func (_ Unimplemented) {{.OperationId}}(w http.ResponseWriter, r *http.Request{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) { - w.WriteHeader(http.StatusNotImplemented) - } - {{end}}{{end}} \ No newline at end of file diff --git a/pkg/codegen/templates/chi/chi-middleware.tmpl b/pkg/codegen/templates/chi/chi-middleware.tmpl deleted file mode 100644 index b5980bb139..0000000000 --- a/pkg/codegen/templates/chi/chi-middleware.tmpl +++ /dev/null @@ -1,272 +0,0 @@ -// ServerInterfaceWrapper converts contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -type MiddlewareFunc func(http.Handler) http.Handler - -{{range .}}{{$opid := .OperationId}} -{{if not .IsAlias}} -// {{$opid}} operation middleware -func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Request) { - {{if or .RequiresParamObject (gt (len .PathParams) 0) }} - var err error - _ = err - {{end}} - - {{range .PathParams}}// ------------- Path parameter "{{.ParamName}}" ------------- - var {{$varName := .GoVariableName}}{{$varName}} {{.TypeDef}} - - {{if .IsPassThrough}} - {{$varName}} = chi.URLParam(r, "{{.ParamName}}") - {{end}} - {{if .IsJson}} - err = json.Unmarshal([]byte(chi.URLParam(r, "{{.ParamName}}")), &{{$varName}}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err}) - return - } - {{end}} - {{if .IsStyled}} - err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", chi.URLParam(r, "{{.ParamName}}"), &{{$varName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}", ValueIsUnescaped: r.URL.RawPath == ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) - return - } - {{end}} - - {{end}} - - {{if and .SecurityDefinitions opts.Compatibility.EnableAuthScopesOnContext -}} - ctx := r.Context() -{{range .SecurityDefinitions}} - ctx = context.WithValue(ctx, {{.ProviderName | sanitizeGoIdentity | ucFirst}}Scopes, {{toStringArray .Scopes}}) -{{end}} - r = r.WithContext(ctx) - {{end}} - - {{if .RequiresParamObject}} - // Parameter object where we will unmarshal all parameters from the context - var params {{.OperationId}}Params - - {{range $paramIdx, $param := .QueryParams}} - {{- if (or (or .Required .IsPassThrough) (or .IsJson .IsStyled)) -}} - // ------------- {{if .Required}}Required{{else}}Optional{{end}} query parameter "{{.ParamName}}" ------------- - {{ end }} - {{ if (or .IsPassThrough .IsJson) }} - if paramValue := r.URL.Query().Get("{{.ParamName}}"); paramValue != "" { - - {{if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue - {{end}} - - {{if .IsJson}} - var value {{.TypeDef}} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err}) - return - } - - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - }{{if .Required}} else { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"}) - return - }{{end}} - {{end}} - {{if .IsStyled}} - err = runtime.BindQueryParameterWithOptions("{{.Style}}", {{.Explode}}, {{.Required}}, "{{.ParamName}}", r.URL.Query(), ¶ms.{{.GoName}}, runtime.BindQueryParameterOptions{Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) - } - return - } - {{end}} - {{end}} - - {{if .HeaderParams}} - headers := r.Header - - {{range .HeaderParams}}// ------------- {{if .Required}}Required{{else}}Optional{{end}} header parameter "{{.ParamName}}" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("{{.ParamName}}")]; found { - var {{.GoName}} {{.TypeDef}} - n := len(valueList) - if n != 1 { - siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "{{.ParamName}}", Count: n}) - return - } - - {{if .IsPassThrough}} - {{.GoName}} = valueList[0] - {{end}} - - {{if .IsJson}} - err = json.Unmarshal([]byte(valueList[0]), &{{.GoName}}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err}) - return - } - {{end}} - - {{if .IsStyled}} - err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", valueList[0], &{{.GoName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) - return - } - {{end}} - - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} - - } {{if .Required}}else { - err := fmt.Errorf("Header parameter {{.ParamName}} is required, but not found") - siw.ErrorHandlerFunc(w, r, &RequiredHeaderError{ParamName: "{{.ParamName}}", Err: err}) - return - }{{end}} - - {{end}} - {{end}} - - {{range .CookieParams}} - { - var cookie *http.Cookie - - if cookie, err = r.Cookie("{{.ParamName}}"); err == nil { - - {{- if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}cookie.Value - {{end}} - - {{- if .IsJson}} - var value {{.TypeDef}} - var decoded string - decoded, err := url.QueryUnescape(cookie.Value) - if err != nil { - err = fmt.Errorf("Error unescaping cookie parameter '{{.ParamName}}'") - siw.ErrorHandlerFunc(w, r, &UnescapedCookieParamError{ParamName: "{{.ParamName}}", Err: err}) - return - } - - err = json.Unmarshal([]byte(decoded), &value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err}) - return - } - - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - - {{- if .IsStyled}} - var value {{.TypeDef}} - err = runtime.BindStyledParameterWithOptions("simple", "{{.ParamName}}", cookie.Value, &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationCookie, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) - return - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - - } - - {{- if .Required}} else { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"}) - return - } - {{- end}} - } - {{end}} - {{end}} - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.{{.OperationId}}(w, r{{genParamNames .PathParams}}{{if .RequiresParamObject}}, params{{end}}) - })) - - {{if opts.Compatibility.ApplyChiMiddlewareFirstToLast}} - for i := len(siw.HandlerMiddlewares) -1; i >= 0; i-- { - handler = siw.HandlerMiddlewares[i](handler) - } - {{else}} - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - {{end}} - - handler.ServeHTTP(w, r) -} -{{end}}{{end}} - -type UnescapedCookieParamError struct { - ParamName string - Err error -} - -func (e *UnescapedCookieParamError) Error() string { - return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) -} - -func (e *UnescapedCookieParamError) Unwrap() error { - return e.Err -} - -type UnmarshalingParamError struct { - ParamName string - Err error -} - -func (e *UnmarshalingParamError) Error() string { - return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) -} - -func (e *UnmarshalingParamError) Unwrap() error { - return e.Err -} - -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -type RequiredHeaderError struct { - ParamName string - Err error -} - -func (e *RequiredHeaderError) Error() string { - return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) -} - -func (e *RequiredHeaderError) Unwrap() error { - return e.Err -} - -type InvalidParamFormatError struct { - ParamName string - Err error -} - -func (e *InvalidParamFormatError) Error() string { - return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) -} - -func (e *InvalidParamFormatError) Unwrap() error { - return e.Err -} - -type TooManyValuesForParamError struct { - ParamName string - Count int -} - -func (e *TooManyValuesForParamError) Error() string { - return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) -} diff --git a/pkg/codegen/templates/chi/hooks.tmpl b/pkg/codegen/templates/chi/hooks.tmpl new file mode 100644 index 0000000000..e8373aca12 --- /dev/null +++ b/pkg/codegen/templates/chi/hooks.tmpl @@ -0,0 +1,37 @@ +{{/* +chi overrides for the shared net/http-family server skeletons. This file is NOT +loaded into the base template tree; it is parsed into a chi-specific clone of the +tree by buildServerTemplates in codegen.go, where these {{define}} blocks replace +the skeletons' {{block}} defaults. +*/}} + +{{/* --- server-middleware.tmpl --- */}} +{{define "middleware.pathParamValue"}}chi.URLParam(r, "{{.ParamName}}"){{end}} +{{define "middleware.valueIsUnescaped"}}r.URL.RawPath == ""{{end}} + +{{/* --- server-handler.tmpl --- */}} +{{/* The {{""}} action makes this override non-empty: text/template ignores a + define whose body is only whitespace/comments and would keep the default. */}} +{{define "handler.serveMuxInterface"}}{{- "" -}}{{end}} +{{define "handler.serverOptions"}}ChiServerOptions{{end}} +{{define "handler.routerType"}}chi.Router{{end}} +{{define "handler.routerVar"}}r{{end}} +{{define "handler.newRouter"}}chi.NewRouter(){{end}} +{{define "handler.register"}}r.Group(func(r chi.Router) { +r.{{.Method | lower | title }}(options.BaseURL+{{.Path | swaggerUriToChiUri | toGoString}}, wrapper.{{.HandlerName}}) +}) +{{end}} + +{{/* --- server-interface.tmpl --- */}} +{{define "interface.unimplemented"}} +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. + +type Unimplemented struct {} + {{range .}}{{if not .IsAlias}}{{.SummaryAsComment .OperationId }} + // ({{.Method}} {{.Path}}) +{{with .DeprecationComment}}// +{{.}} +{{end}} func (_ Unimplemented) {{.OperationId}}(w http.ResponseWriter, r *http.Request{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) { + w.WriteHeader(http.StatusNotImplemented) + } + {{end}}{{end}}{{end}} diff --git a/pkg/codegen/templates/client-partials.tmpl b/pkg/codegen/templates/client-partials.tmpl new file mode 100644 index 0000000000..edcaca1680 --- /dev/null +++ b/pkg/codegen/templates/client-partials.tmpl @@ -0,0 +1,45 @@ +{{/* +Shared client-side request-building partials. These {{define}} blocks are +parsed into the base template tree (see LoadTemplates) and are therefore +available to every template that renders from it -- currently client.tmpl and +initiator.tmpl. Names are globally unique and prefixed to avoid colliding with +other templates in the shared define namespace. + +The header-param serialization block below is byte-identical between the path +Client and the webhook/callback Initiator, so both share it. The query-param +block is NOT shared: the two differ in the URL variable they mutate +(queryURL vs reqURL) and in their explanatory comments, so each keeps its own. + +The partial is invoked with the OperationDefinition as its dot; it emits Go +statements that reference the enclosing builder's `req`, `params`, and `err` +locals, which both call sites declare. +*/ -}} +{{define "client.headerParams" -}} +{{ if .HeaderParams }} + if params != nil { + {{range $paramIdx, $param := .HeaderParams}} + {{if .RequiresNilCheck}} if params.{{.GoName}} != nil { {{end}} + var headerParam{{$paramIdx}} string + {{if .IsPassThrough}} + headerParam{{$paramIdx}} = {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}} + {{end}} + {{if .IsJson}} + var headerParamBuf{{$paramIdx}} []byte + headerParamBuf{{$paramIdx}}, err = json.Marshal({{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}) + if err != nil { + return nil, err + } + headerParam{{$paramIdx}} = string(headerParamBuf{{$paramIdx}}) + {{end}} + {{if .IsStyled}} + headerParam{{$paramIdx}}, err = runtime.StyleParamWithOptions("{{.Style}}", {{.Explode}}, "{{.ParamName}}", {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationHeader, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) + if err != nil { + return nil, err + } + {{end}} + req.Header.Set("{{.ParamName}}", headerParam{{$paramIdx}}) + {{if .RequiresNilCheck}}}{{end}} + {{end}} + } +{{- end }}{{/* if .HeaderParams */}} +{{- end}}{{/* define client.headerParams */}} diff --git a/pkg/codegen/templates/client-with-responses.tmpl b/pkg/codegen/templates/client-with-responses.tmpl index 37fdd83b62..fd250eb938 100644 --- a/pkg/codegen/templates/client-with-responses.tmpl +++ b/pkg/codegen/templates/client-with-responses.tmpl @@ -30,32 +30,12 @@ func WithBaseURL(baseURL string) ClientOption { // ClientWithResponsesInterface is the interface specification for the client with responses above. type ClientWithResponsesInterface interface { {{range . -}} -{{$hasParams := .RequiresParamObject -}} -{{$pathParams := .PathParams -}} {{$opid := .OperationId -}} -{{$parent := . }} -{{$opidSuffix := ""}} -{{- if .HasBody -}} -{{- $opidSuffix = "WithBody" -}} -{{- end -}} -{{$deprecationComment := .DeprecationComment -}} - {{ if .HasBody }} - {{ .GenerateFunctionComment $opid "WithBodyWithResponse" true }} - {{ else }} - {{ .GenerateFunctionComment $opid "WithResponse" true }} - {{ end -}} - {{with $deprecationComment}}// - {{.}} - {{end -}}{{$opid}}{{if .HasBody}}WithBody{{end}}WithResponse(ctx context.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}, reqEditors... RequestEditorFn) (*{{genResponseTypeName $opid}}, error) -{{range .Bodies}} - {{if .IsSupportedByClient -}} - {{ .GenerateFunctionComment $opid $parent (printf "%sWithResponse" .Suffix) true }} - {{with $deprecationComment}}// - {{.}} - {{end -}}{{$opid}}{{.Suffix}}WithResponse(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody, reqEditors... RequestEditorFn) (*{{genResponseTypeName $opid}}, error) - {{end -}} -{{end}}{{/* range .Bodies */}} -{{end}}{{/* range . $opid := .OperationId */}} +{{range .ClientMethodVariants}} + {{.WithResponseInterfaceComment}} + {{$opid}}{{.Suffix}}WithResponse(ctx context.Context{{.ArgsDecl}}, reqEditors... RequestEditorFn) (*{{genResponseTypeName $opid}}, error) +{{end -}} +{{end -}}{{/* range . $opid := .OperationId */}} } {{range .}}{{$opid := .OperationId}}{{$op := .}} @@ -135,46 +115,17 @@ func (r {{genResponseTypeName $opid | ucFirst}}) ContentType() string { {{range .}} {{$opid := .OperationId -}} -{{$parent := . }} -{{$opidSuffix := ""}} -{{- if .HasBody -}} -{{- $opidSuffix = "WithBody" -}} -{{- end -}} -{{$deprecationComment := .DeprecationComment -}} {{/* Generate client methods (with responses)*/}} - -{{ if .HasBody }} - {{ .GenerateFunctionComment $opid "WithBodyWithResponse" true }} -{{ else }} - {{ .GenerateFunctionComment $opid "WithResponse" true }} -{{ end -}} -{{with $deprecationComment}}// -{{.}} -{{end -}}func (c *ClientWithResponses) {{$opid}}{{if .HasBody}}WithBody{{end}}WithResponse(ctx context.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}, reqEditors... RequestEditorFn) (*{{genResponseTypeName $opid}}, error){ - rsp, err := c.{{$opid}}{{if .HasBody}}WithBody{{end}}(ctx{{genParamNames .PathParams}}{{if .RequiresParamObject}}, params{{end}}{{if .HasBody}}, contentType, body{{end}}, reqEditors...) - if err != nil { - return nil, err - } - return Parse{{genResponseTypeName $opid | ucFirst}}(rsp) -} - -{{$hasParams := .RequiresParamObject -}} -{{$pathParams := .PathParams -}} -{{$bodyRequired := .BodyRequired -}} -{{range .Bodies}} -{{if .IsSupportedByClient -}} -{{ .GenerateFunctionComment $opid $parent (printf "%sWithResponse" .Suffix) true }} -{{with $deprecationComment}}{{.}} -{{end -}}func (c *ClientWithResponses) {{$opid}}{{.Suffix}}WithResponse(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody, reqEditors... RequestEditorFn) (*{{genResponseTypeName $opid}}, error) { - rsp, err := c.{{$opid}}{{.Suffix}}(ctx{{genParamNames $pathParams}}{{if $hasParams}}, params{{end}}, body, reqEditors...) +{{range .ClientMethodVariants}} +{{.WithResponseMethodComment}} +func (c *ClientWithResponses) {{$opid}}{{.Suffix}}WithResponse(ctx context.Context{{.ArgsDecl}}, reqEditors... RequestEditorFn) (*{{genResponseTypeName $opid}}, error) { + rsp, err := c.{{$opid}}{{.Suffix}}(ctx{{.CallArgs}}, reqEditors...) if err != nil { return nil, err } return Parse{{genResponseTypeName $opid | ucFirst}}(rsp) } -{{end}} -{{end}} - +{{end -}}{{/* range .ClientMethodVariants */}} {{end}}{{/* operations */}} {{/* Generate parse functions for responses*/}} diff --git a/pkg/codegen/templates/client.tmpl b/pkg/codegen/templates/client.tmpl index 55f298a4cb..1714087beb 100644 --- a/pkg/codegen/templates/client.tmpl +++ b/pkg/codegen/templates/client.tmpl @@ -74,71 +74,22 @@ func WithRequestEditorFn(fn RequestEditorFn) ClientOption { // The interface specification for the client above. type ClientInterface interface { {{range . -}} -{{$hasParams := .RequiresParamObject -}} -{{$pathParams := .PathParams -}} {{$opid := .OperationId -}} -{{$opidSuffix := ""}} -{{$parent := . }} -{{- if .HasBody -}} -{{- $opidSuffix = "WithBody" -}} -{{- end -}} -{{$deprecationComment := .DeprecationComment -}} - {{ if .HasBody }} - {{ .GenerateFunctionComment $opid "WithBody" false }} - {{ else -}} - {{ .GenerateFunctionComment $opid "" false }} - {{ end -}} - {{with $deprecationComment}}// - {{.}} - {{end -}}{{$opid}}{{if .HasBody}}WithBody{{end}}(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}, reqEditors... RequestEditorFn) (*http.Response, error) -{{range .Bodies}} - {{if .IsSupportedByClient -}} - {{ .GenerateFunctionComment $opid $parent .Suffix false }} - {{with $deprecationComment}}// - {{.}} - {{end -}}{{$opid}}{{.Suffix}}(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody, reqEditors... RequestEditorFn) (*http.Response, error) - {{end -}} -{{end}}{{/* range .Bodies */}} -{{end}}{{/* range . $opid := .OperationId */}} +{{range .ClientMethodVariants}} + {{.InterfaceComment}} + {{$opid}}{{.Suffix}}(ctx context.Context{{.ArgsDecl}}, reqEditors... RequestEditorFn) (*http.Response, error) +{{end -}} +{{end -}}{{/* range . $opid := .OperationId */}} } {{/* Generate client methods */}} {{range . -}} -{{$hasParams := .RequiresParamObject -}} -{{$pathParams := .PathParams -}} {{$opid := .OperationId -}} -{{$parent := . }} -{{$opidSuffix := ""}} -{{- if .HasBody -}} -{{- $opidSuffix = "WithBody" -}} -{{- end -}} - -{{ if .HasBody }} - {{ .GenerateFunctionComment $opid "WithBody" false }} -{{ else }} - {{ .GenerateFunctionComment $opid "" false }} -{{ end -}} -{{with .DeprecationComment}}{{.}} -{{end -}}func (c *{{ $clientTypeName }}) {{$opid}}{{if .HasBody}}WithBody{{end}}(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}, reqEditors... RequestEditorFn) (*http.Response, error) { - req, err := New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(c.Server{{genParamNames .PathParams}}{{if $hasParams}}, params{{end}}{{if .HasBody}}, contentType, body{{end}}) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -{{$deprecationComment := .DeprecationComment -}} -{{range .Bodies}} -{{if .IsSupportedByClient -}} -{{ .GenerateFunctionComment $opid $parent .Suffix false }} -{{with $deprecationComment}}{{.}} -{{end -}}func (c *{{ $clientTypeName }}) {{$opid}}{{.Suffix}}(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody, reqEditors... RequestEditorFn) (*http.Response, error) { - req, err := New{{$opid}}Request{{.Suffix}}(c.Server{{genParamNames $pathParams}}{{if $hasParams}}, params{{end}}, body) +{{range .ClientMethodVariants}} +{{.MethodComment}} +func (c *{{ $clientTypeName }}) {{$opid}}{{.Suffix}}(ctx context.Context{{.ArgsDecl}}, reqEditors... RequestEditorFn) (*http.Response, error) { + req, err := New{{$opid}}Request{{.Suffix}}(c.Server{{.CallArgs}}) if err != nil { return nil, err } @@ -148,8 +99,7 @@ type ClientInterface interface { } return c.Client.Do(req) } -{{end -}}{{/* if .IsSupported */}} -{{end}}{{/* range .Bodies */}} +{{end -}}{{/* range .ClientMethodVariants */}} {{end}} {{/* Generate request builders */}} @@ -158,6 +108,7 @@ type ClientInterface interface { {{$pathParams := .PathParams -}} {{$bodyRequired := .BodyRequired -}} {{$opid := .OperationId -}} +{{$generic := .GenericClientVariant -}} {{range .Bodies}} {{if .IsSupportedByClient -}} @@ -192,8 +143,8 @@ func New{{$opid}}Request{{.Suffix}}(server string{{genParamArgs $pathParams}}{{i {{end -}} {{end}} -// New{{$opid}}Request{{if .HasBody}}WithBody{{end}} constructs an http.Request for the {{$opid}} method{{if .HasBody}}, with any body, and a specified content type{{end}} -func New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(server string{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}) (*http.Request, error) { +// New{{$opid}}Request{{$generic.Suffix}} constructs an http.Request for the {{$opid}} method{{if .HasBody}}, with any body, and a specified content type{{end}} +func New{{$opid}}Request{{$generic.Suffix}}(server string{{$generic.ArgsDecl}}) (*http.Request, error) { var err error {{range $paramIdx, $param := .PathParams}} var pathParam{{$paramIdx}} string @@ -275,33 +226,7 @@ func New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(server string{{genParamAr } {{if .HasBody}}req.Header.Add("Content-Type", contentType){{end}} -{{ if .HeaderParams }} - if params != nil { - {{range $paramIdx, $param := .HeaderParams}} - {{if .RequiresNilCheck}} if params.{{.GoName}} != nil { {{end}} - var headerParam{{$paramIdx}} string - {{if .IsPassThrough}} - headerParam{{$paramIdx}} = {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}} - {{end}} - {{if .IsJson}} - var headerParamBuf{{$paramIdx}} []byte - headerParamBuf{{$paramIdx}}, err = json.Marshal({{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}) - if err != nil { - return nil, err - } - headerParam{{$paramIdx}} = string(headerParamBuf{{$paramIdx}}) - {{end}} - {{if .IsStyled}} - headerParam{{$paramIdx}}, err = runtime.StyleParamWithOptions("{{.Style}}", {{.Explode}}, "{{.ParamName}}", {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationHeader, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - return nil, err - } - {{end}} - req.Header.Set("{{.ParamName}}", headerParam{{$paramIdx}}) - {{if .RequiresNilCheck}}}{{end}} - {{end}} - } -{{- end }}{{/* if .HeaderParams */}} +{{template "client.headerParams" .}} {{ if .CookieParams }} if params != nil { diff --git a/pkg/codegen/templates/echo/echo-interface.tmpl b/pkg/codegen/templates/echo/echo-interface.tmpl deleted file mode 100644 index f84da65a11..0000000000 --- a/pkg/codegen/templates/echo/echo-interface.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{range .}}{{if not .IsAlias}}{{.SummaryAsComment .OperationId }} -// ({{.Method}} {{.Path}}) -{{with .DeprecationComment}}// -{{.}} -{{end}}{{.OperationId}}(ctx echo.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error -{{end}}{{end}} -} diff --git a/pkg/codegen/templates/echo/echo-receiver.tmpl b/pkg/codegen/templates/echo/echo-receiver.tmpl index a90d03f834..003e158b38 100644 --- a/pkg/codegen/templates/echo/echo-receiver.tmpl +++ b/pkg/codegen/templates/echo/echo-receiver.tmpl @@ -7,7 +7,7 @@ type {{.Prefix}}ReceiverInterface interface { {{range .Operations -}} {{.SummaryAsComment ""}} // Handle{{.OperationId}}{{$.Prefix}} handles the {{.Method}} {{$.PrefixLower}} for {{.SourceName}}. -Handle{{.OperationId}}{{$.Prefix}}(ctx echo.Context{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error +Handle{{.OperationId}}{{$.Prefix}}(ctx {{$.CtxType}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error {{end}} } @@ -20,7 +20,7 @@ Handle{{.OperationId}}{{$.Prefix}}(ctx echo.Context{{if .RequiresParamObject}}, // error-handling chain reports them as 400. Middlewares are applied in // the order provided -- the last argument becomes the outermost wrapper. func {{$opid}}{{$.Prefix}}Handler(si {{$.Prefix}}ReceiverInterface, middlewares ...echo.MiddlewareFunc) echo.HandlerFunc { - h := echo.HandlerFunc(func(ctx echo.Context) error { + h := echo.HandlerFunc(func(ctx {{$.CtxType}}) error { {{- if .RequiresParamObject}} var err error _ = err diff --git a/pkg/codegen/templates/echo/echo-register.tmpl b/pkg/codegen/templates/echo/echo-register.tmpl index d161fca37d..c7070729a4 100644 --- a/pkg/codegen/templates/echo/echo-register.tmpl +++ b/pkg/codegen/templates/echo/echo-register.tmpl @@ -4,15 +4,15 @@ // are present on both echo.Echo and echo.Group, since we want to allow using // either of them for path registration type EchoRouter interface { - CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) {{.RouteReturnType}} + DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) {{.RouteReturnType}} + GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) {{.RouteReturnType}} + HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) {{.RouteReturnType}} + OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) {{.RouteReturnType}} + PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) {{.RouteReturnType}} + POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) {{.RouteReturnType}} + PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) {{.RouteReturnType}} + TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) {{.RouteReturnType}} } // RegisterHandlersOptions configures RegisterHandlersWithOptions. @@ -42,11 +42,11 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // RegisterHandlersWithOptions registers handlers using the supplied options, // including any per-operation middleware. func RegisterHandlersWithOptions(router EchoRouter, si ServerInterface, options RegisterHandlersOptions) { -{{if .}} +{{if .Operations}} wrapper := ServerInterfaceWrapper{ Handler: si, } {{end}} -{{range .}}router.{{.Method}}(options.BaseURL + {{.Path | swaggerUriToEchoUri | toGoString}}, wrapper.{{.HandlerName}}, options.OperationMiddlewares["{{.MiddlewareKey}}"]...) +{{range .Operations}}router.{{.Method}}(options.BaseURL + {{.Path | swaggerUriToEchoUri | toGoString}}, wrapper.{{.HandlerName}}, options.OperationMiddlewares["{{.MiddlewareKey}}"]...) {{end}} } diff --git a/pkg/codegen/templates/echo/echo-wrappers.tmpl b/pkg/codegen/templates/echo/echo-wrappers.tmpl index 263dca089e..530c3415dc 100644 --- a/pkg/codegen/templates/echo/echo-wrappers.tmpl +++ b/pkg/codegen/templates/echo/echo-wrappers.tmpl @@ -4,7 +4,7 @@ type ServerInterfaceWrapper struct { } {{range .}}{{$opid := .OperationId}}{{if not .IsAlias}}// {{$opid}} converts echo context to params. -func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx echo.Context) error { +func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx {{block "echo.ctxType" .}}echo.Context{{end}}) error { var err error {{range .PathParams}}// ------------- Path parameter "{{.ParamName}}" ------------- var {{$varName := .GoVariableName}}{{$varName}} {{.TypeDef}} diff --git a/pkg/codegen/templates/echo/hooks.tmpl b/pkg/codegen/templates/echo/hooks.tmpl new file mode 100644 index 0000000000..d56e17a888 --- /dev/null +++ b/pkg/codegen/templates/echo/hooks.tmpl @@ -0,0 +1,15 @@ +{{/* +echo (v4) overrides for the shared server skeletons. This file is NOT loaded +into the base template tree; it is parsed into an echo-specific clone of the tree +by buildServerTemplates in codegen.go, where these {{define}} blocks replace the +skeletons' {{block}} defaults. + +echo's wrapper (echo/echo-wrappers.tmpl) and route registration +(echo/echo-register.tmpl) are echo-specific templates that carry the echo v4 +context type (echo.Context) as their default; only the shared +server-interface.tmpl skeleton needs an override here. The echo v5 clone +(templates/echo/v5/hooks.tmpl) additionally overrides wrapper.ctxType. +*/}} + +{{/* --- server-interface.tmpl --- */}} +{{define "interface.handlerSignature"}}(ctx echo.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error{{end}} diff --git a/pkg/codegen/templates/echo/v5/echo-interface.tmpl b/pkg/codegen/templates/echo/v5/echo-interface.tmpl deleted file mode 100644 index d0997e5c66..0000000000 --- a/pkg/codegen/templates/echo/v5/echo-interface.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{range .}}{{if not .IsAlias}}{{.SummaryAsComment .OperationId }} -// ({{.Method}} {{.Path}}) -{{with .DeprecationComment}}// -{{.}} -{{end}}{{.OperationId}}(ctx *echo.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error -{{end}}{{end}} -} diff --git a/pkg/codegen/templates/echo/v5/echo-receiver.tmpl b/pkg/codegen/templates/echo/v5/echo-receiver.tmpl deleted file mode 100644 index b0f10d5b1a..0000000000 --- a/pkg/codegen/templates/echo/v5/echo-receiver.tmpl +++ /dev/null @@ -1,93 +0,0 @@ -// {{.Prefix}}ReceiverInterface represents handlers for receiving inbound -// {{.PrefixLower}} requests. Each {{.PrefixLower}} becomes a Handle*{{.Prefix}} -// method that the implementation fills in. The caller mounts the per- -// {{.PrefixLower}} echo.HandlerFunc returned by {Op}{{.Prefix}}Handler at -// whatever URL path they advertise to senders. -type {{.Prefix}}ReceiverInterface interface { -{{range .Operations -}} -{{.SummaryAsComment ""}} -// Handle{{.OperationId}}{{$.Prefix}} handles the {{.Method}} {{$.PrefixLower}} for {{.SourceName}}. -Handle{{.OperationId}}{{$.Prefix}}(ctx *echo.Context{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error -{{end}} -} - -{{range .Operations -}} -{{$opid := .OperationId -}} -{{$srcName := .SourceName -}} -// {{$opid}}{{$.Prefix}}Handler returns the echo.HandlerFunc for the {{$srcName}} {{$.PrefixLower}}. -// Mount this at the URL path advertised to {{$.PrefixLower}} senders. Errors -// during parameter binding are returned via echo.NewHTTPError so echo's -// error-handling chain reports them as 400. Middlewares are applied in -// the order provided -- the last argument becomes the outermost wrapper. -func {{$opid}}{{$.Prefix}}Handler(si {{$.Prefix}}ReceiverInterface, middlewares ...echo.MiddlewareFunc) echo.HandlerFunc { - h := echo.HandlerFunc(func(ctx *echo.Context) error { -{{- if .RequiresParamObject}} - var err error - _ = err - - // Parameter object where we will unmarshal all parameters from the context. - var params {{$opid}}Params -{{range $paramIdx, $param := .QueryParams}} - // ------------- {{if .Required}}Required{{else}}Optional{{end}} query parameter "{{.ParamName}}" ------------- - {{if .IsStyled}} - err = runtime.BindQueryParameterWithOptions("{{.Style}}", {{.Explode}}, {{.Required}}, "{{.ParamName}}", ctx.QueryParams(), ¶ms.{{.GoName}}, runtime.BindQueryParameterOptions{Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) - } - {{else}} - if paramValue := ctx.QueryParam("{{.ParamName}}"); paramValue != "" { - {{if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue - {{end}} - {{if .IsJson}} - var value {{.TypeDef}} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - }{{if .Required}} else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{.ParamName}} is required, but not found")) - }{{end}} - {{end}} -{{end}} -{{range $paramIdx, $param := .HeaderParams}} - // ------------- {{if .Required}}Required{{else}}Optional{{end}} header parameter "{{.ParamName}}" ------------- - if valueList, found := ctx.Request().Header[http.CanonicalHeaderKey("{{.ParamName}}")]; found { - var {{.GoName}} {{.TypeDef}} - n := len(valueList) - if n != 1 { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for {{.ParamName}}, got %d", n)) - } - {{if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}valueList[0] - {{end}} - {{if .IsJson}} - err = json.Unmarshal([]byte(valueList[0]), &{{.GoName}}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} - {{end}} - {{if .IsStyled}} - err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", valueList[0], &{{.GoName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} - {{end}} - }{{if .Required}} else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter {{.ParamName}} is required, but not found")) - }{{end}} -{{end}} -{{- end}} - return si.Handle{{$opid}}{{$.Prefix}}(ctx{{if .RequiresParamObject}}, params{{end}}) - }) - for _, mw := range middlewares { - h = mw(h) - } - return h -} - -{{end}} diff --git a/pkg/codegen/templates/echo/v5/echo-register.tmpl b/pkg/codegen/templates/echo/v5/echo-register.tmpl deleted file mode 100644 index 95ccf32316..0000000000 --- a/pkg/codegen/templates/echo/v5/echo-register.tmpl +++ /dev/null @@ -1,52 +0,0 @@ - - -// This is a simple interface which specifies echo.Route addition functions which -// are present on both echo.Echo and echo.Group, since we want to allow using -// either of them for path registration -type EchoRouter interface { - CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo - DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo - GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo - HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo - OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo - PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo - POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo - PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo - TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo -} - -// RegisterHandlersOptions configures RegisterHandlersWithOptions. -type RegisterHandlersOptions struct { - // BaseURL is prepended to every registered path so the API can be served - // under a prefix. - BaseURL string - // OperationMiddlewares lets the caller attach per-operation middleware at - // registration time. The map key is the OpenAPI `operationId` value as it - // appears in the spec (the raw, un-normalized form). Operations that have - // no entry are registered with no extra middleware. A nil map disables - // per-operation middleware entirely. - OperationMiddlewares map[string][]echo.MiddlewareFunc -} - -// RegisterHandlers adds each server route to the EchoRouter. -func RegisterHandlers(router EchoRouter, si ServerInterface) { - RegisterHandlersWithOptions(router, si, RegisterHandlersOptions{}) -} - -// RegisterHandlersWithBaseURL registers handlers and prepends BaseURL to the -// paths so the API can be served under a prefix. -func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { - RegisterHandlersWithOptions(router, si, RegisterHandlersOptions{BaseURL: baseURL}) -} - -// RegisterHandlersWithOptions registers handlers using the supplied options, -// including any per-operation middleware. -func RegisterHandlersWithOptions(router EchoRouter, si ServerInterface, options RegisterHandlersOptions) { -{{if .}} - wrapper := ServerInterfaceWrapper{ - Handler: si, - } -{{end}} -{{range .}}router.{{.Method}}(options.BaseURL + {{.Path | swaggerUriToEchoUri | toGoString}}, wrapper.{{.HandlerName}}, options.OperationMiddlewares["{{.MiddlewareKey}}"]...) -{{end}} -} diff --git a/pkg/codegen/templates/echo/v5/echo-wrappers.tmpl b/pkg/codegen/templates/echo/v5/echo-wrappers.tmpl deleted file mode 100644 index 3d690b3847..0000000000 --- a/pkg/codegen/templates/echo/v5/echo-wrappers.tmpl +++ /dev/null @@ -1,133 +0,0 @@ -// ServerInterfaceWrapper converts echo contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface -} - -{{range .}}{{$opid := .OperationId}}{{if not .IsAlias}}// {{$opid}} converts echo context to params. -func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx *echo.Context) error { - var err error -{{range .PathParams}}// ------------- Path parameter "{{.ParamName}}" ------------- - var {{$varName := .GoVariableName}}{{$varName}} {{.TypeDef}} -{{if .IsPassThrough}} - {{$varName}} = ctx.Param("{{.ParamName}}") -{{end}} -{{if .IsJson}} - err = json.Unmarshal([]byte(ctx.Param("{{.ParamName}}")), &{{$varName}}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") - } -{{end}} -{{if .IsStyled}} - err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", ctx.Param("{{.ParamName}}"), &{{$varName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}", ValueIsUnescaped: ctx.Request().URL.RawPath == ""}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) - } -{{end}} -{{end}} - -{{if opts.Compatibility.EnableAuthScopesOnContext -}} -{{range .SecurityDefinitions}} - ctx.Set(string({{.ProviderName | sanitizeGoIdentity | ucFirst}}Scopes), {{toStringArray .Scopes}}) -{{end}} -{{- end}} - -{{if .RequiresParamObject}} - // Parameter object where we will unmarshal all parameters from the context - var params {{.OperationId}}Params -{{range $paramIdx, $param := .QueryParams}} - {{- if (or (or .Required .IsPassThrough) (or .IsJson .IsStyled)) -}} - // ------------- {{if .Required}}Required{{else}}Optional{{end}} query parameter "{{.ParamName}}" ------------- - {{ end }} - {{if .IsStyled}} - err = runtime.BindQueryParameterWithOptions("{{.Style}}", {{.Explode}}, {{.Required}}, "{{.ParamName}}", ctx.QueryParams(), ¶ms.{{.GoName}}, runtime.BindQueryParameterOptions{Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) - } - {{else}} - if paramValue := ctx.QueryParam("{{.ParamName}}"); paramValue != "" { - {{if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue - {{end}} - {{if .IsJson}} - var value {{.TypeDef}} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - }{{if .Required}} else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{.ParamName}} is required, but not found")) - }{{end}} - {{end}} -{{end}} - -{{if .HeaderParams}} - headers := ctx.Request().Header -{{range .HeaderParams}}// ------------- {{if .Required}}Required{{else}}Optional{{end}} header parameter "{{.ParamName}}" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("{{.ParamName}}")]; found { - var {{.GoName}} {{.TypeDef}} - n := len(valueList) - if n != 1 { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for {{.ParamName}}, got %d", n)) - } -{{if .IsPassThrough}} - {{.GoName}} = valueList[0] -{{end}} -{{if .IsJson}} - err = json.Unmarshal([]byte(valueList[0]), &{{.GoName}}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") - } -{{end}} -{{if .IsStyled}} - err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", valueList[0], &{{.GoName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) - } -{{end}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} - } {{if .Required}}else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter {{.ParamName}} is required, but not found")) - }{{end}} -{{end}} -{{end}} - -{{range .CookieParams}} - if cookie, err := ctx.Cookie("{{.ParamName}}"); err == nil { - {{if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}cookie.Value - {{end}} - {{if .IsJson}} - var value {{.TypeDef}} - var decoded string - decoded, err := url.QueryUnescape(cookie.Value) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Error unescaping cookie parameter '{{.ParamName}}'") - } - err = json.Unmarshal([]byte(decoded), &value) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - {{if .IsStyled}} - var value {{.TypeDef}} - err = runtime.BindStyledParameterWithOptions("simple", "{{.ParamName}}", cookie.Value, &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationCookie, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - }{{if .Required}} else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{.ParamName}} is required, but not found")) - }{{end}} - -{{end}}{{/* .CookieParams */}} - -{{end}}{{/* .RequiresParamObject */}} - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.{{.OperationId}}(ctx{{genParamNames .PathParams}}{{if .RequiresParamObject}}, params{{end}}) - return err -} -{{end}}{{end}} diff --git a/pkg/codegen/templates/echo/v5/hooks.tmpl b/pkg/codegen/templates/echo/v5/hooks.tmpl new file mode 100644 index 0000000000..7811f9e4ad --- /dev/null +++ b/pkg/codegen/templates/echo/v5/hooks.tmpl @@ -0,0 +1,23 @@ +{{/* +echo v5 overrides for the shared server skeletons. echo v5 differs from v4 in +exactly one token: the context type is *echo.Context (pointer) instead of +echo.Context. That difference shows up in the ServerInterface method signature +and in the wrapper's per-operation func signature, so this clone overrides both +interface.handlerSignature and wrapper.ctxType. Everything else is inherited +from the shared echo templates (echo/echo-wrappers.tmpl, echo/echo-register.tmpl). +*/}} + +{{/* --- server-interface.tmpl --- */}} +{{define "interface.handlerSignature"}}(ctx *echo.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error{{end}} + +{{/* --- echo/echo-wrappers.tmpl --- */}} +{{define "echo.ctxType"}}*echo.Context{{end}} + +{{/* --- strict/strict-echo.tmpl --- +echo v5 renames the body-only bind helpers: the *echo.DefaultBinder assertion +no longer needs a named receiver (the bind goes through the package-level +echo.BindBody), and form values come from ctx.FormValues rather than +ctx.FormParams. */}} +{{define "strict.echo.binderVar"}}_{{end}} +{{define "strict.echo.bindBodyCall"}}echo.BindBody{{end}} +{{define "strict.echo.formValues"}}FormValues{{end}} diff --git a/pkg/codegen/templates/fiber-v3/fiber-handler.tmpl b/pkg/codegen/templates/fiber-handler.tmpl similarity index 100% rename from pkg/codegen/templates/fiber-v3/fiber-handler.tmpl rename to pkg/codegen/templates/fiber-handler.tmpl diff --git a/pkg/codegen/templates/fiber-v3/fiber-interface.tmpl b/pkg/codegen/templates/fiber-v3/fiber-interface.tmpl deleted file mode 100644 index 466e98e523..0000000000 --- a/pkg/codegen/templates/fiber-v3/fiber-interface.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{range .}}{{if not .IsAlias}}{{.SummaryAsComment .OperationId }} -// ({{.Method}} {{.Path}}) -{{with .DeprecationComment}}// -{{.}} -{{end}}{{.OperationId}}(c fiber.Ctx{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error -{{end}}{{end}} -} diff --git a/pkg/codegen/templates/fiber-v3/fiber-middleware.tmpl b/pkg/codegen/templates/fiber-v3/fiber-middleware.tmpl deleted file mode 100644 index 042a15802b..0000000000 --- a/pkg/codegen/templates/fiber-v3/fiber-middleware.tmpl +++ /dev/null @@ -1,198 +0,0 @@ -// ServerInterfaceWrapper converts contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []HandlerMiddlewareFunc -} - -type MiddlewareFunc fiber.Handler -type HandlerMiddlewareFunc func(c fiber.Ctx, next fiber.Handler) error - -{{range .}}{{$opid := .OperationId}} -{{if not .IsAlias}} -// {{$opid}} operation middleware -func (siw *ServerInterfaceWrapper) {{$opid}}(c fiber.Ctx) error { - - {{if or .RequiresParamObject (gt (len .PathParams) 0) }} - var err error - _ = err - {{end}} - - {{range .PathParams}}// ------------- Path parameter "{{.ParamName}}" ------------- - var {{$varName := .GoVariableName}}{{$varName}} {{.TypeDef}} - - {{if .IsPassThrough}} - {{$varName}}, err = url.PathUnescape(c.Params("{{.ParamName}}")) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Error unescaping path parameter '{{.ParamName}}': %w", err).Error()) - } - {{end}} - {{if .IsJson}} - { - paramValue, decErr := url.PathUnescape(c.Params("{{.ParamName}}")) - if decErr != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Error unescaping path parameter '{{.ParamName}}': %w", decErr).Error()) - } - err = json.Unmarshal([]byte(paramValue), &{{$varName}}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Error unmarshaling parameter '{{.ParamName}}' as JSON: %w", err).Error()) - } - } - {{end}} - {{if .IsStyled}} - err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", c.Params("{{.ParamName}}"), &{{$varName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter {{.ParamName}}: %w", err).Error()) - } - {{end}} - - {{end}} - -{{if opts.Compatibility.EnableAuthScopesOnContext -}} -{{range .SecurityDefinitions}} - c.RequestCtx().SetUserValue(({{.ProviderName | sanitizeGoIdentity | ucFirst}}Scopes), {{toStringArray .Scopes}}) -{{end}} -{{- end}} - - {{if .RequiresParamObject}} - // Parameter object where we will unmarshal all parameters from the context - var params {{.OperationId}}Params - - {{if .QueryParams}} - var query url.Values - query, err = url.ParseQuery(string(c.Request().URI().QueryString())) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for query string: %w", err).Error()) - } - {{end}} - - {{range $paramIdx, $param := .QueryParams}} - {{- if (or (or .Required .IsPassThrough) (or .IsJson .IsStyled)) -}} - // ------------- {{if .Required}}Required{{else}}Optional{{end}} query parameter "{{.ParamName}}" ------------- - {{ end }} - {{ if (or .IsPassThrough .IsJson) }} - if paramValue := c.Query("{{.ParamName}}"); paramValue != "" { - - {{if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue - {{end}} - - {{if .IsJson}} - var value {{.TypeDef}} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Error unmarshaling parameter '{{.ParamName}}' as JSON: %w", err).Error()) - } - - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - }{{if .Required}} else { - return fiber.NewError(fiber.StatusBadRequest, "Query argument {{.ParamName}} is required, but not found") - }{{end}} - {{end}} - {{if .IsStyled}} - err = runtime.BindQueryParameterWithOptions("{{.Style}}", {{.Explode}}, {{.Required}}, "{{.ParamName}}", query, ¶ms.{{.GoName}}, runtime.BindQueryParameterOptions{Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter {{.ParamName}}: %w", err).Error()) - } - {{end}} - {{end}} - - {{if .HeaderParams}} - headers := c.GetReqHeaders() - - {{range .HeaderParams}}// ------------- {{if .Required}}Required{{else}}Optional{{end}} header parameter "{{.ParamName}}" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("{{.ParamName}}")]; found { - var {{.GoName}} {{.TypeDef}} - n := len(valueList) - if n != 1 { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Too many values for ParamName {{.ParamName}}, 1 is required, but %d found", n)) - } - - {{if .IsPassThrough}} - {{.GoName}} = valueList[0] - {{end}} - - {{if .IsJson}} - err = json.Unmarshal([]byte(valueList[0]), &{{.GoName}}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Error unmarshaling parameter '{{.ParamName}}' as JSON: %w", err).Error()) - } - {{end}} - - {{if .IsStyled}} - err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", valueList[0], &{{.GoName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter {{.ParamName}}: %w", err).Error()) - } - {{end}} - - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} - - } {{if .Required}}else { - return fiber.NewError(fiber.StatusBadRequest, "Header parameter {{.ParamName}} is required, but not found") - }{{end}} - - {{end}} - {{end}} - - {{range .CookieParams}} - { - cookie := c.Cookies("{{.ParamName}}") - - if cookie != "" { - - {{- if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}cookie - {{end}} - - {{- if .IsJson}} - var value {{.TypeDef}} - var decoded string - decoded, err := url.QueryUnescape(cookie) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Error unescaping cookie parameter '{{.ParamName}}': %w", err).Error()) - } - - err = json.Unmarshal([]byte(decoded), &value) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Error unmarshaling parameter '{{.ParamName}}' as JSON: %w", err).Error()) - } - - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - - {{- if .IsStyled}} - var value {{.TypeDef}} - err = runtime.BindStyledParameterWithOptions("simple", "{{.ParamName}}", cookie, &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationCookie, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter {{.ParamName}}: %w", err).Error()) - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - - } - - {{- if .Required}} else { - err = fmt.Errorf("Query argument {{.ParamName}} is required, but not found") - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - {{- end}} - } - {{end}} - {{end}} - - handler := func(c fiber.Ctx) error { - return siw.Handler.{{.OperationId}}(c{{genParamNames .PathParams}}{{if .RequiresParamObject}}, params{{end}}) - } - - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } - - return handler(c) -} -{{end}}{{end}} diff --git a/pkg/codegen/templates/fiber-v3/fiber-receiver.tmpl b/pkg/codegen/templates/fiber-v3/fiber-receiver.tmpl deleted file mode 100644 index f28d2bdf42..0000000000 --- a/pkg/codegen/templates/fiber-v3/fiber-receiver.tmpl +++ /dev/null @@ -1,100 +0,0 @@ -// {{.Prefix}}ReceiverInterface represents handlers for receiving inbound -// {{.PrefixLower}} requests. Each {{.PrefixLower}} becomes a Handle*{{.Prefix}} -// method that the implementation fills in. The caller mounts the per- -// {{.PrefixLower}} fiber.Handler returned by {Op}{{.Prefix}}Handler at -// whatever URL path they advertise to senders. -type {{.Prefix}}ReceiverInterface interface { -{{range .Operations -}} -{{.SummaryAsComment ""}} -// Handle{{.OperationId}}{{$.Prefix}} handles the {{.Method}} {{$.PrefixLower}} for {{.SourceName}}. -Handle{{.OperationId}}{{$.Prefix}}(c fiber.Ctx{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error -{{end}} -} - -{{range .Operations -}} -{{$opid := .OperationId -}} -{{$srcName := .SourceName -}} -// {{$opid}}{{$.Prefix}}Handler returns the fiber.Handler for the {{$srcName}} {{$.PrefixLower}}. -// Mount this at the URL path advertised to {{$.PrefixLower}} senders. Errors -// during parameter binding are returned via fiber.NewError so fiber's -// error chain reports them as 400. Per-handler middleware is not -// generated here; use fiber.App.Use() for engine-level middleware. -func {{$opid}}{{$.Prefix}}Handler(si {{$.Prefix}}ReceiverInterface) fiber.Handler { - return func(c fiber.Ctx) error { -{{- if .RequiresParamObject}} - var err error - _ = err - - // Parameter object where we will unmarshal all parameters from the context. - var params {{$opid}}Params -{{if .QueryParams}} - var query url.Values - query, err = url.ParseQuery(string(c.Request().URI().QueryString())) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for query string: %w", err).Error()) - } -{{end}} -{{range $paramIdx, $param := .QueryParams}} - // ------------- {{if .Required}}Required{{else}}Optional{{end}} query parameter "{{.ParamName}}" ------------- - {{if .IsStyled}} - err = runtime.BindQueryParameterWithOptions("{{.Style}}", {{.Explode}}, {{.Required}}, "{{.ParamName}}", query, ¶ms.{{.GoName}}, runtime.BindQueryParameterOptions{Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter {{.ParamName}}: %w", err).Error()) - } - {{else}} - if paramValue := c.Query("{{.ParamName}}"); paramValue != "" { - {{if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue - {{end}} - {{if .IsJson}} - var value {{.TypeDef}} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Error unmarshaling parameter '{{.ParamName}}' as JSON: %w", err).Error()) - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - }{{if .Required}} else { - return fiber.NewError(fiber.StatusBadRequest, "Query argument {{.ParamName}} is required, but not found") - }{{end}} - {{end}} -{{end}} -{{if .HeaderParams}} - headers := c.GetReqHeaders() -{{range $paramIdx, $param := .HeaderParams}} - // ------------- {{if .Required}}Required{{else}}Optional{{end}} header parameter "{{.ParamName}}" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("{{.ParamName}}")]; found { - var {{.GoName}} {{.TypeDef}} - n := len(valueList) - if n != 1 { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Too many values for header {{.ParamName}}, 1 is required, but %d found", n)) - } - {{if .IsPassThrough}} - {{.GoName}} = valueList[0] - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} - {{end}} - {{if .IsJson}} - err = json.Unmarshal([]byte(valueList[0]), &{{.GoName}}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Error unmarshaling parameter '{{.ParamName}}' as JSON: %w", err).Error()) - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} - {{end}} - {{if .IsStyled}} - err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", valueList[0], &{{.GoName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter {{.ParamName}}: %w", err).Error()) - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} - {{end}} - }{{if .Required}} else { - return fiber.NewError(fiber.StatusBadRequest, "Header parameter {{.ParamName}} is required, but not found") - }{{end}} -{{end}} -{{end}} -{{- end}} - return si.Handle{{$opid}}{{$.Prefix}}(c{{if .RequiresParamObject}}, params{{end}}) - } -} - -{{end}} diff --git a/pkg/codegen/templates/fiber-v3/hooks.tmpl b/pkg/codegen/templates/fiber-v3/hooks.tmpl new file mode 100644 index 0000000000..1ac789f1c9 --- /dev/null +++ b/pkg/codegen/templates/fiber-v3/hooks.tmpl @@ -0,0 +1,25 @@ +{{/* +fiber v3 overrides for the shared server skeletons. fiber v3 differs from v2 in +two tokens: the context is passed by value (fiber.Ctx) rather than by pointer +(*fiber.Ctx), and the request-context accessor is c.RequestCtx() rather than +c.Context(). Those show up in the ServerInterface method signature and the +wrapper's func signatures / SetUserValue call, so this clone overrides +interface.handlerSignature, fiber.ctxType and fiber.ctxAccessor. Everything +else is inherited from the shared fiber templates (fiber/fiber-middleware.tmpl, +fiber-handler.tmpl). +*/}} + +{{/* --- server-interface.tmpl --- */}} +{{define "interface.handlerSignature"}}(c fiber.Ctx{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error{{end}} + +{{/* --- fiber/fiber-middleware.tmpl --- */}} +{{define "fiber.ctxType"}}fiber.Ctx{{end}} +{{define "fiber.ctxAccessor"}}RequestCtx{{end}} + +{{/* --- strict/strict-fiber.tmpl --- +fiber v3 binds request bodies through ctx.Bind().Body rather than v2's +ctx.BodyParser, and the request context comes from ctx.Context() rather than +ctx.UserContext(). Everything else in the strict glue (including the optional-body +EOF / len(data) guards) is shared with the v2 shape. */}} +{{define "strict.fiber.bindBody"}}ctx.Bind().Body(&body){{end}} +{{define "strict.fiber.reqContext"}}Context{{end}} diff --git a/pkg/codegen/templates/fiber/fiber-handler.tmpl b/pkg/codegen/templates/fiber/fiber-handler.tmpl deleted file mode 100644 index 8460db83c1..0000000000 --- a/pkg/codegen/templates/fiber/fiber-handler.tmpl +++ /dev/null @@ -1,27 +0,0 @@ -// FiberServerOptions provides options for the Fiber server. -type FiberServerOptions struct { - BaseURL string - Middlewares []MiddlewareFunc - HandlerMiddlewares []HandlerMiddlewareFunc -} - -// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. -func RegisterHandlers(router fiber.Router, si ServerInterface) { - RegisterHandlersWithOptions(router, si, FiberServerOptions{}) -} - -// RegisterHandlersWithOptions creates http.Handler with additional options -func RegisterHandlersWithOptions(router fiber.Router, si ServerInterface, options FiberServerOptions) { -{{if .}}wrapper := ServerInterfaceWrapper{ -Handler: si, -HandlerMiddlewares: options.HandlerMiddlewares, -} - -for _, m := range options.Middlewares { - router.Use(fiber.Handler(m)) -} -{{end}} -{{range .}} -router.{{.Method | lower | title }}(options.BaseURL+{{.Path | swaggerUriToFiberUri | toGoString}}, wrapper.{{.HandlerName}}) -{{end}} -} diff --git a/pkg/codegen/templates/fiber/fiber-interface.tmpl b/pkg/codegen/templates/fiber/fiber-interface.tmpl deleted file mode 100644 index 94c90c0864..0000000000 --- a/pkg/codegen/templates/fiber/fiber-interface.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{range .}}{{if not .IsAlias}}{{.SummaryAsComment .OperationId }} -// ({{.Method}} {{.Path}}) -{{with .DeprecationComment}}// -{{.}} -{{end}}{{.OperationId}}(c *fiber.Ctx{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error -{{end}}{{end}} -} diff --git a/pkg/codegen/templates/fiber/fiber-middleware.tmpl b/pkg/codegen/templates/fiber/fiber-middleware.tmpl index 9766d074fc..35125ce27e 100644 --- a/pkg/codegen/templates/fiber/fiber-middleware.tmpl +++ b/pkg/codegen/templates/fiber/fiber-middleware.tmpl @@ -5,12 +5,12 @@ type ServerInterfaceWrapper struct { } type MiddlewareFunc fiber.Handler -type HandlerMiddlewareFunc func(c *fiber.Ctx, next fiber.Handler) error +type HandlerMiddlewareFunc func(c {{block "fiber.ctxType" .}}*fiber.Ctx{{end}}, next fiber.Handler) error {{range .}}{{$opid := .OperationId}} {{if not .IsAlias}} // {{$opid}} operation middleware -func (siw *ServerInterfaceWrapper) {{$opid}}(c *fiber.Ctx) error { +func (siw *ServerInterfaceWrapper) {{$opid}}(c {{template "fiber.ctxType" .}}) error { {{if or .RequiresParamObject (gt (len .PathParams) 0) }} var err error @@ -49,7 +49,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *fiber.Ctx) error { {{if opts.Compatibility.EnableAuthScopesOnContext -}} {{range .SecurityDefinitions}} - c.Context().SetUserValue(({{.ProviderName | sanitizeGoIdentity | ucFirst}}Scopes), {{toStringArray .Scopes}}) + c.{{block "fiber.ctxAccessor" .}}Context{{end}}().SetUserValue(({{.ProviderName | sanitizeGoIdentity | ucFirst}}Scopes), {{toStringArray .Scopes}}) {{end}} {{- end}} @@ -181,14 +181,14 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *fiber.Ctx) error { {{end}} {{end}} - handler := func(c *fiber.Ctx) error { + handler := func(c {{template "fiber.ctxType" .}}) error { return siw.Handler.{{.OperationId}}(c{{genParamNames .PathParams}}{{if .RequiresParamObject}}, params{{end}}) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { m := siw.HandlerMiddlewares[i] next := handler - handler = func(c *fiber.Ctx) error { + handler = func(c {{template "fiber.ctxType" .}}) error { return m(c, next) } } diff --git a/pkg/codegen/templates/fiber/fiber-receiver.tmpl b/pkg/codegen/templates/fiber/fiber-receiver.tmpl index ea0b4e6470..a41dced7ec 100644 --- a/pkg/codegen/templates/fiber/fiber-receiver.tmpl +++ b/pkg/codegen/templates/fiber/fiber-receiver.tmpl @@ -7,7 +7,7 @@ type {{.Prefix}}ReceiverInterface interface { {{range .Operations -}} {{.SummaryAsComment ""}} // Handle{{.OperationId}}{{$.Prefix}} handles the {{.Method}} {{$.PrefixLower}} for {{.SourceName}}. -Handle{{.OperationId}}{{$.Prefix}}(c *fiber.Ctx{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error +Handle{{.OperationId}}{{$.Prefix}}(c {{$.CtxType}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error {{end}} } @@ -20,7 +20,7 @@ Handle{{.OperationId}}{{$.Prefix}}(c *fiber.Ctx{{if .RequiresParamObject}}, para // error chain reports them as 400. Per-handler middleware is not // generated here; use fiber.App.Use() for engine-level middleware. func {{$opid}}{{$.Prefix}}Handler(si {{$.Prefix}}ReceiverInterface) fiber.Handler { - return func(c *fiber.Ctx) error { + return func(c {{$.CtxType}}) error { {{- if .RequiresParamObject}} var err error _ = err @@ -70,7 +70,8 @@ func {{$opid}}{{$.Prefix}}Handler(si {{$.Prefix}}ReceiverInterface) fiber.Handle return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Too many values for header {{.ParamName}}, 1 is required, but %d found", n)) } {{if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}valueList[0] + {{.GoName}} = valueList[0] + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} {{end}} {{if .IsJson}} err = json.Unmarshal([]byte(valueList[0]), &{{.GoName}}) diff --git a/pkg/codegen/templates/fiber/hooks.tmpl b/pkg/codegen/templates/fiber/hooks.tmpl new file mode 100644 index 0000000000..b54e4a2289 --- /dev/null +++ b/pkg/codegen/templates/fiber/hooks.tmpl @@ -0,0 +1,15 @@ +{{/* +fiber (v2) overrides for the shared server skeletons. This file is NOT loaded +into the base template tree; it is parsed into a fiber-specific clone of the tree +by buildServerTemplates in codegen.go, where these {{define}} blocks replace the +skeletons' {{block}} defaults. + +The shared fiber wrapper (fiber/fiber-middleware.tmpl) carries the fiber v2 +context type (*fiber.Ctx) and accessor (Context) as its wrapper.ctxType / +wrapper.ctxAccessor defaults, so v2 only needs the interface signature override +here. The fiber v3 clone (templates/fiber-v3/hooks.tmpl) additionally overrides +those two blocks. +*/}} + +{{/* --- server-interface.tmpl --- */}} +{{define "interface.handlerSignature"}}(c *fiber.Ctx{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error{{end}} diff --git a/pkg/codegen/templates/gin/gin-interface.tmpl b/pkg/codegen/templates/gin/gin-interface.tmpl deleted file mode 100644 index 6a8a878f28..0000000000 --- a/pkg/codegen/templates/gin/gin-interface.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{range .}}{{if not .IsAlias}}{{.SummaryAsComment .OperationId }} -// ({{.Method}} {{.Path}}) -{{with .DeprecationComment}}// -{{.}} -{{end}}{{.OperationId}}(c *gin.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) -{{end}}{{end}} -} diff --git a/pkg/codegen/templates/gin/hooks.tmpl b/pkg/codegen/templates/gin/hooks.tmpl new file mode 100644 index 0000000000..cbbb86acdc --- /dev/null +++ b/pkg/codegen/templates/gin/hooks.tmpl @@ -0,0 +1,15 @@ +{{/* +gin overrides for the shared server skeletons. This file is NOT loaded into the +base template tree; it is parsed into a gin-specific clone of the tree by +buildServerTemplates in codegen.go, where these {{define}} blocks replace the +skeletons' {{block}} defaults. + +Only the ServerInterface handler signature is shared with the skeleton; gin's +wrapper (gin/gin-wrappers.tmpl) and route registration (gin/gin-register.tmpl) +keep their own gin-specific shape (the 3-arg ErrorHandler and the c.IsAborted() +middleware loop), which does not reduce to a handful of hooks over the net/http +skeleton. +*/}} + +{{/* --- server-interface.tmpl --- */}} +{{define "interface.handlerSignature"}}(c *gin.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}){{end}} diff --git a/pkg/codegen/templates/gorilla/gorilla-interface.tmpl b/pkg/codegen/templates/gorilla/gorilla-interface.tmpl deleted file mode 100644 index d3936346dc..0000000000 --- a/pkg/codegen/templates/gorilla/gorilla-interface.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{range .}}{{if not .IsAlias}}{{.SummaryAsComment .OperationId }} -// ({{.Method}} {{.Path}}) -{{with .DeprecationComment}}// -{{.}} -{{end}}{{.OperationId}}(w http.ResponseWriter, r *http.Request{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) -{{end}}{{end}} -} diff --git a/pkg/codegen/templates/gorilla/gorilla-middleware.tmpl b/pkg/codegen/templates/gorilla/gorilla-middleware.tmpl deleted file mode 100644 index 8b5f54054e..0000000000 --- a/pkg/codegen/templates/gorilla/gorilla-middleware.tmpl +++ /dev/null @@ -1,272 +0,0 @@ -// ServerInterfaceWrapper converts contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -type MiddlewareFunc func(http.Handler) http.Handler - -{{range .}}{{$opid := .OperationId}} -{{if not .IsAlias}} -// {{$opid}} operation middleware -func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Request) { - {{if or .RequiresParamObject (gt (len .PathParams) 0) }} - var err error - _ = err - {{end}} - - {{range .PathParams}}// ------------- Path parameter "{{.ParamName}}" ------------- - var {{$varName := .GoVariableName}}{{$varName}} {{.TypeDef}} - - {{if .IsPassThrough}} - {{$varName}} = mux.Vars(r)["{{.ParamName}}"] - {{end}} - {{if .IsJson}} - err = json.Unmarshal([]byte(mux.Vars(r)["{{.ParamName}}"]), &{{$varName}}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err}) - return - } - {{end}} - {{if .IsStyled}} - err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", mux.Vars(r)["{{.ParamName}}"], &{{$varName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}", ValueIsUnescaped: true}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) - return - } - {{end}} - - {{end}} - - {{if and .SecurityDefinitions opts.Compatibility.EnableAuthScopesOnContext -}} - ctx := r.Context() -{{range .SecurityDefinitions}} - ctx = context.WithValue(ctx, {{.ProviderName | sanitizeGoIdentity | ucFirst}}Scopes, {{toStringArray .Scopes}}) -{{end}} - r = r.WithContext(ctx) - {{end}} - - {{if .RequiresParamObject}} - // Parameter object where we will unmarshal all parameters from the context - var params {{.OperationId}}Params - - {{range $paramIdx, $param := .QueryParams}} - {{- if (or (or .Required .IsPassThrough) (or .IsJson .IsStyled)) -}} - // ------------- {{if .Required}}Required{{else}}Optional{{end}} query parameter "{{.ParamName}}" ------------- - {{ end }} - {{ if (or .IsPassThrough .IsJson) }} - if paramValue := r.URL.Query().Get("{{.ParamName}}"); paramValue != "" { - - {{if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue - {{end}} - - {{if .IsJson}} - var value {{.TypeDef}} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err}) - return - } - - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - }{{if .Required}} else { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"}) - return - }{{end}} - {{end}} - {{if .IsStyled}} - err = runtime.BindQueryParameterWithOptions("{{.Style}}", {{.Explode}}, {{.Required}}, "{{.ParamName}}", r.URL.Query(), ¶ms.{{.GoName}}, runtime.BindQueryParameterOptions{Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) - } - return - } - {{end}} - {{end}} - - {{if .HeaderParams}} - headers := r.Header - - {{range .HeaderParams}}// ------------- {{if .Required}}Required{{else}}Optional{{end}} header parameter "{{.ParamName}}" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("{{.ParamName}}")]; found { - var {{.GoName}} {{.TypeDef}} - n := len(valueList) - if n != 1 { - siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "{{.ParamName}}", Count: n}) - return - } - - {{if .IsPassThrough}} - {{.GoName}} = valueList[0] - {{end}} - - {{if .IsJson}} - err = json.Unmarshal([]byte(valueList[0]), &{{.GoName}}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err}) - return - } - {{end}} - - {{if .IsStyled}} - err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", valueList[0], &{{.GoName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) - return - } - {{end}} - - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} - - } {{if .Required}}else { - err = fmt.Errorf("Header parameter {{.ParamName}} is required, but not found") - siw.ErrorHandlerFunc(w, r, &RequiredHeaderError{ParamName: "{{.ParamName}}", Err: err}) - return - }{{end}} - - {{end}} - {{end}} - - {{range .CookieParams}} - { - var cookie *http.Cookie - - if cookie, err = r.Cookie("{{.ParamName}}"); err == nil { - - {{- if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}cookie.Value - {{end}} - - {{- if .IsJson}} - var value {{.TypeDef}} - var decoded string - decoded, err := url.QueryUnescape(cookie.Value) - if err != nil { - err = fmt.Errorf("Error unescaping cookie parameter '{{.ParamName}}'") - siw.ErrorHandlerFunc(w, r, &UnescapedCookieParamError{ParamName: "{{.ParamName}}", Err: err}) - return - } - - err = json.Unmarshal([]byte(decoded), &value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err}) - return - } - - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - - {{- if .IsStyled}} - var value {{.TypeDef}} - err = runtime.BindStyledParameterWithOptions("simple", "{{.ParamName}}", cookie.Value, &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationCookie, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) - return - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - - } - - {{- if .Required}} else { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"}) - return - } - {{- end}} - } - {{end}} - {{end}} - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.{{.OperationId}}(w, r{{genParamNames .PathParams}}{{if .RequiresParamObject}}, params{{end}}) - })) - - {{if opts.Compatibility.ApplyGorillaMiddlewareFirstToLast}} - for i := len(siw.HandlerMiddlewares) -1; i >= 0; i-- { - handler = siw.HandlerMiddlewares[i](handler) - } - {{else}} - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - {{end}} - - handler.ServeHTTP(w, r) -} -{{end}}{{end}} - -type UnescapedCookieParamError struct { - ParamName string - Err error -} - -func (e *UnescapedCookieParamError) Error() string { - return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) -} - -func (e *UnescapedCookieParamError) Unwrap() error { - return e.Err -} - -type UnmarshalingParamError struct { - ParamName string - Err error -} - -func (e *UnmarshalingParamError) Error() string { - return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) -} - -func (e *UnmarshalingParamError) Unwrap() error { - return e.Err -} - -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -type RequiredHeaderError struct { - ParamName string - Err error -} - -func (e *RequiredHeaderError) Error() string { - return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) -} - -func (e *RequiredHeaderError) Unwrap() error { - return e.Err -} - -type InvalidParamFormatError struct { - ParamName string - Err error -} - -func (e *InvalidParamFormatError) Error() string { - return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) -} - -func (e *InvalidParamFormatError) Unwrap() error { - return e.Err -} - -type TooManyValuesForParamError struct { - ParamName string - Count int -} - -func (e *TooManyValuesForParamError) Error() string { - return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) -} diff --git a/pkg/codegen/templates/gorilla/gorilla-receiver.tmpl b/pkg/codegen/templates/gorilla/gorilla-receiver.tmpl deleted file mode 100644 index 3089faea4e..0000000000 --- a/pkg/codegen/templates/gorilla/gorilla-receiver.tmpl +++ /dev/null @@ -1,116 +0,0 @@ -// {{.Prefix}}ReceiverInterface represents handlers for receiving inbound -// {{.PrefixLower}} requests. Each {{.PrefixLower}} becomes a Handle*{{.Prefix}} -// method that the implementation fills in. The caller mounts the per- -// {{.PrefixLower}} http.Handler returned by {Op}{{.Prefix}}Handler at -// whatever URL path they advertise to senders. -type {{.Prefix}}ReceiverInterface interface { -{{range .Operations -}} -{{.SummaryAsComment ""}} -// Handle{{.OperationId}}{{$.Prefix}} handles the {{.Method}} {{$.PrefixLower}} for {{.SourceName}}. -Handle{{.OperationId}}{{$.Prefix}}(w http.ResponseWriter, r *http.Request{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) -{{end}} -} - -// {{.Prefix}}ReceiverMiddlewareFunc wraps an http.Handler with cross- -// cutting behavior (signature verification, logging, rate limiting, ...). -type {{.Prefix}}ReceiverMiddlewareFunc func(http.Handler) http.Handler - -{{range .Operations -}} -{{$opid := .OperationId -}} -{{$srcName := .SourceName -}} -// {{$opid}}{{$.Prefix}}Handler returns the http.Handler for the {{$srcName}} {{$.PrefixLower}}. -// Mount this at the URL path advertised to {{$.PrefixLower}} senders. errHandler -// may be nil; if so, parameter-binding errors return 400 with the error -// message. Middlewares are applied in the order provided -- the last -// argument becomes the outermost wrapper. -func {{$opid}}{{$.Prefix}}Handler(si {{$.Prefix}}ReceiverInterface, errHandler func(w http.ResponseWriter, r *http.Request, err error), middlewares ...{{$.Prefix}}ReceiverMiddlewareFunc) http.Handler { - if errHandler == nil { - errHandler = func(w http.ResponseWriter, r *http.Request, err error) { - http.Error(w, err.Error(), http.StatusBadRequest) - } - } - var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -{{- if .RequiresParamObject}} - var err error - _ = err - - // Parameter object where we will unmarshal all parameters from the request. - var params {{$opid}}Params -{{range $paramIdx, $param := .QueryParams}} - // ------------- {{if .Required}}Required{{else}}Optional{{end}} query parameter "{{.ParamName}}" ------------- - {{if (or .IsPassThrough .IsJson)}} - if paramValue := r.URL.Query().Get("{{.ParamName}}"); paramValue != "" { - {{if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue - {{end}} - {{if .IsJson}} - var value {{.TypeDef}} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - errHandler(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err}) - return - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - }{{if .Required}} else { - errHandler(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"}) - return - }{{end}} - {{end}} - {{if .IsStyled}} - err = runtime.BindQueryParameterWithOptions("{{.Style}}", {{.Explode}}, {{.Required}}, "{{.ParamName}}", r.URL.Query(), ¶ms.{{.GoName}}, runtime.BindQueryParameterOptions{Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - errHandler(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"}) - } else { - errHandler(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) - } - return - } - {{end}} -{{end}} -{{range $paramIdx, $param := .HeaderParams}} - // ------------- {{if .Required}}Required{{else}}Optional{{end}} header parameter "{{.ParamName}}" ------------- - if valueList, found := r.Header[http.CanonicalHeaderKey("{{.ParamName}}")]; found { - var {{.GoName}} {{.TypeDef}} - n := len(valueList) - if n != 1 { - errHandler(w, r, &TooManyValuesForParamError{ParamName: "{{.ParamName}}", Count: n}) - return - } - {{if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}valueList[0] - {{end}} - {{if .IsJson}} - err = json.Unmarshal([]byte(valueList[0]), &{{.GoName}}) - if err != nil { - errHandler(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err}) - return - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} - {{end}} - {{if .IsStyled}} - err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", valueList[0], &{{.GoName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - errHandler(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) - return - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} - {{end}} - }{{if .Required}} else { - err := fmt.Errorf("Header parameter {{.ParamName}} is required, but not found") - errHandler(w, r, &RequiredHeaderError{ParamName: "{{.ParamName}}", Err: err}) - return - }{{end}} -{{end}} -{{- end}} - si.Handle{{$opid}}{{$.Prefix}}(w, r{{if .RequiresParamObject}}, params{{end}}) - }) - for _, mw := range middlewares { - h = mw(h) - } - return h -} - -{{end}} diff --git a/pkg/codegen/templates/gorilla/gorilla-register.tmpl b/pkg/codegen/templates/gorilla/gorilla-register.tmpl deleted file mode 100644 index 853ab32c35..0000000000 --- a/pkg/codegen/templates/gorilla/gorilla-register.tmpl +++ /dev/null @@ -1,49 +0,0 @@ -// Handler creates http.Handler with routing matching OpenAPI spec. -func Handler(si ServerInterface) http.Handler { - return HandlerWithOptions(si, GorillaServerOptions{}) -} - -type GorillaServerOptions struct { - BaseURL string - BaseRouter *mux.Router - Middlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. -func HandlerFromMux(si ServerInterface, r *mux.Router) http.Handler { - return HandlerWithOptions(si, GorillaServerOptions { - BaseRouter: r, - }) -} - -func HandlerFromMuxWithBaseURL(si ServerInterface, r *mux.Router, baseURL string) http.Handler { - return HandlerWithOptions(si, GorillaServerOptions { - BaseURL: baseURL, - BaseRouter: r, - }) -} - -// HandlerWithOptions creates http.Handler with additional options -func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.Handler { -r := options.BaseRouter - -if r == nil { -r = mux.NewRouter() -} -if options.ErrorHandlerFunc == nil { - options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { - http.Error(w, err.Error(), http.StatusBadRequest) - } -} -{{if .}}wrapper := ServerInterfaceWrapper{ -Handler: si, -HandlerMiddlewares: options.Middlewares, -ErrorHandlerFunc: options.ErrorHandlerFunc, -} -{{end}} -{{range .}} -r.HandleFunc(options.BaseURL+{{.Path | swaggerUriToGorillaUri | toGoString}}, wrapper.{{.HandlerName}}).Methods({{.Method | httpMethodConstant}}) -{{end}} -return r -} diff --git a/pkg/codegen/templates/gorilla/hooks.tmpl b/pkg/codegen/templates/gorilla/hooks.tmpl new file mode 100644 index 0000000000..500a1d859e --- /dev/null +++ b/pkg/codegen/templates/gorilla/hooks.tmpl @@ -0,0 +1,34 @@ +{{/* +gorilla overrides for the shared net/http-family server skeletons. This file is +NOT loaded into the base template tree; it is parsed into a gorilla-specific clone +of the tree by buildServerTemplates in codegen.go, where these {{define}} blocks +replace the skeletons' {{block}} defaults. +*/}} + +{{/* --- server-middleware.tmpl --- */}} +{{define "middleware.pathParamValue"}}mux.Vars(r)["{{.ParamName}}"]{{end}} +{{/* +gorilla selects middleware order with its own compatibility flag; the stdhttp +default uses ApplyChiMiddlewareFirstToLast. Only the flag name differs. +*/}} +{{define "middleware.applyMiddlewares"}}{{if opts.Compatibility.ApplyGorillaMiddlewareFirstToLast}} + for i := len(siw.HandlerMiddlewares) -1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + {{else}} + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + {{end}}{{end}} + +{{/* --- server-handler.tmpl --- */}} +{{/* The {{""}} action makes this override non-empty: text/template ignores a + define whose body is only whitespace/comments and would keep the default. */}} +{{define "handler.serveMuxInterface"}}{{- "" -}}{{end}} +{{define "handler.serverOptions"}}GorillaServerOptions{{end}} +{{define "handler.routerType"}}*mux.Router{{end}} +{{define "handler.routerVar"}}r{{end}} +{{define "handler.newRouter"}}mux.NewRouter(){{end}} +{{define "handler.register"}} +r.HandleFunc(options.BaseURL+{{.Path | swaggerUriToGorillaUri | toGoString}}, wrapper.{{.HandlerName}}).Methods({{.Method | httpMethodConstant}}) +{{end}} diff --git a/pkg/codegen/templates/initiator.tmpl b/pkg/codegen/templates/initiator.tmpl new file mode 100644 index 0000000000..43a129eeea --- /dev/null +++ b/pkg/codegen/templates/initiator.tmpl @@ -0,0 +1,202 @@ +{{if eq .Prefix "Webhook" -}} +// WebhookInitiator sends OpenAPI 3.1 webhook requests to target URLs. +// Modeled on the generated Client, but with no stored Server -- the full +// target URL is provided per-call by the caller (typically discovered +// from a subscription registration). +{{- else -}} +// CallbackInitiator sends OpenAPI callback requests to target URLs. +// Modeled on the generated Client, but with no stored Server -- the full +// target URL is provided per-call by the caller (typically discovered +// from a callback expression on the parent operation's request body, +// e.g. `{$request.body#/callbackUrl}`). +{{- end}} +type {{.Prefix}}Initiator struct { + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// {{.Prefix}}InitiatorOption allows setting custom parameters during construction. +type {{.Prefix}}InitiatorOption func(*{{.Prefix}}Initiator) error + +// New{{.Prefix}}Initiator creates a new {{.Prefix}}Initiator with reasonable defaults. +func New{{.Prefix}}Initiator(opts ...{{.Prefix}}InitiatorOption) (*{{.Prefix}}Initiator, error) { + initiator := {{.Prefix}}Initiator{} + for _, o := range opts { + if err := o(&initiator); err != nil { + return nil, err + } + } + if initiator.Client == nil { + initiator.Client = &http.Client{} + } + return &initiator, nil +} + +// With{{.Prefix}}HTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func With{{.Prefix}}HTTPClient(doer HttpRequestDoer) {{.Prefix}}InitiatorOption { + return func(p *{{.Prefix}}Initiator) error { + p.Client = doer + return nil + } +} + +// With{{.Prefix}}RequestEditorFn allows setting up a callback function, which +// will be called right before sending the {{.PrefixLower}} request. This can be +// used to mutate the request, e.g. to add signature headers. +func With{{.Prefix}}RequestEditorFn(fn RequestEditorFn) {{.Prefix}}InitiatorOption { + return func(p *{{.Prefix}}Initiator) error { + p.RequestEditors = append(p.RequestEditors, fn) + return nil + } +} + +func (p *{{.Prefix}}Initiator) apply{{.Prefix}}Editors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range p.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// {{.Prefix}}InitiatorInterface is the interface specification for the {{.PrefixLower}} initiator. +type {{.Prefix}}InitiatorInterface interface { +{{range .Operations -}} +{{$op := . -}} +{{$opid := .OperationId -}} +{{range $i, $v := .ClientMethodVariants -}} +{{if eq $i 0 -}} + // {{$opid}}{{$v.Suffix}} fires the {{$op.SourceName}} {{$.PrefixLower}}{{if $op.HasBody}} with any body{{end}} +{{else}} +{{end -}} + {{$opid}}{{$v.Suffix}}(ctx context.Context, targetURL string{{$v.ArgsDecl}}, reqEditors... RequestEditorFn) (*http.Response, error) +{{end -}}{{/* range .ClientMethodVariants */}} +{{end}}{{/* range .Operations */}} +} + + +{{/* Generate initiator methods */}} +{{range .Operations -}} +{{$opid := .OperationId -}} +{{range .ClientMethodVariants}} +func (p *{{$.Prefix}}Initiator) {{$opid}}{{.Suffix}}(ctx context.Context, targetURL string{{.ArgsDecl}}, reqEditors... RequestEditorFn) (*http.Response, error) { + req, err := New{{$opid}}{{$.Prefix}}Request{{.Suffix}}(targetURL{{.CallArgs}}) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := p.apply{{$.Prefix}}Editors(ctx, req, reqEditors); err != nil { + return nil, err + } + return p.Client.Do(req) +} +{{end -}}{{/* range .ClientMethodVariants */}} +{{end}}{{/* range .Operations */}} + +{{/* Generate request builders */}} +{{range .Operations -}} +{{$hasParams := .RequiresParamObject -}} +{{$opid := .OperationId -}} +{{$method := .Method -}} +{{$srcName := .SourceName -}} +{{$generic := .GenericClientVariant -}} + +{{range .Bodies}} +{{if .IsSupportedByClient -}} +// New{{$opid}}{{$.Prefix}}Request{{.Suffix}} builds a {{.ContentType}} {{$method}} request for the {{$srcName}} {{$.PrefixLower}} +func New{{$opid}}{{$.Prefix}}Request{{.Suffix}}(targetURL string{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody) (*http.Request, error) { + var bodyReader io.Reader + {{if .IsJSON -}} + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + {{else if eq .NameTag "Formdata" -}} + bodyStr, err := runtime.MarshalForm(body, nil) + if err != nil { + return nil, err + } + bodyReader = strings.NewReader(bodyStr.Encode()) + {{else if eq .NameTag "Text" -}} + if stringer, ok := interface{}(body).(fmt.Stringer); ok { + bodyReader = strings.NewReader(stringer.String()) + } else { + {{if .Schema.IsPrimitive -}} + bodyReader = strings.NewReader(fmt.Sprint(body)) + {{else -}} + return nil, fmt.Errorf("text/plain is not supported for complex types, define a String() method on {{.Schema.TypeDecl}} to marshal it as text") + {{end -}} + } + {{end -}} + return New{{$opid}}{{$.Prefix}}RequestWithBody(targetURL{{if $hasParams}}, params{{end}}, "{{.ContentType}}", bodyReader) +} +{{end -}} +{{end}} + +// New{{$opid}}{{$.Prefix}}Request{{$generic.Suffix}} builds a {{.Method}} request for the {{.SourceName}} {{$.PrefixLower}}{{if .HasBody}} with any body{{end}} +func New{{$opid}}{{$.Prefix}}Request{{$generic.Suffix}}(targetURL string{{$generic.ArgsDecl}}) (*http.Request, error) { + var err error + _ = err + + reqURL, err := url.Parse(targetURL) + if err != nil { + return nil, err + } + +{{if .QueryParams}} + if params != nil { + queryValues := reqURL.Query() + var rawQueryFragments []string + {{range $paramIdx, $param := .QueryParams}} + {{if .RequiresNilCheck}} if params.{{.GoName}} != nil { {{end}} + {{if .IsPassThrough}} + queryValues.Add("{{.ParamName}}", {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}) + {{end}} + {{if .IsJson}} + if queryParamBuf, err := json.Marshal({{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}); err != nil { + return nil, err + } else { + queryValues.Add("{{.ParamName}}", string(queryParamBuf)) + } + {{end}} + {{if .IsStyled}} + if queryFrag, err := runtime.StyleParamWithOptions("{{.Style}}", {{.Explode}}, "{{.ParamName}}", {{if and .RequiresNilCheck .HasOptionalPointer}}*{{end}}params.{{.GoName}}, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + {{end}} + {{if .RequiresNilCheck}}}{{end}} + {{end}} + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + reqURL.RawQuery = strings.Join(rawQueryFragments, "&") + } +{{end}}{{/* if .QueryParams */}} + + req, err := http.NewRequest({{.Method | httpMethodConstant}}, reqURL.String(), {{if .HasBody}}body{{else}}nil{{end}}) + if err != nil { + return nil, err + } + + {{if .HasBody}}req.Header.Add("Content-Type", contentType){{end}} +{{template "client.headerParams" .}} + return req, nil +} + +{{end}}{{/* range .Operations */}} diff --git a/pkg/codegen/templates/iris/hooks.tmpl b/pkg/codegen/templates/iris/hooks.tmpl new file mode 100644 index 0000000000..387e62dc74 --- /dev/null +++ b/pkg/codegen/templates/iris/hooks.tmpl @@ -0,0 +1,15 @@ +{{/* +iris overrides for the shared server skeletons. This file is NOT loaded into the +base template tree; it is parsed into an iris-specific clone of the tree by +buildServerTemplates in codegen.go, where these {{define}} blocks replace the +skeletons' {{block}} defaults. + +Only the ServerInterface handler signature is shared with the skeleton; iris's +wrapper (iris/iris-middleware.tmpl) and route registration (iris/iris-handler.tmpl) +keep their own iris-specific shape (direct ctx.StatusCode error writing, no +ErrorHandlerFunc / custom error types, and the trailing router.Build()), which +does not reduce to a handful of hooks over the net/http skeleton. +*/}} + +{{/* --- server-interface.tmpl --- */}} +{{define "interface.handlerSignature"}}(ctx iris.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}){{end}} diff --git a/pkg/codegen/templates/iris/iris-interface.tmpl b/pkg/codegen/templates/iris/iris-interface.tmpl deleted file mode 100644 index f9f9497325..0000000000 --- a/pkg/codegen/templates/iris/iris-interface.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{range .}}{{if not .IsAlias}}{{.SummaryAsComment .OperationId }} -// ({{.Method}} {{.Path}}) -{{with .DeprecationComment}}// -{{.}} -{{end}}{{.OperationId}}(ctx iris.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) -{{end}}{{end}} -} diff --git a/pkg/codegen/templates/chi/chi-receiver.tmpl b/pkg/codegen/templates/receiver-stdlib.tmpl similarity index 100% rename from pkg/codegen/templates/chi/chi-receiver.tmpl rename to pkg/codegen/templates/receiver-stdlib.tmpl diff --git a/pkg/codegen/templates/server-handler.tmpl b/pkg/codegen/templates/server-handler.tmpl new file mode 100644 index 0000000000..062c3c50a5 --- /dev/null +++ b/pkg/codegen/templates/server-handler.tmpl @@ -0,0 +1,69 @@ +{{/* +Shared handler/registration skeleton for the net/http-family server frameworks +(chi, gorilla, stdhttp). The {{block}} defaults are the stdhttp shape; chi and +gorilla override the hooks via templates//hooks.tmpl. See +server-middleware.tmpl for a description of the layering. + +Hooks: + handler.serveMuxInterface - optional interface decl (stdhttp only; empty elsewhere) + handler.serverOptions - name of the ServerOptions struct type + handler.routerType - Go type of the router/mux + handler.routerVar - local variable / receiver name for the router + handler.newRouter - expression constructing a default router + handler.register - per-operation route registration statement +*/}} +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, {{block "handler.serverOptions" .}}StdHTTPServerOptions{{end}}{}) +} +{{block "handler.serveMuxInterface" .}} +// ServeMux is an abstraction of [http.ServeMux]. +type ServeMux interface { + HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) + http.Handler +} +{{end}} +type {{template "handler.serverOptions" .}} struct { + BaseURL string + BaseRouter {{block "handler.routerType" .}}ServeMux{{end}} + Middlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. +func HandlerFromMux(si ServerInterface, {{block "handler.routerVar" .}}m{{end}} {{template "handler.routerType" .}}) http.Handler { + return HandlerWithOptions(si, {{template "handler.serverOptions" .}} { + BaseRouter: {{template "handler.routerVar" .}}, + }) +} + +func HandlerFromMuxWithBaseURL(si ServerInterface, {{template "handler.routerVar" .}} {{template "handler.routerType" .}}, baseURL string) http.Handler { + return HandlerWithOptions(si, {{template "handler.serverOptions" .}} { + BaseURL: baseURL, + BaseRouter: {{template "handler.routerVar" .}}, + }) +} + +// HandlerWithOptions creates http.Handler with additional options +func HandlerWithOptions(si ServerInterface, options {{template "handler.serverOptions" .}}) http.Handler { +{{template "handler.routerVar" .}} := options.BaseRouter + +if {{template "handler.routerVar" .}} == nil { +{{template "handler.routerVar" .}} = {{block "handler.newRouter" .}}http.NewServeMux(){{end}} +} +if options.ErrorHandlerFunc == nil { + options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } +} +{{if .}} +wrapper := ServerInterfaceWrapper{ +Handler: si, +HandlerMiddlewares: options.Middlewares, +ErrorHandlerFunc: options.ErrorHandlerFunc, +} +{{end}} +{{range .}}{{block "handler.register" .}}m.HandleFunc({{.Method | httpMethodConstant}}+" "+options.BaseURL+{{.Path | swaggerUriToStdHttpUri | toGoString}}, wrapper.{{.HandlerName}}) +{{end}}{{end}} +return {{template "handler.routerVar" .}} +} diff --git a/pkg/codegen/templates/server-interface.tmpl b/pkg/codegen/templates/server-interface.tmpl new file mode 100644 index 0000000000..f17405db44 --- /dev/null +++ b/pkg/codegen/templates/server-interface.tmpl @@ -0,0 +1,25 @@ +{{/* +Shared ServerInterface skeleton for every server framework. The {{block}} +defaults are the net/http-family shape (chi, gorilla, stdhttp); the other +frameworks override the handler signature via their templates//hooks.tmpl. +See server-middleware.tmpl for a description of the layering. + +Hooks: + interface.handlerSignature - the per-operation method parameter list and + return type (everything after the OperationId). + Default is the net/http (w, r, ...) shape with no + return value; echo/fiber add ` error`, gin/iris + swap the receiver/context type. + interface.unimplemented - optional Unimplemented stub type (chi only; empty + by default). +*/}} +// ServerInterface represents all server handlers. +type ServerInterface interface { +{{range .}}{{if not .IsAlias}}{{.SummaryAsComment .OperationId }} +// ({{.Method}} {{.Path}}) +{{with .DeprecationComment}}// +{{.}} +{{end}}{{.OperationId}}{{block "interface.handlerSignature" .}}(w http.ResponseWriter, r *http.Request{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}){{end}} +{{end}}{{end}} +} +{{block "interface.unimplemented" .}}{{end}} \ No newline at end of file diff --git a/pkg/codegen/templates/stdhttp/std-http-middleware.tmpl b/pkg/codegen/templates/server-middleware.tmpl similarity index 85% rename from pkg/codegen/templates/stdhttp/std-http-middleware.tmpl rename to pkg/codegen/templates/server-middleware.tmpl index 5998dbb613..b09368bcb9 100644 --- a/pkg/codegen/templates/stdhttp/std-http-middleware.tmpl +++ b/pkg/codegen/templates/server-middleware.tmpl @@ -1,3 +1,17 @@ +{{/* +Shared middleware/wrapper skeleton for the net/http-family server frameworks +(chi, gorilla, stdhttp). The {{block}} defaults below are the stdhttp shape; +chi and gorilla override the hooks via templates//hooks.tmpl, which +is parsed into a per-framework clone of the template tree (see +buildServerTemplates in codegen.go). stdhttp needs no overrides and executes +this skeleton straight from the base tree. + +Hooks: + middleware.pathParamValue - expression reading a path parameter from the request + middleware.valueIsUnescaped - value for BindStyledParameterOptions.ValueIsUnescaped + middleware.applyMiddlewares - loop that applies per-operation middlewares (differs + only in the compatibility flag name selecting order) +*/}} // ServerInterfaceWrapper converts contexts to parameters. type ServerInterfaceWrapper struct { Handler ServerInterface @@ -20,17 +34,17 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ var {{$varName := .GoVariableName}}{{$varName}} {{.TypeDef}} {{if .IsPassThrough}} - {{$varName}} = r.PathValue("{{.SanitizedParamName}}") + {{$varName}} = {{block "middleware.pathParamValue" .}}r.PathValue("{{.SanitizedParamName}}"){{end}} {{end}} {{if .IsJson}} - err = json.Unmarshal([]byte(r.PathValue("{{.SanitizedParamName}}")), &{{$varName}}) + err = json.Unmarshal([]byte({{template "middleware.pathParamValue" .}}), &{{$varName}}) if err != nil { siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err}) return } {{end}} {{if .IsStyled}} - err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", r.PathValue("{{.SanitizedParamName}}"), &{{$varName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}", ValueIsUnescaped: true}) + err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", {{template "middleware.pathParamValue" .}}, &{{$varName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}", ValueIsUnescaped: {{block "middleware.valueIsUnescaped" .}}true{{end}}}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) return @@ -188,7 +202,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ siw.Handler.{{.OperationId}}(w, r{{genParamNames .PathParams}}{{if .RequiresParamObject}}, params{{end}}) })) - {{if opts.Compatibility.ApplyChiMiddlewareFirstToLast}} + {{block "middleware.applyMiddlewares" .}}{{if opts.Compatibility.ApplyChiMiddlewareFirstToLast}} for i := len(siw.HandlerMiddlewares) -1; i >= 0; i-- { handler = siw.HandlerMiddlewares[i](handler) } @@ -196,7 +210,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ for _, middleware := range siw.HandlerMiddlewares { handler = middleware(handler) } - {{end}} + {{end}}{{end}} handler.ServeHTTP(w, r) } @@ -251,7 +265,7 @@ func (e *RequiredHeaderError) Unwrap() error { type InvalidParamFormatError struct { ParamName string - Err error + Err error } func (e *InvalidParamFormatError) Error() string { diff --git a/pkg/codegen/templates/stdhttp/std-http-handler.tmpl b/pkg/codegen/templates/stdhttp/std-http-handler.tmpl deleted file mode 100644 index 676748599d..0000000000 --- a/pkg/codegen/templates/stdhttp/std-http-handler.tmpl +++ /dev/null @@ -1,55 +0,0 @@ -// Handler creates http.Handler with routing matching OpenAPI spec. -func Handler(si ServerInterface) http.Handler { - return HandlerWithOptions(si, StdHTTPServerOptions{}) -} - -// ServeMux is an abstraction of [http.ServeMux]. -type ServeMux interface { - HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) - http.Handler -} - -type StdHTTPServerOptions struct { - BaseURL string - BaseRouter ServeMux - Middlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. -func HandlerFromMux(si ServerInterface, m ServeMux) http.Handler { - return HandlerWithOptions(si, StdHTTPServerOptions { - BaseRouter: m, - }) -} - -func HandlerFromMuxWithBaseURL(si ServerInterface, m ServeMux, baseURL string) http.Handler { - return HandlerWithOptions(si, StdHTTPServerOptions { - BaseURL: baseURL, - BaseRouter: m, - }) -} - -// HandlerWithOptions creates http.Handler with additional options -func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.Handler { - m := options.BaseRouter - - if m == nil { - m = http.NewServeMux() - } - if options.ErrorHandlerFunc == nil { - options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { - http.Error(w, err.Error(), http.StatusBadRequest) - } - } -{{if .}} - wrapper := ServerInterfaceWrapper{ - Handler: si, - HandlerMiddlewares: options.Middlewares, - ErrorHandlerFunc: options.ErrorHandlerFunc, - } -{{end}} -{{range .}}m.HandleFunc({{.Method | httpMethodConstant}}+" "+options.BaseURL+{{.Path | swaggerUriToStdHttpUri | toGoString}}, wrapper.{{.HandlerName}}) -{{end}} - return m -} diff --git a/pkg/codegen/templates/stdhttp/std-http-interface.tmpl b/pkg/codegen/templates/stdhttp/std-http-interface.tmpl deleted file mode 100644 index d3936346dc..0000000000 --- a/pkg/codegen/templates/stdhttp/std-http-interface.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{range .}}{{if not .IsAlias}}{{.SummaryAsComment .OperationId }} -// ({{.Method}} {{.Path}}) -{{with .DeprecationComment}}// -{{.}} -{{end}}{{.OperationId}}(w http.ResponseWriter, r *http.Request{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) -{{end}}{{end}} -} diff --git a/pkg/codegen/templates/stdhttp/std-http-receiver.tmpl b/pkg/codegen/templates/stdhttp/std-http-receiver.tmpl deleted file mode 100644 index 3089faea4e..0000000000 --- a/pkg/codegen/templates/stdhttp/std-http-receiver.tmpl +++ /dev/null @@ -1,116 +0,0 @@ -// {{.Prefix}}ReceiverInterface represents handlers for receiving inbound -// {{.PrefixLower}} requests. Each {{.PrefixLower}} becomes a Handle*{{.Prefix}} -// method that the implementation fills in. The caller mounts the per- -// {{.PrefixLower}} http.Handler returned by {Op}{{.Prefix}}Handler at -// whatever URL path they advertise to senders. -type {{.Prefix}}ReceiverInterface interface { -{{range .Operations -}} -{{.SummaryAsComment ""}} -// Handle{{.OperationId}}{{$.Prefix}} handles the {{.Method}} {{$.PrefixLower}} for {{.SourceName}}. -Handle{{.OperationId}}{{$.Prefix}}(w http.ResponseWriter, r *http.Request{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) -{{end}} -} - -// {{.Prefix}}ReceiverMiddlewareFunc wraps an http.Handler with cross- -// cutting behavior (signature verification, logging, rate limiting, ...). -type {{.Prefix}}ReceiverMiddlewareFunc func(http.Handler) http.Handler - -{{range .Operations -}} -{{$opid := .OperationId -}} -{{$srcName := .SourceName -}} -// {{$opid}}{{$.Prefix}}Handler returns the http.Handler for the {{$srcName}} {{$.PrefixLower}}. -// Mount this at the URL path advertised to {{$.PrefixLower}} senders. errHandler -// may be nil; if so, parameter-binding errors return 400 with the error -// message. Middlewares are applied in the order provided -- the last -// argument becomes the outermost wrapper. -func {{$opid}}{{$.Prefix}}Handler(si {{$.Prefix}}ReceiverInterface, errHandler func(w http.ResponseWriter, r *http.Request, err error), middlewares ...{{$.Prefix}}ReceiverMiddlewareFunc) http.Handler { - if errHandler == nil { - errHandler = func(w http.ResponseWriter, r *http.Request, err error) { - http.Error(w, err.Error(), http.StatusBadRequest) - } - } - var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -{{- if .RequiresParamObject}} - var err error - _ = err - - // Parameter object where we will unmarshal all parameters from the request. - var params {{$opid}}Params -{{range $paramIdx, $param := .QueryParams}} - // ------------- {{if .Required}}Required{{else}}Optional{{end}} query parameter "{{.ParamName}}" ------------- - {{if (or .IsPassThrough .IsJson)}} - if paramValue := r.URL.Query().Get("{{.ParamName}}"); paramValue != "" { - {{if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue - {{end}} - {{if .IsJson}} - var value {{.TypeDef}} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - errHandler(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err}) - return - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value - {{end}} - }{{if .Required}} else { - errHandler(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"}) - return - }{{end}} - {{end}} - {{if .IsStyled}} - err = runtime.BindQueryParameterWithOptions("{{.Style}}", {{.Explode}}, {{.Required}}, "{{.ParamName}}", r.URL.Query(), ¶ms.{{.GoName}}, runtime.BindQueryParameterOptions{Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - errHandler(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"}) - } else { - errHandler(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) - } - return - } - {{end}} -{{end}} -{{range $paramIdx, $param := .HeaderParams}} - // ------------- {{if .Required}}Required{{else}}Optional{{end}} header parameter "{{.ParamName}}" ------------- - if valueList, found := r.Header[http.CanonicalHeaderKey("{{.ParamName}}")]; found { - var {{.GoName}} {{.TypeDef}} - n := len(valueList) - if n != 1 { - errHandler(w, r, &TooManyValuesForParamError{ParamName: "{{.ParamName}}", Count: n}) - return - } - {{if .IsPassThrough}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}valueList[0] - {{end}} - {{if .IsJson}} - err = json.Unmarshal([]byte(valueList[0]), &{{.GoName}}) - if err != nil { - errHandler(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err}) - return - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} - {{end}} - {{if .IsStyled}} - err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", valueList[0], &{{.GoName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - errHandler(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) - return - } - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} - {{end}} - }{{if .Required}} else { - err := fmt.Errorf("Header parameter {{.ParamName}} is required, but not found") - errHandler(w, r, &RequiredHeaderError{ParamName: "{{.ParamName}}", Err: err}) - return - }{{end}} -{{end}} -{{- end}} - si.Handle{{$opid}}{{$.Prefix}}(w, r{{if .RequiresParamObject}}, params{{end}}) - }) - for _, mw := range middlewares { - h = mw(h) - } - return h -} - -{{end}} diff --git a/pkg/codegen/templates/strict/strict-echo.tmpl b/pkg/codegen/templates/strict/strict-echo.tmpl index 404c0b7265..52bfb309d8 100644 --- a/pkg/codegen/templates/strict/strict-echo.tmpl +++ b/pkg/codegen/templates/strict/strict-echo.tmpl @@ -1,4 +1,4 @@ -type StrictHandlerFunc func(ctx echo.Context, request any) (any, error) +type StrictHandlerFunc func(ctx {{template "echo.ctxType" .}}, request any) (any, error) type StrictMiddlewareFunc func(f StrictHandlerFunc, operationID string) StrictHandlerFunc func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { @@ -14,7 +14,7 @@ type strictHandler struct { {{$opid := .OperationId}} {{if not .IsAlias}} // {{$opid}} operation middleware - func (sh *strictHandler) {{.OperationId}}(ctx echo.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error { + func (sh *strictHandler) {{.OperationId}}(ctx {{template "echo.ctxType" .}}{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error { var request {{$opid | ucFirst}}RequestObject {{range .PathParams -}} @@ -35,10 +35,10 @@ type strictHandler struct { {{if .IsJSON -}} var body {{$opid}}{{.NameTag}}RequestBody var err error - if binder, ok := ctx.Echo().Binder.(*echo.DefaultBinder); ok { + if {{block "strict.echo.binderVar" .}}binder{{end}}, ok := ctx.Echo().Binder.(*echo.DefaultBinder); ok { // Bind only the request body, so that path and query parameters // are not also bound into the body struct. - err = binder.BindBody(ctx, &body) + err = {{block "strict.echo.bindBodyCall" .}}binder.BindBody{{end}}(ctx, &body) } else { // A custom binder is installed on the Echo instance; defer to it // entirely, since echo.Binder does not expose body-only binding. @@ -56,7 +56,7 @@ type strictHandler struct { request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body {{if not .Required -}} } {{end}} {{else if eq .NameTag "Formdata" -}} - if form, err := ctx.FormParams(); err == nil { + if form, err := ctx.{{block "strict.echo.formValues" .}}FormParams{{end}}(); err == nil { var body {{$opid}}{{.NameTag}}RequestBody if err := runtime.BindForm(&body, form, nil, nil); err != nil { return err @@ -100,7 +100,7 @@ type strictHandler struct { {{if $multipleBodies}}}{{end}} {{end}}{{/* range .Bodies */}} - handler := func(ctx echo.Context, request interface{}) (interface{}, error){ + handler := func(ctx {{template "echo.ctxType" .}}, request interface{}) (interface{}, error){ return sh.ssi.{{.OperationId}}(ctx.Request().Context(), request.({{$opid | ucFirst}}RequestObject)) } for _, middleware := range sh.middlewares { diff --git a/pkg/codegen/templates/strict/strict-echo5.tmpl b/pkg/codegen/templates/strict/strict-echo5.tmpl deleted file mode 100644 index 551759c9e8..0000000000 --- a/pkg/codegen/templates/strict/strict-echo5.tmpl +++ /dev/null @@ -1,116 +0,0 @@ -type StrictHandlerFunc func(ctx *echo.Context, request any) (any, error) -type StrictMiddlewareFunc func(f StrictHandlerFunc, operationID string) StrictHandlerFunc - -func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { - return &strictHandler{ssi: ssi, middlewares: middlewares} -} - -type strictHandler struct { - ssi StrictServerInterface - middlewares []StrictMiddlewareFunc -} - -{{range .}} - {{$opid := .OperationId}} - {{if not .IsAlias}} - // {{$opid}} operation middleware - func (sh *strictHandler) {{.OperationId}}(ctx *echo.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error { - var request {{$opid | ucFirst}}RequestObject - - {{range .PathParams -}} - request.{{.GoName}} = {{.GoVariableName}} - {{end -}} - - {{if .RequiresParamObject -}} - request.Params = params - {{end -}} - - {{ if .HasMaskedRequestContentTypes -}} - request.ContentType = ctx.Request().Header.Get("Content-Type") - {{end -}} - - {{$multipleBodies := gt (len .Bodies) 1 -}} - {{range .Bodies -}} - {{if $multipleBodies}}if strings.HasPrefix(ctx.Request().Header.Get("Content-Type"), {{.ContentType | toGoString}}) { {{end}} - {{if .IsJSON -}} - var body {{$opid}}{{.NameTag}}RequestBody - var err error - if _, ok := ctx.Echo().Binder.(*echo.DefaultBinder); ok { - // Bind only the request body, so that path and query parameters - // are not also bound into the body struct. - err = echo.BindBody(ctx, &body) - } else { - // A custom binder is installed on the Echo instance; defer to it - // entirely, since echo.Binder does not expose body-only binding. - err = ctx.Bind(&body) - } - if err != nil { - {{if not .Required -}} - if !errors.Is(err, io.EOF) { - return err - } - {{else -}} - return err - {{end -}} - } {{if not .Required -}} else { {{end}} - request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body - {{if not .Required -}} } {{end}} - {{else if eq .NameTag "Formdata" -}} - if form, err := ctx.FormValues(); err == nil { - var body {{$opid}}{{.NameTag}}RequestBody - if err := runtime.BindForm(&body, form, nil, nil); err != nil { - return err - } - request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body - } else { - return err - } - {{else if eq .NameTag "Multipart" -}} - {{if eq .ContentType "multipart/form-data" -}} - if reader, err := ctx.Request().MultipartReader(); err != nil { - return err - } else { - request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = reader - } - {{else -}} - if _, params, err := mime.ParseMediaType(ctx.Request().Header.Get("Content-Type")); err != nil { - return err - } else if boundary := params["boundary"]; boundary == "" { - return http.ErrMissingBoundary - } else { - request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = multipart.NewReader(ctx.Request().Body, boundary) - } - {{end -}} - {{else if eq .NameTag "Text" -}} - data, err := io.ReadAll(ctx.Request().Body) - if err != nil { - return err - } - body := {{$opid}}{{.NameTag}}RequestBody(data) - request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body - {{else -}} - request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = ctx.Request().Body - {{end}}{{/* if eq .NameTag "JSON" */ -}} - {{if $multipleBodies}}}{{end}} - {{end}}{{/* range .Bodies */}} - - handler := func(ctx *echo.Context, request interface{}) (interface{}, error){ - return sh.ssi.{{.OperationId}}(ctx.Request().Context(), request.({{$opid | ucFirst}}RequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "{{.OperationId}}") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.({{$opid | ucFirst}}ResponseObject); ok { - return validResponse.Visit{{$opid}}Response(ctx.Response()) - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil - } - {{end}} -{{end}} diff --git a/pkg/codegen/templates/strict/strict-fiber-interface.tmpl b/pkg/codegen/templates/strict/strict-fiber-interface.tmpl index 98ce542712..dbc118ce36 100644 --- a/pkg/codegen/templates/strict/strict-fiber-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-fiber-interface.tmpl @@ -17,7 +17,7 @@ } type {{$opid | ucFirst}}ResponseObject interface { - Visit{{$opid}}Response(ctx *fiber.Ctx) error + Visit{{$opid}}Response(ctx {{template "fiber.ctxType" .}}) error } {{range .Responses}} @@ -79,20 +79,8 @@ } {{end}} - func (response {{$receiverTypeName}}) Visit{{$opid}}Response(ctx *fiber.Ctx) error { - {{range $headers -}} - {{if .IsNullable -}} - if response.Headers.{{.GoName}}.IsSpecified() { - ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}}.MustGet())) - } - {{else if .IsOptional -}} - if response.Headers.{{.GoName}} != nil { - ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(*response.Headers.{{.GoName}})) - } - {{else -}} - ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) - {{end -}} - {{end -}} + func (response {{$receiverTypeName}}) Visit{{$opid}}Response(ctx {{template "fiber.ctxType" .}}) error { + {{template "strict.responseHeaders" (dict "Headers" $headers "Setter" "ctx.Response().Header.Set") -}} {{if eq .NameTag "Multipart" -}} writer := multipart.NewWriter(ctx.Response().BodyWriter()) {{end -}} @@ -170,20 +158,8 @@ {{end -}} } {{end -}} - func (response {{$opid}}{{$statusCode}}Response) Visit{{$opid}}Response(ctx *fiber.Ctx) error { - {{range $headers -}} - {{if .IsNullable -}} - if response.Headers.{{.GoName}}.IsSpecified() { - ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}}.MustGet())) - } - {{else if .IsOptional -}} - if response.Headers.{{.GoName}} != nil { - ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(*response.Headers.{{.GoName}})) - } - {{else -}} - ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) - {{end -}} - {{end -}} + func (response {{$opid}}{{$statusCode}}Response) Visit{{$opid}}Response(ctx {{template "fiber.ctxType" .}}) error { + {{template "strict.responseHeaders" (dict "Headers" $headers "Setter" "ctx.Response().Header.Set") -}} ctx.Status({{if $fixedStatusCode}}{{$statusCode}}{{else}}response.StatusCode{{end}}) return nil } diff --git a/pkg/codegen/templates/strict/strict-fiber-v3-interface.tmpl b/pkg/codegen/templates/strict/strict-fiber-v3-interface.tmpl deleted file mode 100644 index b93c5d0f0b..0000000000 --- a/pkg/codegen/templates/strict/strict-fiber-v3-interface.tmpl +++ /dev/null @@ -1,203 +0,0 @@ -{{range .}}{{if not .IsAlias}} - {{$opid := .OperationId -}} - type {{$opid | ucFirst}}RequestObject struct { - {{range .PathParams -}} - {{.GoName | ucFirst}} {{.TypeDef}} {{.JsonTag}} - {{end -}} - {{if .RequiresParamObject -}} - Params {{$opid}}Params - {{end -}} - {{if .HasMaskedRequestContentTypes -}} - ContentType string - {{end -}} - {{$multipleBodies := gt (len .Bodies) 1 -}} - {{range .Bodies -}} - {{if $multipleBodies}}{{.NameTag}}{{end}}Body {{if eq .NameTag "Multipart"}}*multipart.Reader{{else if ne .NameTag ""}}*{{$opid}}{{.NameTag}}RequestBody{{else}}io.Reader{{end}} - {{end -}} - } - - type {{$opid | ucFirst}}ResponseObject interface { - Visit{{$opid}}Response(ctx fiber.Ctx) error - } - - {{range .Responses}} - {{$statusCode := .StatusCode -}} - {{$hasHeaders := ne 0 (len .Headers) -}} - {{$fixedStatusCode := .HasFixedStatusCode -}} - {{$isRef := .IsRef -}} - {{$isExternalRef := .IsExternalRef -}} - {{$ref := .Ref | ucFirstWithPkgName -}} - {{$headers := .Headers -}} - - {{if (and $hasHeaders (not $isRef)) -}} - type {{$opid}}{{$statusCode}}ResponseHeaders struct { - {{range .Headers -}} - {{with .DeprecationComment}}{{.}} - {{end -}}{{.GoName}} {{.GoTypeDef}} - {{end -}} - } - {{end}} - - {{range .Contents}} - {{$receiverTypeName := printf "%s%s%s%s" $opid $statusCode .NameTagOrContentType "Response"}} - {{if and $fixedStatusCode $isRef -}} - {{ if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) (or (eq .NameTag "Multipart") (eq .NameTag "Text")) -}} - type {{$receiverTypeName}} {{$ref}}{{.NameTagOrContentType}}Response - {{else -}} - type {{$receiverTypeName}} struct{ {{$ref}}{{.NameTagOrContentType}}Response } - {{end}} - {{else if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) -}} - type {{$receiverTypeName}} {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{if and .Schema.IsRef (not .Schema.IsExternalRef)}}={{end}} {{.Schema.TypeDecl}}{{else}}io.Reader{{end}} - {{- if and .IsJSON .Schema.HasCustomMarshalJSON}} - - func (t {{$receiverTypeName}}) MarshalJSON() ([]byte, error) { - return {{.Schema.TypeDecl}}(t).MarshalJSON() - } - - func (t *{{$receiverTypeName}}) UnmarshalJSON(b []byte) error { - return (*{{.Schema.TypeDecl}})(t).UnmarshalJSON(b) - } - {{- end}} - {{else -}} - type {{$receiverTypeName}} struct { - Body {{if eq .NameTag "Multipart"}}func(writer *multipart.Writer)error{{else if .IsSupported}}{{.Schema.TypeDecl}}{{else}}io.Reader{{end}} - {{if $hasHeaders -}} - Headers {{if $isRef}}{{$ref}}{{else}}{{$opid}}{{$statusCode}}{{end}}ResponseHeaders - {{end -}} - - {{if not $fixedStatusCode -}} - StatusCode int - {{end -}} - - {{if not .HasFixedContentType -}} - ContentType string - {{end -}} - - {{if not .IsSupported -}} - ContentLength int64 - {{end -}} - } - {{end}} - - func (response {{$receiverTypeName}}) Visit{{$opid}}Response(ctx fiber.Ctx) error { - {{range $headers -}} - {{if .IsNullable -}} - if response.Headers.{{.GoName}}.IsSpecified() { - ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}}.MustGet())) - } - {{else if .IsOptional -}} - if response.Headers.{{.GoName}} != nil { - ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(*response.Headers.{{.GoName}})) - } - {{else -}} - ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) - {{end -}} - {{end -}} - {{if eq .NameTag "Multipart" -}} - writer := multipart.NewWriter(ctx.Response().BodyWriter()) - {{end -}} - ctx.Response().Header.Set("Content-Type", {{if eq .NameTag "Multipart"}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType({{.ContentType | toGoString}}, map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}{{.ContentType | toGoString}}{{else}}response.ContentType{{end}}) - {{if not .IsSupported -}} - if response.ContentLength != 0 { - ctx.Response().Header.Set("Content-Length", fmt.Sprint(response.ContentLength)) - } - {{end -}} - ctx.Status({{if $fixedStatusCode}}{{$statusCode}}{{else}}response.StatusCode{{end}}) - {{$hasBodyVar := or ($hasHeaders) (not $fixedStatusCode) (not .IsSupported)}} - {{if .IsJSON }} - {{$hasUnionElements := ne 0 (len .Schema.UnionElements)}} - return ctx.JSON(&{{if $hasBodyVar}}response.Body{{else}}response{{end}}{{if and $hasUnionElements (not .Schema.IsExternalRef)}}.union{{end}}) - {{else if eq .NameTag "Text" -}} - _, err := ctx.WriteString(fmt.Sprint({{if $hasBodyVar}}response.Body{{else}}response{{end}})) - return err - {{else if eq .NameTag "Formdata" -}} - if form, err := runtime.MarshalForm({{if $hasBodyVar}}response.Body{{else}}response{{end}}, nil); err != nil { - return err - } else { - _, err := ctx.WriteString(form.Encode()) - return err - } - {{else if eq .NameTag "Multipart" -}} - defer writer.Close() - return {{if $hasBodyVar}}response.Body{{else}}response{{end}}(writer); - {{else -}} - {{if .IsStreamingContentType -}} - // Fiber/fasthttp streams through a callback: fasthttp emits - // a chunk each time we call w.Flush(), so clients see - // streaming data immediately instead of waiting on buffering. - ctx.Response().SetBodyStreamWriter(func(w *bufio.Writer) { - if closer, ok := response.Body.(io.ReadCloser); ok { - defer closer.Close() - } - buf := make([]byte, 4096) - for { - n, err := response.Body.Read(buf) - if n > 0 { - if _, writeErr := w.Write(buf[:n]); writeErr != nil { - return - } - if flushErr := w.Flush(); flushErr != nil { - return - } - } - if err != nil { - return - } - } - }) - return nil - {{else -}} - if closer, ok := response.Body.(io.ReadCloser); ok { - defer closer.Close() - } - _, err := io.Copy(ctx.Response().BodyWriter(), response.Body) - return err - {{end}}{{/* if .IsStreamingContentType */ -}} - {{end}}{{/* if eq .NameTag "JSON" */ -}} - } - {{end}} - - {{if eq 0 (len .Contents) -}} - {{if and $fixedStatusCode $isRef -}} - type {{$opid}}{{$statusCode}}Response {{if not $isExternalRef}}={{end}} {{$ref}}Response - {{else -}} - type {{$opid}}{{$statusCode}}Response struct { - {{if $hasHeaders -}} - Headers {{if $isRef}}{{$ref}}{{else}}{{$opid}}{{$statusCode}}{{end}}ResponseHeaders - {{end}} - {{if not $fixedStatusCode -}} - StatusCode int - {{end -}} - } - {{end -}} - func (response {{$opid}}{{$statusCode}}Response) Visit{{$opid}}Response(ctx fiber.Ctx) error { - {{range $headers -}} - {{if .IsNullable -}} - if response.Headers.{{.GoName}}.IsSpecified() { - ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}}.MustGet())) - } - {{else if .IsOptional -}} - if response.Headers.{{.GoName}} != nil { - ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(*response.Headers.{{.GoName}})) - } - {{else -}} - ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) - {{end -}} - {{end -}} - ctx.Status({{if $fixedStatusCode}}{{$statusCode}}{{else}}response.StatusCode{{end}}) - return nil - } - {{end}} - {{end}} -{{end}}{{end}} - -// StrictServerInterface represents all server handlers. -type StrictServerInterface interface { -{{range .}}{{if not .IsAlias}}{{.SummaryAsComment .OperationId }} -// ({{.Method}} {{.Path}}) -{{with .DeprecationComment}}// -{{.}} -{{end}}{{$opid := .OperationId -}} -{{$opid}}(ctx context.Context, request {{$opid | ucFirst}}RequestObject) ({{$opid | ucFirst}}ResponseObject, error) -{{end}}{{end}}{{/* range . */ -}} -} diff --git a/pkg/codegen/templates/strict/strict-fiber-v3.tmpl b/pkg/codegen/templates/strict/strict-fiber-v3.tmpl deleted file mode 100644 index d4f1542586..0000000000 --- a/pkg/codegen/templates/strict/strict-fiber-v3.tmpl +++ /dev/null @@ -1,92 +0,0 @@ -type StrictHandlerFunc func(ctx fiber.Ctx, args any) (any, error) - -type StrictMiddlewareFunc func(f StrictHandlerFunc, operationID string) StrictHandlerFunc - -func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { - return &strictHandler{ssi: ssi, middlewares: middlewares} -} - -type strictHandler struct { - ssi StrictServerInterface - middlewares []StrictMiddlewareFunc -} - -{{range .}} - {{if not .IsAlias}} - {{$opid := .OperationId}} - // {{$opid}} operation middleware - func (sh *strictHandler) {{.OperationId}}(ctx fiber.Ctx{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error { - var request {{$opid | ucFirst}}RequestObject - - {{range .PathParams -}} - {{$varName := .GoVariableName -}} - request.{{.GoName}} = {{.GoVariableName}} - {{end -}} - - {{if .RequiresParamObject -}} - request.Params = params - {{end -}} - - {{ if .HasMaskedRequestContentTypes -}} - request.ContentType = string(ctx.Request().Header.ContentType()) - {{end -}} - - {{$multipleBodies := gt (len .Bodies) 1 -}} - {{range .Bodies -}} - {{if $multipleBodies}}if strings.HasPrefix(string(ctx.Request().Header.ContentType()), "{{.ContentType}}") { {{end}} - {{if .IsJSON }} - var body {{$opid}}{{.NameTag}}RequestBody - if err := ctx.Bind().Body(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body - {{else if eq .NameTag "Formdata" -}} - var body {{$opid}}{{.NameTag}}RequestBody - if err := ctx.Bind().Body(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body - {{else if eq .NameTag "Multipart" -}} - {{if eq .ContentType "multipart/form-data" -}} - request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = multipart.NewReader(bytes.NewReader(ctx.Request().Body()), string(ctx.Request().Header.MultipartFormBoundary())) - {{else -}} - if _, params, err := mime.ParseMediaType(string(ctx.Request().Header.ContentType())); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else if boundary := params["boundary"]; boundary == "" { - return fiber.NewError(fiber.StatusBadRequest, http.ErrMissingBoundary.Error()) - } else { - request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = multipart.NewReader(bytes.NewReader(ctx.Request().Body()), boundary) - } - {{end -}} - {{else if eq .NameTag "Text" -}} - data := ctx.Request().Body() - body := {{$opid}}{{.NameTag}}RequestBody(data) - request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body - {{else -}} - request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = bytes.NewReader(ctx.Request().Body()) - {{end}}{{/* if eq .NameTag "JSON" */ -}} - {{if $multipleBodies}}}{{end}} - {{end}}{{/* range .Bodies */}} - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.{{.OperationId}}(ctx.Context(), request.({{$opid | ucFirst}}RequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "{{.OperationId}}") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.({{$opid | ucFirst}}ResponseObject); ok { - if err := validResponse.Visit{{$opid}}Response(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil - } - {{end}} -{{end}} diff --git a/pkg/codegen/templates/strict/strict-fiber.tmpl b/pkg/codegen/templates/strict/strict-fiber.tmpl index 2bd4e6bf7a..943f9b1b8c 100644 --- a/pkg/codegen/templates/strict/strict-fiber.tmpl +++ b/pkg/codegen/templates/strict/strict-fiber.tmpl @@ -1,4 +1,4 @@ -type StrictHandlerFunc func(ctx *fiber.Ctx, args any) (any, error) +type StrictHandlerFunc func(ctx {{template "fiber.ctxType" .}}, args any) (any, error) type StrictMiddlewareFunc func(f StrictHandlerFunc, operationID string) StrictHandlerFunc func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { @@ -14,7 +14,7 @@ type strictHandler struct { {{$opid := .OperationId}} {{if not .IsAlias}} // {{$opid}} operation middleware - func (sh *strictHandler) {{.OperationId}}(ctx *fiber.Ctx{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error { + func (sh *strictHandler) {{.OperationId}}(ctx {{template "fiber.ctxType" .}}{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error { var request {{$opid | ucFirst}}RequestObject {{range .PathParams -}} @@ -35,7 +35,7 @@ type strictHandler struct { {{if $multipleBodies}}if strings.HasPrefix(string(ctx.Request().Header.ContentType()), {{.ContentType | toGoString}}) { {{end}} {{if .IsJSON }} var body {{$opid}}{{.NameTag}}RequestBody - if err := ctx.BodyParser(&body); err != nil { + if err := {{block "strict.fiber.bindBody" .}}ctx.BodyParser(&body){{end}}; err != nil { {{if not .Required -}} if !errors.Is(err, io.EOF) { return fiber.NewError(fiber.StatusBadRequest, err.Error()) @@ -48,7 +48,7 @@ type strictHandler struct { {{if not .Required -}} } {{end}} {{else if eq .NameTag "Formdata" -}} var body {{$opid}}{{.NameTag}}RequestBody - if err := ctx.BodyParser(&body); err != nil { + if err := {{template "strict.fiber.bindBody" .}}; err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body @@ -80,8 +80,8 @@ type strictHandler struct { {{if $multipleBodies}}}{{end}} {{end}}{{/* range .Bodies */}} - handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.{{.OperationId}}(ctx.UserContext(), request.({{$opid | ucFirst}}RequestObject)) + handler := func(ctx {{template "fiber.ctxType" .}}, request interface{}) (interface{}, error) { + return sh.ssi.{{.OperationId}}(ctx.{{block "strict.fiber.reqContext" .}}UserContext{{end}}(), request.({{$opid | ucFirst}}RequestObject)) } for _, middleware := range sh.middlewares { handler = middleware(handler, "{{.OperationId}}") diff --git a/pkg/codegen/templates/strict/strict-interface.tmpl b/pkg/codegen/templates/strict/strict-interface.tmpl index 1431ac9c0a..b7966071b9 100644 --- a/pkg/codegen/templates/strict/strict-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-interface.tmpl @@ -91,19 +91,7 @@ return err } w.Header().Set("Content-Type", {{if .HasFixedContentType }}{{.ContentType | toGoString}}{{else}}response.ContentType{{end}}) - {{range $headers -}} - {{if .IsNullable -}} - if response.Headers.{{.GoName}}.IsSpecified() { - w.Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}}.MustGet())) - } - {{else if .IsOptional -}} - if response.Headers.{{.GoName}} != nil { - w.Header().Set("{{.Name}}", fmt.Sprint(*response.Headers.{{.GoName}})) - } - {{else -}} - w.Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) - {{end -}} - {{end -}} + {{template "strict.responseHeaders" (dict "Headers" $headers "Setter" "w.Header().Set") -}} w.WriteHeader({{if $fixedStatusCode}}{{$statusCode}}{{else}}response.StatusCode{{end}}) _, err := buf.WriteTo(w) return err @@ -113,19 +101,7 @@ return err } w.Header().Set("Content-Type", {{if .HasFixedContentType }}{{.ContentType | toGoString}}{{else}}response.ContentType{{end}}) - {{range $headers -}} - {{if .IsNullable -}} - if response.Headers.{{.GoName}}.IsSpecified() { - w.Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}}.MustGet())) - } - {{else if .IsOptional -}} - if response.Headers.{{.GoName}} != nil { - w.Header().Set("{{.Name}}", fmt.Sprint(*response.Headers.{{.GoName}})) - } - {{else -}} - w.Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) - {{end -}} - {{end -}} + {{template "strict.responseHeaders" (dict "Headers" $headers "Setter" "w.Header().Set") -}} w.WriteHeader({{if $fixedStatusCode}}{{$statusCode}}{{else}}response.StatusCode{{end}}) _, err = w.Write([]byte(form.Encode())) return err @@ -136,19 +112,7 @@ w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) } {{end -}} - {{range $headers -}} - {{if .IsNullable -}} - if response.Headers.{{.GoName}}.IsSpecified() { - w.Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}}.MustGet())) - } - {{else if .IsOptional -}} - if response.Headers.{{.GoName}} != nil { - w.Header().Set("{{.Name}}", fmt.Sprint(*response.Headers.{{.GoName}})) - } - {{else -}} - w.Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) - {{end -}} - {{end -}} + {{template "strict.responseHeaders" (dict "Headers" $headers "Setter" "w.Header().Set") -}} w.WriteHeader({{if $fixedStatusCode}}{{$statusCode}}{{else}}response.StatusCode{{end}}) {{if eq .NameTag "Text" -}} @@ -210,19 +174,7 @@ } {{end -}} func (response {{$opid}}{{$statusCode}}Response) Visit{{$opid}}Response(w http.ResponseWriter) error { - {{range $headers -}} - {{if .IsNullable -}} - if response.Headers.{{.GoName}}.IsSpecified() { - w.Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}}.MustGet())) - } - {{else if .IsOptional -}} - if response.Headers.{{.GoName}} != nil { - w.Header().Set("{{.Name}}", fmt.Sprint(*response.Headers.{{.GoName}})) - } - {{else -}} - w.Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) - {{end -}} - {{end -}} + {{template "strict.responseHeaders" (dict "Headers" $headers "Setter" "w.Header().Set") -}} w.WriteHeader({{if $fixedStatusCode}}{{$statusCode}}{{else}}response.StatusCode{{end}}) return nil } diff --git a/pkg/codegen/templates/strict/strict-iris-interface.tmpl b/pkg/codegen/templates/strict/strict-iris-interface.tmpl index 77dffa8606..c462b57ff1 100644 --- a/pkg/codegen/templates/strict/strict-iris-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-iris-interface.tmpl @@ -80,19 +80,7 @@ {{end}} func (response {{$receiverTypeName}}) Visit{{$opid}}Response(ctx iris.Context) error { - {{range $headers -}} - {{if .IsNullable -}} - if response.Headers.{{.GoName}}.IsSpecified() { - ctx.ResponseWriter().Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}}.MustGet())) - } - {{else if .IsOptional -}} - if response.Headers.{{.GoName}} != nil { - ctx.ResponseWriter().Header().Set("{{.Name}}", fmt.Sprint(*response.Headers.{{.GoName}})) - } - {{else -}} - ctx.ResponseWriter().Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) - {{end -}} - {{end -}} + {{template "strict.responseHeaders" (dict "Headers" $headers "Setter" "ctx.ResponseWriter().Header().Set") -}} {{if eq .NameTag "Multipart" -}} writer := multipart.NewWriter(ctx.ResponseWriter()) {{end -}} @@ -172,19 +160,7 @@ } {{end -}} func (response {{$opid}}{{$statusCode}}Response) Visit{{$opid}}Response(ctx iris.Context) error { - {{range $headers -}} - {{if .IsNullable -}} - if response.Headers.{{.GoName}}.IsSpecified() { - ctx.ResponseWriter().Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}}.MustGet())) - } - {{else if .IsOptional -}} - if response.Headers.{{.GoName}} != nil { - ctx.ResponseWriter().Header().Set("{{.Name}}", fmt.Sprint(*response.Headers.{{.GoName}})) - } - {{else -}} - ctx.ResponseWriter().Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) - {{end -}} - {{end -}} + {{template "strict.responseHeaders" (dict "Headers" $headers "Setter" "ctx.ResponseWriter().Header().Set") -}} ctx.StatusCode({{if $fixedStatusCode}}{{$statusCode}}{{else}}response.StatusCode{{end}}) return nil } diff --git a/pkg/codegen/templates/strict/strict-partials.tmpl b/pkg/codegen/templates/strict/strict-partials.tmpl new file mode 100644 index 0000000000..b9b857e7e2 --- /dev/null +++ b/pkg/codegen/templates/strict/strict-partials.tmpl @@ -0,0 +1,27 @@ +{{/* +Shared partials for the strict-server response templates. This file contributes +only {{define}} blocks to the base template tree; it emits nothing on its own. + +"strict.responseHeaders" renders the 3-way (nullable / optional / plain) response +header-writing branch that every strict framework needs inside its Visit*Response +methods. The only per-framework difference is the header setter expression, passed +as .Setter (e.g. "w.Header().Set", "ctx.Response().Header.Set"); .Headers is the +response's []HeaderDefinition. Build the argument with the dict helper: +{{`{{template "strict.responseHeaders" (dict "Headers" $headers "Setter" "w.Header().Set")}}`}} +*/}} +{{define "strict.responseHeaders" -}} +{{- $setter := .Setter -}} +{{- range .Headers -}} +{{if .IsNullable -}} +if response.Headers.{{.GoName}}.IsSpecified() { + {{$setter}}("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}}.MustGet())) +} +{{else if .IsOptional -}} +if response.Headers.{{.GoName}} != nil { + {{$setter}}("{{.Name}}", fmt.Sprint(*response.Headers.{{.GoName}})) +} +{{else -}} +{{$setter}}("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) +{{end -}} +{{end -}} +{{end -}} diff --git a/pkg/codegen/templates/webhook-initiator.tmpl b/pkg/codegen/templates/webhook-initiator.tmpl deleted file mode 100644 index 032e3933ed..0000000000 --- a/pkg/codegen/templates/webhook-initiator.tmpl +++ /dev/null @@ -1,235 +0,0 @@ -// WebhookInitiator sends OpenAPI 3.1 webhook requests to target URLs. -// Modeled on the generated Client, but with no stored Server -- the full -// target URL is provided per-call by the caller (typically discovered -// from a subscription registration). -type WebhookInitiator struct { - // Doer for performing requests, typically a *http.Client with any - // customized settings, such as certificate chains. - Client HttpRequestDoer - - // A list of callbacks for modifying requests which are generated before sending over - // the network. - RequestEditors []RequestEditorFn -} - -// WebhookInitiatorOption allows setting custom parameters during construction. -type WebhookInitiatorOption func(*WebhookInitiator) error - -// NewWebhookInitiator creates a new WebhookInitiator with reasonable defaults. -func NewWebhookInitiator(opts ...WebhookInitiatorOption) (*WebhookInitiator, error) { - initiator := WebhookInitiator{} - for _, o := range opts { - if err := o(&initiator); err != nil { - return nil, err - } - } - if initiator.Client == nil { - initiator.Client = &http.Client{} - } - return &initiator, nil -} - -// WithWebhookHTTPClient allows overriding the default Doer, which is -// automatically created using http.Client. This is useful for tests. -func WithWebhookHTTPClient(doer HttpRequestDoer) WebhookInitiatorOption { - return func(p *WebhookInitiator) error { - p.Client = doer - return nil - } -} - -// WithWebhookRequestEditorFn allows setting up a callback function, which -// will be called right before sending the webhook request. This can be -// used to mutate the request, e.g. to add signature headers. -func WithWebhookRequestEditorFn(fn RequestEditorFn) WebhookInitiatorOption { - return func(p *WebhookInitiator) error { - p.RequestEditors = append(p.RequestEditors, fn) - return nil - } -} - -func (p *WebhookInitiator) applyWebhookEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { - for _, r := range p.RequestEditors { - if err := r(ctx, req); err != nil { - return err - } - } - for _, r := range additionalEditors { - if err := r(ctx, req); err != nil { - return err - } - } - return nil -} - -// WebhookInitiatorInterface is the interface specification for the webhook initiator. -type WebhookInitiatorInterface interface { -{{range . -}} -{{$hasParams := .RequiresParamObject -}} -{{$opid := .OperationId -}} - // {{$opid}}{{if .HasBody}}WithBody{{end}} fires the {{.WebhookName}} webhook{{if .HasBody}} with any body{{end}} - {{$opid}}{{if .HasBody}}WithBody{{end}}(ctx context.Context, targetURL string{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}, reqEditors... RequestEditorFn) (*http.Response, error) -{{range .Bodies}} - {{if .IsSupportedByClient -}} - {{$opid}}{{.Suffix}}(ctx context.Context, targetURL string{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody, reqEditors... RequestEditorFn) (*http.Response, error) - {{end -}} -{{end}}{{/* range .Bodies */}} -{{end}}{{/* range . */}} -} - - -{{/* Generate webhook initiator methods */}} -{{range . -}} -{{$hasParams := .RequiresParamObject -}} -{{$opid := .OperationId -}} - -func (p *WebhookInitiator) {{$opid}}{{if .HasBody}}WithBody{{end}}(ctx context.Context, targetURL string{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}, reqEditors... RequestEditorFn) (*http.Response, error) { - req, err := New{{$opid}}WebhookRequest{{if .HasBody}}WithBody{{end}}(targetURL{{if $hasParams}}, params{{end}}{{if .HasBody}}, contentType, body{{end}}) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := p.applyWebhookEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return p.Client.Do(req) -} - -{{range .Bodies}} -{{if .IsSupportedByClient -}} -func (p *WebhookInitiator) {{$opid}}{{.Suffix}}(ctx context.Context, targetURL string{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody, reqEditors... RequestEditorFn) (*http.Response, error) { - req, err := New{{$opid}}WebhookRequest{{.Suffix}}(targetURL{{if $hasParams}}, params{{end}}, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := p.applyWebhookEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return p.Client.Do(req) -} -{{end -}} -{{end}}{{/* range .Bodies */}} -{{end}}{{/* range . */}} - -{{/* Generate webhook request builders */}} -{{range . -}} -{{$hasParams := .RequiresParamObject -}} -{{$opid := .OperationId -}} -{{$method := .Method -}} -{{$webhookName := .WebhookName -}} - -{{range .Bodies}} -{{if .IsSupportedByClient -}} -// New{{$opid}}WebhookRequest{{.Suffix}} builds a {{.ContentType}} {{$method}} request for the {{$webhookName}} webhook -func New{{$opid}}WebhookRequest{{.Suffix}}(targetURL string{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody) (*http.Request, error) { - var bodyReader io.Reader - {{if .IsJSON -}} - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - {{else if eq .NameTag "Formdata" -}} - bodyStr, err := runtime.MarshalForm(body, nil) - if err != nil { - return nil, err - } - bodyReader = strings.NewReader(bodyStr.Encode()) - {{else if eq .NameTag "Text" -}} - if stringer, ok := interface{}(body).(fmt.Stringer); ok { - bodyReader = strings.NewReader(stringer.String()) - } else { - {{if .Schema.IsPrimitive -}} - bodyReader = strings.NewReader(fmt.Sprint(body)) - {{else -}} - return nil, fmt.Errorf("text/plain is not supported for complex types, define a String() method on {{.Schema.TypeDecl}} to marshal it as text") - {{end -}} - } - {{end -}} - return New{{$opid}}WebhookRequestWithBody(targetURL{{if $hasParams}}, params{{end}}, "{{.ContentType}}", bodyReader) -} -{{end -}} -{{end}} - -// New{{$opid}}WebhookRequest{{if .HasBody}}WithBody{{end}} builds a {{.Method}} request for the {{.WebhookName}} webhook{{if .HasBody}} with any body{{end}} -func New{{$opid}}WebhookRequest{{if .HasBody}}WithBody{{end}}(targetURL string{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}) (*http.Request, error) { - var err error - _ = err - - reqURL, err := url.Parse(targetURL) - if err != nil { - return nil, err - } - -{{if .QueryParams}} - if params != nil { - queryValues := reqURL.Query() - var rawQueryFragments []string - {{range $paramIdx, $param := .QueryParams}} - {{if .RequiresNilCheck}} if params.{{.GoName}} != nil { {{end}} - {{if .IsPassThrough}} - queryValues.Add("{{.ParamName}}", {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}) - {{end}} - {{if .IsJson}} - if queryParamBuf, err := json.Marshal({{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}); err != nil { - return nil, err - } else { - queryValues.Add("{{.ParamName}}", string(queryParamBuf)) - } - {{end}} - {{if .IsStyled}} - if queryFrag, err := runtime.StyleParamWithOptions("{{.Style}}", {{.Explode}}, "{{.ParamName}}", {{if and .RequiresNilCheck .HasOptionalPointer}}*{{end}}params.{{.GoName}}, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - {{end}} - {{if .RequiresNilCheck}}}{{end}} - {{end}} - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - reqURL.RawQuery = strings.Join(rawQueryFragments, "&") - } -{{end}}{{/* if .QueryParams */}} - - req, err := http.NewRequest({{.Method | httpMethodConstant}}, reqURL.String(), {{if .HasBody}}body{{else}}nil{{end}}) - if err != nil { - return nil, err - } - - {{if .HasBody}}req.Header.Add("Content-Type", contentType){{end}} -{{ if .HeaderParams }} - if params != nil { - {{range $paramIdx, $param := .HeaderParams}} - {{if .RequiresNilCheck}} if params.{{.GoName}} != nil { {{end}} - var headerParam{{$paramIdx}} string - {{if .IsPassThrough}} - headerParam{{$paramIdx}} = {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}} - {{end}} - {{if .IsJson}} - var headerParamBuf{{$paramIdx}} []byte - headerParamBuf{{$paramIdx}}, err = json.Marshal({{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}) - if err != nil { - return nil, err - } - headerParam{{$paramIdx}} = string(headerParamBuf{{$paramIdx}}) - {{end}} - {{if .IsStyled}} - headerParam{{$paramIdx}}, err = runtime.StyleParamWithOptions("{{.Style}}", {{.Explode}}, "{{.ParamName}}", {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationHeader, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"}) - if err != nil { - return nil, err - } - {{end}} - req.Header.Set("{{.ParamName}}", headerParam{{$paramIdx}}) - {{if .RequiresNilCheck}}}{{end}} - {{end}} - } -{{- end }}{{/* if .HeaderParams */}} - return req, nil -} - -{{end}}{{/* Range */}}