From 78fbdb142e2f6e7f0cd3ac75625c240bed16fe09 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Sun, 19 Jul 2026 13:13:19 -0700 Subject: [PATCH 1/5] refactor(templates): unify router codegen into shared skeletons with per-framework hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change unifies different router code generation where possible. Over the years, I've had to fix many divergences between nearly identical templates, and maybe it would be better to templetize routers based on categories and similarity, rather than one template per backend. Structural changes: * Two-layer template architecture. Generic skeletons (server-interface.tmpl, server-middleware.tmpl, server-handler.tmpl) hold the shared structure, with {{block}} defaults in the net/http (stdhttp) shape. Each framework contributes a small /hooks.tmpl of {{define}} overrides — path-param access, handler signature, registration call, etc. — parsed into a per-framework Clone() of the base template tree (buildServerTemplates in codegen.go). What differs per router is now explicit and named in one small file instead of hidden inside parallel near-copies. * Duplicate templates merged. chi/gorilla/stdhttp receivers were byte-identical and are now one receiver-stdlib.tmpl; same for the fiber v2/v3 handler. The echo v4/v5 and fiber v2/v3 pairs (interface, wrapper, receiver, register, strict interface, strict glue) are parameterized by ctx type/accessor hooks instead of duplicated per version. The webhook and callback initiators were 100% token-substitution copies and are now one initiator.tmpl driven by InitiatorTemplateData{Prefix, ...}. * Strict servers. A shared strict.responseHeaders partial replaces ~20 inline copies of the response-header-writing branch across the strict templates. GenerateStrictServer gained a guard against double-emitting shared types when multiple frameworks are enabled. * Client view model. New client_view.go precomputes per-operation method variants (suffix, signature, call args, doc comment — deprecation folded in), so client.tmpl, client-with-responses.tmpl and initiator.tmpl now range linearly over variants instead of re-deriving the WithBody/suffix/signature conditionals at every call site. client.tmpl conditionals drop from 75 to 39. The header-param serialization block is a shared partial. * Deliberately NOT unified, after attempting: gin/iris/echo middleware and registration (genuinely different error-handling and middleware-composition models) and the fiber/iris strict interfaces (different response write models). Forcing those under one skeleton would relocate complexity, not remove it; they keep their own templates. Templates shrink from 62 files / 6,623 lines to 48 files / 4,457 lines (-33%). Generated code is byte-identical except three deliberate deltas: * gorilla: inert `err :=` normalization in the required-header branch * echo v5 strict: non-required text bodies gain the `len(data) > 0` guard echo v4 already had * fiber v3 strict: gains the optional-body/EOF guards fiber v2 already had Note: template files were renamed/merged/removed, which affects output-options.user-templates overrides keyed to the old template names. Co-Authored-By: Claude Fable 5 --- .../multiplepackages/admin/server.gen.go | 1 + .../import-mapping/samepackage/server.gen.go | 1 + examples/minimal-server/chi/api/ping.gen.go | 1 + .../minimal-server/gorillamux/api/ping.gen.go | 1 + examples/overlay/api/ping.gen.go | 1 + .../chi/api/petstore-server.gen.go | 1 + .../gorilla/api/petstore-server.gen.go | 1 + .../strict/api/petstore-server.gen.go | 1 + .../bodies/responses/headers/headers.gen.go | 1 + internal/test/events/webhooks/chi/doc.go | 4 +- .../to_camel_case/name_normalizer.gen.go | 1 + .../name_normalizer.gen.go | 1 + .../name_normalizer.gen.go | 1 + .../name_normalizer.gen.go | 1 + .../unset/name_normalizer.gen.go | 1 + .../roundtrip/chi/gen/server.gen.go | 1 + .../roundtrip/gorilla/gen/server.gen.go | 1 + .../path_item_refs/bionicle/bionicle.gen.go | 1 + .../fooservice/fooservice.gen.go | 1 + .../gen/spec_base/issue.gen.go | 1 + .../external/pathalias/server.gen.go | 1 + .../removed_ref/gen/spec_base/issue.gen.go | 1 + .../default_response/gen/api/api.gen.go | 1 + .../header_ref/gen/api/api.gen.go | 1 + .../multipackage/pruned_deps/api.gen.go | 1 + .../response_cast/gen/spec_base/issue.gen.go | 1 + .../response_cast/gen/spec_other/issue.gen.go | 1 + .../test/schemas/nullable/nullable.gen.go | 1 + internal/test/schemas/objects/objects.gen.go | 1 + .../test/servers/routers/basic/basic.gen.go | 1 + .../test/servers/routers/smoke/chi/out.gen.go | 1 + .../servers/routers/smoke/gorilla/out.gen.go | 1 + .../test/servers/strict/chi/server.gen.go | 1 + .../test/servers/strict/echo5/server.gen.go | 12 +- .../test/servers/strict/gorilla/server.gen.go | 3 +- pkg/codegen/client_view.go | 130 +++++++++ pkg/codegen/codegen.go | 81 +++++- pkg/codegen/operations.go | 196 +++++++++---- pkg/codegen/template_helpers.go | 20 ++ pkg/codegen/templates/callback-initiator.tmpl | 236 --------------- pkg/codegen/templates/chi/chi-handler.tmpl | 50 ---- pkg/codegen/templates/chi/chi-interface.tmpl | 21 -- pkg/codegen/templates/chi/chi-middleware.tmpl | 272 ------------------ pkg/codegen/templates/chi/hooks.tmpl | 37 +++ pkg/codegen/templates/client-partials.tmpl | 45 +++ .../templates/client-with-responses.tmpl | 69 +---- pkg/codegen/templates/client.tmpl | 103 +------ .../templates/echo/echo-interface.tmpl | 9 - pkg/codegen/templates/echo/echo-receiver.tmpl | 4 +- pkg/codegen/templates/echo/echo-register.tmpl | 22 +- pkg/codegen/templates/echo/echo-wrappers.tmpl | 2 +- pkg/codegen/templates/echo/hooks.tmpl | 15 + .../templates/echo/v5/echo-interface.tmpl | 9 - .../templates/echo/v5/echo-receiver.tmpl | 93 ------ .../templates/echo/v5/echo-register.tmpl | 52 ---- .../templates/echo/v5/echo-wrappers.tmpl | 133 --------- pkg/codegen/templates/echo/v5/hooks.tmpl | 23 ++ .../{fiber-v3 => }/fiber-handler.tmpl | 0 .../templates/fiber-v3/fiber-interface.tmpl | 9 - .../templates/fiber-v3/fiber-middleware.tmpl | 198 ------------- .../templates/fiber-v3/fiber-receiver.tmpl | 100 ------- pkg/codegen/templates/fiber-v3/hooks.tmpl | 25 ++ .../templates/fiber/fiber-handler.tmpl | 27 -- .../templates/fiber/fiber-interface.tmpl | 9 - .../templates/fiber/fiber-middleware.tmpl | 10 +- .../templates/fiber/fiber-receiver.tmpl | 7 +- pkg/codegen/templates/fiber/hooks.tmpl | 15 + pkg/codegen/templates/gin/gin-interface.tmpl | 9 - pkg/codegen/templates/gin/hooks.tmpl | 15 + .../templates/gorilla/gorilla-interface.tmpl | 9 - .../templates/gorilla/gorilla-middleware.tmpl | 272 ------------------ .../templates/gorilla/gorilla-receiver.tmpl | 116 -------- .../templates/gorilla/gorilla-register.tmpl | 49 ---- pkg/codegen/templates/gorilla/hooks.tmpl | 34 +++ pkg/codegen/templates/initiator.tmpl | 202 +++++++++++++ pkg/codegen/templates/iris/hooks.tmpl | 15 + .../templates/iris/iris-interface.tmpl | 9 - ...chi-receiver.tmpl => receiver-stdlib.tmpl} | 0 pkg/codegen/templates/server-handler.tmpl | 69 +++++ pkg/codegen/templates/server-interface.tmpl | 25 ++ ...middleware.tmpl => server-middleware.tmpl} | 26 +- .../templates/stdhttp/std-http-handler.tmpl | 55 ---- .../templates/stdhttp/std-http-interface.tmpl | 9 - .../templates/stdhttp/std-http-receiver.tmpl | 116 -------- pkg/codegen/templates/strict/strict-echo.tmpl | 12 +- .../templates/strict/strict-echo5.tmpl | 116 -------- .../strict/strict-fiber-interface.tmpl | 34 +-- .../strict/strict-fiber-v3-interface.tmpl | 203 ------------- .../templates/strict/strict-fiber-v3.tmpl | 92 ------ .../templates/strict/strict-fiber.tmpl | 12 +- .../templates/strict/strict-interface.tmpl | 56 +--- .../strict/strict-iris-interface.tmpl | 28 +- .../templates/strict/strict-partials.tmpl | 27 ++ pkg/codegen/templates/webhook-initiator.tmpl | 235 --------------- 94 files changed, 1036 insertions(+), 2882 deletions(-) create mode 100644 pkg/codegen/client_view.go delete mode 100644 pkg/codegen/templates/callback-initiator.tmpl delete mode 100644 pkg/codegen/templates/chi/chi-handler.tmpl delete mode 100644 pkg/codegen/templates/chi/chi-interface.tmpl delete mode 100644 pkg/codegen/templates/chi/chi-middleware.tmpl create mode 100644 pkg/codegen/templates/chi/hooks.tmpl create mode 100644 pkg/codegen/templates/client-partials.tmpl delete mode 100644 pkg/codegen/templates/echo/echo-interface.tmpl create mode 100644 pkg/codegen/templates/echo/hooks.tmpl delete mode 100644 pkg/codegen/templates/echo/v5/echo-interface.tmpl delete mode 100644 pkg/codegen/templates/echo/v5/echo-receiver.tmpl delete mode 100644 pkg/codegen/templates/echo/v5/echo-register.tmpl delete mode 100644 pkg/codegen/templates/echo/v5/echo-wrappers.tmpl create mode 100644 pkg/codegen/templates/echo/v5/hooks.tmpl rename pkg/codegen/templates/{fiber-v3 => }/fiber-handler.tmpl (100%) delete mode 100644 pkg/codegen/templates/fiber-v3/fiber-interface.tmpl delete mode 100644 pkg/codegen/templates/fiber-v3/fiber-middleware.tmpl delete mode 100644 pkg/codegen/templates/fiber-v3/fiber-receiver.tmpl create mode 100644 pkg/codegen/templates/fiber-v3/hooks.tmpl delete mode 100644 pkg/codegen/templates/fiber/fiber-handler.tmpl delete mode 100644 pkg/codegen/templates/fiber/fiber-interface.tmpl create mode 100644 pkg/codegen/templates/fiber/hooks.tmpl delete mode 100644 pkg/codegen/templates/gin/gin-interface.tmpl create mode 100644 pkg/codegen/templates/gin/hooks.tmpl delete mode 100644 pkg/codegen/templates/gorilla/gorilla-interface.tmpl delete mode 100644 pkg/codegen/templates/gorilla/gorilla-middleware.tmpl delete mode 100644 pkg/codegen/templates/gorilla/gorilla-receiver.tmpl delete mode 100644 pkg/codegen/templates/gorilla/gorilla-register.tmpl create mode 100644 pkg/codegen/templates/gorilla/hooks.tmpl create mode 100644 pkg/codegen/templates/initiator.tmpl create mode 100644 pkg/codegen/templates/iris/hooks.tmpl delete mode 100644 pkg/codegen/templates/iris/iris-interface.tmpl rename pkg/codegen/templates/{chi/chi-receiver.tmpl => receiver-stdlib.tmpl} (100%) create mode 100644 pkg/codegen/templates/server-handler.tmpl create mode 100644 pkg/codegen/templates/server-interface.tmpl rename pkg/codegen/templates/{stdhttp/std-http-middleware.tmpl => server-middleware.tmpl} (85%) delete mode 100644 pkg/codegen/templates/stdhttp/std-http-handler.tmpl delete mode 100644 pkg/codegen/templates/stdhttp/std-http-interface.tmpl delete mode 100644 pkg/codegen/templates/stdhttp/std-http-receiver.tmpl delete mode 100644 pkg/codegen/templates/strict/strict-echo5.tmpl delete mode 100644 pkg/codegen/templates/strict/strict-fiber-v3-interface.tmpl delete mode 100644 pkg/codegen/templates/strict/strict-fiber-v3.tmpl create mode 100644 pkg/codegen/templates/strict/strict-partials.tmpl delete mode 100644 pkg/codegen/templates/webhook-initiator.tmpl 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..e73e9a11a7 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -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,36 @@ 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. +func NewInitiatorTemplateData(prefix string, ops []OperationDefinition) InitiatorTemplateData { + return InitiatorTemplateData{ + Prefix: prefix, + PrefixLower: strings.ToLower(prefix), + Operations: ops, + } +} + +// 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 { @@ -2249,7 +2285,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 +2299,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 +2313,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 +2327,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 +2341,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 +2356,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 +2374,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 +2388,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 +2402,66 @@ 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") - } - if opts.Generate.Echo5Server { - templates = append(templates, "strict/strict-interface.tmpl", "strict/strict-echo5.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"}, + } + + // The RequestObject/ResponseObject/StrictServerInterface types come from the + // interface template, which several frameworks share (strict-interface.tmpl + // is used by chi/gorilla/stdhttp, echo, gin and echo5). When more than one of + // those frameworks is enabled at once, emitting the interface template per + // framework would redeclare those types and fail to compile, so emit each + // distinct interface template only once. + var out strings.Builder + emittedInterface := make(map[string]bool) + for _, tgt := range targets { + if !tgt.enabled { + continue + } + names := make([]string, 0, 2) + if !emittedInterface[tgt.interfaceTmpl] { + emittedInterface[tgt.interfaceTmpl] = true + names = append(names, tgt.interfaceTmpl) + } + names = append(names, tgt.glueTmpl) + s, err := GenerateTemplates(names, tgt.tree, operations) + if err != nil { + return "", err + } + out.WriteString(s) } - - return GenerateTemplates(templates, t, operations) + return out.String(), nil } func GenerateStrictResponses(t *template.Template, responses []ResponseDefinition) (string, error) { @@ -2424,7 +2488,7 @@ 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) + return GenerateTemplates([]string{"initiator.tmpl"}, t, NewInitiatorTemplateData("Webhook", webhookOps)) } // GenerateCallbackInitiator generates the CallbackInitiator -- the @@ -2433,7 +2497,7 @@ 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) + return GenerateTemplates([]string{"initiator.tmpl"}, t, NewInitiatorTemplateData("Callback", callbackOps)) } // GenerateStdHTTPReceiver renders the merged stdhttp receiver template @@ -2444,22 +2508,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 +2531,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 +2558,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/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 */}} From 32667acddd443b8ad29a1b623dc77bf814076b75 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Sun, 19 Jul 2026 13:26:18 -0700 Subject: [PATCH 2/5] fix(codegen): reject path parameters in initiator operations Review finding (Greptile): if a webhook/callback operation carried path parameters, the initiator methods (driven by ClientMethodVariants) would forward path arguments that the typed request builders do not accept, generating uncompilable code. This is unreachable today -- Webhook- and CallbackOperationDefinitions only bind header/query/cookie parameters, silently dropping in:path -- but the template's correctness depended on that non-local invariant. Make the invariant explicit: NewInitiatorTemplateData now returns an error when any operation has path parameters, so a future change that starts populating them fails loudly at generation time instead of emitting broken code. Co-Authored-By: Claude Fable 5 --- pkg/codegen/operations.go | 29 +++++++++++++++++++++++++---- pkg/codegen/operations_test.go | 17 +++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index e73e9a11a7..ad3f822aa2 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -703,12 +703,25 @@ type InitiatorTemplateData struct { // NewInitiatorTemplateData builds the template input for the given // prefix ("Webhook" or "Callback") and operation list. -func NewInitiatorTemplateData(prefix string, ops []OperationDefinition) InitiatorTemplateData { +// +// 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 uphold this +// by only binding header/query/cookie parameters. +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- @@ -2488,7 +2501,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{"initiator.tmpl"}, t, NewInitiatorTemplateData("Webhook", webhookOps)) + data, err := NewInitiatorTemplateData("Webhook", webhookOps) + if err != nil { + return "", err + } + return GenerateTemplates([]string{"initiator.tmpl"}, t, data) } // GenerateCallbackInitiator generates the CallbackInitiator -- the @@ -2497,7 +2514,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{"initiator.tmpl"}, t, NewInitiatorTemplateData("Callback", 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 diff --git a/pkg/codegen/operations_test.go b/pkg/codegen/operations_test.go index 89f3b102a0..de13843f99 100644 --- a/pkg/codegen/operations_test.go +++ b/pkg/codegen/operations_test.go @@ -917,3 +917,20 @@ 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) +} From dc473c0662f03d0008ed44a21042b5ee06a95d9c Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Sun, 19 Jul 2026 13:31:55 -0700 Subject: [PATCH 3/5] fix(codegen): dedup strict interface by rendered output, not filename Review finding (Greptile): the strict-interface dedup guard keyed on template filename, assuming a filename match means an identical rendering. That's false for fiber v2 + v3, which share strict-fiber-interface.tmpl but render it against different template trees (fiber.ctxType hook: *fiber.Ctx vs fiber.Ctx). With both enabled, v2 emitted its interface and v3's glue then referenced Visit methods with the wrong context type -- uncompilable output. Dedup now compares the rendered interface text: identical renderings (chi/gorilla/stdhttp/echo/gin/echo5 sharing strict-interface.tmpl) are emitted once as before, and divergent renderings of the same template return a clear error, since such a combination can never compile. Single-framework output is byte-identical, including the inter-template separator. Co-Authored-By: Claude Fable 5 --- pkg/codegen/operations.go | 28 ++++++++++++++++-------- pkg/codegen/operations_test.go | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index ad3f822aa2..4b713eedad 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -2455,24 +2455,34 @@ func GenerateStrictServer(t *template.Template, serverTemplates map[string]*temp // is used by chi/gorilla/stdhttp, echo, gin and echo5). When more than one of // those frameworks is enabled at once, emitting the interface template per // framework would redeclare those types and fail to compile, so emit each - // distinct interface template only once. + // distinct interface template only once. The dedup must compare rendered + // output, not filenames: fiber v2 and v3 share strict-fiber-interface.tmpl + // but render it with different context types via the fiber.ctxType hook, so + // a filename match does not imply the second rendering is skippable. var out strings.Builder - emittedInterface := make(map[string]bool) + emittedInterface := make(map[string]string) for _, tgt := range targets { if !tgt.enabled { continue } - names := make([]string, 0, 2) - if !emittedInterface[tgt.interfaceTmpl] { - emittedInterface[tgt.interfaceTmpl] = true - names = append(names, tgt.interfaceTmpl) + iface, err := GenerateTemplates([]string{tgt.interfaceTmpl}, tgt.tree, operations) + if err != nil { + return "", err + } + if prev, seen := emittedInterface[tgt.interfaceTmpl]; !seen { + emittedInterface[tgt.interfaceTmpl] = iface + out.WriteString(iface) + // Same separator GenerateTemplates writes between two templates + // rendered in one call. + out.WriteString("\n") + } else if prev != iface { + return "", fmt.Errorf("the enabled strict servers render %s with incompatible types (fiber v2 and v3 differ in context type) and cannot be generated together", tgt.interfaceTmpl) } - names = append(names, tgt.glueTmpl) - s, err := GenerateTemplates(names, tgt.tree, operations) + glue, err := GenerateTemplates([]string{tgt.glueTmpl}, tgt.tree, operations) if err != nil { return "", err } - out.WriteString(s) + out.WriteString(glue) } return out.String(), nil } diff --git a/pkg/codegen/operations_test.go b/pkg/codegen/operations_test.go index de13843f99..fd782fe967 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" @@ -934,3 +936,40 @@ func TestNewInitiatorTemplateDataRejectsPathParams(t *testing.T) { 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"}} + + // Frameworks whose shared interface template renders identically may be + // combined; the interface must be emitted exactly once. + out, err := GenerateStrictServer(base, clones, ops, Configuration{ + Generate: GenerateOptions{ChiServer: true, GinServer: true}, + }) + require.NoError(t, err) + assert.Equal(t, 1, strings.Count(out, "type StrictServerInterface interface")) + + // Fiber v2 and v3 render the shared interface template with different + // context types; combining them can never compile and must error. + _, err = GenerateStrictServer(base, clones, ops, Configuration{ + Generate: GenerateOptions{FiberServer: true, FiberV3Server: true}, + }) + require.ErrorContains(t, err, "cannot be generated together") +} From 63e7e065d7ccd846d83f87d0125275bfb4222a06 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Sun, 19 Jul 2026 13:39:00 -0700 Subject: [PATCH 4/5] fix(codegen): reject webhook/callback path parameters at extraction Review finding (Greptile): the NewInitiatorTemplateData guard was unreachable -- Webhook/CallbackOperationDefinitions never populate PathParams, they silently dropped in:path parameters before the guard could see them. A spec declaring a path parameter on a webhook or callback operation generated an initiator that sends to targetURL verbatim, so callers could end up requesting a URL that still contains an unsubstituted placeholder. Reject in:path parameters in both extraction functions with an error naming the webhook/callback, operation, and parameter. The NewInitiatorTemplateData check stays as defense in depth for direct callers of the exported API. Behavior deltas: * Specs declaring in:path parameters on webhook/callback operations now fail generation with a clear error instead of silently dropping the parameter. No committed fixture or example is affected. Co-Authored-By: Claude Fable 5 --- pkg/codegen/operations.go | 23 ++++++++++++++-- pkg/codegen/operations_test.go | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index 4b713eedad..91cf27bfff 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -708,8 +708,9 @@ type InitiatorTemplateData struct { // 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 uphold this -// by only binding header/query/cookie parameters. +// 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 { @@ -1640,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) @@ -1782,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) diff --git a/pkg/codegen/operations_test.go b/pkg/codegen/operations_test.go index fd782fe967..301b651789 100644 --- a/pkg/codegen/operations_test.go +++ b/pkg/codegen/operations_test.go @@ -973,3 +973,52 @@ func TestGenerateStrictServerInterfaceDedup(t *testing.T) { }) require.ErrorContains(t, err, "cannot be generated together") } + +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"`) + }) + } +} From fa0c312f640c11f696184e8bf9091dc079ee78a1 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Sun, 19 Jul 2026 13:46:45 -0700 Subject: [PATCH 5/5] fix(codegen): enforce single server type in strict generation Review finding (Greptile): the strict interface dedup compared renderings only within the same template filename, so combinations like echo+fiber would emit both strict/strict-interface.tmpl and strict/strict-fiber-interface.tmpl, redeclaring StrictServerInterface, RequestObject and ResponseObject with incompatible visitor signatures. Rather than growing the cross-template comparison, mirror the invariant Configuration.Validate() already enforces ("only one server type is supported at a time"): GenerateStrictServer now errors when more than one strict target is enabled and emits the single chosen target plainly. This replaces the rendered-output dedup machinery entirely -- it was defending combinations that validated configurations cannot express -- and resolves the fiber-v2/v3 and cross-template collision scenarios with one rule. chi/gorilla/stdhttp remain a single target since they share both templates. Co-Authored-By: Claude Fable 5 --- pkg/codegen/operations.go | 59 +++++++++++++--------------------- pkg/codegen/operations_test.go | 22 +++++++------ 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index 91cf27bfff..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 @@ -2469,41 +2469,28 @@ func GenerateStrictServer(t *template.Template, serverTemplates map[string]*temp {opts.Generate.Echo5Server, serverTemplates["echo5"], "strict/strict-interface.tmpl", "strict/strict-echo.tmpl"}, } - // The RequestObject/ResponseObject/StrictServerInterface types come from the - // interface template, which several frameworks share (strict-interface.tmpl - // is used by chi/gorilla/stdhttp, echo, gin and echo5). When more than one of - // those frameworks is enabled at once, emitting the interface template per - // framework would redeclare those types and fail to compile, so emit each - // distinct interface template only once. The dedup must compare rendered - // output, not filenames: fiber v2 and v3 share strict-fiber-interface.tmpl - // but render it with different context types via the fiber.ctxType hook, so - // a filename match does not imply the second rendering is skippable. - var out strings.Builder - emittedInterface := make(map[string]string) - for _, tgt := range targets { - if !tgt.enabled { + // 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 } - iface, err := GenerateTemplates([]string{tgt.interfaceTmpl}, tgt.tree, operations) - if err != nil { - return "", err - } - if prev, seen := emittedInterface[tgt.interfaceTmpl]; !seen { - emittedInterface[tgt.interfaceTmpl] = iface - out.WriteString(iface) - // Same separator GenerateTemplates writes between two templates - // rendered in one call. - out.WriteString("\n") - } else if prev != iface { - return "", fmt.Errorf("the enabled strict servers render %s with incompatible types (fiber v2 and v3 differ in context type) and cannot be generated together", tgt.interfaceTmpl) - } - glue, err := GenerateTemplates([]string{tgt.glueTmpl}, tgt.tree, operations) - if err != nil { - return "", err + if chosen != nil { + return "", fmt.Errorf("only one server type is supported at a time for strict server generation") } - out.WriteString(glue) + chosen = &targets[i] + } + if chosen == nil { + return "", nil } - return out.String(), nil + return GenerateTemplates([]string{chosen.interfaceTmpl, chosen.glueTmpl}, chosen.tree, operations) } func GenerateStrictResponses(t *template.Template, responses []ResponseDefinition) (string, error) { diff --git a/pkg/codegen/operations_test.go b/pkg/codegen/operations_test.go index 301b651789..0880c36aef 100644 --- a/pkg/codegen/operations_test.go +++ b/pkg/codegen/operations_test.go @@ -958,20 +958,24 @@ func TestGenerateStrictServerInterfaceDedup(t *testing.T) { base, clones := buildStrictTestTrees(t) ops := []OperationDefinition{{OperationId: "Ping"}} - // Frameworks whose shared interface template renders identically may be - // combined; the interface must be emitted exactly once. + // A single enabled server emits the interface exactly once. out, err := GenerateStrictServer(base, clones, ops, Configuration{ - Generate: GenerateOptions{ChiServer: true, GinServer: true}, + Generate: GenerateOptions{ChiServer: true}, }) require.NoError(t, err) assert.Equal(t, 1, strings.Count(out, "type StrictServerInterface interface")) - // Fiber v2 and v3 render the shared interface template with different - // context types; combining them can never compile and must error. - _, err = GenerateStrictServer(base, clones, ops, Configuration{ - Generate: GenerateOptions{FiberServer: true, FiberV3Server: true}, - }) - require.ErrorContains(t, err, "cannot be generated together") + // 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) {