From 5206145c6c6fec742fcdb10d3caa2042a95baf2d Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Fri, 6 Feb 2026 17:04:08 -0800 Subject: [PATCH 01/44] OpenAPI v3.1 support, experimental re-implementation using libopenapi (#2197) This is a prototype implementation of a future versions of oapi-codegen. It's almost a full rewrite, heavily inspired by previous code, and lessons learned. - much more flexibility in configuring generated code - move from kin-openapi to libopenapi to support 3.1 and 3.2 specs - webhook support - callback support - incompatible codegen changes to aggregate types (allOf, anyOf, oneOf) - many existing codegen bugs around schemas fixed --- Makefile | 12 +- README.md | 4 +- experimental/Configuration.md | 246 ++ experimental/Makefile | 35 + experimental/README.md | 164 ++ experimental/cmd/oapi-codegen/main.go | 105 + experimental/examples/callback/client/main.go | 144 ++ experimental/examples/callback/config.yaml | 6 + experimental/examples/callback/doc.go | 13 + experimental/examples/callback/server/main.go | 116 + experimental/examples/callback/tree-farm.yaml | 138 ++ .../examples/callback/treefarm.gen.go | 503 ++++ .../examples/petstore-expanded/chi/Makefile | 35 + .../examples/petstore-expanded/chi/go.mod | 11 + .../examples/petstore-expanded/chi/go.sum | 4 + .../examples/petstore-expanded/chi/main.go | 40 + .../petstore-expanded/chi/server/petstore.go | 135 ++ .../chi/server/server.config.yaml | 6 + .../chi/server/server.gen.go | 1039 ++++++++ .../client/client.config.yaml | 8 + .../petstore-expanded/client/client.gen.go | 1225 ++++++++++ .../petstore-expanded/client/client_test.go | 161 ++ .../client/validator/main.go | 143 ++ .../examples/petstore-expanded/doc.go | 2 + .../petstore-expanded/echo-v4/Makefile | 35 + .../examples/petstore-expanded/echo-v4/go.mod | 23 + .../examples/petstore-expanded/echo-v4/go.sum | 33 + .../petstore-expanded/echo-v4/main.go | 34 + .../echo-v4/server/petstore.go | 123 + .../echo-v4/server/server.config.yaml | 6 + .../echo-v4/server/server.gen.go | 976 ++++++++ .../examples/petstore-expanded/echo/Makefile | 35 + .../examples/petstore-expanded/echo/go.mod | 11 + .../examples/petstore-expanded/echo/go.sum | 16 + .../examples/petstore-expanded/echo/main.go | 34 + .../petstore-expanded/echo/server/petstore.go | 123 + .../echo/server/server.config.yaml | 6 + .../echo/server/server.gen.go | 977 ++++++++ .../examples/petstore-expanded/fiber/Makefile | 35 + .../examples/petstore-expanded/fiber/go.mod | 31 + .../examples/petstore-expanded/fiber/go.sum | 51 + .../examples/petstore-expanded/fiber/main.go | 35 + .../fiber/server/petstore.go | 122 + .../fiber/server/server.config.yaml | 6 + .../fiber/server/server.gen.go | 969 ++++++++ .../examples/petstore-expanded/generate.go | 11 + .../examples/petstore-expanded/gin/Makefile | 35 + .../examples/petstore-expanded/gin/go.mod | 40 + .../examples/petstore-expanded/gin/go.sum | 92 + .../examples/petstore-expanded/gin/main.go | 40 + .../petstore-expanded/gin/server/petstore.go | 126 + .../gin/server/server.config.yaml | 6 + .../gin/server/server.gen.go | 1007 ++++++++ .../petstore-expanded/gorilla/Makefile | 35 + .../examples/petstore-expanded/gorilla/go.mod | 11 + .../examples/petstore-expanded/gorilla/go.sum | 4 + .../petstore-expanded/gorilla/main.go | 40 + .../gorilla/server/petstore.go | 135 ++ .../gorilla/server/server.config.yaml | 6 + .../gorilla/server/server.gen.go | 1035 ++++++++ .../examples/petstore-expanded/iris/Makefile | 35 + .../examples/petstore-expanded/iris/go.mod | 55 + .../examples/petstore-expanded/iris/go.sum | 178 ++ .../examples/petstore-expanded/iris/main.go | 35 + .../petstore-expanded/iris/server/petstore.go | 130 + .../iris/server/server.config.yaml | 6 + .../iris/server/server.gen.go | 973 ++++++++ .../petstore-expanded/models.config.yaml | 2 + .../petstore-expanded/petstore-expanded.yaml | 164 ++ .../petstore-expanded/petstore.gen.go | 117 + .../petstore-expanded/stdhttp/Makefile | 35 + .../examples/petstore-expanded/stdhttp/go.mod | 7 + .../examples/petstore-expanded/stdhttp/go.sum | 2 + .../petstore-expanded/stdhttp/main.go | 39 + .../stdhttp/server/petstore.go | 163 ++ .../stdhttp/server/server.config.yaml | 7 + .../stdhttp/server/server.gen.go | 1009 ++++++++ experimental/examples/webhook/client/main.go | 150 ++ experimental/examples/webhook/config.yaml | 6 + experimental/examples/webhook/doc.go | 13 + .../examples/webhook/door-badge-reader.yaml | 139 ++ .../examples/webhook/doorbadge.gen.go | 1069 +++++++++ experimental/examples/webhook/server/main.go | 186 ++ experimental/go.mod | 27 + experimental/go.sum | 41 + experimental/internal/codegen/clientgen.go | 349 +++ .../internal/codegen/clientgen_test.go | 188 ++ experimental/internal/codegen/codegen.go | 1101 +++++++++ .../internal/codegen/configuration.go | 317 +++ .../internal/codegen/configuration_test.go | 152 ++ experimental/internal/codegen/extension.go | 337 +++ .../codegen/extension_integration_test.go | 201 ++ .../internal/codegen/extension_test.go | 180 ++ experimental/internal/codegen/gather.go | 621 +++++ .../internal/codegen/gather_operations.go | 864 +++++++ experimental/internal/codegen/identifiers.go | 125 + .../internal/codegen/identifiers_test.go | 129 + experimental/internal/codegen/initiatorgen.go | 216 ++ experimental/internal/codegen/inline.go | 92 + experimental/internal/codegen/inline_test.go | 139 ++ experimental/internal/codegen/namemangling.go | 420 ++++ .../internal/codegen/namemangling_test.go | 195 ++ experimental/internal/codegen/operation.go | 287 +++ experimental/internal/codegen/output.go | 764 ++++++ experimental/internal/codegen/paramgen.go | 178 ++ .../internal/codegen/paramgen_test.go | 161 ++ experimental/internal/codegen/receivergen.go | 131 + experimental/internal/codegen/schema.go | 117 + experimental/internal/codegen/schemanames.go | 812 +++++++ experimental/internal/codegen/servergen.go | 180 ++ .../codegen/skip_external_ref_test.go | 68 + experimental/internal/codegen/structtags.go | 172 ++ .../internal/codegen/templates/embed.go | 9 + .../templates/files/client/base.go.tmpl | 95 + .../templates/files/client/interface.go.tmpl | 17 + .../templates/files/client/methods.go.tmpl | 40 + .../files/client/request_builders.go.tmpl | 177 ++ .../templates/files/client/simple.go.tmpl | 121 + .../templates/files/initiator/base.go.tmpl | 73 + .../files/initiator/interface.go.tmpl | 17 + .../templates/files/initiator/methods.go.tmpl | 41 + .../files/initiator/request_builders.go.tmpl | 149 ++ .../templates/files/initiator/simple.go.tmpl | 121 + .../files/params/bind_deep_object.go.tmpl | 271 +++ .../templates/files/params/bind_form.go.tmpl | 38 + .../files/params/bind_form_explode.go.tmpl | 145 ++ .../templates/files/params/bind_label.go.tmpl | 46 + .../files/params/bind_label_explode.go.tmpl | 51 + .../files/params/bind_matrix.go.tmpl | 47 + .../files/params/bind_matrix_explode.go.tmpl | 65 + .../files/params/bind_pipe_delimited.go.tmpl | 33 + .../bind_pipe_delimited_explode.go.tmpl | 9 + .../files/params/bind_simple.go.tmpl | 38 + .../files/params/bind_simple_explode.go.tmpl | 38 + .../files/params/bind_space_delimited.go.tmpl | 33 + .../bind_space_delimited_explode.go.tmpl | 9 + .../templates/files/params/helpers.go.tmpl | 260 ++ .../files/params/style_deep_object.go.tmpl | 74 + .../templates/files/params/style_form.go.tmpl | 132 ++ .../files/params/style_form_explode.go.tmpl | 130 + .../files/params/style_label.go.tmpl | 129 + .../files/params/style_label_explode.go.tmpl | 129 + .../files/params/style_matrix.go.tmpl | 132 ++ .../files/params/style_matrix_explode.go.tmpl | 130 + .../files/params/style_pipe_delimited.go.tmpl | 66 + .../style_pipe_delimited_explode.go.tmpl | 66 + .../files/params/style_simple.go.tmpl | 158 ++ .../files/params/style_simple_explode.go.tmpl | 128 + .../params/style_space_delimited.go.tmpl | 66 + .../style_space_delimited_explode.go.tmpl | 66 + .../files/server/chi/handler.go.tmpl | 59 + .../files/server/chi/interface.go.tmpl | 24 + .../files/server/chi/receiver.go.tmpl | 95 + .../files/server/chi/wrapper.go.tmpl | 166 ++ .../files/server/echo-v4/handler.go.tmpl | 34 + .../files/server/echo-v4/interface.go.tmpl | 24 + .../files/server/echo-v4/receiver.go.tmpl | 72 + .../files/server/echo-v4/wrapper.go.tmpl | 134 ++ .../files/server/echo/handler.go.tmpl | 35 + .../files/server/echo/interface.go.tmpl | 24 + .../files/server/echo/receiver.go.tmpl | 72 + .../files/server/echo/wrapper.go.tmpl | 134 ++ .../templates/files/server/errors.go.tmpl | 79 + .../files/server/fiber/handler.go.tmpl | 31 + .../files/server/fiber/interface.go.tmpl | 24 + .../files/server/fiber/receiver.go.tmpl | 71 + .../files/server/fiber/wrapper.go.tmpl | 137 ++ .../files/server/gin/handler.go.tmpl | 37 + .../files/server/gin/interface.go.tmpl | 24 + .../files/server/gin/receiver.go.tmpl | 78 + .../files/server/gin/wrapper.go.tmpl | 161 ++ .../files/server/gorilla/handler.go.tmpl | 57 + .../files/server/gorilla/interface.go.tmpl | 24 + .../files/server/gorilla/receiver.go.tmpl | 95 + .../files/server/gorilla/wrapper.go.tmpl | 169 ++ .../files/server/iris/handler.go.tmpl | 28 + .../files/server/iris/interface.go.tmpl | 24 + .../files/server/iris/receiver.go.tmpl | 84 + .../files/server/iris/wrapper.go.tmpl | 160 ++ .../files/server/param_types.go.tmpl | 24 + .../files/server/stdhttp/handler.go.tmpl | 63 + .../files/server/stdhttp/interface.go.tmpl | 13 + .../files/server/stdhttp/receiver.go.tmpl | 108 + .../files/server/stdhttp/wrapper.go.tmpl | 166 ++ .../codegen/templates/files/types/date.tmpl | 38 + .../codegen/templates/files/types/email.tmpl | 43 + .../codegen/templates/files/types/file.tmpl | 64 + .../templates/files/types/nullable.tmpl | 104 + .../codegen/templates/files/types/uuid.tmpl | 3 + .../internal/codegen/templates/funcs.go | 151 ++ .../internal/codegen/templates/registry.go | 909 +++++++ .../codegen/templates/test/types/date.gen.go | 46 + .../codegen/templates/test/types/date_test.go | 65 + .../codegen/templates/test/types/email.gen.go | 52 + .../templates/test/types/email_test.go | 176 ++ .../codegen/templates/test/types/file.gen.go | 74 + .../codegen/templates/test/types/file_test.go | 54 + .../codegen/templates/test/types/generate.go | 3 + .../templates/test/types/gentypes/main.go | 109 + .../codegen/templates/test/types/uuid.gen.go | 10 + .../codegen/templates/test/types/uuid_test.go | 53 + .../codegen/test/comprehensive/doc.go | 3 + .../comprehensive/output/comprehensive.gen.go | 2098 +++++++++++++++++ .../test/default_values/default_values.yaml | 158 ++ .../output/default_values.gen.go | 514 ++++ .../output/default_values_test.go | 324 +++ .../codegen/test/external_ref/config.yaml | 5 + .../test/external_ref/external_ref_test.go | 62 + .../test/external_ref/packagea/config.yaml | 4 + .../test/external_ref/packagea/spec.gen.go | 22 + .../test/external_ref/packagea/spec.yaml | 14 + .../test/external_ref/packageb/config.yaml | 2 + .../test/external_ref/packageb/spec.gen.go | 14 + .../test/external_ref/packageb/spec.yaml | 12 + .../codegen/test/external_ref/spec.gen.go | 26 + .../codegen/test/external_ref/spec.yaml | 14 + .../codegen/test/files/comprehensive.yaml | 861 +++++++ .../codegen/test/issues/issue_1029/doc.go | 5 + .../issues/issue_1029/output/types.gen.go | 186 ++ .../issues/issue_1029/output/types_test.go | 89 + .../codegen/test/issues/issue_1029/spec.yaml | 29 + .../codegen/test/issues/issue_1039/doc.go | 5 + .../issues/issue_1039/output/types.gen.go | 291 +++ .../issues/issue_1039/output/types_test.go | 266 +++ .../codegen/test/issues/issue_1039/spec.yaml | 86 + .../codegen/test/issues/issue_1397/doc.go | 5 + .../issues/issue_1397/output/types.gen.go | 114 + .../issues/issue_1397/output/types_test.go | 81 + .../codegen/test/issues/issue_1397/spec.yaml | 57 + .../codegen/test/issues/issue_1429/doc.go | 5 + .../issues/issue_1429/output/types.gen.go | 149 ++ .../issues/issue_1429/output/types_test.go | 37 + .../codegen/test/issues/issue_1429/spec.yaml | 33 + .../codegen/test/issues/issue_1496/doc.go | 5 + .../issues/issue_1496/output/types.gen.go | 168 ++ .../issues/issue_1496/output/types_test.go | 41 + .../codegen/test/issues/issue_1496/spec.yaml | 40 + .../codegen/test/issues/issue_1710/doc.go | 5 + .../issues/issue_1710/output/types.gen.go | 212 ++ .../issues/issue_1710/output/types_test.go | 102 + .../codegen/test/issues/issue_1710/spec.yaml | 76 + .../codegen/test/issues/issue_193/doc.go | 5 + .../test/issues/issue_193/output/types.gen.go | 71 + .../issues/issue_193/output/types_test.go | 76 + .../codegen/test/issues/issue_193/spec.yaml | 27 + .../codegen/test/issues/issue_2102/doc.go | 5 + .../issues/issue_2102/output/types.gen.go | 82 + .../issues/issue_2102/output/types_test.go | 52 + .../codegen/test/issues/issue_2102/spec.yaml | 39 + .../codegen/test/issues/issue_312/doc.go | 6 + .../test/issues/issue_312/output/types.gen.go | 97 + .../issues/issue_312/output/types_test.go | 72 + .../codegen/test/issues/issue_312/spec.yaml | 86 + .../codegen/test/issues/issue_502/doc.go | 5 + .../test/issues/issue_502/output/types.gen.go | 228 ++ .../issues/issue_502/output/types_test.go | 107 + .../codegen/test/issues/issue_502/spec.yaml | 29 + .../codegen/test/issues/issue_52/doc.go | 5 + .../test/issues/issue_52/output/types.gen.go | 87 + .../test/issues/issue_52/output/types_test.go | 64 + .../codegen/test/issues/issue_52/spec.yaml | 41 + .../codegen/test/issues/issue_579/doc.go | 5 + .../test/issues/issue_579/output/types.gen.go | 110 + .../issues/issue_579/output/types_test.go | 64 + .../codegen/test/issues/issue_579/spec.yaml | 30 + .../codegen/test/issues/issue_697/doc.go | 5 + .../test/issues/issue_697/output/types.gen.go | 81 + .../issues/issue_697/output/types_test.go | 58 + .../codegen/test/issues/issue_697/spec.yaml | 27 + .../codegen/test/issues/issue_775/doc.go | 5 + .../test/issues/issue_775/output/types.gen.go | 86 + .../issues/issue_775/output/types_test.go | 37 + .../codegen/test/issues/issue_775/spec.yaml | 22 + .../codegen/test/issues/issue_832/doc.go | 5 + .../test/issues/issue_832/output/types.gen.go | 91 + .../issues/issue_832/output/types_test.go | 62 + .../codegen/test/issues/issue_832/spec.yaml | 45 + .../issue_head_digit_operation_id/doc.go | 5 + .../output/types.gen.go | 69 + .../output/types_test.go | 50 + .../issue_head_digit_operation_id/spec.yaml | 20 + .../issues/issue_illegal_enum_names/doc.go | 5 + .../output/types.gen.go | 80 + .../output/types_test.go | 51 + .../issues/issue_illegal_enum_names/spec.yaml | 37 + .../codegen/test/nested_aggregate/doc.go | 3 + .../nested_aggregate/nested_aggregate.yaml | 62 + .../output/nested_aggregate.gen.go | 400 ++++ .../output/nested_aggregate_test.go | 332 +++ experimental/internal/codegen/typegen.go | 918 ++++++++ experimental/internal/codegen/typemapping.go | 98 + experimental/internal/codegen/typenames.go | 1 + scripts/foreach-module.sh | 20 + 293 files changed, 41797 insertions(+), 7 deletions(-) create mode 100644 experimental/Configuration.md create mode 100644 experimental/Makefile create mode 100644 experimental/README.md create mode 100644 experimental/cmd/oapi-codegen/main.go create mode 100644 experimental/examples/callback/client/main.go create mode 100644 experimental/examples/callback/config.yaml create mode 100644 experimental/examples/callback/doc.go create mode 100644 experimental/examples/callback/server/main.go create mode 100644 experimental/examples/callback/tree-farm.yaml create mode 100644 experimental/examples/callback/treefarm.gen.go create mode 100644 experimental/examples/petstore-expanded/chi/Makefile create mode 100644 experimental/examples/petstore-expanded/chi/go.mod create mode 100644 experimental/examples/petstore-expanded/chi/go.sum create mode 100644 experimental/examples/petstore-expanded/chi/main.go create mode 100644 experimental/examples/petstore-expanded/chi/server/petstore.go create mode 100644 experimental/examples/petstore-expanded/chi/server/server.config.yaml create mode 100644 experimental/examples/petstore-expanded/chi/server/server.gen.go create mode 100644 experimental/examples/petstore-expanded/client/client.config.yaml create mode 100644 experimental/examples/petstore-expanded/client/client.gen.go create mode 100644 experimental/examples/petstore-expanded/client/client_test.go create mode 100644 experimental/examples/petstore-expanded/client/validator/main.go create mode 100644 experimental/examples/petstore-expanded/doc.go create mode 100644 experimental/examples/petstore-expanded/echo-v4/Makefile create mode 100644 experimental/examples/petstore-expanded/echo-v4/go.mod create mode 100644 experimental/examples/petstore-expanded/echo-v4/go.sum create mode 100644 experimental/examples/petstore-expanded/echo-v4/main.go create mode 100644 experimental/examples/petstore-expanded/echo-v4/server/petstore.go create mode 100644 experimental/examples/petstore-expanded/echo-v4/server/server.config.yaml create mode 100644 experimental/examples/petstore-expanded/echo-v4/server/server.gen.go create mode 100644 experimental/examples/petstore-expanded/echo/Makefile create mode 100644 experimental/examples/petstore-expanded/echo/go.mod create mode 100644 experimental/examples/petstore-expanded/echo/go.sum create mode 100644 experimental/examples/petstore-expanded/echo/main.go create mode 100644 experimental/examples/petstore-expanded/echo/server/petstore.go create mode 100644 experimental/examples/petstore-expanded/echo/server/server.config.yaml create mode 100644 experimental/examples/petstore-expanded/echo/server/server.gen.go create mode 100644 experimental/examples/petstore-expanded/fiber/Makefile create mode 100644 experimental/examples/petstore-expanded/fiber/go.mod create mode 100644 experimental/examples/petstore-expanded/fiber/go.sum create mode 100644 experimental/examples/petstore-expanded/fiber/main.go create mode 100644 experimental/examples/petstore-expanded/fiber/server/petstore.go create mode 100644 experimental/examples/petstore-expanded/fiber/server/server.config.yaml create mode 100644 experimental/examples/petstore-expanded/fiber/server/server.gen.go create mode 100644 experimental/examples/petstore-expanded/generate.go create mode 100644 experimental/examples/petstore-expanded/gin/Makefile create mode 100644 experimental/examples/petstore-expanded/gin/go.mod create mode 100644 experimental/examples/petstore-expanded/gin/go.sum create mode 100644 experimental/examples/petstore-expanded/gin/main.go create mode 100644 experimental/examples/petstore-expanded/gin/server/petstore.go create mode 100644 experimental/examples/petstore-expanded/gin/server/server.config.yaml create mode 100644 experimental/examples/petstore-expanded/gin/server/server.gen.go create mode 100644 experimental/examples/petstore-expanded/gorilla/Makefile create mode 100644 experimental/examples/petstore-expanded/gorilla/go.mod create mode 100644 experimental/examples/petstore-expanded/gorilla/go.sum create mode 100644 experimental/examples/petstore-expanded/gorilla/main.go create mode 100644 experimental/examples/petstore-expanded/gorilla/server/petstore.go create mode 100644 experimental/examples/petstore-expanded/gorilla/server/server.config.yaml create mode 100644 experimental/examples/petstore-expanded/gorilla/server/server.gen.go create mode 100644 experimental/examples/petstore-expanded/iris/Makefile create mode 100644 experimental/examples/petstore-expanded/iris/go.mod create mode 100644 experimental/examples/petstore-expanded/iris/go.sum create mode 100644 experimental/examples/petstore-expanded/iris/main.go create mode 100644 experimental/examples/petstore-expanded/iris/server/petstore.go create mode 100644 experimental/examples/petstore-expanded/iris/server/server.config.yaml create mode 100644 experimental/examples/petstore-expanded/iris/server/server.gen.go create mode 100644 experimental/examples/petstore-expanded/models.config.yaml create mode 100644 experimental/examples/petstore-expanded/petstore-expanded.yaml create mode 100644 experimental/examples/petstore-expanded/petstore.gen.go create mode 100644 experimental/examples/petstore-expanded/stdhttp/Makefile create mode 100644 experimental/examples/petstore-expanded/stdhttp/go.mod create mode 100644 experimental/examples/petstore-expanded/stdhttp/go.sum create mode 100644 experimental/examples/petstore-expanded/stdhttp/main.go create mode 100644 experimental/examples/petstore-expanded/stdhttp/server/petstore.go create mode 100644 experimental/examples/petstore-expanded/stdhttp/server/server.config.yaml create mode 100644 experimental/examples/petstore-expanded/stdhttp/server/server.gen.go create mode 100644 experimental/examples/webhook/client/main.go create mode 100644 experimental/examples/webhook/config.yaml create mode 100644 experimental/examples/webhook/doc.go create mode 100644 experimental/examples/webhook/door-badge-reader.yaml create mode 100644 experimental/examples/webhook/doorbadge.gen.go create mode 100644 experimental/examples/webhook/server/main.go create mode 100644 experimental/go.mod create mode 100644 experimental/go.sum create mode 100644 experimental/internal/codegen/clientgen.go create mode 100644 experimental/internal/codegen/clientgen_test.go create mode 100644 experimental/internal/codegen/codegen.go create mode 100644 experimental/internal/codegen/configuration.go create mode 100644 experimental/internal/codegen/configuration_test.go create mode 100644 experimental/internal/codegen/extension.go create mode 100644 experimental/internal/codegen/extension_integration_test.go create mode 100644 experimental/internal/codegen/extension_test.go create mode 100644 experimental/internal/codegen/gather.go create mode 100644 experimental/internal/codegen/gather_operations.go create mode 100644 experimental/internal/codegen/identifiers.go create mode 100644 experimental/internal/codegen/identifiers_test.go create mode 100644 experimental/internal/codegen/initiatorgen.go create mode 100644 experimental/internal/codegen/inline.go create mode 100644 experimental/internal/codegen/inline_test.go create mode 100644 experimental/internal/codegen/namemangling.go create mode 100644 experimental/internal/codegen/namemangling_test.go create mode 100644 experimental/internal/codegen/operation.go create mode 100644 experimental/internal/codegen/output.go create mode 100644 experimental/internal/codegen/paramgen.go create mode 100644 experimental/internal/codegen/paramgen_test.go create mode 100644 experimental/internal/codegen/receivergen.go create mode 100644 experimental/internal/codegen/schema.go create mode 100644 experimental/internal/codegen/schemanames.go create mode 100644 experimental/internal/codegen/servergen.go create mode 100644 experimental/internal/codegen/skip_external_ref_test.go create mode 100644 experimental/internal/codegen/structtags.go create mode 100644 experimental/internal/codegen/templates/embed.go create mode 100644 experimental/internal/codegen/templates/files/client/base.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/client/interface.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/client/methods.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/client/request_builders.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/client/simple.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/initiator/base.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/initiator/interface.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/initiator/methods.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/initiator/request_builders.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/initiator/simple.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/bind_deep_object.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/bind_form.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/bind_form_explode.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/bind_label.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/bind_label_explode.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/bind_matrix.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/bind_matrix_explode.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/bind_pipe_delimited.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/bind_pipe_delimited_explode.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/bind_simple.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/bind_simple_explode.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/bind_space_delimited.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/bind_space_delimited_explode.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/helpers.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/style_deep_object.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/style_form.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/style_form_explode.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/style_label.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/style_label_explode.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/style_matrix.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/style_matrix_explode.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/style_pipe_delimited.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/style_pipe_delimited_explode.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/style_simple.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/style_simple_explode.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/style_space_delimited.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/params/style_space_delimited_explode.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/chi/handler.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/chi/interface.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/chi/receiver.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/chi/wrapper.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/echo-v4/handler.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/echo-v4/interface.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/echo-v4/receiver.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/echo-v4/wrapper.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/echo/handler.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/echo/interface.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/echo/receiver.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/echo/wrapper.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/errors.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/fiber/handler.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/fiber/interface.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/fiber/receiver.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/fiber/wrapper.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/gin/handler.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/gin/interface.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/gin/receiver.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/gin/wrapper.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/gorilla/handler.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/gorilla/interface.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/gorilla/receiver.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/gorilla/wrapper.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/iris/handler.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/iris/interface.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/iris/receiver.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/iris/wrapper.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/param_types.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/stdhttp/handler.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/stdhttp/interface.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/stdhttp/receiver.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/server/stdhttp/wrapper.go.tmpl create mode 100644 experimental/internal/codegen/templates/files/types/date.tmpl create mode 100644 experimental/internal/codegen/templates/files/types/email.tmpl create mode 100644 experimental/internal/codegen/templates/files/types/file.tmpl create mode 100644 experimental/internal/codegen/templates/files/types/nullable.tmpl create mode 100644 experimental/internal/codegen/templates/files/types/uuid.tmpl create mode 100644 experimental/internal/codegen/templates/funcs.go create mode 100644 experimental/internal/codegen/templates/registry.go create mode 100644 experimental/internal/codegen/templates/test/types/date.gen.go create mode 100644 experimental/internal/codegen/templates/test/types/date_test.go create mode 100644 experimental/internal/codegen/templates/test/types/email.gen.go create mode 100644 experimental/internal/codegen/templates/test/types/email_test.go create mode 100644 experimental/internal/codegen/templates/test/types/file.gen.go create mode 100644 experimental/internal/codegen/templates/test/types/file_test.go create mode 100644 experimental/internal/codegen/templates/test/types/generate.go create mode 100644 experimental/internal/codegen/templates/test/types/gentypes/main.go create mode 100644 experimental/internal/codegen/templates/test/types/uuid.gen.go create mode 100644 experimental/internal/codegen/templates/test/types/uuid_test.go create mode 100644 experimental/internal/codegen/test/comprehensive/doc.go create mode 100644 experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go create mode 100644 experimental/internal/codegen/test/default_values/default_values.yaml create mode 100644 experimental/internal/codegen/test/default_values/output/default_values.gen.go create mode 100644 experimental/internal/codegen/test/default_values/output/default_values_test.go create mode 100644 experimental/internal/codegen/test/external_ref/config.yaml create mode 100644 experimental/internal/codegen/test/external_ref/external_ref_test.go create mode 100644 experimental/internal/codegen/test/external_ref/packagea/config.yaml create mode 100644 experimental/internal/codegen/test/external_ref/packagea/spec.gen.go create mode 100644 experimental/internal/codegen/test/external_ref/packagea/spec.yaml create mode 100644 experimental/internal/codegen/test/external_ref/packageb/config.yaml create mode 100644 experimental/internal/codegen/test/external_ref/packageb/spec.gen.go create mode 100644 experimental/internal/codegen/test/external_ref/packageb/spec.yaml create mode 100644 experimental/internal/codegen/test/external_ref/spec.gen.go create mode 100644 experimental/internal/codegen/test/external_ref/spec.yaml create mode 100644 experimental/internal/codegen/test/files/comprehensive.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_1029/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_1029/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_1029/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_1039/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_1039/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_1039/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_1397/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_1397/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_1397/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_1429/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_1429/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_1429/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_1496/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_1496/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_1496/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_1710/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_1710/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_1710/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_193/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_193/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_193/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_193/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_2102/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_2102/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_2102/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_312/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_312/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_312/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_312/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_502/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_502/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_502/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_502/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_52/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_52/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_52/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_52/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_579/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_579/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_579/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_579/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_697/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_697/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_697/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_697/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_775/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_775/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_775/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_775/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_832/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_832/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_832/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_832/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_head_digit_operation_id/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_head_digit_operation_id/spec.yaml create mode 100644 experimental/internal/codegen/test/issues/issue_illegal_enum_names/doc.go create mode 100644 experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go create mode 100644 experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types_test.go create mode 100644 experimental/internal/codegen/test/issues/issue_illegal_enum_names/spec.yaml create mode 100644 experimental/internal/codegen/test/nested_aggregate/doc.go create mode 100644 experimental/internal/codegen/test/nested_aggregate/nested_aggregate.yaml create mode 100644 experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go create mode 100644 experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate_test.go create mode 100644 experimental/internal/codegen/typegen.go create mode 100644 experimental/internal/codegen/typemapping.go create mode 100644 experimental/internal/codegen/typenames.go create mode 100755 scripts/foreach-module.sh diff --git a/Makefile b/Makefile index 069527cf0e..89b6de0f6d 100644 --- a/Makefile +++ b/Makefile @@ -19,34 +19,34 @@ lint: tools # run the root module explicitly, to prevent recursive calls by re-invoking `make ...` top-level $(GOBIN)/golangci-lint run ./... # then, for all child modules, use a module-managed `Makefile` - git ls-files '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && env GOBIN=$(GOBIN) make lint' + GOBIN=$(GOBIN) ./scripts/foreach-module.sh lint lint-ci: tools # for the root module, explicitly run the step, to prevent recursive calls $(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m # then, for all child modules, use a module-managed `Makefile` - git ls-files '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && env GOBIN=$(GOBIN) make lint-ci' + GOBIN=$(GOBIN) ./scripts/foreach-module.sh lint-ci generate: # for the root module, explicitly run the step, to prevent recursive calls go generate ./... # then, for all child modules, use a module-managed `Makefile` - git ls-files '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && make generate' + GOBIN=$(GOBIN) ./scripts/foreach-module.sh generate test: # for the root module, explicitly run the step, to prevent recursive calls go test -cover ./... # then, for all child modules, use a module-managed `Makefile` - git ls-files '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && make test' + GOBIN=$(GOBIN) ./scripts/foreach-module.sh test tidy: # for the root module, explicitly run the step, to prevent recursive calls go mod tidy # then, for all child modules, use a module-managed `Makefile` - git ls-files '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && make tidy' + GOBIN=$(GOBIN) ./scripts/foreach-module.sh tidy tidy-ci: # for the root module, explicitly run the step, to prevent recursive calls tidied -verbose # then, for all child modules, use a module-managed `Makefile` - git ls-files '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && make tidy-ci' + GOBIN=$(GOBIN) ./scripts/foreach-module.sh tidy-ci diff --git a/README.md b/README.md index 9cb70c7c3d..b5ed0c8b98 100644 --- a/README.md +++ b/README.md @@ -393,7 +393,9 @@ We can see that this provides the best means to focus on the implementation of t - Single-file output - Support multiple OpenAPI files by having a package-per-OpenAPI file - Support of OpenAPI 3.0 - - OpenAPI 3.1 support is [awaiting upstream support](https://github.com/oapi-codegen/oapi-codegen/issues/373) + - OpenAPI 3.1 support is in experimental form, as a complete rewrite using [libopenapi](https://github.com/pb33f/libopenapi). Please look in the + `./experimental` directory. This is potentially the future V3 of `oapi-codegen` and is functionally complete, just not deeply tested yet. Many OpenAPI 3.1 + features are supported, such as webhooks and callbacks. - Note that this does not include OpenAPI 2.0 (aka Swagger) - Extract parameters from requests, to reduce work required by your implementation - Implicit `additionalProperties` are ignored by default ([more details](#additional-properties-additionalproperties)) diff --git a/experimental/Configuration.md b/experimental/Configuration.md new file mode 100644 index 0000000000..55f550e2f3 --- /dev/null +++ b/experimental/Configuration.md @@ -0,0 +1,246 @@ +# Configuration Reference + +`oapi-codegen` is configured using a YAML file. All sections are optional — you only need to include what you want to change from the defaults. + +Below is a fully annotated configuration file showing every option. + +```yaml +# The Go package name for generated code. +# Can also be set with -package flag. +package: myapi + +# Output file path. +# Can also be set with -output flag. +# Default: .gen.go +output: types.gen.go + +# Generation controls which parts of the code are generated. +generation: + # Server framework to generate code for. + # Supported: "std-http", "chi", "echo", "echo/v4", "gin", "gorilla", "fiber", "iris" + # Default: "" (no server code generated) + server: std-http + + # Generate an HTTP client that returns *http.Response. + # Default: false + client: true + + # Generate a SimpleClient wrapper with typed responses. + # Requires client: true. + # Default: false + simple-client: true + + # Use model types from an external package instead of generating them locally. + # When set, models are imported rather than generated. + # Default: not set (models are generated locally) + models-package: + path: github.com/org/project/models + alias: models # optional, defaults to last segment of path + +# Type mappings: OpenAPI type/format to Go type. +# User values are merged on top of defaults — you only need to specify overrides. +type-mapping: + integer: + default: + type: int # default + formats: + int32: + type: int32 # default + int64: + type: int64 # default + number: + default: + type: float32 # default + formats: + double: + type: float64 # default + boolean: + default: + type: bool # default + string: + default: + type: string # default + formats: + byte: + type: "[]byte" # default + date: + type: Date # default, custom template type + date-time: + type: time.Time # default + import: time + uuid: + type: UUID # default, custom template type + email: + type: Email # default, custom template type + binary: + type: File # default, custom template type + json: + type: json.RawMessage # default + import: encoding/json + # Add your own format mappings: + money: + type: decimal.Decimal + import: github.com/shopspring/decimal + +# Name mangling: controls how OpenAPI names become Go identifiers. +# User values are merged on top of defaults. +name-mangling: + # Prefix prepended when a name starts with a digit. + # Default: "N" (e.g., "123foo" becomes "N123foo") + numeric-prefix: "N" + + # Prefix prepended when a name conflicts with a Go keyword. + # Default: "" (uses keyword-suffix instead) + keyword-prefix: "" + + # Characters that mark word boundaries (next letter is capitalised). + # Default includes most punctuation: - # @ ! $ & = . + : ; _ ~ space ( ) { } [ ] | < > ? / \ + word-separators: "-#@!$&=.+:;_~ (){}[]|<>?/\\" + + # Words that should remain all-uppercase. + initialisms: + - ACL + - API + - ASCII + - CPU + - CSS + - DB + - DNS + - EOF + - GUID + - HTML + - HTTP + - HTTPS + - ID + - IP + - JSON + - QPS + - RAM + - RPC + - SLA + - SMTP + - SQL + - SSH + - TCP + - TLS + - TTL + - UDP + - UI + - UID + - GID + - URI + - URL + - UTF8 + - UUID + - VM + - XML + - XMPP + - XSRF + - XSS + - SIP + - RTP + - AMQP + - TS + + # Characters that get replaced with words when they appear at the start of a name. + character-substitutions: + "$": DollarSign + "-": Minus + "+": Plus + "&": And + "|": Or + "~": Tilde + "=": Equal + ">": GreaterThan + "<": LessThan + "#": Hash + ".": Dot + "*": Asterisk + "^": Caret + "%": Percent + "_": Underscore + "@": At + "!": Bang + "?": Question + "/": Slash + "\\": Backslash + ":": Colon + ";": Semicolon + "'": Apos + "\"": Quote + "`": Backtick + "(": LParen + ")": RParen + "[": LBracket + "]": RBracket + "{": LBrace + "}": RBrace + +# Name substitutions: direct overrides for specific generated names. +name-substitutions: + # Override type names during generation. + type-names: + foo: MyCustomFoo # Schema "foo" generates type "MyCustomFoo" instead of "Foo" + # Override property/field names during generation. + property-names: + bar: MyCustomBar # Property "bar" generates field "MyCustomBar" instead of "Bar" + +# Import mapping: resolve external $ref targets to Go packages. +# Required when your spec references schemas from other files. +import-mapping: + ../common/api.yaml: github.com/org/project/common + https://example.com/specs/shared.yaml: github.com/org/shared + # Use "-" to indicate types should stay in the current package + ./local-types.yaml: "-" + +# Content types: regexp patterns controlling which media types generate models. +# Only request/response bodies with matching content types will have types generated. +# Default: JSON types only. +content-types: + - "^application/json$" + - "^application/.*\\+json$" + # Add custom patterns as needed: + # - "^application/xml$" + # - "^text/plain$" + +# Content type short names: maps content type patterns to short names +# used in generated type names (e.g., "FindPetsJSONResponse"). +content-type-short-names: + - pattern: "^application/json$" + short-name: JSON + - pattern: "^application/xml$" + short-name: XML + - pattern: "^text/plain$" + short-name: Text + # ... defaults cover JSON, XML, Text, HTML, Binary, Multipart, Form + +# Struct tags: controls which struct tags are generated and their format. +# Uses Go text/template syntax. If specified, completely replaces the defaults. +# Default: json and form tags. +struct-tags: + tags: + - name: json + template: '{{if .JSONIgnore}}-{{else}}{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}{{if .OmitZero}},omitzero{{end}}{{end}}' + - name: form + template: '{{if .JSONIgnore}}-{{else}}{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}{{end}}' + # Add additional tags: + - name: yaml + template: '{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}' + - name: db + template: '{{ .FieldName }}' +``` + +## Struct tag template variables + +The struct tag templates have access to the following fields: + +| Variable | Type | Description | +|----------|------|-------------| +| `.FieldName` | `string` | The original field name from the OpenAPI spec | +| `.GoFieldName` | `string` | The Go identifier name after name mangling | +| `.IsOptional` | `bool` | Whether the field is optional in the schema | +| `.IsNullable` | `bool` | Whether the field is nullable | +| `.IsPointer` | `bool` | Whether the field is rendered as a pointer type | +| `.OmitEmpty` | `bool` | Whether `omitempty` should be applied (from extensions or optionality) | +| `.OmitZero` | `bool` | Whether `omitzero` should be applied (from `x-oapi-codegen-omitzero` extension) | +| `.JSONIgnore` | `bool` | Whether the field should be ignored in JSON (from `x-go-json-ignore` extension) | diff --git a/experimental/Makefile b/experimental/Makefile new file mode 100644 index 0000000000..a2ddf18fb1 --- /dev/null +++ b/experimental/Makefile @@ -0,0 +1,35 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + +generate: + $(call execute-if-go-124,go generate ./...) + +test: + @echo "Skipping tests in experimental module" + +tidy: + $(call execute-if-go-124,go mod tidy) + +tidy-ci: + $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/README.md b/experimental/README.md new file mode 100644 index 0000000000..4ce7d6310a --- /dev/null +++ b/experimental/README.md @@ -0,0 +1,164 @@ +# `oapi-codegen` V3 + +This is an experimental prototype of a V3 version of oapi-codegen. The generated code and command line options are not yet stable. Use at your +own risk. + +## What is new in Version 3 +This directory contains an experimental version of oapi-codegen's future V3 version, which is based on [libopenapi](https://github.com/pb33f/libopenapi), +instead of the prior [kin-openapi](https://github.com/getkin/kin-openapi). This change necessitated a nearly complete rewrite, but we strive to be as +compatible as possible. + +What is working: + - All model, client and server generation as in earlier versions. + - We have added Webhook and Callback support, please see `./examples`, which contains the ubiquitous OpenAPI pet shop implemented in all supported servers + and examples of webhooks and callbacks implemented on top of the `http.ServeMux` server, with no additional imports. + - Model generation of `allOf`, `anyOf`, `oneOf` is much more robust, since these were a source of many problems in earlier versions. + - Echo V5 support has been added (Go 1.25 required) + +What is missing: + - We have not yet created any runtime code like request validation middleware. + +## Differences in V3 + +V3 is a brand new implementation, and may (will) contain new bugs, but also strives to fix many current, existing bugs. We've run quite a few +conformance tests against specifications in old Issues, and we're looking pretty good! Please try this out, and if it failes in some way, please +file Issues. + +### Aggregate Schemas + +V3 implements `oneOf`, `anyOf`, `allOf` differently. Our prior versions had pretty good handling for `allOf`, where we merge all the constituent schemas +into a schema object that contains all the fields of the originals. It makes sense, since this is composition. + +`oneOf` and `anyOf` were handled by deferred parsing, where the JSON was captured into `json.RawMessage` and helper functions were created on each +type to parse the JSON as any constituent type with the user's help. + +Given the following schemas: + +```yaml +components: + schemas: + Cat: + type: object + properties: + name: + type: string + color: + type: string + Dog: + type: object + properties: + name: + type: string + color: + type: string + Fish: + type: object + properties: + species: + type: string + isSaltwater: + type: boolean + NamedPet: + anyOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + SpecificPet: + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Fish' +``` + +#### V2 output + +V2 generates both `NamedPet` (anyOf) and `SpecificPet` (oneOf) identically as opaque wrappers around `json.RawMessage`: + +```go +type NamedPet struct { + union json.RawMessage +} + +type SpecificPet struct { + union json.RawMessage +} +``` + +The actual variant types are invisible at the struct level. To access the underlying data, the user must call generated helper methods: + +```go +// NamedPet (anyOf) helpers +func (t NamedPet) AsCat() (Cat, error) +func (t *NamedPet) FromCat(v Cat) error +// + +// SpecificPet (oneOf) helpers + +func (t SpecificPet) AsFish() (Fish, error) +func (t *SpecificPet) FromFish(v Fish) error +func (t *SpecificPet) MergeFish(v Fish) error +// +``` + +Note that `anyOf` and `oneOf` produce identical types and method signatures; there is no semantic difference in the generated code. + +#### V3 output + +V3 generates structs with exported pointer fields for each variant, making the union members visible at the type level. Crucially, `anyOf` and `oneOf` now have different marshal/unmarshal semantics. + +**`anyOf` (NamedPet)** — `MarshalJSON` merges all non-nil variants into a single JSON object. `UnmarshalJSON` tries every variant and keeps whichever succeed: + +```go +type NamedPet struct { + Cat *Cat + Dog *Dog +} + +``` + +**`oneOf` (SpecificPet)** — `MarshalJSON` returns an error unless exactly one field is non-nil. `UnmarshalJSON` returns an error unless the JSON matches exactly one variant: + +```go +type SpecificPet struct { + Cat *Cat + Dog *Dog + Fish *Fish +} +``` + +### OpenAPI V3.1 Feature Support + +Thanks to [libopenapi](https://github.com/pb33f/libopenapi), we are able to parse OpenAPI 3.1 and 3.2 specifications. They are functionally similar, you can +read the differences between `nullable` fields yourself, but they add some new functionality, namely `webhooks` and `callbacks`. We support all of them in +this prototype. `callbacks` and `webhooks` are basically the inverse of `paths`. Webhooks contain no URL element in their definition, so we can't register handlers +for you in your http router of choice, you have to do that yourself. Callbacks support complex request URL's which may reference the original request. This is +something you need to pull out of the request body, and doing it generically is difficult, so we punt this problem, for now, to our users. + +Please see the [webhook example](examples/webhook/). It creates a little server that pretends to be a door badge reader, and it generates an event stream +about people coming and going. Any number of clients may subscribe to this event. See the [doc.go](examples/webhook/doc.go) for usage examples. + +The [callback example](examples/callback), creates a little server that pretends to plant trees. Each tree planting request contains a callback to be notified +when tree planting is complete. We invoke those in a random order via delays, and the client prints out callbacks as they happen. Please see [doc.go](examples/callback/doc.go) for usage. + +### Flexible Configuration + +oapi-codegen V3 tries to make no assumptions about which initialisms, struct tags, or name mangling that is correct for you. A very [flexible configuration file](Configuration.md) allows you to override anything. + + +### No runtime dependency + +V2 generated code relied on `github.com/oapi-codegen/runtime` for parameter binding and styling. This was a complaint from lots of people due to various +audit requirements. V3 embeds all necessary helper functions and helper types into the spec. There are no longer generic, parameterized functions that +handle arbitrary parameters, but rather very specific functions for each kind of parameter, and we call the correct little helper versus a generic +runtime helper. + + +## Installation + +Go 1.24 is required, install like so: + + go get -tool github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen@latest + +You can then run the code generator + + //go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen + diff --git a/experimental/cmd/oapi-codegen/main.go b/experimental/cmd/oapi-codegen/main.go new file mode 100644 index 0000000000..24c5320c90 --- /dev/null +++ b/experimental/cmd/oapi-codegen/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/pb33f/libopenapi" + "github.com/pb33f/libopenapi/datamodel" + + "gopkg.in/yaml.v3" + + "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen" +) + +func main() { + configPath := flag.String("config", "", "path to configuration file") + flagPackage := flag.String("package", "", "Go package name for generated code") + flagOutput := flag.String("output", "", "output file path (default: .gen.go)") + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s [options] \n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Arguments:\n") + fmt.Fprintf(os.Stderr, " spec-path path to OpenAPI spec file\n\n") + fmt.Fprintf(os.Stderr, "Options:\n") + flag.PrintDefaults() + } + flag.Parse() + + if flag.NArg() != 1 { + flag.Usage() + os.Exit(1) + } + + specPath := flag.Arg(0) + + // Parse the OpenAPI spec + specData, err := os.ReadFile(specPath) + if err != nil { + fmt.Fprintf(os.Stderr, "error reading spec: %v\n", err) + os.Exit(1) + } + + // Configure libopenapi to skip resolving external references. + // We handle external $refs via import mappings — the referenced specs + // don't need to be fetched or parsed. See pb33f/libopenapi#519. + docConfig := datamodel.NewDocumentConfiguration() + docConfig.SkipExternalRefResolution = true + + doc, err := libopenapi.NewDocumentWithConfiguration(specData, docConfig) + if err != nil { + fmt.Fprintf(os.Stderr, "error parsing spec: %v\n", err) + os.Exit(1) + } + + // Parse config if provided, otherwise use empty config + var cfg codegen.Configuration + if *configPath != "" { + configData, err := os.ReadFile(*configPath) + if err != nil { + fmt.Fprintf(os.Stderr, "error reading config: %v\n", err) + os.Exit(1) + } + if err := yaml.Unmarshal(configData, &cfg); err != nil { + fmt.Fprintf(os.Stderr, "error parsing config: %v\n", err) + os.Exit(1) + } + } + + // Flags override config file values + if *flagPackage != "" { + cfg.PackageName = *flagPackage + } + if *flagOutput != "" { + cfg.Output = *flagOutput + } + + // Default output to .gen.go + if cfg.Output == "" { + base := filepath.Base(specPath) + ext := filepath.Ext(base) + cfg.Output = strings.TrimSuffix(base, ext) + ".gen.go" + } + + // Default package name from output file + if cfg.PackageName == "" { + cfg.PackageName = "api" + } + + // Generate code + code, err := codegen.Generate(doc, specData, cfg) + if err != nil { + fmt.Fprintf(os.Stderr, "error generating code: %v\n", err) + os.Exit(1) + } + + // Write output + if err := os.WriteFile(cfg.Output, []byte(code), 0644); err != nil { + fmt.Fprintf(os.Stderr, "error writing output: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Generated %s\n", cfg.Output) +} diff --git a/experimental/examples/callback/client/main.go b/experimental/examples/callback/client/main.go new file mode 100644 index 0000000000..9e618448a3 --- /dev/null +++ b/experimental/examples/callback/client/main.go @@ -0,0 +1,144 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "log" + "net" + "net/http" + "sync" + "sync/atomic" + + treefarm "github.com/oapi-codegen/oapi-codegen/experimental/examples/callback" +) + +// trees and cities for our planting requests +var treeKinds = []string{ + "oak", "maple", "pine", "birch", "willow", + "cedar", "elm", "ash", "cherry", "walnut", +} + +var cities = []string{ + "Providence", "Austin", "Denver", "Seattle", "Chicago", + "Boston", "Miami", "Nashville", "Savannah", "Mountain View", +} + +// CallbackReceiver implements treefarm.CallbackReceiverInterface. +type CallbackReceiver struct { + received atomic.Int32 + total int + done chan struct{} + once sync.Once + + mu sync.Mutex + ordinals map[string]int // UUID string -> 1-based planting order +} + +var _ treefarm.CallbackReceiverInterface = (*CallbackReceiver)(nil) + +func NewCallbackReceiver(total int) *CallbackReceiver { + return &CallbackReceiver{ + total: total, + done: make(chan struct{}), + ordinals: make(map[string]int), + } +} + +func (cr *CallbackReceiver) Register(id string, ordinal int) { + cr.mu.Lock() + cr.ordinals[id] = ordinal + cr.mu.Unlock() +} + +func (cr *CallbackReceiver) HandleTreePlantedCallback(w http.ResponseWriter, r *http.Request) { + var result treefarm.TreePlantingResult + if err := json.NewDecoder(r.Body).Decode(&result); err != nil { + log.Printf("Error decoding callback: %v", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + cr.mu.Lock() + ordinal := cr.ordinals[result.ID.String()] + cr.mu.Unlock() + + n := cr.received.Add(1) + log.Printf("Callback %d/%d received: tree #%d success=%v", n, cr.total, ordinal, result.Success) + + w.WriteHeader(http.StatusOK) + + if int(n) >= cr.total { + cr.once.Do(func() { close(cr.done) }) + } +} + +func main() { + serverAddr := flag.String("server", "http://localhost:8080", "Tree farm server address") + flag.Parse() + + const numTrees = 10 + + // Start callback receiver on an ephemeral port. + receiver := NewCallbackReceiver(numTrees) + + mux := http.NewServeMux() + mux.Handle("/tree_callback", treefarm.TreePlantedCallbackHandler(receiver, nil)) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + log.Fatalf("Failed to listen: %v", err) + } + callbackPort := listener.Addr().(*net.TCPAddr).Port + callbackURL := fmt.Sprintf("http://localhost:%d/tree_callback", callbackPort) + log.Printf("Callback receiver listening on port %d", callbackPort) + + go func() { + if err := http.Serve(listener, mux); err != nil { + log.Printf("Callback server stopped: %v", err) + } + }() + + // Send 10 tree planting requests. + client := &http.Client{} + for i := range numTrees { + req := treefarm.TreePlantingRequest{ + Kind: treeKinds[i], + Location: cities[i], + CallbackURL: callbackURL, + } + + body, err := json.Marshal(req) + if err != nil { + log.Fatalf("Failed to marshal request: %v", err) + } + + resp, err := client.Post( + *serverAddr+"/api/plant_tree", + "application/json", + bytes.NewReader(body), + ) + if err != nil { + log.Fatalf("Failed to plant tree %d: %v", i+1, err) + } + + var accepted treefarm.TreeWithID + if err := json.NewDecoder(resp.Body).Decode(&accepted); err != nil { + _ = resp.Body.Close() + log.Fatalf("Failed to decode response: %v", err) + } + _ = resp.Body.Close() + + receiver.Register(accepted.ID.String(), i+1) + log.Printf("Planted tree %d/%d: id=%s kind=%q location=%q", + i+1, numTrees, accepted.ID, accepted.Kind, accepted.Location) + } + + log.Printf("All %d trees planted, waiting for callbacks...", numTrees) + + // Wait for all callbacks. + <-receiver.done + + log.Printf("All %d callbacks received, done!", numTrees) +} diff --git a/experimental/examples/callback/config.yaml b/experimental/examples/callback/config.yaml new file mode 100644 index 0000000000..29392d1127 --- /dev/null +++ b/experimental/examples/callback/config.yaml @@ -0,0 +1,6 @@ +package: treefarm +output: treefarm.gen.go +generation: + callback-initiator: true + callback-receiver: true + server: std-http diff --git a/experimental/examples/callback/doc.go b/experimental/examples/callback/doc.go new file mode 100644 index 0000000000..c441b88a3a --- /dev/null +++ b/experimental/examples/callback/doc.go @@ -0,0 +1,13 @@ +//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config config.yaml tree-farm.yaml + +// Package treefarm provides an example of how to handle OpenAPI callbacks. +// We create a server which plants trees. The client asks the server to plant +// a tree and requests a callback when the planting is done. +// +// The server program will wait 1-5 seconds before notifying the client that a +// tree has been planted. +// +// You can run the example by running these two commands in parallel +// go run ./server --port 8080 +// go run ./client --server http://localhost:8080 +package treefarm diff --git a/experimental/examples/callback/server/main.go b/experimental/examples/callback/server/main.go new file mode 100644 index 0000000000..4b255de3b6 --- /dev/null +++ b/experimental/examples/callback/server/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "log" + "math/rand/v2" + "net" + "net/http" + "time" + + "github.com/google/uuid" + + treefarm "github.com/oapi-codegen/oapi-codegen/experimental/examples/callback" +) + +// TreeFarm implements treefarm.ServerInterface. +type TreeFarm struct { + initiator *treefarm.CallbackInitiator +} + +var _ treefarm.ServerInterface = (*TreeFarm)(nil) + +func NewTreeFarm() *TreeFarm { + initiator, err := treefarm.NewCallbackInitiator() + if err != nil { + log.Fatalf("Failed to create callback initiator: %v", err) + } + return &TreeFarm{initiator: initiator} +} + +func sendError(w http.ResponseWriter, code int, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _ = json.NewEncoder(w).Encode(treefarm.Error{ + Code: int32(code), + Message: message, + }) +} + +func (tf *TreeFarm) PlantTree(w http.ResponseWriter, r *http.Request) { + log.Printf("Received PlantTree request from %s", r.RemoteAddr) + + var req treefarm.TreePlantingRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + sendError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + if req.CallbackURL == "" { + sendError(w, http.StatusBadRequest, "callbackUrl is required") + return + } + + id := uuid.New() + + log.Printf("Accepted tree planting: id=%s kind=%q location=%q callbackUrl=%q", + id, req.Kind, req.Location, req.CallbackURL) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + _ = json.NewEncoder(w).Encode(treefarm.TreeWithID{ + Location: req.Location, + Kind: req.Kind, + ID: id, + }) + + go tf.plantAndNotify(id, req) +} + +func (tf *TreeFarm) plantAndNotify(id uuid.UUID, req treefarm.TreePlantingRequest) { + delay := time.Duration(1+rand.IntN(5)) * time.Second + log.Printf("Planting tree %s (kind=%q, location=%q) — will complete in %s", + id, req.Kind, req.Location, delay) + + time.Sleep(delay) + + result := treefarm.TreePlantedJSONRequestBody{ + ID: id, + Kind: req.Kind, + Location: req.Location, + Success: true, + } + + log.Printf("Tree %s planted, invoking callback at %s", id, req.CallbackURL) + + resp, err := tf.initiator.TreePlanted(context.Background(), req.CallbackURL, result) + if err != nil { + log.Printf("Callback to %s failed: %v", req.CallbackURL, err) + return + } + defer func() { _ = resp.Body.Close() }() + + log.Printf("Callback to %s returned status %d", req.CallbackURL, resp.StatusCode) +} + +func main() { + port := flag.String("port", "8080", "Port for HTTP server") + flag.Parse() + + farm := NewTreeFarm() + + mux := http.NewServeMux() + treefarm.HandlerFromMux(farm, mux) + + // Wrap with request logging. + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) + mux.ServeHTTP(w, r) + }) + + addr := net.JoinHostPort("0.0.0.0", *port) + log.Printf("Tree Farm server listening on %s", addr) + log.Fatal(http.ListenAndServe(addr, handler)) +} diff --git a/experimental/examples/callback/tree-farm.yaml b/experimental/examples/callback/tree-farm.yaml new file mode 100644 index 0000000000..4cad525c08 --- /dev/null +++ b/experimental/examples/callback/tree-farm.yaml @@ -0,0 +1,138 @@ +openapi: "3.1.0" +info: + version: 1.0.0 + title: Tree Farm + description: | + A tree planting service that demonstrates OpenAPI callbacks. + The client submits a tree planting request along with a callback URL. + When the tree has been planted, the server sends a POST to the callback URL + to notify the client of the result. + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html +paths: + /api/plant_tree: + post: + summary: Request a tree planting + description: | + Submits a request to plant a tree at the specified location. + This is a long-running operation — the server accepts the request + and later notifies the caller via the provided callback URL. + operationId: PlantTree + requestBody: + $ref: '#/components/requestBodies/TreePlanting' + responses: + '202': + description: Tree planting request accepted + content: + application/json: + schema: + $ref: '#/components/schemas/TreeWithId' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + callbacks: + treePlanted: + '{$request.body#/callbackUrl}': + post: + summary: Tree planting result notification + description: | + Sent by the server to the callback URL when the tree planting + operation completes (successfully or not). + operationId: TreePlanted + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TreePlantingResult' + responses: + '200': + description: Callback received successfully + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Tree: + type: object + description: A tree to be planted + required: + - location + - kind + properties: + location: + type: string + description: Where to plant the tree (e.g. "north meadow") + kind: + type: string + description: What kind of tree to plant (e.g. "oak") + + TreePlantingRequest: + description: | + A tree planting request, combining the tree details with a callback URL + for completion notification. + allOf: + - $ref: '#/components/schemas/Tree' + - type: object + required: + - callbackUrl + properties: + callbackUrl: + type: string + format: uri + description: URL to receive the planting result callback + + TreeWithId: + description: A tree with a server-assigned identifier + allOf: + - $ref: '#/components/schemas/Tree' + - type: object + required: + - id + properties: + id: + type: string + format: uuid + description: Unique identifier for this planting request + + TreePlantingResult: + description: The result of a tree planting operation, sent via callback + allOf: + - $ref: '#/components/schemas/TreeWithId' + - type: object + required: + - success + properties: + success: + type: boolean + description: Whether the tree was successfully planted + + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + description: Error code + message: + type: string + description: Error message + + requestBodies: + TreePlanting: + description: Tree planting request + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TreePlantingRequest' diff --git a/experimental/examples/callback/treefarm.gen.go b/experimental/examples/callback/treefarm.gen.go new file mode 100644 index 0000000000..a1018c08e9 --- /dev/null +++ b/experimental/examples/callback/treefarm.gen.go @@ -0,0 +1,503 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package treefarm + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "sync" + + "github.com/google/uuid" +) + +// #/components/schemas/Tree +// A tree to be planted +type Tree struct { + Location string `json:"location" form:"location"` // Where to plant the tree (e.g. "north meadow") + Kind string `json:"kind" form:"kind"` // What kind of tree to plant (e.g. "oak") +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Tree) ApplyDefaults() { +} + +// #/components/schemas/TreePlantingRequest +// A tree planting request, combining the tree details with a callback URL +// for completion notification. +type TreePlantingRequest struct { + Location string `json:"location" form:"location"` // Where to plant the tree (e.g. "north meadow") + Kind string `json:"kind" form:"kind"` // What kind of tree to plant (e.g. "oak") + CallbackURL string `json:"callbackUrl" form:"callbackUrl"` // URL to receive the planting result callback +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *TreePlantingRequest) ApplyDefaults() { +} + +// #/components/schemas/TreeWithId +// A tree with a server-assigned identifier +type TreeWithID struct { + Location string `json:"location" form:"location"` // Where to plant the tree (e.g. "north meadow") + Kind string `json:"kind" form:"kind"` // What kind of tree to plant (e.g. "oak") + ID UUID `json:"id" form:"id"` // Unique identifier for this planting request +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *TreeWithID) ApplyDefaults() { +} + +// #/components/schemas/TreePlantingResult +// The result of a tree planting operation, sent via callback +type TreePlantingResult struct { + Location string `json:"location" form:"location"` // Where to plant the tree (e.g. "north meadow") + Kind string `json:"kind" form:"kind"` // What kind of tree to plant (e.g. "oak") + ID UUID `json:"id" form:"id"` // Unique identifier for this planting request + Success bool `json:"success" form:"success"` // Whether the tree was successfully planted +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *TreePlantingResult) ApplyDefaults() { +} + +// #/components/schemas/Error +type Error struct { + Code int32 `json:"code" form:"code"` // Error code + Message string `json:"message" form:"message"` // Error message +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Error) ApplyDefaults() { +} + +type UUID = uuid.UUID + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/8RXwW7bOBC96ysGaQG3QCO76U23bJEFAhSbIHGQ44IWxxYbilTJUbzG7gL7EfuF+yUL", + "khJFy4rr9NKcopDzZvhm5s1EN6hYIwo4+5R/zBdnmVBrXWQAz2is0KqAj/kiX2QAJEhiAUuDCL8yU2cA", + "HG1pREP+3l8ZAMAlkLvQSKZIqA1YNM+iRKCKEXCstbJkGKGFmwbV5e01lEzKFSufbO4BlhVCKQUqAtuu", + "akEW2AjT4LcWLQGTWm1gK6gCFmHg4e5LQHqsUAFVGKwrZmGFqAIM8g/+yIWHBiwq7vzc3twvgbQ/SgE9", + "HmlQmsR6F45DjHrtvwzaVpLzK0WJymLhTRSrsYDLhpUVwoWnEaA1soCKqLHFfL7dbnPmz3NtNvPO2s6/", + "XH+++u3+6vwiX+QV1TJrGFXWoc5ZI+b+Eb+7hwVHjbYUfgOwbV0zsyvgrudpn7/u2kTy3M99JL2nmXQw", + "7XEYBeoaLMVaIAepS+Zg8giyrIQF4UBcis5Nq5RLnG7Q+Jvw3z//pvyzssSGbEeldxuxmOIgGaEJ7Au0", + "MT1o4Fkw/9kY/Sw48ok6gMHvNS/g1r3FVXF32Pn7RfNdEZ2+NbguYPZmXuq60QoV2flwUaCdO4TbjtFZ", + "hLKNdukbgGYXi4vZ8DmifTld154N5IlVqRWhohQIgDWNFIH6+Ver1f4pgC0rrNn4r9OPC3fDsx4FVdd8", + "lg0hr1kr6cVXtAr/aLAk5IDGaPMz4r5yjvuQo6QMGNSnC3kKPPvzbcd6vtJ892bemz4Y+fdsP4S0x2Ko", + "fa+NM+nkoCvY8NKR4QvdF7vQactql/bIhCzBdk/iRg0+/Ax955iT6OT3nW3LEq1dt1LuQPvuep9nLxi6", + "xlkOFI6uTbZQeigM8gLItHhwPFkhp9XJ8Wo5rdb7Fr7zGZsdPOygoZPGXsymvO5l9nOfLYMlimfkkNKe", + "HdoedNprO+4EXk9l9nvcntSRw4mD6Q4D4jKOLwDaNViAXn3FkqYGVLdXkIYV9iM8GxVYDPM8jqTkT09C", + "9RaNcYVNIs1rb5E+NgRlyew31V5gjxUaHGZkbMZ3mG9yOFPaUAU1Mq63Z+8jiAvm9Z4YeUO/dHRsBK+d", + "M82ezt5nkduhtn17FscH/+X0lvXBacZK+PkdH8eRmJB2aveKeGtterlxypNqYS8zTMqbdZq273XrLLk7", + "UTHT1RCuJ8KenExVQjpBHowcV/8LqeqeXDMqoDXimNw75SbdS0JYX0aTo/c/JDMM5eJIb3TJCOPinFkr", + "Ngo5CI7K703mJ7Au+AlkC/4jHLeCHyVZiW8tJq/3BUluMR2X+FTH2ESG91e2uO27Nhz/axIH5gf3PwX5", + "9TTm8gfpH69jr05CN3JOyER3czodK60lsqOLzGOFVLldpReKLbN7Ey9Kt0fxI+LIBJhS9lJzTD5rtJZt", + "8IiwO4NDqRWKcIPp4OwLSyj6dPGSBvuI92PoInitmgekPvwsXaJi+GlJThfjlGJnR/aug63g2CYwNf1P", + "36d8LLPs/wAAAP//y9dX5mEQAAA=", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Request a tree planting + // (POST /api/plant_tree) + PlantTree(w http.ResponseWriter, r *http.Request) +} + +// ServerInterfaceWrapper converts HTTP requests to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// MiddlewareFunc is a middleware function type. +type MiddlewareFunc func(http.Handler) http.Handler + +// PlantTree operation middleware +func (siw *ServerInterfaceWrapper) PlantTree(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PlantTree(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// 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)) + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + +// StdHTTPServerOptions configures the StdHTTP server. +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, + }) +} + +// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. +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) + } + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + m.HandleFunc("POST "+options.BaseURL+"/api/plant_tree", wrapper.PlantTree) + return m +} + +// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. +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 +} + +// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. +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 +} + +// RequiredParamError is returned when a required parameter is missing. +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +// RequiredHeaderError is returned when a required header is missing. +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 +} + +// InvalidParamFormatError is returned when a parameter has an invalid format. +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 +} + +// TooManyValuesForParamError is returned when a parameter has too many values. +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) +} + +type TreePlantedJSONRequestBody = TreePlantingResult + +// RequestEditorFn is the function signature for the RequestEditor callback function. +// It may already be defined if client code is also generated; this is a compatible redeclaration. +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// HttpRequestDoer performs HTTP requests. +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// CallbackInitiator sends callback requests to target URLs. +// Unlike Client, it has no stored base URL — the full target URL is provided per-call. +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 request. This can be used to mutate the request. +func WithCallbackRequestEditorFn(fn RequestEditorFn) CallbackInitiatorOption { + return func(p *CallbackInitiator) error { + p.RequestEditors = append(p.RequestEditors, fn) + return nil + } +} + +func (p *CallbackInitiator) applyEditors(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 { + // TreePlantedWithBody sends a POST callback request + TreePlantedWithBody(ctx context.Context, targetURL string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + TreePlanted(ctx context.Context, targetURL string, body TreePlantedJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +// TreePlantedWithBody sends a POST callback request +// Tree planting result notification +func (p *CallbackInitiator) TreePlantedWithBody(ctx context.Context, targetURL string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewTreePlantedCallbackRequestWithBody(targetURL, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := p.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return p.Client.Do(req) +} + +// TreePlanted sends a POST callback request with JSON body +func (p *CallbackInitiator) TreePlanted(ctx context.Context, targetURL string, body TreePlantedJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewTreePlantedCallbackRequest(targetURL, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := p.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return p.Client.Do(req) +} + +// NewTreePlantedCallbackRequest creates a POST request for the callback with application/json body +func NewTreePlantedCallbackRequest(targetURL string, body TreePlantedJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewTreePlantedCallbackRequestWithBody(targetURL, "application/json", bodyReader) +} + +// NewTreePlantedCallbackRequestWithBody creates a POST request for the callback with any body +func NewTreePlantedCallbackRequestWithBody(targetURL string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + parsedURL, err := url.Parse(targetURL) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", parsedURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// CallbackHttpError represents an HTTP error response. +// The type parameter E is the type of the parsed error body. +type CallbackHttpError[E any] struct { + StatusCode int + Body E + RawBody []byte +} + +func (e *CallbackHttpError[E]) Error() string { + return fmt.Sprintf("HTTP %d", e.StatusCode) +} + +// SimpleCallbackInitiator wraps CallbackInitiator with typed responses for operations that have +// unambiguous response types. Methods return the success type directly, +// and HTTP errors are returned as *CallbackHttpError[E] where E is the error type. +type SimpleCallbackInitiator struct { + *CallbackInitiator +} + +// NewSimpleCallbackInitiator creates a new SimpleCallbackInitiator which wraps a CallbackInitiator. +func NewSimpleCallbackInitiator(opts ...CallbackInitiatorOption) (*SimpleCallbackInitiator, error) { + initiator, err := NewCallbackInitiator(opts...) + if err != nil { + return nil, err + } + return &SimpleCallbackInitiator{CallbackInitiator: initiator}, nil +} + +// CallbackReceiverInterface represents handlers for receiving callback requests. +type CallbackReceiverInterface interface { + // Tree planting result notification + // HandleTreePlantedCallback handles the POST callback request. + HandleTreePlantedCallback(w http.ResponseWriter, r *http.Request) +} + +// CallbackReceiverMiddlewareFunc is a middleware function for callback receiver handlers. +type CallbackReceiverMiddlewareFunc func(http.Handler) http.Handler + +// TreePlantedCallbackHandler returns an http.Handler for the TreePlanted callback. +// The caller is responsible for registering this handler at the appropriate path. +func TreePlantedCallbackHandler(si CallbackReceiverInterface, errHandler func(w http.ResponseWriter, r *http.Request, err error), middlewares ...CallbackReceiverMiddlewareFunc) http.Handler { + if errHandler == nil { + errHandler = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + si.HandleTreePlantedCallback(w, r) + })) + + for _, middleware := range middlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) + }) +} diff --git a/experimental/examples/petstore-expanded/chi/Makefile b/experimental/examples/petstore-expanded/chi/Makefile new file mode 100644 index 0000000000..42389f4137 --- /dev/null +++ b/experimental/examples/petstore-expanded/chi/Makefile @@ -0,0 +1,35 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + +generate: + $(call execute-if-go-124,go generate ./...) + +test: + $(call execute-if-go-124,go test -cover ./...) + +tidy: + $(call execute-if-go-124,go mod tidy) + +tidy-ci: + $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/chi/go.mod b/experimental/examples/petstore-expanded/chi/go.mod new file mode 100644 index 0000000000..3c9d10741e --- /dev/null +++ b/experimental/examples/petstore-expanded/chi/go.mod @@ -0,0 +1,11 @@ +module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/chi + +go 1.24.0 + +require ( + github.com/go-chi/chi/v5 v5.2.0 + github.com/google/uuid v1.6.0 + github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 +) + +replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/chi/go.sum b/experimental/examples/petstore-expanded/chi/go.sum new file mode 100644 index 0000000000..ad495c8907 --- /dev/null +++ b/experimental/examples/petstore-expanded/chi/go.sum @@ -0,0 +1,4 @@ +github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= +github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/experimental/examples/petstore-expanded/chi/main.go b/experimental/examples/petstore-expanded/chi/main.go new file mode 100644 index 0000000000..8a4e0e3790 --- /dev/null +++ b/experimental/examples/petstore-expanded/chi/main.go @@ -0,0 +1,40 @@ +//go:build go1.22 + +// This is an example of implementing the Pet Store from the OpenAPI documentation +// found at: +// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml + +package main + +import ( + "flag" + "log" + "net" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/chi/server" +) + +func main() { + port := flag.String("port", "8080", "Port for test HTTP server") + flag.Parse() + + // Create an instance of our handler which satisfies the generated interface + petStore := server.NewPetStore() + + r := chi.NewRouter() + + // We now register our petStore above as the handler for the interface + server.HandlerFromMux(petStore, r) + + s := &http.Server{ + Handler: r, + Addr: net.JoinHostPort("0.0.0.0", *port), + } + + log.Printf("Server listening on %s", s.Addr) + + // And we serve HTTP until the world ends. + log.Fatal(s.ListenAndServe()) +} diff --git a/experimental/examples/petstore-expanded/chi/server/petstore.go b/experimental/examples/petstore-expanded/chi/server/petstore.go new file mode 100644 index 0000000000..2f52c8e271 --- /dev/null +++ b/experimental/examples/petstore-expanded/chi/server/petstore.go @@ -0,0 +1,135 @@ +//go:build go1.22 + +package server + +import ( + "encoding/json" + "fmt" + "net/http" + "sync" + + petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" +) + +// PetStore implements the ServerInterface. +type PetStore struct { + Pets map[int64]petstore.Pet + NextId int64 + Lock sync.Mutex +} + +// Make sure we conform to ServerInterface +var _ ServerInterface = (*PetStore)(nil) + +// NewPetStore creates a new PetStore. +func NewPetStore() *PetStore { + return &PetStore{ + Pets: make(map[int64]petstore.Pet), + NextId: 1000, + } +} + +// sendPetStoreError wraps sending of an error in the Error format. +func sendPetStoreError(w http.ResponseWriter, code int, message string) { + petErr := petstore.Error{ + Code: int32(code), + Message: message, + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _ = json.NewEncoder(w).Encode(petErr) +} + +// FindPets returns all pets, optionally filtered by tags and limited. +func (p *PetStore) FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) { + p.Lock.Lock() + defer p.Lock.Unlock() + + var result []petstore.Pet + + for _, pet := range p.Pets { + if params.Tags != nil { + // If we have tags, filter pets by tag + for _, t := range *params.Tags { + if pet.Tag != nil && (*pet.Tag == t) { + result = append(result, pet) + } + } + } else { + // Add all pets if we're not filtering + result = append(result, pet) + } + + if params.Limit != nil { + l := int(*params.Limit) + if len(result) >= l { + // We're at the limit + break + } + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(result) +} + +// AddPet creates a new pet. +func (p *PetStore) AddPet(w http.ResponseWriter, r *http.Request) { + // We expect a NewPet object in the request body. + var newPet petstore.NewPet + if err := json.NewDecoder(r.Body).Decode(&newPet); err != nil { + sendPetStoreError(w, http.StatusBadRequest, "Invalid format for NewPet") + return + } + + // We now have a pet, let's add it to our "database". + p.Lock.Lock() + defer p.Lock.Unlock() + + // We handle pets, not NewPets, which have an additional ID field + var pet petstore.Pet + pet.Name = newPet.Name + pet.Tag = newPet.Tag + pet.ID = p.NextId + p.NextId++ + + // Insert into map + p.Pets[pet.ID] = pet + + // Now, we have to return the Pet + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(pet) +} + +// FindPetByID returns a pet by ID. +func (p *PetStore) FindPetByID(w http.ResponseWriter, r *http.Request, id int64) { + p.Lock.Lock() + defer p.Lock.Unlock() + + pet, found := p.Pets[id] + if !found { + sendPetStoreError(w, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(pet) +} + +// DeletePet deletes a pet by ID. +func (p *PetStore) DeletePet(w http.ResponseWriter, r *http.Request, id int64) { + p.Lock.Lock() + defer p.Lock.Unlock() + + _, found := p.Pets[id] + if !found { + sendPetStoreError(w, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) + return + } + delete(p.Pets, id) + + w.WriteHeader(http.StatusNoContent) +} diff --git a/experimental/examples/petstore-expanded/chi/server/server.config.yaml b/experimental/examples/petstore-expanded/chi/server/server.config.yaml new file mode 100644 index 0000000000..b3b3509411 --- /dev/null +++ b/experimental/examples/petstore-expanded/chi/server/server.config.yaml @@ -0,0 +1,6 @@ +package: server +generation: + server: chi + models-package: + path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded + alias: petstore diff --git a/experimental/examples/petstore-expanded/chi/server/server.gen.go b/experimental/examples/petstore-expanded/chi/server/server.gen.go new file mode 100644 index 0000000000..167ae93385 --- /dev/null +++ b/experimental/examples/petstore-expanded/chi/server/server.gen.go @@ -0,0 +1,1039 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package server + +import ( + "bytes" + "encoding" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "reflect" + "sort" + "strconv" + "strings" + "time" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Returns all pets + // (GET /pets) + FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) + // Creates a new pet + // (POST /pets) + AddPet(w http.ResponseWriter, r *http.Request) + // Deletes a pet by ID + // (DELETE /pets/{id}) + DeletePet(w http.ResponseWriter, r *http.Request, id int64) + // Returns a pet by ID + // (GET /pets/{id}) + FindPetByID(w http.ResponseWriter, r *http.Request, id int64) +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. +type Unimplemented struct{} + +// Returns all pets +// (GET /pets) +func (_ Unimplemented) FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Creates a new pet +// (POST /pets) +func (_ Unimplemented) AddPet(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Deletes a pet by ID +// (DELETE /pets/{id}) +func (_ Unimplemented) DeletePet(w http.ResponseWriter, r *http.Request, id int64) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Returns a pet by ID +// (GET /pets/{id}) +func (_ Unimplemented) FindPetByID(w http.ResponseWriter, r *http.Request, id int64) { + w.WriteHeader(http.StatusNotImplemented) +} + +// FindPetsParams defines parameters for FindPets. +type FindPetsParams struct { + // tags (optional) + Tags *[]string `form:"tags" json:"tags"` + // limit (optional) + Limit *int32 `form:"limit" json:"limit"` +} + +// ServerInterfaceWrapper converts HTTP requests to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// MiddlewareFunc is a middleware function type. +type MiddlewareFunc func(http.Handler) http.Handler + +// FindPets operation middleware +func (siw *ServerInterfaceWrapper) FindPets(w http.ResponseWriter, r *http.Request) { + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params FindPetsParams + + // ------------- Optional query parameter "tags" ------------- + err = BindFormExplodeParam("tags", false, r.URL.Query(), ¶ms.Tags) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tags", Err: err}) + return + } + + // ------------- Optional query parameter "limit" ------------- + err = BindFormExplodeParam("limit", false, r.URL.Query(), ¶ms.Limit) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.FindPets(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// AddPet operation middleware +func (siw *ServerInterfaceWrapper) AddPet(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.AddPet(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// DeletePet operation middleware +func (siw *ServerInterfaceWrapper) DeletePet(w http.ResponseWriter, r *http.Request) { + var err error + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, chi.URLParam(r, "id"), &id) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeletePet(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// FindPetByID operation middleware +func (siw *ServerInterfaceWrapper) FindPetByID(w http.ResponseWriter, r *http.Request) { + var err error + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, chi.URLParam(r, "id"), &id) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.FindPetByID(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{}) +} + +// ChiServerOptions configures the Chi server. +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, + }) +} + +// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. +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) + } + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/pets", wrapper.FindPets) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/pets", wrapper.AddPet) + }) + r.Group(func(r chi.Router) { + r.Delete(options.BaseURL+"/pets/{id}", wrapper.DeletePet) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/pets/{id}", wrapper.FindPetByID) + }) + return r +} + +// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. +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 +} + +// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. +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 +} + +// RequiredParamError is returned when a required parameter is missing. +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +// RequiredHeaderError is returned when a required header is missing. +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 +} + +// InvalidParamFormatError is returned when a parameter has an invalid format. +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 +} + +// TooManyValuesForParamError is returned when a parameter has too many values. +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) +} + +// ParamLocation indicates where a parameter is located in an HTTP request. +type ParamLocation int + +const ( + ParamLocationUndefined ParamLocation = iota + ParamLocationQuery + ParamLocationPath + ParamLocationHeader + ParamLocationCookie +) + +// Binder is an interface for types that can bind themselves from a string value. +type Binder interface { + Bind(value string) error +} + +// DateFormat is the format used for date (without time) parameters. +const DateFormat = "2006-01-02" + +// Date represents a date (without time) for OpenAPI date format. +type Date struct { + time.Time +} + +// UnmarshalText implements encoding.TextUnmarshaler for Date. +func (d *Date) UnmarshalText(data []byte) error { + t, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = t + return nil +} + +// MarshalText implements encoding.TextMarshaler for Date. +func (d Date) MarshalText() ([]byte, error) { + return []byte(d.Format(DateFormat)), nil +} + +// Format returns the date formatted according to layout. +func (d Date) Format(layout string) string { + return d.Time.Format(layout) +} + +// primitiveToString converts a primitive value to a string representation. +// It handles basic Go types, time.Time, types.Date, and types that implement +// json.Marshaler or fmt.Stringer. +func primitiveToString(value interface{}) (string, error) { + // Check for known types first (time, date, uuid) + if res, ok := marshalKnownTypes(value); ok { + return res, nil + } + + // Dereference pointers for optional values + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + kind := t.Kind() + + switch kind { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + case reflect.Float32: + return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil + case reflect.Bool: + if v.Bool() { + return "true", nil + } + return "false", nil + case reflect.String: + return v.String(), nil + case reflect.Struct: + // Check if it's a UUID + if u, ok := value.(uuid.UUID); ok { + return u.String(), nil + } + // Check if it implements json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + var i2 interface{} + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to decode JSON: %w", err) + } + return primitiveToString(i2) + } + fallthrough + default: + if s, ok := value.(fmt.Stringer); ok { + return s.String(), nil + } + return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) + } +} + +// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. +func marshalKnownTypes(value interface{}) (string, bool) { + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + tt := v.Convert(reflect.TypeOf(time.Time{})) + timeVal := tt.Interface().(time.Time) + return timeVal.Format(time.RFC3339Nano), true + } + + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + d := v.Convert(reflect.TypeOf(Date{})) + dateVal := d.Interface().(Date) + return dateVal.Format(DateFormat), true + } + + if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { + u := v.Convert(reflect.TypeOf(uuid.UUID{})) + uuidVal := u.Interface().(uuid.UUID) + return uuidVal.String(), true + } + + return "", false +} + +// escapeParameterString escapes a parameter value based on its location. +// Query and path parameters need URL escaping; headers and cookies do not. +func escapeParameterString(value string, paramLocation ParamLocation) string { + switch paramLocation { + case ParamLocationQuery: + return url.QueryEscape(value) + case ParamLocationPath: + return url.PathEscape(value) + default: + return value + } +} + +// unescapeParameterString unescapes a parameter value based on its location. +func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { + switch paramLocation { + case ParamLocationQuery, ParamLocationUndefined: + return url.QueryUnescape(value) + case ParamLocationPath: + return url.PathUnescape(value) + default: + return value, nil + } +} + +// sortedKeys returns the keys of a map in sorted order. +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// BindStringToObject binds a string value to a destination object. +// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. +func BindStringToObject(src string, dst interface{}) error { + // Check for TextUnmarshaler + if tu, ok := dst.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(src)) + } + + // Check for Binder interface + if b, ok := dst.(Binder); ok { + return b.Bind(src) + } + + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Ptr { + return fmt.Errorf("dst must be a pointer, got %T", dst) + } + v = v.Elem() + + switch v.Kind() { + case reflect.String: + v.SetString(src) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + v.SetInt(i) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + u, err := strconv.ParseUint(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse uint: %w", err) + } + v.SetUint(u) + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(src, 64) + if err != nil { + return fmt.Errorf("failed to parse float: %w", err) + } + v.SetFloat(f) + case reflect.Bool: + b, err := strconv.ParseBool(src) + if err != nil { + return fmt.Errorf("failed to parse bool: %w", err) + } + v.SetBool(b) + default: + // Try JSON unmarshal as a fallback + return json.Unmarshal([]byte(src), dst) + } + return nil +} + +// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. +func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + newArray := reflect.MakeSlice(t, len(parts), len(parts)) + for i, p := range parts { + err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) + if err != nil { + return fmt.Errorf("error setting array element: %w", err) + } + } + v.Set(newArray) + return nil +} + +// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. +func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { + var fields []string + if explode { + fields = make([]string, len(parts)) + for i, property := range parts { + propertyParts := strings.Split(property, "=") + if len(propertyParts) != 2 { + return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) + } + fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" + } + } else { + if len(parts)%2 != 0 { + return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) + } + fields = make([]string, len(parts)/2) + for i := 0; i < len(parts); i += 2 { + key := parts[i] + value := parts[i+1] + fields[i/2] = "\"" + key + "\":\"" + value + "\"" + } + } + jsonParam := "{" + strings.Join(fields, ",") + "}" + return json.Unmarshal([]byte(jsonParam), dest) +} + +// BindFormExplodeParam binds a form-style parameter with explode to a destination. +// Form style is the default for query and cookie parameters. +// This handles the exploded case where arrays come as multiple query params. +// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) +// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) +func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { + dv := reflect.Indirect(reflect.ValueOf(dest)) + v := dv + var output interface{} + + if required { + output = dest + } else { + // For optional parameters, allocate if nil + if v.IsNil() { + t := v.Type() + newValue := reflect.New(t.Elem()) + output = newValue.Interface() + } else { + output = v.Interface() + } + v = reflect.Indirect(reflect.ValueOf(output)) + } + + t := v.Type() + k := t.Kind() + + values, found := queryParams[paramName] + + switch k { + case reflect.Slice: + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := bindSplitPartsToDestinationArray(values, output) + if err != nil { + return err + } + case reflect.Struct: + // For exploded objects, fields are spread across query params + fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) + if err != nil { + return err + } + if !fieldsPresent { + return nil + } + default: + // Primitive + if len(values) == 0 { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + if len(values) != 1 { + return fmt.Errorf("multiple values for single value parameter '%s'", paramName) + } + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := BindStringToObject(values[0], output) + if err != nil { + return err + } + } + + if !required { + dv.Set(reflect.ValueOf(output)) + } + return nil +} + +// bindParamsToExplodedObject binds query params to struct fields for exploded objects. +func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { + binder, v, t := indirectBinder(dest) + if binder != nil { + _, found := values[paramName] + if !found { + return false, nil + } + return true, BindStringToObject(values.Get(paramName), dest) + } + if t.Kind() != reflect.Struct { + return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) + } + + fieldsPresent := false + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + if !v.Field(i).CanSet() { + continue + } + + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + + fieldVal, found := values[fieldName] + if found { + if len(fieldVal) != 1 { + return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) + } + err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) + if err != nil { + return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) + } + fieldsPresent = true + } + } + return fieldsPresent, nil +} + +// indirectBinder checks if dest implements Binder and returns reflect values. +func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { + v := reflect.ValueOf(dest) + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Binder); ok { + return u, reflect.Value{}, nil + } + } + v = reflect.Indirect(v) + t := v.Type() + // Handle special types like time.Time and Date + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return dest, reflect.Value{}, nil + } + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + return dest, reflect.Value{}, nil + } + return nil, v, t +} + +// BindSimpleParam binds a simple-style parameter without explode to a destination. +// Simple style is the default for path and header parameters. +// Arrays: a,b,c -> []string{"a", "b", "c"} +// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} +func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + // Split on comma and bind as key,value pairs + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) + case reflect.Slice: + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} + +// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. +// Form style is the default for query and cookie parameters. +// Primitives: paramName=value +// Arrays: paramName=a¶mName=b¶mName=c +// Objects: key1=value1&key2=value2 +func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleFormExplodeSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleFormExplodeStruct(paramName, paramLocation, value) + case reflect.Map: + return styleFormExplodeMap(paramName, paramLocation, value) + default: + return styleFormExplodePrimitive(paramName, paramLocation, value) + } +} + +func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Form with explode: paramName=a¶mName=b¶mName=c + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, "&"+prefix), nil +} + +func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleFormExplodeParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. +// Simple style is the default for path and header parameters. +// Arrays are comma-separated: a,b,c +// Objects are key,value pairs: key1,value1,key2,value2 +func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return escapeParameterString(string(b), paramLocation), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleSimpleSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleSimpleStruct(paramName, paramLocation, value) + case reflect.Map: + return styleSimpleMap(paramName, paramLocation, value) + default: + return styleSimplePrimitive(paramLocation, value) + } +} + +func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return escapeParameterString(strVal, paramLocation), nil +} + +func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return escapeParameterString(timeVal, paramLocation), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleSimpleParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +// structToFieldDict converts a struct to a map of field names to string values. +func structToFieldDict(value interface{}) (map[string]string, error) { + v := reflect.ValueOf(value) + t := reflect.TypeOf(value) + fieldDict := make(map[string]string) + + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + f := v.Field(i) + + // Skip nil optional fields + if f.Type().Kind() == reflect.Ptr && f.IsNil() { + continue + } + str, err := primitiveToString(f.Interface()) + if err != nil { + return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) + } + fieldDict[fieldName] = str + } + return fieldDict, nil +} diff --git a/experimental/examples/petstore-expanded/client/client.config.yaml b/experimental/examples/petstore-expanded/client/client.config.yaml new file mode 100644 index 0000000000..eee5ae5073 --- /dev/null +++ b/experimental/examples/petstore-expanded/client/client.config.yaml @@ -0,0 +1,8 @@ +package: client +output: client.gen.go +generation: + client: true + simple-client: true + models-package: + path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded + alias: petstore diff --git a/experimental/examples/petstore-expanded/client/client.gen.go b/experimental/examples/petstore-expanded/client/client.gen.go new file mode 100644 index 0000000000..d095e9cfda --- /dev/null +++ b/experimental/examples/petstore-expanded/client/client.gen.go @@ -0,0 +1,1225 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package client + +import ( + "bytes" + "context" + "encoding" + "encoding/json" + "errors" + "fmt" + "github.com/google/uuid" + petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" + "io" + "net/http" + "net/url" + "reflect" + "sort" + "strconv" + "strings" + "time" +) + +type addPetJSONRequestBody = petstore.NewPet + +// RequestEditorFn is the function signature for the RequestEditor callback function. +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// HttpRequestDoer performs HTTP requests. +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // 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 +} + +// ClientOption allows setting custom parameters during construction. +type ClientOption func(*Client) error + +// NewClient creates a new Client with reasonable defaults. +func NewClient(server string, opts ...ClientOption) (*Client, error) { + client := Client{ + Server: server, + } + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // Ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // Create httpClient if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.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 +} + +// ClientInterface is the interface specification for the client. +type ClientInterface interface { + // FindPets makes a GET request to /pets + FindPets(ctx context.Context, params *FindPetsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // AddPetWithBody makes a POST request to /pets + AddPetWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + AddPet(ctx context.Context, body addPetJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // DeletePet makes a DELETE request to /pets/{id} + DeletePet(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error) + // FindPetByID makes a GET request to /pets/{id} + FindPetByID(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +// FindPetsParams defines parameters for FindPets. +type FindPetsParams struct { + // tags (optional) + Tags *[]string `form:"tags" json:"tags"` + // limit (optional) + Limit *int32 `form:"limit" json:"limit"` +} + +// FindPets makes a GET request to /pets +// Returns all pets +func (c *Client) FindPets(ctx context.Context, params *FindPetsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewFindPetsRequest(c.Server, params) + 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) +} + +// AddPetWithBody makes a POST request to /pets +// Creates a new pet +func (c *Client) AddPetWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewAddPetRequestWithBody(c.Server, contentType, body) + 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) +} + +// AddPet makes a POST request to /pets with JSON body +func (c *Client) AddPet(ctx context.Context, body addPetJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewAddPetRequest(c.Server, body) + 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) +} + +// DeletePet makes a DELETE request to /pets/{id} +// Deletes a pet by ID +func (c *Client) DeletePet(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeletePetRequest(c.Server, id) + 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) +} + +// FindPetByID makes a GET request to /pets/{id} +// Returns a pet by ID +func (c *Client) FindPetByID(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewFindPetByIDRequest(c.Server, id) + 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) +} + +// NewFindPetsRequest creates a GET request for /pets +func NewFindPetsRequest(server string, params *FindPetsParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/pets") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + if params.Tags != nil { + if queryFrag, err := StyleFormExplodeParam("tags", ParamLocationQuery, *params.Tags); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + } + if params.Limit != nil { + if queryFrag, err := StyleFormExplodeParam("limit", ParamLocationQuery, *params.Limit); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + } + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewAddPetRequest creates a POST request for /pets with application/json body +func NewAddPetRequest(server string, body addPetJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewAddPetRequestWithBody(server, "application/json", bodyReader) +} + +// NewAddPetRequestWithBody creates a POST request for /pets with any body +func NewAddPetRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/pets") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewDeletePetRequest creates a DELETE request for /pets/{id} +func NewDeletePetRequest(server string, id int64) (*http.Request, error) { + var err error + + var pathParam0 string + pathParam0, err = StyleSimpleParam("id", ParamLocationPath, id) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/pets/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewFindPetByIDRequest creates a GET request for /pets/{id} +func NewFindPetByIDRequest(server string, id int64) (*http.Request, error) { + var err error + + var pathParam0 string + pathParam0, err = StyleSimpleParam("id", ParamLocationPath, id) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/pets/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// ClientHttpError represents an HTTP error response from the server. +// The type parameter E is the type of the parsed error body. +type ClientHttpError[E any] struct { + StatusCode int + Body E + RawBody []byte +} + +func (e *ClientHttpError[E]) Error() string { + return fmt.Sprintf("HTTP %d", e.StatusCode) +} + +// SimpleClient wraps Client with typed responses for operations that have +// unambiguous response types. Methods return the success type directly, +// and HTTP errors are returned as *ClientHttpError[E] where E is the error type. +type SimpleClient struct { + *Client +} + +// NewSimpleClient creates a new SimpleClient which wraps a Client. +func NewSimpleClient(server string, opts ...ClientOption) (*SimpleClient, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &SimpleClient{Client: client}, nil +} + +// FindPets makes a GET request to /pets and returns the parsed response. +// Returns all pets +// On success, returns the response body. On HTTP error, returns *ClientHttpError[petstore.Error]. +func (c *SimpleClient) FindPets(ctx context.Context, params *FindPetsParams, reqEditors ...RequestEditorFn) ([]petstore.Pet, error) { + var result []petstore.Pet + resp, err := c.Client.FindPets(ctx, params, reqEditors...) + if err != nil { + return result, err + } + defer resp.Body.Close() + + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return result, err + } + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + if err := json.Unmarshal(rawBody, &result); err != nil { + return result, err + } + return result, nil + } + + // Parse error response + var errBody petstore.Error + _ = json.Unmarshal(rawBody, &errBody) // Best effort parse + return result, &ClientHttpError[petstore.Error]{ + StatusCode: resp.StatusCode, + Body: errBody, + RawBody: rawBody, + } +} + +// AddPet makes a POST request to /pets and returns the parsed response. +// Creates a new pet +// On success, returns the response body. On HTTP error, returns *ClientHttpError[petstore.Error]. +func (c *SimpleClient) AddPet(ctx context.Context, body addPetJSONRequestBody, reqEditors ...RequestEditorFn) (petstore.Pet, error) { + var result petstore.Pet + resp, err := c.Client.AddPet(ctx, body, reqEditors...) + if err != nil { + return result, err + } + defer resp.Body.Close() + + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return result, err + } + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + if err := json.Unmarshal(rawBody, &result); err != nil { + return result, err + } + return result, nil + } + + // Parse error response + var errBody petstore.Error + _ = json.Unmarshal(rawBody, &errBody) // Best effort parse + return result, &ClientHttpError[petstore.Error]{ + StatusCode: resp.StatusCode, + Body: errBody, + RawBody: rawBody, + } +} + +// FindPetByID makes a GET request to /pets/{id} and returns the parsed response. +// Returns a pet by ID +// On success, returns the response body. On HTTP error, returns *ClientHttpError[petstore.Error]. +func (c *SimpleClient) FindPetByID(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (petstore.Pet, error) { + var result petstore.Pet + resp, err := c.Client.FindPetByID(ctx, id, reqEditors...) + if err != nil { + return result, err + } + defer resp.Body.Close() + + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return result, err + } + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + if err := json.Unmarshal(rawBody, &result); err != nil { + return result, err + } + return result, nil + } + + // Parse error response + var errBody petstore.Error + _ = json.Unmarshal(rawBody, &errBody) // Best effort parse + return result, &ClientHttpError[petstore.Error]{ + StatusCode: resp.StatusCode, + Body: errBody, + RawBody: rawBody, + } +} + +// ParamLocation indicates where a parameter is located in an HTTP request. +type ParamLocation int + +const ( + ParamLocationUndefined ParamLocation = iota + ParamLocationQuery + ParamLocationPath + ParamLocationHeader + ParamLocationCookie +) + +// Binder is an interface for types that can bind themselves from a string value. +type Binder interface { + Bind(value string) error +} + +// DateFormat is the format used for date (without time) parameters. +const DateFormat = "2006-01-02" + +// Date represents a date (without time) for OpenAPI date format. +type Date struct { + time.Time +} + +// UnmarshalText implements encoding.TextUnmarshaler for Date. +func (d *Date) UnmarshalText(data []byte) error { + t, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = t + return nil +} + +// MarshalText implements encoding.TextMarshaler for Date. +func (d Date) MarshalText() ([]byte, error) { + return []byte(d.Format(DateFormat)), nil +} + +// Format returns the date formatted according to layout. +func (d Date) Format(layout string) string { + return d.Time.Format(layout) +} + +// primitiveToString converts a primitive value to a string representation. +// It handles basic Go types, time.Time, types.Date, and types that implement +// json.Marshaler or fmt.Stringer. +func primitiveToString(value interface{}) (string, error) { + // Check for known types first (time, date, uuid) + if res, ok := marshalKnownTypes(value); ok { + return res, nil + } + + // Dereference pointers for optional values + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + kind := t.Kind() + + switch kind { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + case reflect.Float32: + return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil + case reflect.Bool: + if v.Bool() { + return "true", nil + } + return "false", nil + case reflect.String: + return v.String(), nil + case reflect.Struct: + // Check if it's a UUID + if u, ok := value.(uuid.UUID); ok { + return u.String(), nil + } + // Check if it implements json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + var i2 interface{} + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to decode JSON: %w", err) + } + return primitiveToString(i2) + } + fallthrough + default: + if s, ok := value.(fmt.Stringer); ok { + return s.String(), nil + } + return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) + } +} + +// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. +func marshalKnownTypes(value interface{}) (string, bool) { + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + tt := v.Convert(reflect.TypeOf(time.Time{})) + timeVal := tt.Interface().(time.Time) + return timeVal.Format(time.RFC3339Nano), true + } + + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + d := v.Convert(reflect.TypeOf(Date{})) + dateVal := d.Interface().(Date) + return dateVal.Format(DateFormat), true + } + + if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { + u := v.Convert(reflect.TypeOf(uuid.UUID{})) + uuidVal := u.Interface().(uuid.UUID) + return uuidVal.String(), true + } + + return "", false +} + +// escapeParameterString escapes a parameter value based on its location. +// Query and path parameters need URL escaping; headers and cookies do not. +func escapeParameterString(value string, paramLocation ParamLocation) string { + switch paramLocation { + case ParamLocationQuery: + return url.QueryEscape(value) + case ParamLocationPath: + return url.PathEscape(value) + default: + return value + } +} + +// unescapeParameterString unescapes a parameter value based on its location. +func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { + switch paramLocation { + case ParamLocationQuery, ParamLocationUndefined: + return url.QueryUnescape(value) + case ParamLocationPath: + return url.PathUnescape(value) + default: + return value, nil + } +} + +// sortedKeys returns the keys of a map in sorted order. +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// BindStringToObject binds a string value to a destination object. +// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. +func BindStringToObject(src string, dst interface{}) error { + // Check for TextUnmarshaler + if tu, ok := dst.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(src)) + } + + // Check for Binder interface + if b, ok := dst.(Binder); ok { + return b.Bind(src) + } + + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Ptr { + return fmt.Errorf("dst must be a pointer, got %T", dst) + } + v = v.Elem() + + switch v.Kind() { + case reflect.String: + v.SetString(src) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + v.SetInt(i) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + u, err := strconv.ParseUint(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse uint: %w", err) + } + v.SetUint(u) + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(src, 64) + if err != nil { + return fmt.Errorf("failed to parse float: %w", err) + } + v.SetFloat(f) + case reflect.Bool: + b, err := strconv.ParseBool(src) + if err != nil { + return fmt.Errorf("failed to parse bool: %w", err) + } + v.SetBool(b) + default: + // Try JSON unmarshal as a fallback + return json.Unmarshal([]byte(src), dst) + } + return nil +} + +// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. +func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + newArray := reflect.MakeSlice(t, len(parts), len(parts)) + for i, p := range parts { + err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) + if err != nil { + return fmt.Errorf("error setting array element: %w", err) + } + } + v.Set(newArray) + return nil +} + +// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. +func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { + var fields []string + if explode { + fields = make([]string, len(parts)) + for i, property := range parts { + propertyParts := strings.Split(property, "=") + if len(propertyParts) != 2 { + return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) + } + fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" + } + } else { + if len(parts)%2 != 0 { + return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) + } + fields = make([]string, len(parts)/2) + for i := 0; i < len(parts); i += 2 { + key := parts[i] + value := parts[i+1] + fields[i/2] = "\"" + key + "\":\"" + value + "\"" + } + } + jsonParam := "{" + strings.Join(fields, ",") + "}" + return json.Unmarshal([]byte(jsonParam), dest) +} + +// BindFormExplodeParam binds a form-style parameter with explode to a destination. +// Form style is the default for query and cookie parameters. +// This handles the exploded case where arrays come as multiple query params. +// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) +// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) +func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { + dv := reflect.Indirect(reflect.ValueOf(dest)) + v := dv + var output interface{} + + if required { + output = dest + } else { + // For optional parameters, allocate if nil + if v.IsNil() { + t := v.Type() + newValue := reflect.New(t.Elem()) + output = newValue.Interface() + } else { + output = v.Interface() + } + v = reflect.Indirect(reflect.ValueOf(output)) + } + + t := v.Type() + k := t.Kind() + + values, found := queryParams[paramName] + + switch k { + case reflect.Slice: + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := bindSplitPartsToDestinationArray(values, output) + if err != nil { + return err + } + case reflect.Struct: + // For exploded objects, fields are spread across query params + fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) + if err != nil { + return err + } + if !fieldsPresent { + return nil + } + default: + // Primitive + if len(values) == 0 { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + if len(values) != 1 { + return fmt.Errorf("multiple values for single value parameter '%s'", paramName) + } + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := BindStringToObject(values[0], output) + if err != nil { + return err + } + } + + if !required { + dv.Set(reflect.ValueOf(output)) + } + return nil +} + +// bindParamsToExplodedObject binds query params to struct fields for exploded objects. +func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { + binder, v, t := indirectBinder(dest) + if binder != nil { + _, found := values[paramName] + if !found { + return false, nil + } + return true, BindStringToObject(values.Get(paramName), dest) + } + if t.Kind() != reflect.Struct { + return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) + } + + fieldsPresent := false + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + if !v.Field(i).CanSet() { + continue + } + + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + + fieldVal, found := values[fieldName] + if found { + if len(fieldVal) != 1 { + return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) + } + err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) + if err != nil { + return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) + } + fieldsPresent = true + } + } + return fieldsPresent, nil +} + +// indirectBinder checks if dest implements Binder and returns reflect values. +func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { + v := reflect.ValueOf(dest) + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Binder); ok { + return u, reflect.Value{}, nil + } + } + v = reflect.Indirect(v) + t := v.Type() + // Handle special types like time.Time and Date + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return dest, reflect.Value{}, nil + } + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + return dest, reflect.Value{}, nil + } + return nil, v, t +} + +// BindSimpleParam binds a simple-style parameter without explode to a destination. +// Simple style is the default for path and header parameters. +// Arrays: a,b,c -> []string{"a", "b", "c"} +// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} +func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + // Split on comma and bind as key,value pairs + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) + case reflect.Slice: + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} + +// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. +// Form style is the default for query and cookie parameters. +// Primitives: paramName=value +// Arrays: paramName=a¶mName=b¶mName=c +// Objects: key1=value1&key2=value2 +func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleFormExplodeSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleFormExplodeStruct(paramName, paramLocation, value) + case reflect.Map: + return styleFormExplodeMap(paramName, paramLocation, value) + default: + return styleFormExplodePrimitive(paramName, paramLocation, value) + } +} + +func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Form with explode: paramName=a¶mName=b¶mName=c + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, "&"+prefix), nil +} + +func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleFormExplodeParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. +// Simple style is the default for path and header parameters. +// Arrays are comma-separated: a,b,c +// Objects are key,value pairs: key1,value1,key2,value2 +func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return escapeParameterString(string(b), paramLocation), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleSimpleSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleSimpleStruct(paramName, paramLocation, value) + case reflect.Map: + return styleSimpleMap(paramName, paramLocation, value) + default: + return styleSimplePrimitive(paramLocation, value) + } +} + +func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return escapeParameterString(strVal, paramLocation), nil +} + +func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return escapeParameterString(timeVal, paramLocation), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleSimpleParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +// structToFieldDict converts a struct to a map of field names to string values. +func structToFieldDict(value interface{}) (map[string]string, error) { + v := reflect.ValueOf(value) + t := reflect.TypeOf(value) + fieldDict := make(map[string]string) + + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + f := v.Field(i) + + // Skip nil optional fields + if f.Type().Kind() == reflect.Ptr && f.IsNil() { + continue + } + str, err := primitiveToString(f.Interface()) + if err != nil { + return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) + } + fieldDict[fieldName] = str + } + return fieldDict, nil +} diff --git a/experimental/examples/petstore-expanded/client/client_test.go b/experimental/examples/petstore-expanded/client/client_test.go new file mode 100644 index 0000000000..76ad5ef8e4 --- /dev/null +++ b/experimental/examples/petstore-expanded/client/client_test.go @@ -0,0 +1,161 @@ +package client + +import ( + "context" + "net/http" + "testing" + + petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestClientTypes verifies all generated types exist and have correct structure. +// If this test compiles, the type generation is correct. +func TestClientTypes(t *testing.T) { + // Core client types + var _ *Client + var _ *SimpleClient + var _ ClientInterface + var _ ClientOption + var _ RequestEditorFn + var _ HttpRequestDoer + + // Param types + var _ *FindPetsParams + + // Request body type alias + var _ addPetJSONRequestBody + + // Error type with generic parameter + var _ *ClientHttpError[petstore.Error] +} + +// TestClientStructure verifies Client struct has expected fields by accessing them. +func TestClientStructure(t *testing.T) { + c := &Client{} + + // Access fields - compiler validates they exist with correct types + var _ = c.Server + var _ = c.Client + var _ = c.RequestEditors +} + +// TestClientImplementsInterface verifies Client implements ClientInterface. +func TestClientImplementsInterface(t *testing.T) { + var _ ClientInterface = (*Client)(nil) +} + +// TestClientInterfaceMethods verifies ClientInterface methods exist with correct signatures. +// The fact that Client implements ClientInterface (tested above) validates all method signatures. +// Here we use method expressions to verify exact signatures without needing an instance. +func TestClientInterfaceMethods(t *testing.T) { + // Method expressions verify signatures at compile time + var _ = (*Client).FindPets + var _ = (*Client).AddPetWithBody + var _ = (*Client).AddPet + var _ = (*Client).DeletePet + var _ = (*Client).FindPetByID +} + +// TestSimpleClientMethods verifies SimpleClient methods return typed responses. +func TestSimpleClientMethods(t *testing.T) { + // Use method expressions to verify signatures without needing an instance + // Compiler validates return types - these use the petstore package types + var _ = (*SimpleClient).FindPets + var _ = (*SimpleClient).AddPet + var _ = (*SimpleClient).FindPetByID +} + +// TestFindPetsParamsStructure verifies param struct fields. +func TestFindPetsParamsStructure(t *testing.T) { + p := &FindPetsParams{} + + // Access fields - compiler validates they exist with correct types + var _ = p.Tags + var _ = p.Limit +} + +// TestRequestBodyTypeAlias verifies the type alias points to correct type. +func TestRequestBodyTypeAlias(t *testing.T) { + // Compiler validates the alias is compatible with petstore.NewPet + var body addPetJSONRequestBody + var newPet petstore.NewPet + + // Bidirectional assignment proves they're the same type + body = newPet + newPet = body + _ = body + _ = newPet +} + +// TestPetstoreTypes verifies that generated types use short names directly. +func TestPetstoreTypes(t *testing.T) { + // Types should be defined directly with short names (no SchemaComponent suffix) + var _ petstore.Pet + var _ petstore.NewPet + var _ petstore.Error +} + +// TestClientHttpErrorImplementsError verifies ClientHttpError implements error interface. +func TestClientHttpErrorImplementsError(t *testing.T) { + var _ error = (*ClientHttpError[petstore.Error])(nil) +} + +// TestClientHttpErrorStructure verifies error type fields. +func TestClientHttpErrorStructure(t *testing.T) { + e := &ClientHttpError[petstore.Error]{} + + // Access fields - compiler validates they exist with correct types + var _ = e.StatusCode + var _ = e.Body + var _ = e.RawBody +} + +// TestRequestBuilders verifies request builder functions exist with correct signatures. +func TestRequestBuilders(t *testing.T) { + var _ = NewFindPetsRequest + var _ = NewAddPetRequestWithBody + var _ = NewAddPetRequest + var _ = NewDeletePetRequest + var _ = NewFindPetByIDRequest +} + +// TestNewClientConstructor verifies the constructor works correctly. +func TestNewClientConstructor(t *testing.T) { + client, err := NewClient("https://api.example.com") + require.NoError(t, err) + require.NotNil(t, client) + + // Verify trailing slash is added + assert.Equal(t, "https://api.example.com/", client.Server) + // Verify default http.Client is created + assert.NotNil(t, client.Client) +} + +// TestClientOptions verifies client options work correctly. +func TestClientOptions(t *testing.T) { + customClient := &http.Client{} + + client, err := NewClient("https://api.example.com", + WithHTTPClient(customClient), + WithRequestEditorFn(func(ctx context.Context, req *http.Request) error { + req.Header.Set("X-Custom", "value") + return nil + }), + ) + require.NoError(t, err) + + assert.Equal(t, customClient, client.Client) + assert.Len(t, client.RequestEditors, 1) +} + +// TestClientHttpErrorMessage verifies the error message format. +func TestClientHttpErrorMessage(t *testing.T) { + err := &ClientHttpError[petstore.Error]{ + StatusCode: 404, + Body: petstore.Error{Code: 404, Message: "Not Found"}, + } + + assert.Contains(t, err.Error(), "404") +} diff --git a/experimental/examples/petstore-expanded/client/validator/main.go b/experimental/examples/petstore-expanded/client/validator/main.go new file mode 100644 index 0000000000..d84632f301 --- /dev/null +++ b/experimental/examples/petstore-expanded/client/validator/main.go @@ -0,0 +1,143 @@ +package main + +import ( + "context" + "flag" + "log" + "os" + "strings" + + petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" + "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/client" +) + +func ptr[T any](v T) *T { return &v } + +func main() { + serverURL := flag.String("server", "http://localhost:8080", "Petstore server URL") + flag.Parse() + + log.SetFlags(0) + log.Printf("Petstore Validator") + log.Printf("==================") + log.Printf("Server: %s", *serverURL) + log.Println() + + c, err := client.NewSimpleClient(*serverURL) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + // Step 1: Add Fido the Dog and Sushi the Cat + log.Println("--- Step 1: Creating pets ---") + + fido, err := c.AddPet(ctx, petstore.NewPet{Name: "Fido", Tag: ptr("Dog")}) + if err != nil { + log.Fatalf("Failed to create Fido: %v", err) + } + log.Printf("Created pet: %s (tag=%s, id=%d)", fido.Name, derefTag(fido.Tag), fido.ID) + + sushi, err := c.AddPet(ctx, petstore.NewPet{Name: "Sushi", Tag: ptr("Cat")}) + if err != nil { + log.Fatalf("Failed to create Sushi: %v", err) + } + log.Printf("Created pet: %s (tag=%s, id=%d)", sushi.Name, derefTag(sushi.Tag), sushi.ID) + log.Println() + + // Step 2: List all pets + log.Println("--- Step 2: Listing all pets ---") + pets, err := c.FindPets(ctx, nil) + if err != nil { + log.Fatalf("Failed to list pets: %v", err) + } + printPets(pets) + log.Println() + + // Step 3: Delete Fido + log.Printf("--- Step 3: Deleting Fido (id=%d) ---", fido.ID) + resp, err := c.DeletePet(ctx, fido.ID) + if err != nil { + log.Fatalf("Failed to delete Fido: %v", err) + } + _ = resp.Body.Close() + if resp.StatusCode == 204 { + log.Printf("Deleted Fido successfully (HTTP %d)", resp.StatusCode) + } else { + log.Fatalf("Unexpected status deleting Fido: HTTP %d", resp.StatusCode) + } + log.Println() + + // Step 4: Add Slimy the Lizard + log.Println("--- Step 4: Creating Slimy the Lizard ---") + slimy, err := c.AddPet(ctx, petstore.NewPet{Name: "Slimy", Tag: ptr("Lizard")}) + if err != nil { + log.Fatalf("Failed to create Slimy: %v", err) + } + log.Printf("Created pet: %s (tag=%s, id=%d)", slimy.Name, derefTag(slimy.Tag), slimy.ID) + log.Println() + + // Step 5: List all pets again + log.Println("--- Step 5: Listing all pets (after changes) ---") + pets, err = c.FindPets(ctx, nil) + if err != nil { + log.Fatalf("Failed to list pets: %v", err) + } + printPets(pets) + log.Println() + + // Validate final state + log.Println("--- Validation ---") + ok := true + if len(pets) != 2 { + log.Printf("FAIL: expected 2 pets, got %d", len(pets)) + ok = false + } + names := map[string]bool{} + for _, p := range pets { + names[p.Name] = true + } + if !names["Sushi"] { + log.Printf("FAIL: Sushi not found in pet list") + ok = false + } + if !names["Slimy"] { + log.Printf("FAIL: Slimy not found in pet list") + ok = false + } + if names["Fido"] { + log.Printf("FAIL: Fido should have been deleted but is still present") + ok = false + } + + if ok { + log.Println("PASS: All validations passed!") + } else { + log.Println("FAIL: Some validations failed") + os.Exit(1) + } +} + +func derefTag(tag *string) string { + if tag == nil { + return "" + } + return *tag +} + +func printPets(pets []petstore.Pet) { + if len(pets) == 0 { + log.Println(" (no pets)") + return + } + maxName := 0 + for _, p := range pets { + if len(p.Name) > maxName { + maxName = len(p.Name) + } + } + for _, p := range pets { + padding := strings.Repeat(" ", maxName-len(p.Name)) + log.Printf(" - %s%s tag=%-8s id=%d", p.Name, padding, derefTag(p.Tag), p.ID) + } +} diff --git a/experimental/examples/petstore-expanded/doc.go b/experimental/examples/petstore-expanded/doc.go new file mode 100644 index 0000000000..d3e6eede3d --- /dev/null +++ b/experimental/examples/petstore-expanded/doc.go @@ -0,0 +1,2 @@ +// Package petstore provides generated types for the Petstore API. +package petstore diff --git a/experimental/examples/petstore-expanded/echo-v4/Makefile b/experimental/examples/petstore-expanded/echo-v4/Makefile new file mode 100644 index 0000000000..42389f4137 --- /dev/null +++ b/experimental/examples/petstore-expanded/echo-v4/Makefile @@ -0,0 +1,35 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + +generate: + $(call execute-if-go-124,go generate ./...) + +test: + $(call execute-if-go-124,go test -cover ./...) + +tidy: + $(call execute-if-go-124,go mod tidy) + +tidy-ci: + $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/echo-v4/go.mod b/experimental/examples/petstore-expanded/echo-v4/go.mod new file mode 100644 index 0000000000..241f767a53 --- /dev/null +++ b/experimental/examples/petstore-expanded/echo-v4/go.mod @@ -0,0 +1,23 @@ +module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/echo-v4 + +go 1.24.0 + +require ( + github.com/google/uuid v1.6.0 + github.com/labstack/echo/v4 v4.13.3 + github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 +) + +require ( + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.33.0 // indirect +) + +replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/echo-v4/go.sum b/experimental/examples/petstore-expanded/echo-v4/go.sum new file mode 100644 index 0000000000..d6b2e1f6c1 --- /dev/null +++ b/experimental/examples/petstore-expanded/echo-v4/go.sum @@ -0,0 +1,33 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/experimental/examples/petstore-expanded/echo-v4/main.go b/experimental/examples/petstore-expanded/echo-v4/main.go new file mode 100644 index 0000000000..5f89fc8cd3 --- /dev/null +++ b/experimental/examples/petstore-expanded/echo-v4/main.go @@ -0,0 +1,34 @@ +//go:build go1.22 + +// This is an example of implementing the Pet Store from the OpenAPI documentation +// found at: +// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml + +package main + +import ( + "flag" + "log" + "net" + + "github.com/labstack/echo/v4" + "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/echo-v4/server" +) + +func main() { + port := flag.String("port", "8080", "Port for test HTTP server") + flag.Parse() + + // Create an instance of our handler which satisfies the generated interface + petStore := server.NewPetStore() + + e := echo.New() + + // We now register our petStore above as the handler for the interface + server.RegisterHandlers(e, petStore) + + log.Printf("Server listening on %s", net.JoinHostPort("0.0.0.0", *port)) + + // And we serve HTTP until the world ends. + log.Fatal(e.Start(net.JoinHostPort("0.0.0.0", *port))) +} diff --git a/experimental/examples/petstore-expanded/echo-v4/server/petstore.go b/experimental/examples/petstore-expanded/echo-v4/server/petstore.go new file mode 100644 index 0000000000..9ee8d05a1b --- /dev/null +++ b/experimental/examples/petstore-expanded/echo-v4/server/petstore.go @@ -0,0 +1,123 @@ +//go:build go1.22 + +package server + +import ( + "net/http" + "sync" + + "github.com/labstack/echo/v4" + petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" +) + +// PetStore implements the ServerInterface. +type PetStore struct { + Pets map[int64]petstore.Pet + NextId int64 + Lock sync.Mutex +} + +// Make sure we conform to ServerInterface +var _ ServerInterface = (*PetStore)(nil) + +// NewPetStore creates a new PetStore. +func NewPetStore() *PetStore { + return &PetStore{ + Pets: make(map[int64]petstore.Pet), + NextId: 1000, + } +} + +// sendPetStoreError wraps sending of an error in the Error format. +func sendPetStoreError(ctx echo.Context, code int, message string) error { + petErr := petstore.Error{ + Code: int32(code), + Message: message, + } + return ctx.JSON(code, petErr) +} + +// FindPets returns all pets, optionally filtered by tags and limited. +func (p *PetStore) FindPets(ctx echo.Context, params FindPetsParams) error { + p.Lock.Lock() + defer p.Lock.Unlock() + + var result []petstore.Pet + + for _, pet := range p.Pets { + if params.Tags != nil { + // If we have tags, filter pets by tag + for _, t := range *params.Tags { + if pet.Tag != nil && (*pet.Tag == t) { + result = append(result, pet) + } + } + } else { + // Add all pets if we're not filtering + result = append(result, pet) + } + + if params.Limit != nil { + l := int(*params.Limit) + if len(result) >= l { + // We're at the limit + break + } + } + } + + return ctx.JSON(http.StatusOK, result) +} + +// AddPet creates a new pet. +func (p *PetStore) AddPet(ctx echo.Context) error { + // We expect a NewPet object in the request body. + var newPet petstore.NewPet + if err := ctx.Bind(&newPet); err != nil { + return sendPetStoreError(ctx, http.StatusBadRequest, "Invalid format for NewPet") + } + + // We now have a pet, let's add it to our "database". + p.Lock.Lock() + defer p.Lock.Unlock() + + // We handle pets, not NewPets, which have an additional ID field + var pet petstore.Pet + pet.Name = newPet.Name + pet.Tag = newPet.Tag + pet.ID = p.NextId + p.NextId++ + + // Insert into map + p.Pets[pet.ID] = pet + + // Now, we have to return the Pet + return ctx.JSON(http.StatusCreated, pet) +} + +// FindPetByID returns a pet by ID. +func (p *PetStore) FindPetByID(ctx echo.Context, id int64) error { + p.Lock.Lock() + defer p.Lock.Unlock() + + pet, found := p.Pets[id] + if !found { + return sendPetStoreError(ctx, http.StatusNotFound, "Could not find pet with ID") + } + + return ctx.JSON(http.StatusOK, pet) +} + +// DeletePet deletes a pet by ID. +func (p *PetStore) DeletePet(ctx echo.Context, id int64) error { + p.Lock.Lock() + defer p.Lock.Unlock() + + _, found := p.Pets[id] + if !found { + return sendPetStoreError(ctx, http.StatusNotFound, "Could not find pet with ID") + } + delete(p.Pets, id) + + return ctx.NoContent(http.StatusNoContent) +} diff --git a/experimental/examples/petstore-expanded/echo-v4/server/server.config.yaml b/experimental/examples/petstore-expanded/echo-v4/server/server.config.yaml new file mode 100644 index 0000000000..c4f40a9a65 --- /dev/null +++ b/experimental/examples/petstore-expanded/echo-v4/server/server.config.yaml @@ -0,0 +1,6 @@ +package: server +generation: + server: echo/v4 + models-package: + path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded + alias: petstore diff --git a/experimental/examples/petstore-expanded/echo-v4/server/server.gen.go b/experimental/examples/petstore-expanded/echo-v4/server/server.gen.go new file mode 100644 index 0000000000..af329680a5 --- /dev/null +++ b/experimental/examples/petstore-expanded/echo-v4/server/server.gen.go @@ -0,0 +1,976 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package server + +import ( + "bytes" + "encoding" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "reflect" + "sort" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Returns all pets + // (GET /pets) + FindPets(ctx echo.Context, params FindPetsParams) error + // Creates a new pet + // (POST /pets) + AddPet(ctx echo.Context) error + // Deletes a pet by ID + // (DELETE /pets/{id}) + DeletePet(ctx echo.Context, id int64) error + // Returns a pet by ID + // (GET /pets/{id}) + FindPetByID(ctx echo.Context, id int64) error +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. +type Unimplemented struct{} + +// Returns all pets +// (GET /pets) +func (_ Unimplemented) FindPets(ctx echo.Context, params FindPetsParams) error { + return ctx.NoContent(http.StatusNotImplemented) +} + +// Creates a new pet +// (POST /pets) +func (_ Unimplemented) AddPet(ctx echo.Context) error { + return ctx.NoContent(http.StatusNotImplemented) +} + +// Deletes a pet by ID +// (DELETE /pets/{id}) +func (_ Unimplemented) DeletePet(ctx echo.Context, id int64) error { + return ctx.NoContent(http.StatusNotImplemented) +} + +// Returns a pet by ID +// (GET /pets/{id}) +func (_ Unimplemented) FindPetByID(ctx echo.Context, id int64) error { + return ctx.NoContent(http.StatusNotImplemented) +} + +// FindPetsParams defines parameters for FindPets. +type FindPetsParams struct { + // tags (optional) + Tags *[]string `form:"tags" json:"tags"` + // limit (optional) + Limit *int32 `form:"limit" json:"limit"` +} + +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +// FindPets converts echo context to params. +func (w *ServerInterfaceWrapper) FindPets(ctx echo.Context) error { + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params FindPetsParams + + // ------------- Optional query parameter "tags" ------------- + err = BindFormExplodeParam("tags", false, ctx.QueryParams(), ¶ms.Tags) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter tags: %s", err)) + } + + // ------------- Optional query parameter "limit" ------------- + err = BindFormExplodeParam("limit", false, ctx.QueryParams(), ¶ms.Limit) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter limit: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.FindPets(ctx, params) + return err +} + +// AddPet converts echo context to params. +func (w *ServerInterfaceWrapper) AddPet(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.AddPet(ctx) + return err +} + +// DeletePet converts echo context to params. +func (w *ServerInterfaceWrapper) DeletePet(ctx echo.Context) error { + var err error + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, ctx.Param("id"), &id) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.DeletePet(ctx, id) + return err +} + +// FindPetByID converts echo context to params. +func (w *ServerInterfaceWrapper) FindPetByID(ctx echo.Context) error { + var err error + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, ctx.Param("id"), &id) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.FindPetByID(ctx, id) + return err +} + +// EchoRouter is an interface for echo.Echo and echo.Group. +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 +} + +// RegisterHandlers adds each server route to the EchoRouter. +func RegisterHandlers(router EchoRouter, si ServerInterface) { + RegisterHandlersWithBaseURL(router, si, "") +} + +// RegisterHandlersWithBaseURL adds each server route to the EchoRouter with a base URL prefix. +func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { + + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + router.GET(baseURL+"/pets", wrapper.FindPets) + router.POST(baseURL+"/pets", wrapper.AddPet) + router.DELETE(baseURL+"/pets/:id", wrapper.DeletePet) + router.GET(baseURL+"/pets/:id", wrapper.FindPetByID) +} + +// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. +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 +} + +// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. +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 +} + +// RequiredParamError is returned when a required parameter is missing. +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +// RequiredHeaderError is returned when a required header is missing. +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 +} + +// InvalidParamFormatError is returned when a parameter has an invalid format. +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 +} + +// TooManyValuesForParamError is returned when a parameter has too many values. +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) +} + +// ParamLocation indicates where a parameter is located in an HTTP request. +type ParamLocation int + +const ( + ParamLocationUndefined ParamLocation = iota + ParamLocationQuery + ParamLocationPath + ParamLocationHeader + ParamLocationCookie +) + +// Binder is an interface for types that can bind themselves from a string value. +type Binder interface { + Bind(value string) error +} + +// DateFormat is the format used for date (without time) parameters. +const DateFormat = "2006-01-02" + +// Date represents a date (without time) for OpenAPI date format. +type Date struct { + time.Time +} + +// UnmarshalText implements encoding.TextUnmarshaler for Date. +func (d *Date) UnmarshalText(data []byte) error { + t, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = t + return nil +} + +// MarshalText implements encoding.TextMarshaler for Date. +func (d Date) MarshalText() ([]byte, error) { + return []byte(d.Format(DateFormat)), nil +} + +// Format returns the date formatted according to layout. +func (d Date) Format(layout string) string { + return d.Time.Format(layout) +} + +// primitiveToString converts a primitive value to a string representation. +// It handles basic Go types, time.Time, types.Date, and types that implement +// json.Marshaler or fmt.Stringer. +func primitiveToString(value interface{}) (string, error) { + // Check for known types first (time, date, uuid) + if res, ok := marshalKnownTypes(value); ok { + return res, nil + } + + // Dereference pointers for optional values + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + kind := t.Kind() + + switch kind { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + case reflect.Float32: + return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil + case reflect.Bool: + if v.Bool() { + return "true", nil + } + return "false", nil + case reflect.String: + return v.String(), nil + case reflect.Struct: + // Check if it's a UUID + if u, ok := value.(uuid.UUID); ok { + return u.String(), nil + } + // Check if it implements json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + var i2 interface{} + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to decode JSON: %w", err) + } + return primitiveToString(i2) + } + fallthrough + default: + if s, ok := value.(fmt.Stringer); ok { + return s.String(), nil + } + return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) + } +} + +// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. +func marshalKnownTypes(value interface{}) (string, bool) { + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + tt := v.Convert(reflect.TypeOf(time.Time{})) + timeVal := tt.Interface().(time.Time) + return timeVal.Format(time.RFC3339Nano), true + } + + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + d := v.Convert(reflect.TypeOf(Date{})) + dateVal := d.Interface().(Date) + return dateVal.Format(DateFormat), true + } + + if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { + u := v.Convert(reflect.TypeOf(uuid.UUID{})) + uuidVal := u.Interface().(uuid.UUID) + return uuidVal.String(), true + } + + return "", false +} + +// escapeParameterString escapes a parameter value based on its location. +// Query and path parameters need URL escaping; headers and cookies do not. +func escapeParameterString(value string, paramLocation ParamLocation) string { + switch paramLocation { + case ParamLocationQuery: + return url.QueryEscape(value) + case ParamLocationPath: + return url.PathEscape(value) + default: + return value + } +} + +// unescapeParameterString unescapes a parameter value based on its location. +func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { + switch paramLocation { + case ParamLocationQuery, ParamLocationUndefined: + return url.QueryUnescape(value) + case ParamLocationPath: + return url.PathUnescape(value) + default: + return value, nil + } +} + +// sortedKeys returns the keys of a map in sorted order. +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// BindStringToObject binds a string value to a destination object. +// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. +func BindStringToObject(src string, dst interface{}) error { + // Check for TextUnmarshaler + if tu, ok := dst.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(src)) + } + + // Check for Binder interface + if b, ok := dst.(Binder); ok { + return b.Bind(src) + } + + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Ptr { + return fmt.Errorf("dst must be a pointer, got %T", dst) + } + v = v.Elem() + + switch v.Kind() { + case reflect.String: + v.SetString(src) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + v.SetInt(i) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + u, err := strconv.ParseUint(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse uint: %w", err) + } + v.SetUint(u) + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(src, 64) + if err != nil { + return fmt.Errorf("failed to parse float: %w", err) + } + v.SetFloat(f) + case reflect.Bool: + b, err := strconv.ParseBool(src) + if err != nil { + return fmt.Errorf("failed to parse bool: %w", err) + } + v.SetBool(b) + default: + // Try JSON unmarshal as a fallback + return json.Unmarshal([]byte(src), dst) + } + return nil +} + +// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. +func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + newArray := reflect.MakeSlice(t, len(parts), len(parts)) + for i, p := range parts { + err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) + if err != nil { + return fmt.Errorf("error setting array element: %w", err) + } + } + v.Set(newArray) + return nil +} + +// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. +func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { + var fields []string + if explode { + fields = make([]string, len(parts)) + for i, property := range parts { + propertyParts := strings.Split(property, "=") + if len(propertyParts) != 2 { + return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) + } + fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" + } + } else { + if len(parts)%2 != 0 { + return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) + } + fields = make([]string, len(parts)/2) + for i := 0; i < len(parts); i += 2 { + key := parts[i] + value := parts[i+1] + fields[i/2] = "\"" + key + "\":\"" + value + "\"" + } + } + jsonParam := "{" + strings.Join(fields, ",") + "}" + return json.Unmarshal([]byte(jsonParam), dest) +} + +// BindFormExplodeParam binds a form-style parameter with explode to a destination. +// Form style is the default for query and cookie parameters. +// This handles the exploded case where arrays come as multiple query params. +// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) +// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) +func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { + dv := reflect.Indirect(reflect.ValueOf(dest)) + v := dv + var output interface{} + + if required { + output = dest + } else { + // For optional parameters, allocate if nil + if v.IsNil() { + t := v.Type() + newValue := reflect.New(t.Elem()) + output = newValue.Interface() + } else { + output = v.Interface() + } + v = reflect.Indirect(reflect.ValueOf(output)) + } + + t := v.Type() + k := t.Kind() + + values, found := queryParams[paramName] + + switch k { + case reflect.Slice: + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := bindSplitPartsToDestinationArray(values, output) + if err != nil { + return err + } + case reflect.Struct: + // For exploded objects, fields are spread across query params + fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) + if err != nil { + return err + } + if !fieldsPresent { + return nil + } + default: + // Primitive + if len(values) == 0 { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + if len(values) != 1 { + return fmt.Errorf("multiple values for single value parameter '%s'", paramName) + } + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := BindStringToObject(values[0], output) + if err != nil { + return err + } + } + + if !required { + dv.Set(reflect.ValueOf(output)) + } + return nil +} + +// bindParamsToExplodedObject binds query params to struct fields for exploded objects. +func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { + binder, v, t := indirectBinder(dest) + if binder != nil { + _, found := values[paramName] + if !found { + return false, nil + } + return true, BindStringToObject(values.Get(paramName), dest) + } + if t.Kind() != reflect.Struct { + return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) + } + + fieldsPresent := false + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + if !v.Field(i).CanSet() { + continue + } + + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + + fieldVal, found := values[fieldName] + if found { + if len(fieldVal) != 1 { + return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) + } + err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) + if err != nil { + return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) + } + fieldsPresent = true + } + } + return fieldsPresent, nil +} + +// indirectBinder checks if dest implements Binder and returns reflect values. +func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { + v := reflect.ValueOf(dest) + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Binder); ok { + return u, reflect.Value{}, nil + } + } + v = reflect.Indirect(v) + t := v.Type() + // Handle special types like time.Time and Date + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return dest, reflect.Value{}, nil + } + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + return dest, reflect.Value{}, nil + } + return nil, v, t +} + +// BindSimpleParam binds a simple-style parameter without explode to a destination. +// Simple style is the default for path and header parameters. +// Arrays: a,b,c -> []string{"a", "b", "c"} +// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} +func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + // Split on comma and bind as key,value pairs + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) + case reflect.Slice: + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} + +// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. +// Form style is the default for query and cookie parameters. +// Primitives: paramName=value +// Arrays: paramName=a¶mName=b¶mName=c +// Objects: key1=value1&key2=value2 +func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleFormExplodeSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleFormExplodeStruct(paramName, paramLocation, value) + case reflect.Map: + return styleFormExplodeMap(paramName, paramLocation, value) + default: + return styleFormExplodePrimitive(paramName, paramLocation, value) + } +} + +func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Form with explode: paramName=a¶mName=b¶mName=c + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, "&"+prefix), nil +} + +func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleFormExplodeParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. +// Simple style is the default for path and header parameters. +// Arrays are comma-separated: a,b,c +// Objects are key,value pairs: key1,value1,key2,value2 +func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return escapeParameterString(string(b), paramLocation), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleSimpleSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleSimpleStruct(paramName, paramLocation, value) + case reflect.Map: + return styleSimpleMap(paramName, paramLocation, value) + default: + return styleSimplePrimitive(paramLocation, value) + } +} + +func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return escapeParameterString(strVal, paramLocation), nil +} + +func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return escapeParameterString(timeVal, paramLocation), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleSimpleParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +// structToFieldDict converts a struct to a map of field names to string values. +func structToFieldDict(value interface{}) (map[string]string, error) { + v := reflect.ValueOf(value) + t := reflect.TypeOf(value) + fieldDict := make(map[string]string) + + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + f := v.Field(i) + + // Skip nil optional fields + if f.Type().Kind() == reflect.Ptr && f.IsNil() { + continue + } + str, err := primitiveToString(f.Interface()) + if err != nil { + return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) + } + fieldDict[fieldName] = str + } + return fieldDict, nil +} diff --git a/experimental/examples/petstore-expanded/echo/Makefile b/experimental/examples/petstore-expanded/echo/Makefile new file mode 100644 index 0000000000..42389f4137 --- /dev/null +++ b/experimental/examples/petstore-expanded/echo/Makefile @@ -0,0 +1,35 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + +generate: + $(call execute-if-go-124,go generate ./...) + +test: + $(call execute-if-go-124,go test -cover ./...) + +tidy: + $(call execute-if-go-124,go mod tidy) + +tidy-ci: + $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/echo/go.mod b/experimental/examples/petstore-expanded/echo/go.mod new file mode 100644 index 0000000000..3576808d2d --- /dev/null +++ b/experimental/examples/petstore-expanded/echo/go.mod @@ -0,0 +1,11 @@ +module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/echo + +go 1.25.0 + +require ( + github.com/google/uuid v1.6.0 + github.com/labstack/echo/v5 v5.0.0 + github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 +) + +replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/echo/go.sum b/experimental/examples/petstore-expanded/echo/go.sum new file mode 100644 index 0000000000..9dd4179c6c --- /dev/null +++ b/experimental/examples/petstore-expanded/echo/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/labstack/echo/v5 v5.0.0 h1:JHKGrI0cbNsNMyKvranuY0C94O4hSM7yc/HtwcV3Na4= +github.com/labstack/echo/v5 v5.0.0/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/experimental/examples/petstore-expanded/echo/main.go b/experimental/examples/petstore-expanded/echo/main.go new file mode 100644 index 0000000000..6f0483ac28 --- /dev/null +++ b/experimental/examples/petstore-expanded/echo/main.go @@ -0,0 +1,34 @@ +//go:build go1.22 + +// This is an example of implementing the Pet Store from the OpenAPI documentation +// found at: +// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml + +package main + +import ( + "flag" + "log" + "net" + + "github.com/labstack/echo/v5" + "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/echo/server" +) + +func main() { + port := flag.String("port", "8080", "Port for test HTTP server") + flag.Parse() + + // Create an instance of our handler which satisfies the generated interface + petStore := server.NewPetStore() + + e := echo.New() + + // We now register our petStore above as the handler for the interface + server.RegisterHandlers(e, petStore) + + log.Printf("Server listening on %s", net.JoinHostPort("0.0.0.0", *port)) + + // And we serve HTTP until the world ends. + log.Fatal(e.Start(net.JoinHostPort("0.0.0.0", *port))) +} diff --git a/experimental/examples/petstore-expanded/echo/server/petstore.go b/experimental/examples/petstore-expanded/echo/server/petstore.go new file mode 100644 index 0000000000..276030da81 --- /dev/null +++ b/experimental/examples/petstore-expanded/echo/server/petstore.go @@ -0,0 +1,123 @@ +//go:build go1.22 + +package server + +import ( + "net/http" + "sync" + + "github.com/labstack/echo/v5" + petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" +) + +// PetStore implements the ServerInterface. +type PetStore struct { + Pets map[int64]petstore.Pet + NextId int64 + Lock sync.Mutex +} + +// Make sure we conform to ServerInterface +var _ ServerInterface = (*PetStore)(nil) + +// NewPetStore creates a new PetStore. +func NewPetStore() *PetStore { + return &PetStore{ + Pets: make(map[int64]petstore.Pet), + NextId: 1000, + } +} + +// sendPetStoreError wraps sending of an error in the Error format. +func sendPetStoreError(ctx *echo.Context, code int, message string) error { + petErr := petstore.Error{ + Code: int32(code), + Message: message, + } + return ctx.JSON(code, petErr) +} + +// FindPets returns all pets, optionally filtered by tags and limited. +func (p *PetStore) FindPets(ctx *echo.Context, params FindPetsParams) error { + p.Lock.Lock() + defer p.Lock.Unlock() + + var result []petstore.Pet + + for _, pet := range p.Pets { + if params.Tags != nil { + // If we have tags, filter pets by tag + for _, t := range *params.Tags { + if pet.Tag != nil && (*pet.Tag == t) { + result = append(result, pet) + } + } + } else { + // Add all pets if we're not filtering + result = append(result, pet) + } + + if params.Limit != nil { + l := int(*params.Limit) + if len(result) >= l { + // We're at the limit + break + } + } + } + + return ctx.JSON(http.StatusOK, result) +} + +// AddPet creates a new pet. +func (p *PetStore) AddPet(ctx *echo.Context) error { + // We expect a NewPet object in the request body. + var newPet petstore.NewPet + if err := ctx.Bind(&newPet); err != nil { + return sendPetStoreError(ctx, http.StatusBadRequest, "Invalid format for NewPet") + } + + // We now have a pet, let's add it to our "database". + p.Lock.Lock() + defer p.Lock.Unlock() + + // We handle pets, not NewPets, which have an additional ID field + var pet petstore.Pet + pet.Name = newPet.Name + pet.Tag = newPet.Tag + pet.ID = p.NextId + p.NextId++ + + // Insert into map + p.Pets[pet.ID] = pet + + // Now, we have to return the Pet + return ctx.JSON(http.StatusCreated, pet) +} + +// FindPetByID returns a pet by ID. +func (p *PetStore) FindPetByID(ctx *echo.Context, id int64) error { + p.Lock.Lock() + defer p.Lock.Unlock() + + pet, found := p.Pets[id] + if !found { + return sendPetStoreError(ctx, http.StatusNotFound, "Could not find pet with ID") + } + + return ctx.JSON(http.StatusOK, pet) +} + +// DeletePet deletes a pet by ID. +func (p *PetStore) DeletePet(ctx *echo.Context, id int64) error { + p.Lock.Lock() + defer p.Lock.Unlock() + + _, found := p.Pets[id] + if !found { + return sendPetStoreError(ctx, http.StatusNotFound, "Could not find pet with ID") + } + delete(p.Pets, id) + + return ctx.NoContent(http.StatusNoContent) +} diff --git a/experimental/examples/petstore-expanded/echo/server/server.config.yaml b/experimental/examples/petstore-expanded/echo/server/server.config.yaml new file mode 100644 index 0000000000..16a0b6b82d --- /dev/null +++ b/experimental/examples/petstore-expanded/echo/server/server.config.yaml @@ -0,0 +1,6 @@ +package: server +generation: + server: echo + models-package: + path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded + alias: petstore diff --git a/experimental/examples/petstore-expanded/echo/server/server.gen.go b/experimental/examples/petstore-expanded/echo/server/server.gen.go new file mode 100644 index 0000000000..da099a985f --- /dev/null +++ b/experimental/examples/petstore-expanded/echo/server/server.gen.go @@ -0,0 +1,977 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package server + +import ( + "bytes" + "encoding" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "reflect" + "sort" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + "github.com/labstack/echo/v5" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Returns all pets + // (GET /pets) + FindPets(ctx *echo.Context, params FindPetsParams) error + // Creates a new pet + // (POST /pets) + AddPet(ctx *echo.Context) error + // Deletes a pet by ID + // (DELETE /pets/{id}) + DeletePet(ctx *echo.Context, id int64) error + // Returns a pet by ID + // (GET /pets/{id}) + FindPetByID(ctx *echo.Context, id int64) error +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. +type Unimplemented struct{} + +// Returns all pets +// (GET /pets) +func (_ Unimplemented) FindPets(ctx *echo.Context, params FindPetsParams) error { + return ctx.NoContent(http.StatusNotImplemented) +} + +// Creates a new pet +// (POST /pets) +func (_ Unimplemented) AddPet(ctx *echo.Context) error { + return ctx.NoContent(http.StatusNotImplemented) +} + +// Deletes a pet by ID +// (DELETE /pets/{id}) +func (_ Unimplemented) DeletePet(ctx *echo.Context, id int64) error { + return ctx.NoContent(http.StatusNotImplemented) +} + +// Returns a pet by ID +// (GET /pets/{id}) +func (_ Unimplemented) FindPetByID(ctx *echo.Context, id int64) error { + return ctx.NoContent(http.StatusNotImplemented) +} + +// FindPetsParams defines parameters for FindPets. +type FindPetsParams struct { + // tags (optional) + Tags *[]string `form:"tags" json:"tags"` + // limit (optional) + Limit *int32 `form:"limit" json:"limit"` +} + +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +// FindPets converts echo context to params. +func (w *ServerInterfaceWrapper) FindPets(ctx *echo.Context) error { + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params FindPetsParams + + // ------------- Optional query parameter "tags" ------------- + err = BindFormExplodeParam("tags", false, ctx.QueryParams(), ¶ms.Tags) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter tags: %s", err)) + } + + // ------------- Optional query parameter "limit" ------------- + err = BindFormExplodeParam("limit", false, ctx.QueryParams(), ¶ms.Limit) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter limit: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.FindPets(ctx, params) + return err +} + +// AddPet converts echo context to params. +func (w *ServerInterfaceWrapper) AddPet(ctx *echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.AddPet(ctx) + return err +} + +// DeletePet converts echo context to params. +func (w *ServerInterfaceWrapper) DeletePet(ctx *echo.Context) error { + var err error + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, ctx.Param("id"), &id) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.DeletePet(ctx, id) + return err +} + +// FindPetByID converts echo context to params. +func (w *ServerInterfaceWrapper) FindPetByID(ctx *echo.Context) error { + var err error + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, ctx.Param("id"), &id) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.FindPetByID(ctx, id) + return err +} + +// EchoRouter is an interface for echo.Echo and echo.Group. +type EchoRouter interface { + Add(method string, path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + 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 +} + +// RegisterHandlers adds each server route to the EchoRouter. +func RegisterHandlers(router EchoRouter, si ServerInterface) { + RegisterHandlersWithBaseURL(router, si, "") +} + +// RegisterHandlersWithBaseURL adds each server route to the EchoRouter with a base URL prefix. +func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { + + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + router.GET(baseURL+"/pets", wrapper.FindPets) + router.POST(baseURL+"/pets", wrapper.AddPet) + router.DELETE(baseURL+"/pets/:id", wrapper.DeletePet) + router.GET(baseURL+"/pets/:id", wrapper.FindPetByID) +} + +// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. +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 +} + +// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. +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 +} + +// RequiredParamError is returned when a required parameter is missing. +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +// RequiredHeaderError is returned when a required header is missing. +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 +} + +// InvalidParamFormatError is returned when a parameter has an invalid format. +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 +} + +// TooManyValuesForParamError is returned when a parameter has too many values. +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) +} + +// ParamLocation indicates where a parameter is located in an HTTP request. +type ParamLocation int + +const ( + ParamLocationUndefined ParamLocation = iota + ParamLocationQuery + ParamLocationPath + ParamLocationHeader + ParamLocationCookie +) + +// Binder is an interface for types that can bind themselves from a string value. +type Binder interface { + Bind(value string) error +} + +// DateFormat is the format used for date (without time) parameters. +const DateFormat = "2006-01-02" + +// Date represents a date (without time) for OpenAPI date format. +type Date struct { + time.Time +} + +// UnmarshalText implements encoding.TextUnmarshaler for Date. +func (d *Date) UnmarshalText(data []byte) error { + t, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = t + return nil +} + +// MarshalText implements encoding.TextMarshaler for Date. +func (d Date) MarshalText() ([]byte, error) { + return []byte(d.Format(DateFormat)), nil +} + +// Format returns the date formatted according to layout. +func (d Date) Format(layout string) string { + return d.Time.Format(layout) +} + +// primitiveToString converts a primitive value to a string representation. +// It handles basic Go types, time.Time, types.Date, and types that implement +// json.Marshaler or fmt.Stringer. +func primitiveToString(value interface{}) (string, error) { + // Check for known types first (time, date, uuid) + if res, ok := marshalKnownTypes(value); ok { + return res, nil + } + + // Dereference pointers for optional values + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + kind := t.Kind() + + switch kind { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + case reflect.Float32: + return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil + case reflect.Bool: + if v.Bool() { + return "true", nil + } + return "false", nil + case reflect.String: + return v.String(), nil + case reflect.Struct: + // Check if it's a UUID + if u, ok := value.(uuid.UUID); ok { + return u.String(), nil + } + // Check if it implements json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + var i2 interface{} + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to decode JSON: %w", err) + } + return primitiveToString(i2) + } + fallthrough + default: + if s, ok := value.(fmt.Stringer); ok { + return s.String(), nil + } + return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) + } +} + +// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. +func marshalKnownTypes(value interface{}) (string, bool) { + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + tt := v.Convert(reflect.TypeOf(time.Time{})) + timeVal := tt.Interface().(time.Time) + return timeVal.Format(time.RFC3339Nano), true + } + + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + d := v.Convert(reflect.TypeOf(Date{})) + dateVal := d.Interface().(Date) + return dateVal.Format(DateFormat), true + } + + if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { + u := v.Convert(reflect.TypeOf(uuid.UUID{})) + uuidVal := u.Interface().(uuid.UUID) + return uuidVal.String(), true + } + + return "", false +} + +// escapeParameterString escapes a parameter value based on its location. +// Query and path parameters need URL escaping; headers and cookies do not. +func escapeParameterString(value string, paramLocation ParamLocation) string { + switch paramLocation { + case ParamLocationQuery: + return url.QueryEscape(value) + case ParamLocationPath: + return url.PathEscape(value) + default: + return value + } +} + +// unescapeParameterString unescapes a parameter value based on its location. +func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { + switch paramLocation { + case ParamLocationQuery, ParamLocationUndefined: + return url.QueryUnescape(value) + case ParamLocationPath: + return url.PathUnescape(value) + default: + return value, nil + } +} + +// sortedKeys returns the keys of a map in sorted order. +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// BindStringToObject binds a string value to a destination object. +// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. +func BindStringToObject(src string, dst interface{}) error { + // Check for TextUnmarshaler + if tu, ok := dst.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(src)) + } + + // Check for Binder interface + if b, ok := dst.(Binder); ok { + return b.Bind(src) + } + + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Ptr { + return fmt.Errorf("dst must be a pointer, got %T", dst) + } + v = v.Elem() + + switch v.Kind() { + case reflect.String: + v.SetString(src) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + v.SetInt(i) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + u, err := strconv.ParseUint(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse uint: %w", err) + } + v.SetUint(u) + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(src, 64) + if err != nil { + return fmt.Errorf("failed to parse float: %w", err) + } + v.SetFloat(f) + case reflect.Bool: + b, err := strconv.ParseBool(src) + if err != nil { + return fmt.Errorf("failed to parse bool: %w", err) + } + v.SetBool(b) + default: + // Try JSON unmarshal as a fallback + return json.Unmarshal([]byte(src), dst) + } + return nil +} + +// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. +func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + newArray := reflect.MakeSlice(t, len(parts), len(parts)) + for i, p := range parts { + err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) + if err != nil { + return fmt.Errorf("error setting array element: %w", err) + } + } + v.Set(newArray) + return nil +} + +// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. +func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { + var fields []string + if explode { + fields = make([]string, len(parts)) + for i, property := range parts { + propertyParts := strings.Split(property, "=") + if len(propertyParts) != 2 { + return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) + } + fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" + } + } else { + if len(parts)%2 != 0 { + return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) + } + fields = make([]string, len(parts)/2) + for i := 0; i < len(parts); i += 2 { + key := parts[i] + value := parts[i+1] + fields[i/2] = "\"" + key + "\":\"" + value + "\"" + } + } + jsonParam := "{" + strings.Join(fields, ",") + "}" + return json.Unmarshal([]byte(jsonParam), dest) +} + +// BindFormExplodeParam binds a form-style parameter with explode to a destination. +// Form style is the default for query and cookie parameters. +// This handles the exploded case where arrays come as multiple query params. +// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) +// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) +func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { + dv := reflect.Indirect(reflect.ValueOf(dest)) + v := dv + var output interface{} + + if required { + output = dest + } else { + // For optional parameters, allocate if nil + if v.IsNil() { + t := v.Type() + newValue := reflect.New(t.Elem()) + output = newValue.Interface() + } else { + output = v.Interface() + } + v = reflect.Indirect(reflect.ValueOf(output)) + } + + t := v.Type() + k := t.Kind() + + values, found := queryParams[paramName] + + switch k { + case reflect.Slice: + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := bindSplitPartsToDestinationArray(values, output) + if err != nil { + return err + } + case reflect.Struct: + // For exploded objects, fields are spread across query params + fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) + if err != nil { + return err + } + if !fieldsPresent { + return nil + } + default: + // Primitive + if len(values) == 0 { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + if len(values) != 1 { + return fmt.Errorf("multiple values for single value parameter '%s'", paramName) + } + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := BindStringToObject(values[0], output) + if err != nil { + return err + } + } + + if !required { + dv.Set(reflect.ValueOf(output)) + } + return nil +} + +// bindParamsToExplodedObject binds query params to struct fields for exploded objects. +func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { + binder, v, t := indirectBinder(dest) + if binder != nil { + _, found := values[paramName] + if !found { + return false, nil + } + return true, BindStringToObject(values.Get(paramName), dest) + } + if t.Kind() != reflect.Struct { + return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) + } + + fieldsPresent := false + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + if !v.Field(i).CanSet() { + continue + } + + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + + fieldVal, found := values[fieldName] + if found { + if len(fieldVal) != 1 { + return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) + } + err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) + if err != nil { + return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) + } + fieldsPresent = true + } + } + return fieldsPresent, nil +} + +// indirectBinder checks if dest implements Binder and returns reflect values. +func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { + v := reflect.ValueOf(dest) + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Binder); ok { + return u, reflect.Value{}, nil + } + } + v = reflect.Indirect(v) + t := v.Type() + // Handle special types like time.Time and Date + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return dest, reflect.Value{}, nil + } + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + return dest, reflect.Value{}, nil + } + return nil, v, t +} + +// BindSimpleParam binds a simple-style parameter without explode to a destination. +// Simple style is the default for path and header parameters. +// Arrays: a,b,c -> []string{"a", "b", "c"} +// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} +func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + // Split on comma and bind as key,value pairs + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) + case reflect.Slice: + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} + +// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. +// Form style is the default for query and cookie parameters. +// Primitives: paramName=value +// Arrays: paramName=a¶mName=b¶mName=c +// Objects: key1=value1&key2=value2 +func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleFormExplodeSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleFormExplodeStruct(paramName, paramLocation, value) + case reflect.Map: + return styleFormExplodeMap(paramName, paramLocation, value) + default: + return styleFormExplodePrimitive(paramName, paramLocation, value) + } +} + +func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Form with explode: paramName=a¶mName=b¶mName=c + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, "&"+prefix), nil +} + +func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleFormExplodeParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. +// Simple style is the default for path and header parameters. +// Arrays are comma-separated: a,b,c +// Objects are key,value pairs: key1,value1,key2,value2 +func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return escapeParameterString(string(b), paramLocation), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleSimpleSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleSimpleStruct(paramName, paramLocation, value) + case reflect.Map: + return styleSimpleMap(paramName, paramLocation, value) + default: + return styleSimplePrimitive(paramLocation, value) + } +} + +func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return escapeParameterString(strVal, paramLocation), nil +} + +func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return escapeParameterString(timeVal, paramLocation), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleSimpleParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +// structToFieldDict converts a struct to a map of field names to string values. +func structToFieldDict(value interface{}) (map[string]string, error) { + v := reflect.ValueOf(value) + t := reflect.TypeOf(value) + fieldDict := make(map[string]string) + + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + f := v.Field(i) + + // Skip nil optional fields + if f.Type().Kind() == reflect.Ptr && f.IsNil() { + continue + } + str, err := primitiveToString(f.Interface()) + if err != nil { + return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) + } + fieldDict[fieldName] = str + } + return fieldDict, nil +} diff --git a/experimental/examples/petstore-expanded/fiber/Makefile b/experimental/examples/petstore-expanded/fiber/Makefile new file mode 100644 index 0000000000..42389f4137 --- /dev/null +++ b/experimental/examples/petstore-expanded/fiber/Makefile @@ -0,0 +1,35 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + +generate: + $(call execute-if-go-124,go generate ./...) + +test: + $(call execute-if-go-124,go test -cover ./...) + +tidy: + $(call execute-if-go-124,go mod tidy) + +tidy-ci: + $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/fiber/go.mod b/experimental/examples/petstore-expanded/fiber/go.mod new file mode 100644 index 0000000000..aa60e0d81c --- /dev/null +++ b/experimental/examples/petstore-expanded/fiber/go.mod @@ -0,0 +1,31 @@ +module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/fiber + +go 1.24.0 + +require ( + github.com/gofiber/fiber/v3 v3.0.0-beta.4 + github.com/google/uuid v1.6.0 + github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 +) + +require ( + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gofiber/schema v1.2.0 // indirect + github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/tinylib/msgp v1.2.5 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.58.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.33.0 // indirect +) + +replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/fiber/go.sum b/experimental/examples/petstore-expanded/fiber/go.sum new file mode 100644 index 0000000000..00a56bd56e --- /dev/null +++ b/experimental/examples/petstore-expanded/fiber/go.sum @@ -0,0 +1,51 @@ +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gofiber/fiber/v3 v3.0.0-beta.4 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0= +github.com/gofiber/fiber/v3 v3.0.0-beta.4/go.mod h1:/WFUoHRkZEsGHyy2+fYcdqi109IVOFbVwxv1n1RU+kk= +github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg= +github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= +github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ= +github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= +github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= +github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/experimental/examples/petstore-expanded/fiber/main.go b/experimental/examples/petstore-expanded/fiber/main.go new file mode 100644 index 0000000000..a429cc08f3 --- /dev/null +++ b/experimental/examples/petstore-expanded/fiber/main.go @@ -0,0 +1,35 @@ +//go:build go1.22 + +// This is an example of implementing the Pet Store from the OpenAPI documentation +// found at: +// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml + +package main + +import ( + "flag" + "log" + "net" + + "github.com/gofiber/fiber/v3" + "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/fiber/server" +) + +func main() { + port := flag.String("port", "8080", "Port for test HTTP server") + flag.Parse() + + // Create an instance of our handler which satisfies the generated interface + petStore := server.NewPetStore() + + app := fiber.New() + + // We now register our petStore above as the handler for the interface + server.RegisterHandlers(app, petStore) + + addr := net.JoinHostPort("0.0.0.0", *port) + log.Printf("Server listening on %s", addr) + + // And we serve HTTP until the world ends. + log.Fatal(app.Listen(addr)) +} diff --git a/experimental/examples/petstore-expanded/fiber/server/petstore.go b/experimental/examples/petstore-expanded/fiber/server/petstore.go new file mode 100644 index 0000000000..0692078618 --- /dev/null +++ b/experimental/examples/petstore-expanded/fiber/server/petstore.go @@ -0,0 +1,122 @@ +//go:build go1.22 + +package server + +import ( + "sync" + + "github.com/gofiber/fiber/v3" + petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" +) + +// PetStore implements the ServerInterface. +type PetStore struct { + Pets map[int64]petstore.Pet + NextId int64 + Lock sync.Mutex +} + +// Make sure we conform to ServerInterface +var _ ServerInterface = (*PetStore)(nil) + +// NewPetStore creates a new PetStore. +func NewPetStore() *PetStore { + return &PetStore{ + Pets: make(map[int64]petstore.Pet), + NextId: 1000, + } +} + +// sendPetStoreError wraps sending of an error in the Error format. +func sendPetStoreError(c fiber.Ctx, code int, message string) error { + petErr := petstore.Error{ + Code: int32(code), + Message: message, + } + return c.Status(code).JSON(petErr) +} + +// FindPets returns all pets, optionally filtered by tags and limited. +func (p *PetStore) FindPets(c fiber.Ctx, params FindPetsParams) error { + p.Lock.Lock() + defer p.Lock.Unlock() + + var result []petstore.Pet + + for _, pet := range p.Pets { + if params.Tags != nil { + // If we have tags, filter pets by tag + for _, t := range *params.Tags { + if pet.Tag != nil && (*pet.Tag == t) { + result = append(result, pet) + } + } + } else { + // Add all pets if we're not filtering + result = append(result, pet) + } + + if params.Limit != nil { + l := int(*params.Limit) + if len(result) >= l { + // We're at the limit + break + } + } + } + + return c.Status(fiber.StatusOK).JSON(result) +} + +// AddPet creates a new pet. +func (p *PetStore) AddPet(c fiber.Ctx) error { + // We expect a NewPet object in the request body. + var newPet petstore.NewPet + if err := c.Bind().JSON(&newPet); err != nil { + return sendPetStoreError(c, fiber.StatusBadRequest, "Invalid format for NewPet") + } + + // We now have a pet, let's add it to our "database". + p.Lock.Lock() + defer p.Lock.Unlock() + + // We handle pets, not NewPets, which have an additional ID field + var pet petstore.Pet + pet.Name = newPet.Name + pet.Tag = newPet.Tag + pet.ID = p.NextId + p.NextId++ + + // Insert into map + p.Pets[pet.ID] = pet + + // Now, we have to return the Pet + return c.Status(fiber.StatusCreated).JSON(pet) +} + +// FindPetByID returns a pet by ID. +func (p *PetStore) FindPetByID(c fiber.Ctx, id int64) error { + p.Lock.Lock() + defer p.Lock.Unlock() + + pet, found := p.Pets[id] + if !found { + return sendPetStoreError(c, fiber.StatusNotFound, "Could not find pet with ID") + } + + return c.Status(fiber.StatusOK).JSON(pet) +} + +// DeletePet deletes a pet by ID. +func (p *PetStore) DeletePet(c fiber.Ctx, id int64) error { + p.Lock.Lock() + defer p.Lock.Unlock() + + _, found := p.Pets[id] + if !found { + return sendPetStoreError(c, fiber.StatusNotFound, "Could not find pet with ID") + } + delete(p.Pets, id) + + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/experimental/examples/petstore-expanded/fiber/server/server.config.yaml b/experimental/examples/petstore-expanded/fiber/server/server.config.yaml new file mode 100644 index 0000000000..1038e55a98 --- /dev/null +++ b/experimental/examples/petstore-expanded/fiber/server/server.config.yaml @@ -0,0 +1,6 @@ +package: server +generation: + server: fiber + models-package: + path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded + alias: petstore diff --git a/experimental/examples/petstore-expanded/fiber/server/server.gen.go b/experimental/examples/petstore-expanded/fiber/server/server.gen.go new file mode 100644 index 0000000000..a38d4cfb87 --- /dev/null +++ b/experimental/examples/petstore-expanded/fiber/server/server.gen.go @@ -0,0 +1,969 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package server + +import ( + "bytes" + "encoding" + "encoding/json" + "errors" + "fmt" + "net/url" + "reflect" + "sort" + "strconv" + "strings" + "time" + + "github.com/gofiber/fiber/v3" + "github.com/google/uuid" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Returns all pets + // (GET /pets) + FindPets(c fiber.Ctx, params FindPetsParams) error + // Creates a new pet + // (POST /pets) + AddPet(c fiber.Ctx) error + // Deletes a pet by ID + // (DELETE /pets/{id}) + DeletePet(c fiber.Ctx, id int64) error + // Returns a pet by ID + // (GET /pets/{id}) + FindPetByID(c fiber.Ctx, id int64) error +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. +type Unimplemented struct{} + +// Returns all pets +// (GET /pets) +func (_ Unimplemented) FindPets(c fiber.Ctx, params FindPetsParams) error { + return c.SendStatus(fiber.StatusNotImplemented) +} + +// Creates a new pet +// (POST /pets) +func (_ Unimplemented) AddPet(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusNotImplemented) +} + +// Deletes a pet by ID +// (DELETE /pets/{id}) +func (_ Unimplemented) DeletePet(c fiber.Ctx, id int64) error { + return c.SendStatus(fiber.StatusNotImplemented) +} + +// Returns a pet by ID +// (GET /pets/{id}) +func (_ Unimplemented) FindPetByID(c fiber.Ctx, id int64) error { + return c.SendStatus(fiber.StatusNotImplemented) +} + +// FindPetsParams defines parameters for FindPets. +type FindPetsParams struct { + // tags (optional) + Tags *[]string `form:"tags" json:"tags"` + // limit (optional) + Limit *int32 `form:"limit" json:"limit"` +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +// FindPets operation middleware +func (siw *ServerInterfaceWrapper) FindPets(c fiber.Ctx) error { + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params FindPetsParams + + var query url.Values + query, err = url.ParseQuery(string(c.Request().URI().QueryString())) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for query string: %s", err)) + } + + // ------------- Optional query parameter "tags" ------------- + err = BindFormExplodeParam("tags", false, query, ¶ms.Tags) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter tags: %s", err)) + } + + // ------------- Optional query parameter "limit" ------------- + err = BindFormExplodeParam("limit", false, query, ¶ms.Limit) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter limit: %s", err)) + } + + return siw.Handler.FindPets(c, params) +} + +// AddPet operation middleware +func (siw *ServerInterfaceWrapper) AddPet(c fiber.Ctx) error { + + return siw.Handler.AddPet(c) +} + +// DeletePet operation middleware +func (siw *ServerInterfaceWrapper) DeletePet(c fiber.Ctx) error { + var err error + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, c.Params("id"), &id) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + + return siw.Handler.DeletePet(c, id) +} + +// FindPetByID operation middleware +func (siw *ServerInterfaceWrapper) FindPetByID(c fiber.Ctx) error { + var err error + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, c.Params("id"), &id) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + + return siw.Handler.FindPetByID(c, id) +} + +// FiberServerOptions provides options for the Fiber server. +type FiberServerOptions struct { + BaseURL string + Middlewares []fiber.Handler +} + +// 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) { + + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + for _, m := range options.Middlewares { + router.Use(m) + } + + router.Get(options.BaseURL+"/pets", wrapper.FindPets) + router.Post(options.BaseURL+"/pets", wrapper.AddPet) + router.Delete(options.BaseURL+"/pets/:id", wrapper.DeletePet) + router.Get(options.BaseURL+"/pets/:id", wrapper.FindPetByID) +} + +// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. +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 +} + +// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. +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 +} + +// RequiredParamError is returned when a required parameter is missing. +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +// RequiredHeaderError is returned when a required header is missing. +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 +} + +// InvalidParamFormatError is returned when a parameter has an invalid format. +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 +} + +// TooManyValuesForParamError is returned when a parameter has too many values. +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) +} + +// ParamLocation indicates where a parameter is located in an HTTP request. +type ParamLocation int + +const ( + ParamLocationUndefined ParamLocation = iota + ParamLocationQuery + ParamLocationPath + ParamLocationHeader + ParamLocationCookie +) + +// Binder is an interface for types that can bind themselves from a string value. +type Binder interface { + Bind(value string) error +} + +// DateFormat is the format used for date (without time) parameters. +const DateFormat = "2006-01-02" + +// Date represents a date (without time) for OpenAPI date format. +type Date struct { + time.Time +} + +// UnmarshalText implements encoding.TextUnmarshaler for Date. +func (d *Date) UnmarshalText(data []byte) error { + t, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = t + return nil +} + +// MarshalText implements encoding.TextMarshaler for Date. +func (d Date) MarshalText() ([]byte, error) { + return []byte(d.Format(DateFormat)), nil +} + +// Format returns the date formatted according to layout. +func (d Date) Format(layout string) string { + return d.Time.Format(layout) +} + +// primitiveToString converts a primitive value to a string representation. +// It handles basic Go types, time.Time, types.Date, and types that implement +// json.Marshaler or fmt.Stringer. +func primitiveToString(value interface{}) (string, error) { + // Check for known types first (time, date, uuid) + if res, ok := marshalKnownTypes(value); ok { + return res, nil + } + + // Dereference pointers for optional values + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + kind := t.Kind() + + switch kind { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + case reflect.Float32: + return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil + case reflect.Bool: + if v.Bool() { + return "true", nil + } + return "false", nil + case reflect.String: + return v.String(), nil + case reflect.Struct: + // Check if it's a UUID + if u, ok := value.(uuid.UUID); ok { + return u.String(), nil + } + // Check if it implements json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + var i2 interface{} + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to decode JSON: %w", err) + } + return primitiveToString(i2) + } + fallthrough + default: + if s, ok := value.(fmt.Stringer); ok { + return s.String(), nil + } + return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) + } +} + +// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. +func marshalKnownTypes(value interface{}) (string, bool) { + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + tt := v.Convert(reflect.TypeOf(time.Time{})) + timeVal := tt.Interface().(time.Time) + return timeVal.Format(time.RFC3339Nano), true + } + + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + d := v.Convert(reflect.TypeOf(Date{})) + dateVal := d.Interface().(Date) + return dateVal.Format(DateFormat), true + } + + if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { + u := v.Convert(reflect.TypeOf(uuid.UUID{})) + uuidVal := u.Interface().(uuid.UUID) + return uuidVal.String(), true + } + + return "", false +} + +// escapeParameterString escapes a parameter value based on its location. +// Query and path parameters need URL escaping; headers and cookies do not. +func escapeParameterString(value string, paramLocation ParamLocation) string { + switch paramLocation { + case ParamLocationQuery: + return url.QueryEscape(value) + case ParamLocationPath: + return url.PathEscape(value) + default: + return value + } +} + +// unescapeParameterString unescapes a parameter value based on its location. +func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { + switch paramLocation { + case ParamLocationQuery, ParamLocationUndefined: + return url.QueryUnescape(value) + case ParamLocationPath: + return url.PathUnescape(value) + default: + return value, nil + } +} + +// sortedKeys returns the keys of a map in sorted order. +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// BindStringToObject binds a string value to a destination object. +// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. +func BindStringToObject(src string, dst interface{}) error { + // Check for TextUnmarshaler + if tu, ok := dst.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(src)) + } + + // Check for Binder interface + if b, ok := dst.(Binder); ok { + return b.Bind(src) + } + + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Ptr { + return fmt.Errorf("dst must be a pointer, got %T", dst) + } + v = v.Elem() + + switch v.Kind() { + case reflect.String: + v.SetString(src) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + v.SetInt(i) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + u, err := strconv.ParseUint(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse uint: %w", err) + } + v.SetUint(u) + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(src, 64) + if err != nil { + return fmt.Errorf("failed to parse float: %w", err) + } + v.SetFloat(f) + case reflect.Bool: + b, err := strconv.ParseBool(src) + if err != nil { + return fmt.Errorf("failed to parse bool: %w", err) + } + v.SetBool(b) + default: + // Try JSON unmarshal as a fallback + return json.Unmarshal([]byte(src), dst) + } + return nil +} + +// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. +func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + newArray := reflect.MakeSlice(t, len(parts), len(parts)) + for i, p := range parts { + err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) + if err != nil { + return fmt.Errorf("error setting array element: %w", err) + } + } + v.Set(newArray) + return nil +} + +// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. +func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { + var fields []string + if explode { + fields = make([]string, len(parts)) + for i, property := range parts { + propertyParts := strings.Split(property, "=") + if len(propertyParts) != 2 { + return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) + } + fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" + } + } else { + if len(parts)%2 != 0 { + return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) + } + fields = make([]string, len(parts)/2) + for i := 0; i < len(parts); i += 2 { + key := parts[i] + value := parts[i+1] + fields[i/2] = "\"" + key + "\":\"" + value + "\"" + } + } + jsonParam := "{" + strings.Join(fields, ",") + "}" + return json.Unmarshal([]byte(jsonParam), dest) +} + +// BindFormExplodeParam binds a form-style parameter with explode to a destination. +// Form style is the default for query and cookie parameters. +// This handles the exploded case where arrays come as multiple query params. +// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) +// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) +func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { + dv := reflect.Indirect(reflect.ValueOf(dest)) + v := dv + var output interface{} + + if required { + output = dest + } else { + // For optional parameters, allocate if nil + if v.IsNil() { + t := v.Type() + newValue := reflect.New(t.Elem()) + output = newValue.Interface() + } else { + output = v.Interface() + } + v = reflect.Indirect(reflect.ValueOf(output)) + } + + t := v.Type() + k := t.Kind() + + values, found := queryParams[paramName] + + switch k { + case reflect.Slice: + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := bindSplitPartsToDestinationArray(values, output) + if err != nil { + return err + } + case reflect.Struct: + // For exploded objects, fields are spread across query params + fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) + if err != nil { + return err + } + if !fieldsPresent { + return nil + } + default: + // Primitive + if len(values) == 0 { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + if len(values) != 1 { + return fmt.Errorf("multiple values for single value parameter '%s'", paramName) + } + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := BindStringToObject(values[0], output) + if err != nil { + return err + } + } + + if !required { + dv.Set(reflect.ValueOf(output)) + } + return nil +} + +// bindParamsToExplodedObject binds query params to struct fields for exploded objects. +func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { + binder, v, t := indirectBinder(dest) + if binder != nil { + _, found := values[paramName] + if !found { + return false, nil + } + return true, BindStringToObject(values.Get(paramName), dest) + } + if t.Kind() != reflect.Struct { + return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) + } + + fieldsPresent := false + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + if !v.Field(i).CanSet() { + continue + } + + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + + fieldVal, found := values[fieldName] + if found { + if len(fieldVal) != 1 { + return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) + } + err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) + if err != nil { + return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) + } + fieldsPresent = true + } + } + return fieldsPresent, nil +} + +// indirectBinder checks if dest implements Binder and returns reflect values. +func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { + v := reflect.ValueOf(dest) + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Binder); ok { + return u, reflect.Value{}, nil + } + } + v = reflect.Indirect(v) + t := v.Type() + // Handle special types like time.Time and Date + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return dest, reflect.Value{}, nil + } + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + return dest, reflect.Value{}, nil + } + return nil, v, t +} + +// BindSimpleParam binds a simple-style parameter without explode to a destination. +// Simple style is the default for path and header parameters. +// Arrays: a,b,c -> []string{"a", "b", "c"} +// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} +func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + // Split on comma and bind as key,value pairs + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) + case reflect.Slice: + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} + +// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. +// Form style is the default for query and cookie parameters. +// Primitives: paramName=value +// Arrays: paramName=a¶mName=b¶mName=c +// Objects: key1=value1&key2=value2 +func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleFormExplodeSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleFormExplodeStruct(paramName, paramLocation, value) + case reflect.Map: + return styleFormExplodeMap(paramName, paramLocation, value) + default: + return styleFormExplodePrimitive(paramName, paramLocation, value) + } +} + +func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Form with explode: paramName=a¶mName=b¶mName=c + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, "&"+prefix), nil +} + +func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleFormExplodeParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. +// Simple style is the default for path and header parameters. +// Arrays are comma-separated: a,b,c +// Objects are key,value pairs: key1,value1,key2,value2 +func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return escapeParameterString(string(b), paramLocation), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleSimpleSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleSimpleStruct(paramName, paramLocation, value) + case reflect.Map: + return styleSimpleMap(paramName, paramLocation, value) + default: + return styleSimplePrimitive(paramLocation, value) + } +} + +func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return escapeParameterString(strVal, paramLocation), nil +} + +func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return escapeParameterString(timeVal, paramLocation), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleSimpleParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +// structToFieldDict converts a struct to a map of field names to string values. +func structToFieldDict(value interface{}) (map[string]string, error) { + v := reflect.ValueOf(value) + t := reflect.TypeOf(value) + fieldDict := make(map[string]string) + + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + f := v.Field(i) + + // Skip nil optional fields + if f.Type().Kind() == reflect.Ptr && f.IsNil() { + continue + } + str, err := primitiveToString(f.Interface()) + if err != nil { + return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) + } + fieldDict[fieldName] = str + } + return fieldDict, nil +} diff --git a/experimental/examples/petstore-expanded/generate.go b/experimental/examples/petstore-expanded/generate.go new file mode 100644 index 0000000000..b8a15f94ef --- /dev/null +++ b/experimental/examples/petstore-expanded/generate.go @@ -0,0 +1,11 @@ +//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config models.config.yaml petstore-expanded.yaml +//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config stdhttp/server/server.config.yaml -output stdhttp/server/server.gen.go petstore-expanded.yaml +//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config chi/server/server.config.yaml -output chi/server/server.gen.go petstore-expanded.yaml +//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config echo-v4/server/server.config.yaml -output echo-v4/server/server.gen.go petstore-expanded.yaml +//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config echo/server/server.config.yaml -output echo/server/server.gen.go petstore-expanded.yaml +//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config gin/server/server.config.yaml -output gin/server/server.gen.go petstore-expanded.yaml +//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config gorilla/server/server.config.yaml -output gorilla/server/server.gen.go petstore-expanded.yaml +//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config fiber/server/server.config.yaml -output fiber/server/server.gen.go petstore-expanded.yaml +//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config iris/server/server.config.yaml -output iris/server/server.gen.go petstore-expanded.yaml + +package petstore diff --git a/experimental/examples/petstore-expanded/gin/Makefile b/experimental/examples/petstore-expanded/gin/Makefile new file mode 100644 index 0000000000..42389f4137 --- /dev/null +++ b/experimental/examples/petstore-expanded/gin/Makefile @@ -0,0 +1,35 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + +generate: + $(call execute-if-go-124,go generate ./...) + +test: + $(call execute-if-go-124,go test -cover ./...) + +tidy: + $(call execute-if-go-124,go mod tidy) + +tidy-ci: + $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/gin/go.mod b/experimental/examples/petstore-expanded/gin/go.mod new file mode 100644 index 0000000000..eed1c070da --- /dev/null +++ b/experimental/examples/petstore-expanded/gin/go.mod @@ -0,0 +1,40 @@ +module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/gin + +go 1.24.0 + +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/google/uuid v1.6.0 + github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.33.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/gin/go.sum b/experimental/examples/petstore-expanded/gin/go.sum new file mode 100644 index 0000000000..fa550cb75e --- /dev/null +++ b/experimental/examples/petstore-expanded/gin/go.sum @@ -0,0 +1,92 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/experimental/examples/petstore-expanded/gin/main.go b/experimental/examples/petstore-expanded/gin/main.go new file mode 100644 index 0000000000..91148c8846 --- /dev/null +++ b/experimental/examples/petstore-expanded/gin/main.go @@ -0,0 +1,40 @@ +//go:build go1.22 + +// This is an example of implementing the Pet Store from the OpenAPI documentation +// found at: +// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml + +package main + +import ( + "flag" + "log" + "net" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/gin/server" +) + +func main() { + port := flag.String("port", "8080", "Port for test HTTP server") + flag.Parse() + + // Create an instance of our handler which satisfies the generated interface + petStore := server.NewPetStore() + + r := gin.Default() + + // We now register our petStore above as the handler for the interface + server.RegisterHandlers(r, petStore) + + s := &http.Server{ + Handler: r, + Addr: net.JoinHostPort("0.0.0.0", *port), + } + + log.Printf("Server listening on %s", s.Addr) + + // And we serve HTTP until the world ends. + log.Fatal(s.ListenAndServe()) +} diff --git a/experimental/examples/petstore-expanded/gin/server/petstore.go b/experimental/examples/petstore-expanded/gin/server/petstore.go new file mode 100644 index 0000000000..45f4801f25 --- /dev/null +++ b/experimental/examples/petstore-expanded/gin/server/petstore.go @@ -0,0 +1,126 @@ +//go:build go1.22 + +package server + +import ( + "net/http" + "sync" + + "github.com/gin-gonic/gin" + petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" +) + +// PetStore implements the ServerInterface. +type PetStore struct { + Pets map[int64]petstore.Pet + NextId int64 + Lock sync.Mutex +} + +// Make sure we conform to ServerInterface +var _ ServerInterface = (*PetStore)(nil) + +// NewPetStore creates a new PetStore. +func NewPetStore() *PetStore { + return &PetStore{ + Pets: make(map[int64]petstore.Pet), + NextId: 1000, + } +} + +// sendPetStoreError wraps sending of an error in the Error format. +func sendPetStoreError(c *gin.Context, code int, message string) { + petErr := petstore.Error{ + Code: int32(code), + Message: message, + } + c.JSON(code, petErr) +} + +// FindPets returns all pets, optionally filtered by tags and limited. +func (p *PetStore) FindPets(c *gin.Context, params FindPetsParams) { + p.Lock.Lock() + defer p.Lock.Unlock() + + var result []petstore.Pet + + for _, pet := range p.Pets { + if params.Tags != nil { + // If we have tags, filter pets by tag + for _, t := range *params.Tags { + if pet.Tag != nil && (*pet.Tag == t) { + result = append(result, pet) + } + } + } else { + // Add all pets if we're not filtering + result = append(result, pet) + } + + if params.Limit != nil { + l := int(*params.Limit) + if len(result) >= l { + // We're at the limit + break + } + } + } + + c.JSON(http.StatusOK, result) +} + +// AddPet creates a new pet. +func (p *PetStore) AddPet(c *gin.Context) { + // We expect a NewPet object in the request body. + var newPet petstore.NewPet + if err := c.ShouldBindJSON(&newPet); err != nil { + sendPetStoreError(c, http.StatusBadRequest, "Invalid format for NewPet") + return + } + + // We now have a pet, let's add it to our "database". + p.Lock.Lock() + defer p.Lock.Unlock() + + // We handle pets, not NewPets, which have an additional ID field + var pet petstore.Pet + pet.Name = newPet.Name + pet.Tag = newPet.Tag + pet.ID = p.NextId + p.NextId++ + + // Insert into map + p.Pets[pet.ID] = pet + + // Now, we have to return the Pet + c.JSON(http.StatusCreated, pet) +} + +// FindPetByID returns a pet by ID. +func (p *PetStore) FindPetByID(c *gin.Context, id int64) { + p.Lock.Lock() + defer p.Lock.Unlock() + + pet, found := p.Pets[id] + if !found { + sendPetStoreError(c, http.StatusNotFound, "Could not find pet with ID") + return + } + + c.JSON(http.StatusOK, pet) +} + +// DeletePet deletes a pet by ID. +func (p *PetStore) DeletePet(c *gin.Context, id int64) { + p.Lock.Lock() + defer p.Lock.Unlock() + + _, found := p.Pets[id] + if !found { + sendPetStoreError(c, http.StatusNotFound, "Could not find pet with ID") + return + } + delete(p.Pets, id) + + c.Status(http.StatusNoContent) +} diff --git a/experimental/examples/petstore-expanded/gin/server/server.config.yaml b/experimental/examples/petstore-expanded/gin/server/server.config.yaml new file mode 100644 index 0000000000..9b26a5e3b9 --- /dev/null +++ b/experimental/examples/petstore-expanded/gin/server/server.config.yaml @@ -0,0 +1,6 @@ +package: server +generation: + server: gin + models-package: + path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded + alias: petstore diff --git a/experimental/examples/petstore-expanded/gin/server/server.gen.go b/experimental/examples/petstore-expanded/gin/server/server.gen.go new file mode 100644 index 0000000000..87d877c5c8 --- /dev/null +++ b/experimental/examples/petstore-expanded/gin/server/server.gen.go @@ -0,0 +1,1007 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package server + +import ( + "bytes" + "encoding" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "reflect" + "sort" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Returns all pets + // (GET /pets) + FindPets(c *gin.Context, params FindPetsParams) + // Creates a new pet + // (POST /pets) + AddPet(c *gin.Context) + // Deletes a pet by ID + // (DELETE /pets/{id}) + DeletePet(c *gin.Context, id int64) + // Returns a pet by ID + // (GET /pets/{id}) + FindPetByID(c *gin.Context, id int64) +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. +type Unimplemented struct{} + +// Returns all pets +// (GET /pets) +func (_ Unimplemented) FindPets(c *gin.Context, params FindPetsParams) { + c.Status(http.StatusNotImplemented) +} + +// Creates a new pet +// (POST /pets) +func (_ Unimplemented) AddPet(c *gin.Context) { + c.Status(http.StatusNotImplemented) +} + +// Deletes a pet by ID +// (DELETE /pets/{id}) +func (_ Unimplemented) DeletePet(c *gin.Context, id int64) { + c.Status(http.StatusNotImplemented) +} + +// Returns a pet by ID +// (GET /pets/{id}) +func (_ Unimplemented) FindPetByID(c *gin.Context, id int64) { + c.Status(http.StatusNotImplemented) +} + +// FindPetsParams defines parameters for FindPets. +type FindPetsParams struct { + // tags (optional) + Tags *[]string `form:"tags" json:"tags"` + // limit (optional) + Limit *int32 `form:"limit" json:"limit"` +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandler func(*gin.Context, error, int) +} + +// MiddlewareFunc is a middleware function type. +type MiddlewareFunc func(c *gin.Context) + +// FindPets operation middleware +func (siw *ServerInterfaceWrapper) FindPets(c *gin.Context) { + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params FindPetsParams + + // ------------- Optional query parameter "tags" ------------- + err = BindFormExplodeParam("tags", false, c.Request.URL.Query(), ¶ms.Tags) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter tags: %w", err), http.StatusBadRequest) + return + } + + // ------------- Optional query parameter "limit" ------------- + err = BindFormExplodeParam("limit", false, c.Request.URL.Query(), ¶ms.Limit) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter limit: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.FindPets(c, params) +} + +// AddPet operation middleware +func (siw *ServerInterfaceWrapper) AddPet(c *gin.Context) { + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.AddPet(c) +} + +// DeletePet operation middleware +func (siw *ServerInterfaceWrapper) DeletePet(c *gin.Context) { + var err error + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, c.Param("id"), &id) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter id: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.DeletePet(c, id) +} + +// FindPetByID operation middleware +func (siw *ServerInterfaceWrapper) FindPetByID(c *gin.Context) { + var err error + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, c.Param("id"), &id) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter id: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.FindPetByID(c, id) +} + +// GinServerOptions provides options for the Gin server. +type GinServerOptions struct { + BaseURL string + Middlewares []MiddlewareFunc + ErrorHandler func(*gin.Context, error, int) +} + +// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. +func RegisterHandlers(router gin.IRouter, si ServerInterface) { + RegisterHandlersWithOptions(router, si, GinServerOptions{}) +} + +// RegisterHandlersWithOptions creates http.Handler with additional options. +func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options GinServerOptions) { + + errorHandler := options.ErrorHandler + if errorHandler == nil { + errorHandler = func(c *gin.Context, err error, statusCode int) { + c.JSON(statusCode, gin.H{"msg": err.Error()}) + } + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandler: errorHandler, + } + + router.GET(options.BaseURL+"/pets", wrapper.FindPets) + router.POST(options.BaseURL+"/pets", wrapper.AddPet) + router.DELETE(options.BaseURL+"/pets/:id", wrapper.DeletePet) + router.GET(options.BaseURL+"/pets/:id", wrapper.FindPetByID) +} + +// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. +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 +} + +// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. +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 +} + +// RequiredParamError is returned when a required parameter is missing. +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +// RequiredHeaderError is returned when a required header is missing. +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 +} + +// InvalidParamFormatError is returned when a parameter has an invalid format. +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 +} + +// TooManyValuesForParamError is returned when a parameter has too many values. +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) +} + +// ParamLocation indicates where a parameter is located in an HTTP request. +type ParamLocation int + +const ( + ParamLocationUndefined ParamLocation = iota + ParamLocationQuery + ParamLocationPath + ParamLocationHeader + ParamLocationCookie +) + +// Binder is an interface for types that can bind themselves from a string value. +type Binder interface { + Bind(value string) error +} + +// DateFormat is the format used for date (without time) parameters. +const DateFormat = "2006-01-02" + +// Date represents a date (without time) for OpenAPI date format. +type Date struct { + time.Time +} + +// UnmarshalText implements encoding.TextUnmarshaler for Date. +func (d *Date) UnmarshalText(data []byte) error { + t, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = t + return nil +} + +// MarshalText implements encoding.TextMarshaler for Date. +func (d Date) MarshalText() ([]byte, error) { + return []byte(d.Format(DateFormat)), nil +} + +// Format returns the date formatted according to layout. +func (d Date) Format(layout string) string { + return d.Time.Format(layout) +} + +// primitiveToString converts a primitive value to a string representation. +// It handles basic Go types, time.Time, types.Date, and types that implement +// json.Marshaler or fmt.Stringer. +func primitiveToString(value interface{}) (string, error) { + // Check for known types first (time, date, uuid) + if res, ok := marshalKnownTypes(value); ok { + return res, nil + } + + // Dereference pointers for optional values + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + kind := t.Kind() + + switch kind { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + case reflect.Float32: + return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil + case reflect.Bool: + if v.Bool() { + return "true", nil + } + return "false", nil + case reflect.String: + return v.String(), nil + case reflect.Struct: + // Check if it's a UUID + if u, ok := value.(uuid.UUID); ok { + return u.String(), nil + } + // Check if it implements json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + var i2 interface{} + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to decode JSON: %w", err) + } + return primitiveToString(i2) + } + fallthrough + default: + if s, ok := value.(fmt.Stringer); ok { + return s.String(), nil + } + return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) + } +} + +// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. +func marshalKnownTypes(value interface{}) (string, bool) { + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + tt := v.Convert(reflect.TypeOf(time.Time{})) + timeVal := tt.Interface().(time.Time) + return timeVal.Format(time.RFC3339Nano), true + } + + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + d := v.Convert(reflect.TypeOf(Date{})) + dateVal := d.Interface().(Date) + return dateVal.Format(DateFormat), true + } + + if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { + u := v.Convert(reflect.TypeOf(uuid.UUID{})) + uuidVal := u.Interface().(uuid.UUID) + return uuidVal.String(), true + } + + return "", false +} + +// escapeParameterString escapes a parameter value based on its location. +// Query and path parameters need URL escaping; headers and cookies do not. +func escapeParameterString(value string, paramLocation ParamLocation) string { + switch paramLocation { + case ParamLocationQuery: + return url.QueryEscape(value) + case ParamLocationPath: + return url.PathEscape(value) + default: + return value + } +} + +// unescapeParameterString unescapes a parameter value based on its location. +func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { + switch paramLocation { + case ParamLocationQuery, ParamLocationUndefined: + return url.QueryUnescape(value) + case ParamLocationPath: + return url.PathUnescape(value) + default: + return value, nil + } +} + +// sortedKeys returns the keys of a map in sorted order. +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// BindStringToObject binds a string value to a destination object. +// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. +func BindStringToObject(src string, dst interface{}) error { + // Check for TextUnmarshaler + if tu, ok := dst.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(src)) + } + + // Check for Binder interface + if b, ok := dst.(Binder); ok { + return b.Bind(src) + } + + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Ptr { + return fmt.Errorf("dst must be a pointer, got %T", dst) + } + v = v.Elem() + + switch v.Kind() { + case reflect.String: + v.SetString(src) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + v.SetInt(i) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + u, err := strconv.ParseUint(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse uint: %w", err) + } + v.SetUint(u) + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(src, 64) + if err != nil { + return fmt.Errorf("failed to parse float: %w", err) + } + v.SetFloat(f) + case reflect.Bool: + b, err := strconv.ParseBool(src) + if err != nil { + return fmt.Errorf("failed to parse bool: %w", err) + } + v.SetBool(b) + default: + // Try JSON unmarshal as a fallback + return json.Unmarshal([]byte(src), dst) + } + return nil +} + +// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. +func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + newArray := reflect.MakeSlice(t, len(parts), len(parts)) + for i, p := range parts { + err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) + if err != nil { + return fmt.Errorf("error setting array element: %w", err) + } + } + v.Set(newArray) + return nil +} + +// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. +func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { + var fields []string + if explode { + fields = make([]string, len(parts)) + for i, property := range parts { + propertyParts := strings.Split(property, "=") + if len(propertyParts) != 2 { + return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) + } + fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" + } + } else { + if len(parts)%2 != 0 { + return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) + } + fields = make([]string, len(parts)/2) + for i := 0; i < len(parts); i += 2 { + key := parts[i] + value := parts[i+1] + fields[i/2] = "\"" + key + "\":\"" + value + "\"" + } + } + jsonParam := "{" + strings.Join(fields, ",") + "}" + return json.Unmarshal([]byte(jsonParam), dest) +} + +// BindFormExplodeParam binds a form-style parameter with explode to a destination. +// Form style is the default for query and cookie parameters. +// This handles the exploded case where arrays come as multiple query params. +// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) +// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) +func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { + dv := reflect.Indirect(reflect.ValueOf(dest)) + v := dv + var output interface{} + + if required { + output = dest + } else { + // For optional parameters, allocate if nil + if v.IsNil() { + t := v.Type() + newValue := reflect.New(t.Elem()) + output = newValue.Interface() + } else { + output = v.Interface() + } + v = reflect.Indirect(reflect.ValueOf(output)) + } + + t := v.Type() + k := t.Kind() + + values, found := queryParams[paramName] + + switch k { + case reflect.Slice: + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := bindSplitPartsToDestinationArray(values, output) + if err != nil { + return err + } + case reflect.Struct: + // For exploded objects, fields are spread across query params + fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) + if err != nil { + return err + } + if !fieldsPresent { + return nil + } + default: + // Primitive + if len(values) == 0 { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + if len(values) != 1 { + return fmt.Errorf("multiple values for single value parameter '%s'", paramName) + } + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := BindStringToObject(values[0], output) + if err != nil { + return err + } + } + + if !required { + dv.Set(reflect.ValueOf(output)) + } + return nil +} + +// bindParamsToExplodedObject binds query params to struct fields for exploded objects. +func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { + binder, v, t := indirectBinder(dest) + if binder != nil { + _, found := values[paramName] + if !found { + return false, nil + } + return true, BindStringToObject(values.Get(paramName), dest) + } + if t.Kind() != reflect.Struct { + return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) + } + + fieldsPresent := false + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + if !v.Field(i).CanSet() { + continue + } + + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + + fieldVal, found := values[fieldName] + if found { + if len(fieldVal) != 1 { + return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) + } + err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) + if err != nil { + return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) + } + fieldsPresent = true + } + } + return fieldsPresent, nil +} + +// indirectBinder checks if dest implements Binder and returns reflect values. +func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { + v := reflect.ValueOf(dest) + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Binder); ok { + return u, reflect.Value{}, nil + } + } + v = reflect.Indirect(v) + t := v.Type() + // Handle special types like time.Time and Date + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return dest, reflect.Value{}, nil + } + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + return dest, reflect.Value{}, nil + } + return nil, v, t +} + +// BindSimpleParam binds a simple-style parameter without explode to a destination. +// Simple style is the default for path and header parameters. +// Arrays: a,b,c -> []string{"a", "b", "c"} +// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} +func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + // Split on comma and bind as key,value pairs + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) + case reflect.Slice: + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} + +// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. +// Form style is the default for query and cookie parameters. +// Primitives: paramName=value +// Arrays: paramName=a¶mName=b¶mName=c +// Objects: key1=value1&key2=value2 +func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleFormExplodeSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleFormExplodeStruct(paramName, paramLocation, value) + case reflect.Map: + return styleFormExplodeMap(paramName, paramLocation, value) + default: + return styleFormExplodePrimitive(paramName, paramLocation, value) + } +} + +func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Form with explode: paramName=a¶mName=b¶mName=c + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, "&"+prefix), nil +} + +func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleFormExplodeParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. +// Simple style is the default for path and header parameters. +// Arrays are comma-separated: a,b,c +// Objects are key,value pairs: key1,value1,key2,value2 +func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return escapeParameterString(string(b), paramLocation), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleSimpleSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleSimpleStruct(paramName, paramLocation, value) + case reflect.Map: + return styleSimpleMap(paramName, paramLocation, value) + default: + return styleSimplePrimitive(paramLocation, value) + } +} + +func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return escapeParameterString(strVal, paramLocation), nil +} + +func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return escapeParameterString(timeVal, paramLocation), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleSimpleParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +// structToFieldDict converts a struct to a map of field names to string values. +func structToFieldDict(value interface{}) (map[string]string, error) { + v := reflect.ValueOf(value) + t := reflect.TypeOf(value) + fieldDict := make(map[string]string) + + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + f := v.Field(i) + + // Skip nil optional fields + if f.Type().Kind() == reflect.Ptr && f.IsNil() { + continue + } + str, err := primitiveToString(f.Interface()) + if err != nil { + return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) + } + fieldDict[fieldName] = str + } + return fieldDict, nil +} diff --git a/experimental/examples/petstore-expanded/gorilla/Makefile b/experimental/examples/petstore-expanded/gorilla/Makefile new file mode 100644 index 0000000000..42389f4137 --- /dev/null +++ b/experimental/examples/petstore-expanded/gorilla/Makefile @@ -0,0 +1,35 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + +generate: + $(call execute-if-go-124,go generate ./...) + +test: + $(call execute-if-go-124,go test -cover ./...) + +tidy: + $(call execute-if-go-124,go mod tidy) + +tidy-ci: + $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/gorilla/go.mod b/experimental/examples/petstore-expanded/gorilla/go.mod new file mode 100644 index 0000000000..fa094711da --- /dev/null +++ b/experimental/examples/petstore-expanded/gorilla/go.mod @@ -0,0 +1,11 @@ +module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/gorilla + +go 1.24.0 + +require ( + github.com/google/uuid v1.6.0 + github.com/gorilla/mux v1.8.1 + github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 +) + +replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/gorilla/go.sum b/experimental/examples/petstore-expanded/gorilla/go.sum new file mode 100644 index 0000000000..c9af5271c5 --- /dev/null +++ b/experimental/examples/petstore-expanded/gorilla/go.sum @@ -0,0 +1,4 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/experimental/examples/petstore-expanded/gorilla/main.go b/experimental/examples/petstore-expanded/gorilla/main.go new file mode 100644 index 0000000000..1bde88bcca --- /dev/null +++ b/experimental/examples/petstore-expanded/gorilla/main.go @@ -0,0 +1,40 @@ +//go:build go1.22 + +// This is an example of implementing the Pet Store from the OpenAPI documentation +// found at: +// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml + +package main + +import ( + "flag" + "log" + "net" + "net/http" + + "github.com/gorilla/mux" + "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/gorilla/server" +) + +func main() { + port := flag.String("port", "8080", "Port for test HTTP server") + flag.Parse() + + // Create an instance of our handler which satisfies the generated interface + petStore := server.NewPetStore() + + r := mux.NewRouter() + + // We now register our petStore above as the handler for the interface + server.HandlerFromMux(petStore, r) + + s := &http.Server{ + Handler: r, + Addr: net.JoinHostPort("0.0.0.0", *port), + } + + log.Printf("Server listening on %s", s.Addr) + + // And we serve HTTP until the world ends. + log.Fatal(s.ListenAndServe()) +} diff --git a/experimental/examples/petstore-expanded/gorilla/server/petstore.go b/experimental/examples/petstore-expanded/gorilla/server/petstore.go new file mode 100644 index 0000000000..2f52c8e271 --- /dev/null +++ b/experimental/examples/petstore-expanded/gorilla/server/petstore.go @@ -0,0 +1,135 @@ +//go:build go1.22 + +package server + +import ( + "encoding/json" + "fmt" + "net/http" + "sync" + + petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" +) + +// PetStore implements the ServerInterface. +type PetStore struct { + Pets map[int64]petstore.Pet + NextId int64 + Lock sync.Mutex +} + +// Make sure we conform to ServerInterface +var _ ServerInterface = (*PetStore)(nil) + +// NewPetStore creates a new PetStore. +func NewPetStore() *PetStore { + return &PetStore{ + Pets: make(map[int64]petstore.Pet), + NextId: 1000, + } +} + +// sendPetStoreError wraps sending of an error in the Error format. +func sendPetStoreError(w http.ResponseWriter, code int, message string) { + petErr := petstore.Error{ + Code: int32(code), + Message: message, + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _ = json.NewEncoder(w).Encode(petErr) +} + +// FindPets returns all pets, optionally filtered by tags and limited. +func (p *PetStore) FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) { + p.Lock.Lock() + defer p.Lock.Unlock() + + var result []petstore.Pet + + for _, pet := range p.Pets { + if params.Tags != nil { + // If we have tags, filter pets by tag + for _, t := range *params.Tags { + if pet.Tag != nil && (*pet.Tag == t) { + result = append(result, pet) + } + } + } else { + // Add all pets if we're not filtering + result = append(result, pet) + } + + if params.Limit != nil { + l := int(*params.Limit) + if len(result) >= l { + // We're at the limit + break + } + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(result) +} + +// AddPet creates a new pet. +func (p *PetStore) AddPet(w http.ResponseWriter, r *http.Request) { + // We expect a NewPet object in the request body. + var newPet petstore.NewPet + if err := json.NewDecoder(r.Body).Decode(&newPet); err != nil { + sendPetStoreError(w, http.StatusBadRequest, "Invalid format for NewPet") + return + } + + // We now have a pet, let's add it to our "database". + p.Lock.Lock() + defer p.Lock.Unlock() + + // We handle pets, not NewPets, which have an additional ID field + var pet petstore.Pet + pet.Name = newPet.Name + pet.Tag = newPet.Tag + pet.ID = p.NextId + p.NextId++ + + // Insert into map + p.Pets[pet.ID] = pet + + // Now, we have to return the Pet + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(pet) +} + +// FindPetByID returns a pet by ID. +func (p *PetStore) FindPetByID(w http.ResponseWriter, r *http.Request, id int64) { + p.Lock.Lock() + defer p.Lock.Unlock() + + pet, found := p.Pets[id] + if !found { + sendPetStoreError(w, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(pet) +} + +// DeletePet deletes a pet by ID. +func (p *PetStore) DeletePet(w http.ResponseWriter, r *http.Request, id int64) { + p.Lock.Lock() + defer p.Lock.Unlock() + + _, found := p.Pets[id] + if !found { + sendPetStoreError(w, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) + return + } + delete(p.Pets, id) + + w.WriteHeader(http.StatusNoContent) +} diff --git a/experimental/examples/petstore-expanded/gorilla/server/server.config.yaml b/experimental/examples/petstore-expanded/gorilla/server/server.config.yaml new file mode 100644 index 0000000000..5de1d21f15 --- /dev/null +++ b/experimental/examples/petstore-expanded/gorilla/server/server.config.yaml @@ -0,0 +1,6 @@ +package: server +generation: + server: gorilla + models-package: + path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded + alias: petstore diff --git a/experimental/examples/petstore-expanded/gorilla/server/server.gen.go b/experimental/examples/petstore-expanded/gorilla/server/server.gen.go new file mode 100644 index 0000000000..957f88149e --- /dev/null +++ b/experimental/examples/petstore-expanded/gorilla/server/server.gen.go @@ -0,0 +1,1035 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package server + +import ( + "bytes" + "encoding" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "reflect" + "sort" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + "github.com/gorilla/mux" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Returns all pets + // (GET /pets) + FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) + // Creates a new pet + // (POST /pets) + AddPet(w http.ResponseWriter, r *http.Request) + // Deletes a pet by ID + // (DELETE /pets/{id}) + DeletePet(w http.ResponseWriter, r *http.Request, id int64) + // Returns a pet by ID + // (GET /pets/{id}) + FindPetByID(w http.ResponseWriter, r *http.Request, id int64) +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. +type Unimplemented struct{} + +// Returns all pets +// (GET /pets) +func (_ Unimplemented) FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Creates a new pet +// (POST /pets) +func (_ Unimplemented) AddPet(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Deletes a pet by ID +// (DELETE /pets/{id}) +func (_ Unimplemented) DeletePet(w http.ResponseWriter, r *http.Request, id int64) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Returns a pet by ID +// (GET /pets/{id}) +func (_ Unimplemented) FindPetByID(w http.ResponseWriter, r *http.Request, id int64) { + w.WriteHeader(http.StatusNotImplemented) +} + +// FindPetsParams defines parameters for FindPets. +type FindPetsParams struct { + // tags (optional) + Tags *[]string `form:"tags" json:"tags"` + // limit (optional) + Limit *int32 `form:"limit" json:"limit"` +} + +// ServerInterfaceWrapper converts HTTP requests to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// MiddlewareFunc is a middleware function type. +type MiddlewareFunc func(http.Handler) http.Handler + +// FindPets operation middleware +func (siw *ServerInterfaceWrapper) FindPets(w http.ResponseWriter, r *http.Request) { + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params FindPetsParams + + // ------------- Optional query parameter "tags" ------------- + err = BindFormExplodeParam("tags", false, r.URL.Query(), ¶ms.Tags) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tags", Err: err}) + return + } + + // ------------- Optional query parameter "limit" ------------- + err = BindFormExplodeParam("limit", false, r.URL.Query(), ¶ms.Limit) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.FindPets(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// AddPet operation middleware +func (siw *ServerInterfaceWrapper) AddPet(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.AddPet(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// DeletePet operation middleware +func (siw *ServerInterfaceWrapper) DeletePet(w http.ResponseWriter, r *http.Request) { + var err error + + pathParams := mux.Vars(r) + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, pathParams["id"], &id) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeletePet(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// FindPetByID operation middleware +func (siw *ServerInterfaceWrapper) FindPetByID(w http.ResponseWriter, r *http.Request) { + var err error + + pathParams := mux.Vars(r) + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, pathParams["id"], &id) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.FindPetByID(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, GorillaServerOptions{}) +} + +// GorillaServerOptions configures the Gorilla server. +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, + }) +} + +// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. +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) + } + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + r.HandleFunc(options.BaseURL+"/pets", wrapper.FindPets).Methods("GET") + r.HandleFunc(options.BaseURL+"/pets", wrapper.AddPet).Methods("POST") + r.HandleFunc(options.BaseURL+"/pets/{id}", wrapper.DeletePet).Methods("DELETE") + r.HandleFunc(options.BaseURL+"/pets/{id}", wrapper.FindPetByID).Methods("GET") + return r +} + +// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. +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 +} + +// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. +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 +} + +// RequiredParamError is returned when a required parameter is missing. +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +// RequiredHeaderError is returned when a required header is missing. +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 +} + +// InvalidParamFormatError is returned when a parameter has an invalid format. +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 +} + +// TooManyValuesForParamError is returned when a parameter has too many values. +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) +} + +// ParamLocation indicates where a parameter is located in an HTTP request. +type ParamLocation int + +const ( + ParamLocationUndefined ParamLocation = iota + ParamLocationQuery + ParamLocationPath + ParamLocationHeader + ParamLocationCookie +) + +// Binder is an interface for types that can bind themselves from a string value. +type Binder interface { + Bind(value string) error +} + +// DateFormat is the format used for date (without time) parameters. +const DateFormat = "2006-01-02" + +// Date represents a date (without time) for OpenAPI date format. +type Date struct { + time.Time +} + +// UnmarshalText implements encoding.TextUnmarshaler for Date. +func (d *Date) UnmarshalText(data []byte) error { + t, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = t + return nil +} + +// MarshalText implements encoding.TextMarshaler for Date. +func (d Date) MarshalText() ([]byte, error) { + return []byte(d.Format(DateFormat)), nil +} + +// Format returns the date formatted according to layout. +func (d Date) Format(layout string) string { + return d.Time.Format(layout) +} + +// primitiveToString converts a primitive value to a string representation. +// It handles basic Go types, time.Time, types.Date, and types that implement +// json.Marshaler or fmt.Stringer. +func primitiveToString(value interface{}) (string, error) { + // Check for known types first (time, date, uuid) + if res, ok := marshalKnownTypes(value); ok { + return res, nil + } + + // Dereference pointers for optional values + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + kind := t.Kind() + + switch kind { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + case reflect.Float32: + return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil + case reflect.Bool: + if v.Bool() { + return "true", nil + } + return "false", nil + case reflect.String: + return v.String(), nil + case reflect.Struct: + // Check if it's a UUID + if u, ok := value.(uuid.UUID); ok { + return u.String(), nil + } + // Check if it implements json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + var i2 interface{} + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to decode JSON: %w", err) + } + return primitiveToString(i2) + } + fallthrough + default: + if s, ok := value.(fmt.Stringer); ok { + return s.String(), nil + } + return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) + } +} + +// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. +func marshalKnownTypes(value interface{}) (string, bool) { + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + tt := v.Convert(reflect.TypeOf(time.Time{})) + timeVal := tt.Interface().(time.Time) + return timeVal.Format(time.RFC3339Nano), true + } + + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + d := v.Convert(reflect.TypeOf(Date{})) + dateVal := d.Interface().(Date) + return dateVal.Format(DateFormat), true + } + + if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { + u := v.Convert(reflect.TypeOf(uuid.UUID{})) + uuidVal := u.Interface().(uuid.UUID) + return uuidVal.String(), true + } + + return "", false +} + +// escapeParameterString escapes a parameter value based on its location. +// Query and path parameters need URL escaping; headers and cookies do not. +func escapeParameterString(value string, paramLocation ParamLocation) string { + switch paramLocation { + case ParamLocationQuery: + return url.QueryEscape(value) + case ParamLocationPath: + return url.PathEscape(value) + default: + return value + } +} + +// unescapeParameterString unescapes a parameter value based on its location. +func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { + switch paramLocation { + case ParamLocationQuery, ParamLocationUndefined: + return url.QueryUnescape(value) + case ParamLocationPath: + return url.PathUnescape(value) + default: + return value, nil + } +} + +// sortedKeys returns the keys of a map in sorted order. +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// BindStringToObject binds a string value to a destination object. +// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. +func BindStringToObject(src string, dst interface{}) error { + // Check for TextUnmarshaler + if tu, ok := dst.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(src)) + } + + // Check for Binder interface + if b, ok := dst.(Binder); ok { + return b.Bind(src) + } + + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Ptr { + return fmt.Errorf("dst must be a pointer, got %T", dst) + } + v = v.Elem() + + switch v.Kind() { + case reflect.String: + v.SetString(src) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + v.SetInt(i) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + u, err := strconv.ParseUint(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse uint: %w", err) + } + v.SetUint(u) + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(src, 64) + if err != nil { + return fmt.Errorf("failed to parse float: %w", err) + } + v.SetFloat(f) + case reflect.Bool: + b, err := strconv.ParseBool(src) + if err != nil { + return fmt.Errorf("failed to parse bool: %w", err) + } + v.SetBool(b) + default: + // Try JSON unmarshal as a fallback + return json.Unmarshal([]byte(src), dst) + } + return nil +} + +// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. +func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + newArray := reflect.MakeSlice(t, len(parts), len(parts)) + for i, p := range parts { + err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) + if err != nil { + return fmt.Errorf("error setting array element: %w", err) + } + } + v.Set(newArray) + return nil +} + +// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. +func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { + var fields []string + if explode { + fields = make([]string, len(parts)) + for i, property := range parts { + propertyParts := strings.Split(property, "=") + if len(propertyParts) != 2 { + return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) + } + fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" + } + } else { + if len(parts)%2 != 0 { + return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) + } + fields = make([]string, len(parts)/2) + for i := 0; i < len(parts); i += 2 { + key := parts[i] + value := parts[i+1] + fields[i/2] = "\"" + key + "\":\"" + value + "\"" + } + } + jsonParam := "{" + strings.Join(fields, ",") + "}" + return json.Unmarshal([]byte(jsonParam), dest) +} + +// BindFormExplodeParam binds a form-style parameter with explode to a destination. +// Form style is the default for query and cookie parameters. +// This handles the exploded case where arrays come as multiple query params. +// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) +// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) +func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { + dv := reflect.Indirect(reflect.ValueOf(dest)) + v := dv + var output interface{} + + if required { + output = dest + } else { + // For optional parameters, allocate if nil + if v.IsNil() { + t := v.Type() + newValue := reflect.New(t.Elem()) + output = newValue.Interface() + } else { + output = v.Interface() + } + v = reflect.Indirect(reflect.ValueOf(output)) + } + + t := v.Type() + k := t.Kind() + + values, found := queryParams[paramName] + + switch k { + case reflect.Slice: + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := bindSplitPartsToDestinationArray(values, output) + if err != nil { + return err + } + case reflect.Struct: + // For exploded objects, fields are spread across query params + fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) + if err != nil { + return err + } + if !fieldsPresent { + return nil + } + default: + // Primitive + if len(values) == 0 { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + if len(values) != 1 { + return fmt.Errorf("multiple values for single value parameter '%s'", paramName) + } + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := BindStringToObject(values[0], output) + if err != nil { + return err + } + } + + if !required { + dv.Set(reflect.ValueOf(output)) + } + return nil +} + +// bindParamsToExplodedObject binds query params to struct fields for exploded objects. +func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { + binder, v, t := indirectBinder(dest) + if binder != nil { + _, found := values[paramName] + if !found { + return false, nil + } + return true, BindStringToObject(values.Get(paramName), dest) + } + if t.Kind() != reflect.Struct { + return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) + } + + fieldsPresent := false + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + if !v.Field(i).CanSet() { + continue + } + + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + + fieldVal, found := values[fieldName] + if found { + if len(fieldVal) != 1 { + return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) + } + err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) + if err != nil { + return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) + } + fieldsPresent = true + } + } + return fieldsPresent, nil +} + +// indirectBinder checks if dest implements Binder and returns reflect values. +func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { + v := reflect.ValueOf(dest) + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Binder); ok { + return u, reflect.Value{}, nil + } + } + v = reflect.Indirect(v) + t := v.Type() + // Handle special types like time.Time and Date + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return dest, reflect.Value{}, nil + } + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + return dest, reflect.Value{}, nil + } + return nil, v, t +} + +// BindSimpleParam binds a simple-style parameter without explode to a destination. +// Simple style is the default for path and header parameters. +// Arrays: a,b,c -> []string{"a", "b", "c"} +// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} +func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + // Split on comma and bind as key,value pairs + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) + case reflect.Slice: + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} + +// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. +// Form style is the default for query and cookie parameters. +// Primitives: paramName=value +// Arrays: paramName=a¶mName=b¶mName=c +// Objects: key1=value1&key2=value2 +func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleFormExplodeSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleFormExplodeStruct(paramName, paramLocation, value) + case reflect.Map: + return styleFormExplodeMap(paramName, paramLocation, value) + default: + return styleFormExplodePrimitive(paramName, paramLocation, value) + } +} + +func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Form with explode: paramName=a¶mName=b¶mName=c + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, "&"+prefix), nil +} + +func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleFormExplodeParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. +// Simple style is the default for path and header parameters. +// Arrays are comma-separated: a,b,c +// Objects are key,value pairs: key1,value1,key2,value2 +func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return escapeParameterString(string(b), paramLocation), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleSimpleSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleSimpleStruct(paramName, paramLocation, value) + case reflect.Map: + return styleSimpleMap(paramName, paramLocation, value) + default: + return styleSimplePrimitive(paramLocation, value) + } +} + +func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return escapeParameterString(strVal, paramLocation), nil +} + +func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return escapeParameterString(timeVal, paramLocation), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleSimpleParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +// structToFieldDict converts a struct to a map of field names to string values. +func structToFieldDict(value interface{}) (map[string]string, error) { + v := reflect.ValueOf(value) + t := reflect.TypeOf(value) + fieldDict := make(map[string]string) + + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + f := v.Field(i) + + // Skip nil optional fields + if f.Type().Kind() == reflect.Ptr && f.IsNil() { + continue + } + str, err := primitiveToString(f.Interface()) + if err != nil { + return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) + } + fieldDict[fieldName] = str + } + return fieldDict, nil +} diff --git a/experimental/examples/petstore-expanded/iris/Makefile b/experimental/examples/petstore-expanded/iris/Makefile new file mode 100644 index 0000000000..42389f4137 --- /dev/null +++ b/experimental/examples/petstore-expanded/iris/Makefile @@ -0,0 +1,35 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + +generate: + $(call execute-if-go-124,go generate ./...) + +test: + $(call execute-if-go-124,go test -cover ./...) + +tidy: + $(call execute-if-go-124,go mod tidy) + +tidy-ci: + $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/iris/go.mod b/experimental/examples/petstore-expanded/iris/go.mod new file mode 100644 index 0000000000..b13819f2a5 --- /dev/null +++ b/experimental/examples/petstore-expanded/iris/go.mod @@ -0,0 +1,55 @@ +module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/iris + +go 1.24.0 + +require ( + github.com/google/uuid v1.6.0 + github.com/kataras/iris/v12 v12.2.11 + github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 +) + +require ( + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect + github.com/CloudyKit/jet/v6 v6.2.0 // indirect + github.com/Joker/jade v1.1.3 // indirect + github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/flosch/pongo2/v4 v4.0.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/iris-contrib/schema v0.0.6 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/kataras/blocks v0.0.8 // indirect + github.com/kataras/golog v0.1.11 // indirect + github.com/kataras/pio v0.0.13 // indirect + github.com/kataras/sitemap v0.0.6 // indirect + github.com/kataras/tunnel v0.0.4 // indirect + github.com/klauspost/compress v1.17.7 // indirect + github.com/mailgun/raymond/v2 v2.0.48 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/microcosm-cc/bluemonday v1.0.26 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/schollz/closestmatch v2.1.0+incompatible // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/tdewolff/minify/v2 v2.20.19 // indirect + github.com/tdewolff/parse/v2 v2.7.12 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/yosssi/ace v0.0.5 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/iris/go.sum b/experimental/examples/petstore-expanded/iris/go.sum new file mode 100644 index 0000000000..a06439c9fc --- /dev/null +++ b/experimental/examples/petstore-expanded/iris/go.sum @@ -0,0 +1,178 @@ +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= +github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= +github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= +github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= +github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= +github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= +github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= +github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= +github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 h1:4gjrh/PN2MuWCCElk8/I4OCKRKWCCo2zEct3VKCbibU= +github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= +github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= +github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= +github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= +github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= +github.com/kataras/golog v0.1.11 h1:dGkcCVsIpqiAMWTlebn/ZULHxFvfG4K43LF1cNWSh20= +github.com/kataras/golog v0.1.11/go.mod h1:mAkt1vbPowFUuUGvexyQ5NFW6djEgGyxQBIARJ0AH4A= +github.com/kataras/iris/v12 v12.2.11 h1:sGgo43rMPfzDft8rjVhPs6L3qDJy3TbBrMD/zGL1pzk= +github.com/kataras/iris/v12 v12.2.11/go.mod h1:uMAeX8OqG9vqdhyrIPv8Lajo/wXTtAF43wchP9WHt2w= +github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= +github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= +github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= +github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= +github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= +github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= +github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= +github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= +github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo= +github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM= +github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ= +github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= +github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= +github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= +github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= +github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= +moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= diff --git a/experimental/examples/petstore-expanded/iris/main.go b/experimental/examples/petstore-expanded/iris/main.go new file mode 100644 index 0000000000..48d035a84d --- /dev/null +++ b/experimental/examples/petstore-expanded/iris/main.go @@ -0,0 +1,35 @@ +//go:build go1.22 + +// This is an example of implementing the Pet Store from the OpenAPI documentation +// found at: +// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml + +package main + +import ( + "flag" + "log" + "net" + + "github.com/kataras/iris/v12" + "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/iris/server" +) + +func main() { + port := flag.String("port", "8080", "Port for test HTTP server") + flag.Parse() + + // Create an instance of our handler which satisfies the generated interface + petStore := server.NewPetStore() + + app := iris.New() + + // We now register our petStore above as the handler for the interface + server.RegisterHandlers(app, petStore) + + addr := net.JoinHostPort("0.0.0.0", *port) + log.Printf("Server listening on %s", addr) + + // And we serve HTTP until the world ends. + log.Fatal(app.Listen(addr)) +} diff --git a/experimental/examples/petstore-expanded/iris/server/petstore.go b/experimental/examples/petstore-expanded/iris/server/petstore.go new file mode 100644 index 0000000000..cab647706c --- /dev/null +++ b/experimental/examples/petstore-expanded/iris/server/petstore.go @@ -0,0 +1,130 @@ +//go:build go1.22 + +package server + +import ( + "net/http" + "sync" + + "github.com/kataras/iris/v12" + petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" +) + +// PetStore implements the ServerInterface. +type PetStore struct { + Pets map[int64]petstore.Pet + NextId int64 + Lock sync.Mutex +} + +// Make sure we conform to ServerInterface +var _ ServerInterface = (*PetStore)(nil) + +// NewPetStore creates a new PetStore. +func NewPetStore() *PetStore { + return &PetStore{ + Pets: make(map[int64]petstore.Pet), + NextId: 1000, + } +} + +// sendPetStoreError wraps sending of an error in the Error format. +func sendPetStoreError(ctx iris.Context, code int, message string) { + petErr := petstore.Error{ + Code: int32(code), + Message: message, + } + ctx.StatusCode(code) + _ = ctx.JSON(petErr) +} + +// FindPets returns all pets, optionally filtered by tags and limited. +func (p *PetStore) FindPets(ctx iris.Context, params FindPetsParams) { + p.Lock.Lock() + defer p.Lock.Unlock() + + var result []petstore.Pet + + for _, pet := range p.Pets { + if params.Tags != nil { + // If we have tags, filter pets by tag + for _, t := range *params.Tags { + if pet.Tag != nil && (*pet.Tag == t) { + result = append(result, pet) + } + } + } else { + // Add all pets if we're not filtering + result = append(result, pet) + } + + if params.Limit != nil { + l := int(*params.Limit) + if len(result) >= l { + // We're at the limit + break + } + } + } + + ctx.StatusCode(http.StatusOK) + _ = ctx.JSON(result) +} + +// AddPet creates a new pet. +func (p *PetStore) AddPet(ctx iris.Context) { + // We expect a NewPet object in the request body. + var newPet petstore.NewPet + if err := ctx.ReadJSON(&newPet); err != nil { + sendPetStoreError(ctx, http.StatusBadRequest, "Invalid format for NewPet") + return + } + + // We now have a pet, let's add it to our "database". + p.Lock.Lock() + defer p.Lock.Unlock() + + // We handle pets, not NewPets, which have an additional ID field + var pet petstore.Pet + pet.Name = newPet.Name + pet.Tag = newPet.Tag + pet.ID = p.NextId + p.NextId++ + + // Insert into map + p.Pets[pet.ID] = pet + + // Now, we have to return the Pet + ctx.StatusCode(http.StatusCreated) + _ = ctx.JSON(pet) +} + +// FindPetByID returns a pet by ID. +func (p *PetStore) FindPetByID(ctx iris.Context, id int64) { + p.Lock.Lock() + defer p.Lock.Unlock() + + pet, found := p.Pets[id] + if !found { + sendPetStoreError(ctx, http.StatusNotFound, "Could not find pet with ID") + return + } + + ctx.StatusCode(http.StatusOK) + _ = ctx.JSON(pet) +} + +// DeletePet deletes a pet by ID. +func (p *PetStore) DeletePet(ctx iris.Context, id int64) { + p.Lock.Lock() + defer p.Lock.Unlock() + + _, found := p.Pets[id] + if !found { + sendPetStoreError(ctx, http.StatusNotFound, "Could not find pet with ID") + return + } + delete(p.Pets, id) + + ctx.StatusCode(http.StatusNoContent) +} diff --git a/experimental/examples/petstore-expanded/iris/server/server.config.yaml b/experimental/examples/petstore-expanded/iris/server/server.config.yaml new file mode 100644 index 0000000000..443b44a84b --- /dev/null +++ b/experimental/examples/petstore-expanded/iris/server/server.config.yaml @@ -0,0 +1,6 @@ +package: server +generation: + server: iris + models-package: + path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded + alias: petstore diff --git a/experimental/examples/petstore-expanded/iris/server/server.gen.go b/experimental/examples/petstore-expanded/iris/server/server.gen.go new file mode 100644 index 0000000000..1c59478225 --- /dev/null +++ b/experimental/examples/petstore-expanded/iris/server/server.gen.go @@ -0,0 +1,973 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package server + +import ( + "bytes" + "encoding" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "reflect" + "sort" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + "github.com/kataras/iris/v12" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Returns all pets + // (GET /pets) + FindPets(ctx iris.Context, params FindPetsParams) + // Creates a new pet + // (POST /pets) + AddPet(ctx iris.Context) + // Deletes a pet by ID + // (DELETE /pets/{id}) + DeletePet(ctx iris.Context, id int64) + // Returns a pet by ID + // (GET /pets/{id}) + FindPetByID(ctx iris.Context, id int64) +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. +type Unimplemented struct{} + +// Returns all pets +// (GET /pets) +func (_ Unimplemented) FindPets(ctx iris.Context, params FindPetsParams) { + ctx.StatusCode(http.StatusNotImplemented) +} + +// Creates a new pet +// (POST /pets) +func (_ Unimplemented) AddPet(ctx iris.Context) { + ctx.StatusCode(http.StatusNotImplemented) +} + +// Deletes a pet by ID +// (DELETE /pets/{id}) +func (_ Unimplemented) DeletePet(ctx iris.Context, id int64) { + ctx.StatusCode(http.StatusNotImplemented) +} + +// Returns a pet by ID +// (GET /pets/{id}) +func (_ Unimplemented) FindPetByID(ctx iris.Context, id int64) { + ctx.StatusCode(http.StatusNotImplemented) +} + +// FindPetsParams defines parameters for FindPets. +type FindPetsParams struct { + // tags (optional) + Tags *[]string `form:"tags" json:"tags"` + // limit (optional) + Limit *int32 `form:"limit" json:"limit"` +} + +// ServerInterfaceWrapper converts iris contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +// FindPets converts iris context to params. +func (w *ServerInterfaceWrapper) FindPets(ctx iris.Context) { + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params FindPetsParams + + // ------------- Optional query parameter "tags" ------------- + err = BindFormExplodeParam("tags", false, ctx.Request().URL.Query(), ¶ms.Tags) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(fmt.Sprintf("Invalid format for parameter tags: %s", err)) + return + } + + // ------------- Optional query parameter "limit" ------------- + err = BindFormExplodeParam("limit", false, ctx.Request().URL.Query(), ¶ms.Limit) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(fmt.Sprintf("Invalid format for parameter limit: %s", err)) + return + } + + // Invoke the callback with all the unmarshaled arguments + w.Handler.FindPets(ctx, params) +} + +// AddPet converts iris context to params. +func (w *ServerInterfaceWrapper) AddPet(ctx iris.Context) { + + // Invoke the callback with all the unmarshaled arguments + w.Handler.AddPet(ctx) +} + +// DeletePet converts iris context to params. +func (w *ServerInterfaceWrapper) DeletePet(ctx iris.Context) { + var err error + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, ctx.Params().Get("id"), &id) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(fmt.Sprintf("Invalid format for parameter id: %s", err)) + return + } + + // Invoke the callback with all the unmarshaled arguments + w.Handler.DeletePet(ctx, id) +} + +// FindPetByID converts iris context to params. +func (w *ServerInterfaceWrapper) FindPetByID(ctx iris.Context) { + var err error + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, ctx.Params().Get("id"), &id) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(fmt.Sprintf("Invalid format for parameter id: %s", err)) + return + } + + // Invoke the callback with all the unmarshaled arguments + w.Handler.FindPetByID(ctx, id) +} + +// IrisServerOptions is the option for iris server. +type IrisServerOptions struct { + BaseURL string + Middlewares []iris.Handler +} + +// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. +func RegisterHandlers(router *iris.Application, si ServerInterface) { + RegisterHandlersWithOptions(router, si, IrisServerOptions{}) +} + +// RegisterHandlersWithOptions creates http.Handler with additional options. +func RegisterHandlersWithOptions(router *iris.Application, si ServerInterface, options IrisServerOptions) { + + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + router.Get(options.BaseURL+"/pets", wrapper.FindPets) + router.Post(options.BaseURL+"/pets", wrapper.AddPet) + router.Delete(options.BaseURL+"/pets/:id", wrapper.DeletePet) + router.Get(options.BaseURL+"/pets/:id", wrapper.FindPetByID) + router.Build() +} + +// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. +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 +} + +// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. +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 +} + +// RequiredParamError is returned when a required parameter is missing. +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +// RequiredHeaderError is returned when a required header is missing. +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 +} + +// InvalidParamFormatError is returned when a parameter has an invalid format. +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 +} + +// TooManyValuesForParamError is returned when a parameter has too many values. +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) +} + +// ParamLocation indicates where a parameter is located in an HTTP request. +type ParamLocation int + +const ( + ParamLocationUndefined ParamLocation = iota + ParamLocationQuery + ParamLocationPath + ParamLocationHeader + ParamLocationCookie +) + +// Binder is an interface for types that can bind themselves from a string value. +type Binder interface { + Bind(value string) error +} + +// DateFormat is the format used for date (without time) parameters. +const DateFormat = "2006-01-02" + +// Date represents a date (without time) for OpenAPI date format. +type Date struct { + time.Time +} + +// UnmarshalText implements encoding.TextUnmarshaler for Date. +func (d *Date) UnmarshalText(data []byte) error { + t, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = t + return nil +} + +// MarshalText implements encoding.TextMarshaler for Date. +func (d Date) MarshalText() ([]byte, error) { + return []byte(d.Format(DateFormat)), nil +} + +// Format returns the date formatted according to layout. +func (d Date) Format(layout string) string { + return d.Time.Format(layout) +} + +// primitiveToString converts a primitive value to a string representation. +// It handles basic Go types, time.Time, types.Date, and types that implement +// json.Marshaler or fmt.Stringer. +func primitiveToString(value interface{}) (string, error) { + // Check for known types first (time, date, uuid) + if res, ok := marshalKnownTypes(value); ok { + return res, nil + } + + // Dereference pointers for optional values + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + kind := t.Kind() + + switch kind { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + case reflect.Float32: + return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil + case reflect.Bool: + if v.Bool() { + return "true", nil + } + return "false", nil + case reflect.String: + return v.String(), nil + case reflect.Struct: + // Check if it's a UUID + if u, ok := value.(uuid.UUID); ok { + return u.String(), nil + } + // Check if it implements json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + var i2 interface{} + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to decode JSON: %w", err) + } + return primitiveToString(i2) + } + fallthrough + default: + if s, ok := value.(fmt.Stringer); ok { + return s.String(), nil + } + return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) + } +} + +// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. +func marshalKnownTypes(value interface{}) (string, bool) { + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + tt := v.Convert(reflect.TypeOf(time.Time{})) + timeVal := tt.Interface().(time.Time) + return timeVal.Format(time.RFC3339Nano), true + } + + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + d := v.Convert(reflect.TypeOf(Date{})) + dateVal := d.Interface().(Date) + return dateVal.Format(DateFormat), true + } + + if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { + u := v.Convert(reflect.TypeOf(uuid.UUID{})) + uuidVal := u.Interface().(uuid.UUID) + return uuidVal.String(), true + } + + return "", false +} + +// escapeParameterString escapes a parameter value based on its location. +// Query and path parameters need URL escaping; headers and cookies do not. +func escapeParameterString(value string, paramLocation ParamLocation) string { + switch paramLocation { + case ParamLocationQuery: + return url.QueryEscape(value) + case ParamLocationPath: + return url.PathEscape(value) + default: + return value + } +} + +// unescapeParameterString unescapes a parameter value based on its location. +func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { + switch paramLocation { + case ParamLocationQuery, ParamLocationUndefined: + return url.QueryUnescape(value) + case ParamLocationPath: + return url.PathUnescape(value) + default: + return value, nil + } +} + +// sortedKeys returns the keys of a map in sorted order. +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// BindStringToObject binds a string value to a destination object. +// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. +func BindStringToObject(src string, dst interface{}) error { + // Check for TextUnmarshaler + if tu, ok := dst.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(src)) + } + + // Check for Binder interface + if b, ok := dst.(Binder); ok { + return b.Bind(src) + } + + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Ptr { + return fmt.Errorf("dst must be a pointer, got %T", dst) + } + v = v.Elem() + + switch v.Kind() { + case reflect.String: + v.SetString(src) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + v.SetInt(i) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + u, err := strconv.ParseUint(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse uint: %w", err) + } + v.SetUint(u) + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(src, 64) + if err != nil { + return fmt.Errorf("failed to parse float: %w", err) + } + v.SetFloat(f) + case reflect.Bool: + b, err := strconv.ParseBool(src) + if err != nil { + return fmt.Errorf("failed to parse bool: %w", err) + } + v.SetBool(b) + default: + // Try JSON unmarshal as a fallback + return json.Unmarshal([]byte(src), dst) + } + return nil +} + +// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. +func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + newArray := reflect.MakeSlice(t, len(parts), len(parts)) + for i, p := range parts { + err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) + if err != nil { + return fmt.Errorf("error setting array element: %w", err) + } + } + v.Set(newArray) + return nil +} + +// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. +func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { + var fields []string + if explode { + fields = make([]string, len(parts)) + for i, property := range parts { + propertyParts := strings.Split(property, "=") + if len(propertyParts) != 2 { + return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) + } + fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" + } + } else { + if len(parts)%2 != 0 { + return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) + } + fields = make([]string, len(parts)/2) + for i := 0; i < len(parts); i += 2 { + key := parts[i] + value := parts[i+1] + fields[i/2] = "\"" + key + "\":\"" + value + "\"" + } + } + jsonParam := "{" + strings.Join(fields, ",") + "}" + return json.Unmarshal([]byte(jsonParam), dest) +} + +// BindFormExplodeParam binds a form-style parameter with explode to a destination. +// Form style is the default for query and cookie parameters. +// This handles the exploded case where arrays come as multiple query params. +// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) +// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) +func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { + dv := reflect.Indirect(reflect.ValueOf(dest)) + v := dv + var output interface{} + + if required { + output = dest + } else { + // For optional parameters, allocate if nil + if v.IsNil() { + t := v.Type() + newValue := reflect.New(t.Elem()) + output = newValue.Interface() + } else { + output = v.Interface() + } + v = reflect.Indirect(reflect.ValueOf(output)) + } + + t := v.Type() + k := t.Kind() + + values, found := queryParams[paramName] + + switch k { + case reflect.Slice: + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := bindSplitPartsToDestinationArray(values, output) + if err != nil { + return err + } + case reflect.Struct: + // For exploded objects, fields are spread across query params + fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) + if err != nil { + return err + } + if !fieldsPresent { + return nil + } + default: + // Primitive + if len(values) == 0 { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + if len(values) != 1 { + return fmt.Errorf("multiple values for single value parameter '%s'", paramName) + } + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := BindStringToObject(values[0], output) + if err != nil { + return err + } + } + + if !required { + dv.Set(reflect.ValueOf(output)) + } + return nil +} + +// bindParamsToExplodedObject binds query params to struct fields for exploded objects. +func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { + binder, v, t := indirectBinder(dest) + if binder != nil { + _, found := values[paramName] + if !found { + return false, nil + } + return true, BindStringToObject(values.Get(paramName), dest) + } + if t.Kind() != reflect.Struct { + return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) + } + + fieldsPresent := false + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + if !v.Field(i).CanSet() { + continue + } + + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + + fieldVal, found := values[fieldName] + if found { + if len(fieldVal) != 1 { + return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) + } + err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) + if err != nil { + return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) + } + fieldsPresent = true + } + } + return fieldsPresent, nil +} + +// indirectBinder checks if dest implements Binder and returns reflect values. +func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { + v := reflect.ValueOf(dest) + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Binder); ok { + return u, reflect.Value{}, nil + } + } + v = reflect.Indirect(v) + t := v.Type() + // Handle special types like time.Time and Date + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return dest, reflect.Value{}, nil + } + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + return dest, reflect.Value{}, nil + } + return nil, v, t +} + +// BindSimpleParam binds a simple-style parameter without explode to a destination. +// Simple style is the default for path and header parameters. +// Arrays: a,b,c -> []string{"a", "b", "c"} +// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} +func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + // Split on comma and bind as key,value pairs + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) + case reflect.Slice: + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} + +// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. +// Form style is the default for query and cookie parameters. +// Primitives: paramName=value +// Arrays: paramName=a¶mName=b¶mName=c +// Objects: key1=value1&key2=value2 +func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleFormExplodeSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleFormExplodeStruct(paramName, paramLocation, value) + case reflect.Map: + return styleFormExplodeMap(paramName, paramLocation, value) + default: + return styleFormExplodePrimitive(paramName, paramLocation, value) + } +} + +func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Form with explode: paramName=a¶mName=b¶mName=c + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, "&"+prefix), nil +} + +func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleFormExplodeParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. +// Simple style is the default for path and header parameters. +// Arrays are comma-separated: a,b,c +// Objects are key,value pairs: key1,value1,key2,value2 +func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return escapeParameterString(string(b), paramLocation), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleSimpleSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleSimpleStruct(paramName, paramLocation, value) + case reflect.Map: + return styleSimpleMap(paramName, paramLocation, value) + default: + return styleSimplePrimitive(paramLocation, value) + } +} + +func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return escapeParameterString(strVal, paramLocation), nil +} + +func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return escapeParameterString(timeVal, paramLocation), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleSimpleParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +// structToFieldDict converts a struct to a map of field names to string values. +func structToFieldDict(value interface{}) (map[string]string, error) { + v := reflect.ValueOf(value) + t := reflect.TypeOf(value) + fieldDict := make(map[string]string) + + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + f := v.Field(i) + + // Skip nil optional fields + if f.Type().Kind() == reflect.Ptr && f.IsNil() { + continue + } + str, err := primitiveToString(f.Interface()) + if err != nil { + return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) + } + fieldDict[fieldName] = str + } + return fieldDict, nil +} diff --git a/experimental/examples/petstore-expanded/models.config.yaml b/experimental/examples/petstore-expanded/models.config.yaml new file mode 100644 index 0000000000..47d99a649f --- /dev/null +++ b/experimental/examples/petstore-expanded/models.config.yaml @@ -0,0 +1,2 @@ +package: petstore +output: petstore.gen.go diff --git a/experimental/examples/petstore-expanded/petstore-expanded.yaml b/experimental/examples/petstore-expanded/petstore-expanded.yaml new file mode 100644 index 0000000000..f9f84f2854 --- /dev/null +++ b/experimental/examples/petstore-expanded/petstore-expanded.yaml @@ -0,0 +1,164 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification + termsOfService: https://swagger.io/terms/ + contact: + name: Swagger API Team + email: apiteam@swagger.io + url: https://swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html +servers: + - url: https://petstore.swagger.io/api +paths: + /pets: + get: + summary: Returns all pets + description: | + Returns all pets from the system that the user has access to + Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. + + Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. + operationId: findPets + parameters: + - name: tags + in: query + description: tags to filter by + required: false + style: form + schema: + type: array + items: + type: string + - name: limit + in: query + description: maximum number of results to return + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: pet response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + summary: Creates a new pet + description: Creates a new pet in the store. Duplicates are allowed + operationId: addPet + requestBody: + description: Pet to add to the store + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NewPet' + responses: + '200': + description: pet response + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /pets/{id}: + get: + summary: Returns a pet by ID + description: Returns a pet based on a single ID + operationId: findPetByID + parameters: + - name: id + in: path + description: ID of pet to fetch + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: pet response + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + summary: Deletes a pet by ID + description: deletes a single pet based on the ID supplied + operationId: deletePet + parameters: + - name: id + in: path + description: ID of pet to delete + required: true + schema: + type: integer + format: int64 + responses: + '204': + description: pet deleted + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Pet: + allOf: + - $ref: '#/components/schemas/NewPet' + - required: + - id + properties: + id: + type: integer + format: int64 + description: Unique id of the pet + + NewPet: + required: + - name + properties: + name: + type: string + description: Name of the pet + tag: + type: string + description: Type of the pet + + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + description: Error code + message: + type: string + description: Error message diff --git a/experimental/examples/petstore-expanded/petstore.gen.go b/experimental/examples/petstore-expanded/petstore.gen.go new file mode 100644 index 0000000000..4149df2b00 --- /dev/null +++ b/experimental/examples/petstore-expanded/petstore.gen.go @@ -0,0 +1,117 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package petstore + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/Pet +type Pet struct { + Name string `json:"name" form:"name"` // Name of the pet + Tag *string `json:"tag,omitempty" form:"tag,omitempty"` // Type of the pet + ID int64 `json:"id" form:"id"` // Unique id of the pet +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Pet) ApplyDefaults() { +} + +// #/components/schemas/NewPet +type NewPet struct { + Name string `json:"name" form:"name"` // Name of the pet + Tag *string `json:"tag,omitempty" form:"tag,omitempty"` // Type of the pet +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *NewPet) ApplyDefaults() { +} + +// #/components/schemas/Error +type Error struct { + Code int32 `json:"code" form:"code"` // Error code + Message string `json:"message" form:"message"` // Error message +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Error) ApplyDefaults() { +} + +// #/paths//pets/get/responses/200/content/application/json/schema +type FindPetsJSONResponse = []Pet + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/+xXXW9byQ19168gtgXyoly5yaIPemo2TgED3cStd/tOz6UkLubLQ44coe1/Lzj3Srqy", + "ZG+CYvuF9YMNz3A4hzyHHN6UKWLmJXzztrvqrr6ZcVyl5QxgS0U4xSX8ztZnAMrqaQl3j7heU4FbUtFU", + "aAbQk7jCWZv5OxAM2RO8u70B3aBCFRJAyOMBQAGMQJ8HM03QU0hRtKASrAi1FhLgCLoh+JQpmqe33RVI", + "Jscrdmg3GSIqQT6t7qhs2dESNqpZlouFDBA7TotmspgBuBQVnVpkABHDJBJz/wNhaFsUkP0SMLMShj8c", + "XbXdWvyla2YAnh1Foan/dxndhuBNy96Ts4+Pjx22/S6V9WI8LYs/3bz/8PHuw+s33VW30eBnQsWYML+v", + "T13s89lNwsXMs4y6afbNYgC0pjFyAKkhYNkt4S+ktUQB9L5xM+6fkPn3cRHOrGFVUmgMyU6UwkC1/V+F", + "CmyMZOdIBDQdnHzEAEK9kdFzoKg1AIl28D2So4gCSiGnAoJrVmUBwcwU5xDJQdmk6KqAUJgYsAIG0g7e", + "USSMgArrglvuEbCuK80BHTC66rkd7eB9LXjPWguknhP4VCjMIZWIhYDWpECeRnSR3BxcLVIFuAdPTqt0", + "cF1ZIDBoLZllDrn6LUcsdheVZLHPQTk67mtU2GLhKvBTFU0d3ETYoIONgUARguxRCaFnpzVYOm6ikikT", + "FbDnzOI4rgGjWjTH2D2vq8dD5HmDhbTgPolmDyF5EmUCDplKz5apv/IWwxAQen6oGKBntMwUFHiw2Lbk", + "WSGmCJqKpmIp4RXF/nB7B7cFSSiqwaTI4QigloiwTb5qRoUtRYpogIfk2q+AtZiPm3j0vKIyZn2Fjj3L", + "ySXtBvs1P/LrQFKPnozYfm55dFRQLTD728FdlUyxZ8uyRxNPn3wqc1OgkFMTdYuyScWinsOWNuyqR+Co", + "VPoawPM9ldTB96ncM1BlCamf0mDbTdgeHUfGbnaQ/B31jY8qsCKToE/3qbRjlI66KVVLDV2rkIDN7UgB", + "i58D1ZOaGYgHX02NptEObjco5P1QHpnKeLwlu5FMCiusju/rkHbc32N20/Nb8iOBvKVScH56tVULcD8/", + "lGPk+00HPypk8p6ikjxUgpykktXTvpS6lgrc14KV3j6je0/7sFo+5w3IQRyxRgdaWNRigS0rUgd/rOII", + "SFtP6CsfasH6hTjyVLjBGVS8PxBMMxWbhFwNghECri1k8iNbHfy5DkdD8sbbwB7VQUFHKPNDCwKszkpl", + "sBxFOoQ9SmRsNYeaNMkYwcBxfoQylm9k4T1gMQyOtfZsUEUQqu7VNhI53HSStHZfB7dTYlrmRoy5kHIN", + "k/41iKbOJyq3BtyNek7Z6opTvOmXsOLY3x4fjozFsjA+VsPP6/EpVFzLYRGA4xIeKpXdZO3k3TF7mwxW", + "7JUK3E8NCz1ULmQA0AtNdkR3NqGsUgnTVbehgMvJCoDuMi0BS8HdyTorBTk13RuLFo7rs8g8B9avCS3g", + "Zw72stRwTwXSCgpJ9driLe2V/bJgnw2Lh9fjZMdygtr23r6Z7X1LtjY4CfjVm6urV8vnoGfSw6GJjY1W", + "FPUUCubsx0lt8ZOk+DSpl+C/xMyz7NjPbwutlvDqNwuXQk6RospiuEAWt6SvZsdwVli9PhthjfQ527tg", + "D0hJ5ZeK8iXAH+ziAXJOcj63vS+E2ibqSI/GyaXB7cxoP08PIyNc1wG4mdhI7n16pP5SlWNvRT47ypFE", + "v0v9bjm7mMFbUhMy9r39Odw4Oxe0lnpcvpDdl3N7ObMv5fUjPU608F8p/v9PHbfvkMXfuP/HF3yMtDzf", + "7+Dm+pKsn5ihPaA2vYFwXHs6nrr0UH23O2y/9FZx/6Sd2wfVc4m+ubYOngfRr0jd5mLzPtH6v9C7f//t", + "r/L997fhnjwpnWn2ui3/rGb7g9ko0hPpWoe8uQapFsLlDjw4ODbhX0a8wy3/SfV++7J6B4D9/6KIjjt2", + "fNwcPN0emyF6/2k1pfTLXzOzPvB1gvD1qSRyMWkp05M5ivvLU+8lRi9zepGDH2P7WuPehGZSt3GlHRjw", + "L2dPtPZE0LPnUTe9z35mSn8C5yMGmiI5nMX117r6YZfpLKhG90sxudTT5N9AIrh+KUo7cA7tnJVL8/0Z", + "5gbvFMOI4GujHzzt4f8zAAD//6ZcwVZEFgAA", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/examples/petstore-expanded/stdhttp/Makefile b/experimental/examples/petstore-expanded/stdhttp/Makefile new file mode 100644 index 0000000000..42389f4137 --- /dev/null +++ b/experimental/examples/petstore-expanded/stdhttp/Makefile @@ -0,0 +1,35 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + +generate: + $(call execute-if-go-124,go generate ./...) + +test: + $(call execute-if-go-124,go test -cover ./...) + +tidy: + $(call execute-if-go-124,go mod tidy) + +tidy-ci: + $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/stdhttp/go.mod b/experimental/examples/petstore-expanded/stdhttp/go.mod new file mode 100644 index 0000000000..e211ad75fb --- /dev/null +++ b/experimental/examples/petstore-expanded/stdhttp/go.mod @@ -0,0 +1,7 @@ +module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/stdhttp + +go 1.24.0 + +require github.com/google/uuid v1.6.0 + +replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/stdhttp/go.sum b/experimental/examples/petstore-expanded/stdhttp/go.sum new file mode 100644 index 0000000000..7790d7c3e0 --- /dev/null +++ b/experimental/examples/petstore-expanded/stdhttp/go.sum @@ -0,0 +1,2 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/experimental/examples/petstore-expanded/stdhttp/main.go b/experimental/examples/petstore-expanded/stdhttp/main.go new file mode 100644 index 0000000000..9f438a128d --- /dev/null +++ b/experimental/examples/petstore-expanded/stdhttp/main.go @@ -0,0 +1,39 @@ +//go:build go1.22 + +// This is an example of implementing the Pet Store from the OpenAPI documentation +// found at: +// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml + +package main + +import ( + "flag" + "log" + "net" + "net/http" + + "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/stdhttp/server" +) + +func main() { + port := flag.String("port", "8080", "Port for test HTTP server") + flag.Parse() + + // Create an instance of our handler which satisfies the generated interface + petStore := server.NewPetStore() + + r := http.NewServeMux() + + // We now register our petStore above as the handler for the interface + server.HandlerFromMux(petStore, r) + + s := &http.Server{ + Handler: r, + Addr: net.JoinHostPort("0.0.0.0", *port), + } + + log.Printf("Server listening on %s", s.Addr) + + // And we serve HTTP until the world ends. + log.Fatal(s.ListenAndServe()) +} diff --git a/experimental/examples/petstore-expanded/stdhttp/server/petstore.go b/experimental/examples/petstore-expanded/stdhttp/server/petstore.go new file mode 100644 index 0000000000..f05a50f1e6 --- /dev/null +++ b/experimental/examples/petstore-expanded/stdhttp/server/petstore.go @@ -0,0 +1,163 @@ +//go:build go1.22 + +package server + +import ( + "encoding/json" + "fmt" + "net/http" + "sync" +) + +// Pet defines model for Pet. +type Pet struct { + // Id Unique id of the pet + Id int64 `json:"id"` + + // Name Name of the pet + Name string `json:"name"` + + // Tag Type of the pet + Tag *string `json:"tag,omitempty"` +} + +// NewPet defines model for NewPet. +type NewPet struct { + // Name Name of the pet + Name string `json:"name"` + + // Tag Type of the pet + Tag *string `json:"tag,omitempty"` +} + +// Error defines model for Error. +type Error struct { + // Code Error code + Code int32 `json:"code"` + + // Message Error message + Message string `json:"message"` +} + +// PetStore implements the ServerInterface. +type PetStore struct { + Pets map[int64]Pet + NextId int64 + Lock sync.Mutex +} + +// Make sure we conform to ServerInterface +var _ ServerInterface = (*PetStore)(nil) + +// NewPetStore creates a new PetStore. +func NewPetStore() *PetStore { + return &PetStore{ + Pets: make(map[int64]Pet), + NextId: 1000, + } +} + +// sendPetStoreError wraps sending of an error in the Error format. +func sendPetStoreError(w http.ResponseWriter, code int, message string) { + petErr := Error{ + Code: int32(code), + Message: message, + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _ = json.NewEncoder(w).Encode(petErr) +} + +// FindPets returns all pets, optionally filtered by tags and limited. +func (p *PetStore) FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) { + p.Lock.Lock() + defer p.Lock.Unlock() + + var result []Pet + + for _, pet := range p.Pets { + if params.Tags != nil { + // If we have tags, filter pets by tag + for _, t := range *params.Tags { + if pet.Tag != nil && (*pet.Tag == t) { + result = append(result, pet) + } + } + } else { + // Add all pets if we're not filtering + result = append(result, pet) + } + + if params.Limit != nil { + l := int(*params.Limit) + if len(result) >= l { + // We're at the limit + break + } + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(result) +} + +// AddPet creates a new pet. +func (p *PetStore) AddPet(w http.ResponseWriter, r *http.Request) { + // We expect a NewPet object in the request body. + var newPet NewPet + if err := json.NewDecoder(r.Body).Decode(&newPet); err != nil { + sendPetStoreError(w, http.StatusBadRequest, "Invalid format for NewPet") + return + } + + // We now have a pet, let's add it to our "database". + p.Lock.Lock() + defer p.Lock.Unlock() + + // We handle pets, not NewPets, which have an additional ID field + var pet Pet + pet.Name = newPet.Name + pet.Tag = newPet.Tag + pet.Id = p.NextId + p.NextId++ + + // Insert into map + p.Pets[pet.Id] = pet + + // Now, we have to return the Pet + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(pet) +} + +// FindPetByID returns a pet by ID. +func (p *PetStore) FindPetByID(w http.ResponseWriter, r *http.Request, id int64) { + p.Lock.Lock() + defer p.Lock.Unlock() + + pet, found := p.Pets[id] + if !found { + sendPetStoreError(w, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(pet) +} + +// DeletePet deletes a pet by ID. +func (p *PetStore) DeletePet(w http.ResponseWriter, r *http.Request, id int64) { + p.Lock.Lock() + defer p.Lock.Unlock() + + _, found := p.Pets[id] + if !found { + sendPetStoreError(w, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) + return + } + delete(p.Pets, id) + + w.WriteHeader(http.StatusNoContent) +} diff --git a/experimental/examples/petstore-expanded/stdhttp/server/server.config.yaml b/experimental/examples/petstore-expanded/stdhttp/server/server.config.yaml new file mode 100644 index 0000000000..4bb9655ead --- /dev/null +++ b/experimental/examples/petstore-expanded/stdhttp/server/server.config.yaml @@ -0,0 +1,7 @@ +package: server +output: server.gen.go +generation: + server: std-http + models-package: + path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded + alias: petstore diff --git a/experimental/examples/petstore-expanded/stdhttp/server/server.gen.go b/experimental/examples/petstore-expanded/stdhttp/server/server.gen.go new file mode 100644 index 0000000000..a65e2e57e1 --- /dev/null +++ b/experimental/examples/petstore-expanded/stdhttp/server/server.gen.go @@ -0,0 +1,1009 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package server + +import ( + "bytes" + "encoding" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "reflect" + "sort" + "strconv" + "strings" + "time" + + "github.com/google/uuid" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Returns all pets + // (GET /pets) + FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) + // Creates a new pet + // (POST /pets) + AddPet(w http.ResponseWriter, r *http.Request) + // Deletes a pet by ID + // (DELETE /pets/{id}) + DeletePet(w http.ResponseWriter, r *http.Request, id int64) + // Returns a pet by ID + // (GET /pets/{id}) + FindPetByID(w http.ResponseWriter, r *http.Request, id int64) +} + +// FindPetsParams defines parameters for FindPets. +type FindPetsParams struct { + // tags (optional) + Tags *[]string `form:"tags" json:"tags"` + // limit (optional) + Limit *int32 `form:"limit" json:"limit"` +} + +// ServerInterfaceWrapper converts HTTP requests to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// MiddlewareFunc is a middleware function type. +type MiddlewareFunc func(http.Handler) http.Handler + +// FindPets operation middleware +func (siw *ServerInterfaceWrapper) FindPets(w http.ResponseWriter, r *http.Request) { + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params FindPetsParams + + // ------------- Optional query parameter "tags" ------------- + err = BindFormExplodeParam("tags", false, r.URL.Query(), ¶ms.Tags) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tags", Err: err}) + return + } + + // ------------- Optional query parameter "limit" ------------- + err = BindFormExplodeParam("limit", false, r.URL.Query(), ¶ms.Limit) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.FindPets(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// AddPet operation middleware +func (siw *ServerInterfaceWrapper) AddPet(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.AddPet(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// DeletePet operation middleware +func (siw *ServerInterfaceWrapper) DeletePet(w http.ResponseWriter, r *http.Request) { + var err error + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, r.PathValue("id"), &id) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeletePet(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// FindPetByID operation middleware +func (siw *ServerInterfaceWrapper) FindPetByID(w http.ResponseWriter, r *http.Request) { + var err error + + // ------------- Path parameter "id" ------------- + var id int64 + + err = BindSimpleParam("id", ParamLocationPath, r.PathValue("id"), &id) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.FindPetByID(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// 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)) + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + +// StdHTTPServerOptions configures the StdHTTP server. +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, + }) +} + +// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. +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) + } + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + m.HandleFunc("GET "+options.BaseURL+"/pets", wrapper.FindPets) + m.HandleFunc("POST "+options.BaseURL+"/pets", wrapper.AddPet) + m.HandleFunc("DELETE "+options.BaseURL+"/pets/{id}", wrapper.DeletePet) + m.HandleFunc("GET "+options.BaseURL+"/pets/{id}", wrapper.FindPetByID) + return m +} + +// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. +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 +} + +// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. +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 +} + +// RequiredParamError is returned when a required parameter is missing. +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +// RequiredHeaderError is returned when a required header is missing. +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 +} + +// InvalidParamFormatError is returned when a parameter has an invalid format. +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 +} + +// TooManyValuesForParamError is returned when a parameter has too many values. +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) +} + +// ParamLocation indicates where a parameter is located in an HTTP request. +type ParamLocation int + +const ( + ParamLocationUndefined ParamLocation = iota + ParamLocationQuery + ParamLocationPath + ParamLocationHeader + ParamLocationCookie +) + +// Binder is an interface for types that can bind themselves from a string value. +type Binder interface { + Bind(value string) error +} + +// DateFormat is the format used for date (without time) parameters. +const DateFormat = "2006-01-02" + +// Date represents a date (without time) for OpenAPI date format. +type Date struct { + time.Time +} + +// UnmarshalText implements encoding.TextUnmarshaler for Date. +func (d *Date) UnmarshalText(data []byte) error { + t, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = t + return nil +} + +// MarshalText implements encoding.TextMarshaler for Date. +func (d Date) MarshalText() ([]byte, error) { + return []byte(d.Format(DateFormat)), nil +} + +// Format returns the date formatted according to layout. +func (d Date) Format(layout string) string { + return d.Time.Format(layout) +} + +// primitiveToString converts a primitive value to a string representation. +// It handles basic Go types, time.Time, types.Date, and types that implement +// json.Marshaler or fmt.Stringer. +func primitiveToString(value interface{}) (string, error) { + // Check for known types first (time, date, uuid) + if res, ok := marshalKnownTypes(value); ok { + return res, nil + } + + // Dereference pointers for optional values + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + kind := t.Kind() + + switch kind { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + case reflect.Float32: + return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil + case reflect.Bool: + if v.Bool() { + return "true", nil + } + return "false", nil + case reflect.String: + return v.String(), nil + case reflect.Struct: + // Check if it's a UUID + if u, ok := value.(uuid.UUID); ok { + return u.String(), nil + } + // Check if it implements json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + var i2 interface{} + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to decode JSON: %w", err) + } + return primitiveToString(i2) + } + fallthrough + default: + if s, ok := value.(fmt.Stringer); ok { + return s.String(), nil + } + return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) + } +} + +// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. +func marshalKnownTypes(value interface{}) (string, bool) { + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + tt := v.Convert(reflect.TypeOf(time.Time{})) + timeVal := tt.Interface().(time.Time) + return timeVal.Format(time.RFC3339Nano), true + } + + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + d := v.Convert(reflect.TypeOf(Date{})) + dateVal := d.Interface().(Date) + return dateVal.Format(DateFormat), true + } + + if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { + u := v.Convert(reflect.TypeOf(uuid.UUID{})) + uuidVal := u.Interface().(uuid.UUID) + return uuidVal.String(), true + } + + return "", false +} + +// escapeParameterString escapes a parameter value based on its location. +// Query and path parameters need URL escaping; headers and cookies do not. +func escapeParameterString(value string, paramLocation ParamLocation) string { + switch paramLocation { + case ParamLocationQuery: + return url.QueryEscape(value) + case ParamLocationPath: + return url.PathEscape(value) + default: + return value + } +} + +// unescapeParameterString unescapes a parameter value based on its location. +func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { + switch paramLocation { + case ParamLocationQuery, ParamLocationUndefined: + return url.QueryUnescape(value) + case ParamLocationPath: + return url.PathUnescape(value) + default: + return value, nil + } +} + +// sortedKeys returns the keys of a map in sorted order. +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// BindStringToObject binds a string value to a destination object. +// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. +func BindStringToObject(src string, dst interface{}) error { + // Check for TextUnmarshaler + if tu, ok := dst.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(src)) + } + + // Check for Binder interface + if b, ok := dst.(Binder); ok { + return b.Bind(src) + } + + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Ptr { + return fmt.Errorf("dst must be a pointer, got %T", dst) + } + v = v.Elem() + + switch v.Kind() { + case reflect.String: + v.SetString(src) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + v.SetInt(i) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + u, err := strconv.ParseUint(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse uint: %w", err) + } + v.SetUint(u) + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(src, 64) + if err != nil { + return fmt.Errorf("failed to parse float: %w", err) + } + v.SetFloat(f) + case reflect.Bool: + b, err := strconv.ParseBool(src) + if err != nil { + return fmt.Errorf("failed to parse bool: %w", err) + } + v.SetBool(b) + default: + // Try JSON unmarshal as a fallback + return json.Unmarshal([]byte(src), dst) + } + return nil +} + +// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. +func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + newArray := reflect.MakeSlice(t, len(parts), len(parts)) + for i, p := range parts { + err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) + if err != nil { + return fmt.Errorf("error setting array element: %w", err) + } + } + v.Set(newArray) + return nil +} + +// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. +func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { + var fields []string + if explode { + fields = make([]string, len(parts)) + for i, property := range parts { + propertyParts := strings.Split(property, "=") + if len(propertyParts) != 2 { + return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) + } + fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" + } + } else { + if len(parts)%2 != 0 { + return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) + } + fields = make([]string, len(parts)/2) + for i := 0; i < len(parts); i += 2 { + key := parts[i] + value := parts[i+1] + fields[i/2] = "\"" + key + "\":\"" + value + "\"" + } + } + jsonParam := "{" + strings.Join(fields, ",") + "}" + return json.Unmarshal([]byte(jsonParam), dest) +} + +// BindFormExplodeParam binds a form-style parameter with explode to a destination. +// Form style is the default for query and cookie parameters. +// This handles the exploded case where arrays come as multiple query params. +// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) +// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) +func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { + dv := reflect.Indirect(reflect.ValueOf(dest)) + v := dv + var output interface{} + + if required { + output = dest + } else { + // For optional parameters, allocate if nil + if v.IsNil() { + t := v.Type() + newValue := reflect.New(t.Elem()) + output = newValue.Interface() + } else { + output = v.Interface() + } + v = reflect.Indirect(reflect.ValueOf(output)) + } + + t := v.Type() + k := t.Kind() + + values, found := queryParams[paramName] + + switch k { + case reflect.Slice: + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := bindSplitPartsToDestinationArray(values, output) + if err != nil { + return err + } + case reflect.Struct: + // For exploded objects, fields are spread across query params + fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) + if err != nil { + return err + } + if !fieldsPresent { + return nil + } + default: + // Primitive + if len(values) == 0 { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + if len(values) != 1 { + return fmt.Errorf("multiple values for single value parameter '%s'", paramName) + } + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := BindStringToObject(values[0], output) + if err != nil { + return err + } + } + + if !required { + dv.Set(reflect.ValueOf(output)) + } + return nil +} + +// bindParamsToExplodedObject binds query params to struct fields for exploded objects. +func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { + binder, v, t := indirectBinder(dest) + if binder != nil { + _, found := values[paramName] + if !found { + return false, nil + } + return true, BindStringToObject(values.Get(paramName), dest) + } + if t.Kind() != reflect.Struct { + return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) + } + + fieldsPresent := false + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + if !v.Field(i).CanSet() { + continue + } + + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + + fieldVal, found := values[fieldName] + if found { + if len(fieldVal) != 1 { + return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) + } + err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) + if err != nil { + return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) + } + fieldsPresent = true + } + } + return fieldsPresent, nil +} + +// indirectBinder checks if dest implements Binder and returns reflect values. +func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { + v := reflect.ValueOf(dest) + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Binder); ok { + return u, reflect.Value{}, nil + } + } + v = reflect.Indirect(v) + t := v.Type() + // Handle special types like time.Time and Date + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return dest, reflect.Value{}, nil + } + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + return dest, reflect.Value{}, nil + } + return nil, v, t +} + +// BindSimpleParam binds a simple-style parameter without explode to a destination. +// Simple style is the default for path and header parameters. +// Arrays: a,b,c -> []string{"a", "b", "c"} +// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} +func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + // Split on comma and bind as key,value pairs + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) + case reflect.Slice: + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} + +// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. +// Form style is the default for query and cookie parameters. +// Primitives: paramName=value +// Arrays: paramName=a¶mName=b¶mName=c +// Objects: key1=value1&key2=value2 +func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleFormExplodeSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleFormExplodeStruct(paramName, paramLocation, value) + case reflect.Map: + return styleFormExplodeMap(paramName, paramLocation, value) + default: + return styleFormExplodePrimitive(paramName, paramLocation, value) + } +} + +func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Form with explode: paramName=a¶mName=b¶mName=c + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, "&"+prefix), nil +} + +func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleFormExplodeParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. +// Simple style is the default for path and header parameters. +// Arrays are comma-separated: a,b,c +// Objects are key,value pairs: key1,value1,key2,value2 +func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return escapeParameterString(string(b), paramLocation), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleSimpleSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleSimpleStruct(paramName, paramLocation, value) + case reflect.Map: + return styleSimpleMap(paramName, paramLocation, value) + default: + return styleSimplePrimitive(paramLocation, value) + } +} + +func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return escapeParameterString(strVal, paramLocation), nil +} + +func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return escapeParameterString(timeVal, paramLocation), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleSimpleParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +// structToFieldDict converts a struct to a map of field names to string values. +func structToFieldDict(value interface{}) (map[string]string, error) { + v := reflect.ValueOf(value) + t := reflect.TypeOf(value) + fieldDict := make(map[string]string) + + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + f := v.Field(i) + + // Skip nil optional fields + if f.Type().Kind() == reflect.Ptr && f.IsNil() { + continue + } + str, err := primitiveToString(f.Interface()) + if err != nil { + return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) + } + fieldDict[fieldName] = str + } + return fieldDict, nil +} diff --git a/experimental/examples/webhook/client/main.go b/experimental/examples/webhook/client/main.go new file mode 100644 index 0000000000..2d12c19acc --- /dev/null +++ b/experimental/examples/webhook/client/main.go @@ -0,0 +1,150 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "flag" + "fmt" + "log" + "net" + "net/http" + "time" + + "github.com/google/uuid" + + doorbadge "github.com/oapi-codegen/oapi-codegen/experimental/examples/webhook" +) + +// WebhookReceiver implements doorbadge.WebhookReceiverInterface. +type WebhookReceiver struct{} + +var _ doorbadge.WebhookReceiverInterface = (*WebhookReceiver)(nil) + +func (wr *WebhookReceiver) HandleEnterEventWebhook(w http.ResponseWriter, r *http.Request) { + var person doorbadge.Person + if err := json.NewDecoder(r.Body).Decode(&person); err != nil { + log.Printf("Error decoding enter event: %v", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + log.Printf("ENTER: %s", person.Name) + w.WriteHeader(http.StatusOK) +} + +func (wr *WebhookReceiver) HandleExitEventWebhook(w http.ResponseWriter, r *http.Request) { + var person doorbadge.Person + if err := json.NewDecoder(r.Body).Decode(&person); err != nil { + log.Printf("Error decoding exit event: %v", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + log.Printf("EXIT: %s", person.Name) + w.WriteHeader(http.StatusOK) +} + +func registerWebhook(client *http.Client, serverAddr, kind, url string) (uuid.UUID, error) { + body, err := json.Marshal(doorbadge.WebhookRegistration{URL: url}) + if err != nil { + return uuid.UUID{}, err + } + + resp, err := client.Post( + serverAddr+"/api/webhook/"+kind, + "application/json", + bytes.NewReader(body), + ) + if err != nil { + return uuid.UUID{}, err + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusCreated { + return uuid.UUID{}, fmt.Errorf("unexpected status %d", resp.StatusCode) + } + + var regResp doorbadge.WebhookRegistrationResponse + if err := json.NewDecoder(resp.Body).Decode(®Resp); err != nil { + return uuid.UUID{}, err + } + return regResp.ID, nil +} + +func deregisterWebhook(client *http.Client, serverAddr string, id uuid.UUID) error { + req, err := http.NewRequest(http.MethodDelete, serverAddr+"/api/webhook/"+id.String(), nil) + if err != nil { + return err + } + resp, err := client.Do(req) + if err != nil { + return err + } + _ = resp.Body.Close() + return nil +} + +func main() { + serverAddr := flag.String("server", "http://localhost:8080", "Badge reader server address") + duration := flag.Duration("duration", 30*time.Second, "How long to listen for events") + flag.Parse() + + // Start the webhook receiver on an ephemeral port. + receiver := &WebhookReceiver{} + + mux := http.NewServeMux() + mux.Handle("POST /enter", doorbadge.EnterEventWebhookHandler(receiver, nil)) + mux.Handle("POST /exit", doorbadge.ExitEventWebhookHandler(receiver, nil)) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + log.Fatalf("Failed to listen: %v", err) + } + callbackPort := listener.Addr().(*net.TCPAddr).Port + baseURL := fmt.Sprintf("http://localhost:%d", callbackPort) + log.Printf("Webhook receiver listening on port %d", callbackPort) + + srv := &http.Server{Handler: mux} + go func() { + if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { + log.Printf("Webhook server stopped: %v", err) + } + }() + + // Register webhooks for both event kinds. + client := &http.Client{} + + kinds := [2]string{"enterEvent", "exitEvent"} + urls := [2]string{baseURL + "/enter", baseURL + "/exit"} + var registrationIDs [2]uuid.UUID + + for i, kind := range kinds { + id, err := registerWebhook(client, *serverAddr, kind, urls[i]) + if err != nil { + log.Fatalf("Failed to register %s webhook: %v", kind, err) + } + registrationIDs[i] = id + log.Printf("Registered %s webhook: id=%s url=%s", kind, id, urls[i]) + } + + log.Printf("Listening for events for %s...", *duration) + + // Wait for the specified duration. + time.Sleep(*duration) + + // Deregister webhooks cleanly. + log.Printf("Duration elapsed, deregistering webhooks...") + for i, id := range registrationIDs { + if err := deregisterWebhook(client, *serverAddr, id); err != nil { + log.Printf("Failed to deregister %s webhook: %v", kinds[i], err) + continue + } + log.Printf("Deregistered %s webhook: id=%s", kinds[i], id) + } + + // Shut down the local webhook server. + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + _ = srv.Shutdown(ctx) + + log.Printf("Done!") +} diff --git a/experimental/examples/webhook/config.yaml b/experimental/examples/webhook/config.yaml new file mode 100644 index 0000000000..6a95d87ee1 --- /dev/null +++ b/experimental/examples/webhook/config.yaml @@ -0,0 +1,6 @@ +package: doorbadge +output: doorbadge.gen.go +generation: + webhook-initiator: true + webhook-receiver: true + server: std-http diff --git a/experimental/examples/webhook/doc.go b/experimental/examples/webhook/doc.go new file mode 100644 index 0000000000..ee6d3b6920 --- /dev/null +++ b/experimental/examples/webhook/doc.go @@ -0,0 +1,13 @@ +//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config config.yaml door-badge-reader.yaml + +// Package doorbadge provides an example of OpenAPI 3.1 webhooks. +// A door badge reader server generates random enter/exit events and +// notifies registered webhook listeners. +// +// You can run the example by running these two commands in parallel: +// +// go run ./server --port 8080 +// go run ./client --server http://localhost:8080 +// +// You can run multiple clients and they will all get the notifications +package doorbadge diff --git a/experimental/examples/webhook/door-badge-reader.yaml b/experimental/examples/webhook/door-badge-reader.yaml new file mode 100644 index 0000000000..ac37ce40dd --- /dev/null +++ b/experimental/examples/webhook/door-badge-reader.yaml @@ -0,0 +1,139 @@ +openapi: "3.1.0" +info: + version: 1.0.0 + title: Door Badge Reader + description: | + A door badge reader service that demonstrates OpenAPI 3.1 webhooks. + Clients register for enterEvent and exitEvent webhooks. The server + randomly generates badge events and notifies all registered listeners. + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html +paths: + /api/webhook/{kind}: + post: + summary: Register a webhook + description: | + Registers a webhook for the given event kind. The server will POST + to the provided URL whenever an event of that kind occurs. + operationId: RegisterWebhook + parameters: + - name: kind + in: path + required: true + schema: + type: string + enum: + - enterEvent + - exitEvent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookRegistration' + responses: + '201': + description: Webhook registered + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookRegistrationResponse' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /api/webhook/{id}: + delete: + summary: Deregister a webhook + description: Removes a previously registered webhook by its ID. + operationId: DeregisterWebhook + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '204': + description: Webhook deregistered + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +webhooks: + enterEvent: + post: + summary: Person entered the building + operationId: EnterEvent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Person' + responses: + '200': + description: Event received + exitEvent: + post: + summary: Person exited the building + operationId: ExitEvent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Person' + responses: + '200': + description: Event received +components: + schemas: + WebhookRegistration: + type: object + required: + - url + properties: + url: + type: string + format: uri + description: URL to receive webhook events + + WebhookRegistrationResponse: + type: object + required: + - id + properties: + id: + type: string + format: uuid + description: Unique identifier for this webhook registration + + Person: + type: object + required: + - name + properties: + name: + type: string + description: Name of the person who badged in or out + + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + description: Error code + message: + type: string + description: Error message diff --git a/experimental/examples/webhook/doorbadge.gen.go b/experimental/examples/webhook/doorbadge.gen.go new file mode 100644 index 0000000000..702d74db62 --- /dev/null +++ b/experimental/examples/webhook/doorbadge.gen.go @@ -0,0 +1,1069 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package doorbadge + +import ( + "bytes" + "compress/gzip" + "context" + "encoding" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/google/uuid" +) + +// #/components/schemas/WebhookRegistration +type WebhookRegistration struct { + URL string `json:"url" form:"url"` // URL to receive webhook events +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *WebhookRegistration) ApplyDefaults() { +} + +// #/components/schemas/WebhookRegistrationResponse +type WebhookRegistrationResponse struct { + ID UUID `json:"id" form:"id"` // Unique identifier for this webhook registration +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *WebhookRegistrationResponse) ApplyDefaults() { +} + +// #/components/schemas/Person +type Person struct { + Name string `json:"name" form:"name"` // Name of the person who badged in or out +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Person) ApplyDefaults() { +} + +// #/components/schemas/Error +type Error struct { + Code int32 `json:"code" form:"code"` // Error code + Message string `json:"message" form:"message"` // Error message +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Error) ApplyDefaults() { +} + +// #/paths//api/webhook/{kind}/post/parameters/0/schema +type PostAPIWebhookKindParameter string + +const ( + PostAPIWebhookKindParameter_enterEvent PostAPIWebhookKindParameter = "enterEvent" + PostAPIWebhookKindParameter_exitEvent PostAPIWebhookKindParameter = "exitEvent" +) + +type UUID = uuid.UUID + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/9RWTY/bNhC961cM0gI+RfLu9qTbJuvDAkGycFL0TItjaxKJZIaUvEbb/16Q1GdX3l0D", + "RZv6ZEnDmfceh4+jDSphKIc3N+lVun6TkNrrPAFokS1plcNVuk7XCYAjV2EOd1ozvBPygLBFIZETAIm2", + "YDIuxP+RAADcgvSBuxDIIRAscksFgiuFA4m1VtaxcGjhk0F1+3APN+kVHHFXav3NpiHP+4pQOQuMB7IO", + "GfaaAZVD3rSoHAglAR/JxadhLXwpMdQL+ABYKKnr6gQHVBhrRmjYhvQ+jdKO9oQWRFUN9VBC5f8o5ICo", + "ogKVxTxkVaLGHG6NKEqE66ASQMNVDqVzxuZZdjweUxG+p5oPWbfaZh/u328+ft68vU7XaenqKjHCldZn", + "zYShrOOR/f6NlPwzFjPauvgPwDZ1LfiUw7aXRfTcu4iFLfG/Pt6OC4KirkQ4UIsqCgK+7lREOFJVwcOn", + "z1+GVE6HVYZ1SxIl/Lr9AMcSFfpw0SfS+7jdPiHoomi421gAbfxOkFb3ciTy24yFESxq9Hjzoe7bTnaf", + "cXgJQCoHL+LkFeP3hhhlDo4bnHywRYm1yCdvANzJYA7WManD7AOqpp6HegxjCz791LdjMsJA695peRrz", + "nMFWaOVQuWk9YUxFRRAq+2q1mmNZogLwM+M+h9VPWaFro5Vv8SxG2qxTOAoeN2A1ILVG+/4c862u11er", + "afpZY3W5JqdlErnA5SU25/hczGjbMVklI+69aCp3lkqj8NFg4VACMmv+L4hsfOHVExOg3gIkVujwiQnc", + "Ib/KBrZY69b7GxjGlnRjq9PU6HpD2J2AnIX7u8WTOlZ7/Vmlf+Wk7jXXwuXQNEO95Y7+5eWOlrjQ0/+j", + "HupvQr949KpnbpIHZKtVjEUZrH3XUCVHkWddsPm7//1gLhfpPG9s6/NtEMcJxgKpDQ0wmPorJHwk9xoF", + "f+xr4p8VcCzgV3Q14uIF9+6zxpOud1+xmKoUBJl4TMNV70HsJXY0xennseRF9xi8g+kcJT/gON1zGtwy", + "TpDJOS79TXQhp8HBliiRvIRRM3PfOSVF3xsEkqjC7MvdMEh2oMcTLpFk7IwL+fh74BlG4Zp4mdMM/EdR", + "Y5wvEUw8esdSx7leAinQDLpxEXSwxQsxF1ri5LFGa8XhORZ+wVMWpBwekBe2hpS7uT57gjziOYYOwaVC", + "xUw9/L8CAAD//+XREy3yDQAA", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Deregister a webhook + // (DELETE /api/webhook/{id}) + DeregisterWebhook(w http.ResponseWriter, r *http.Request, id uuid.UUID) + // Register a webhook + // (POST /api/webhook/{kind}) + RegisterWebhook(w http.ResponseWriter, r *http.Request, kind string) +} + +// ServerInterfaceWrapper converts HTTP requests to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// MiddlewareFunc is a middleware function type. +type MiddlewareFunc func(http.Handler) http.Handler + +// DeregisterWebhook operation middleware +func (siw *ServerInterfaceWrapper) DeregisterWebhook(w http.ResponseWriter, r *http.Request) { + var err error + + // ------------- Path parameter "id" ------------- + var id uuid.UUID + + err = BindSimpleParam("id", ParamLocationPath, r.PathValue("id"), &id) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeregisterWebhook(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// RegisterWebhook operation middleware +func (siw *ServerInterfaceWrapper) RegisterWebhook(w http.ResponseWriter, r *http.Request) { + var err error + + // ------------- Path parameter "kind" ------------- + var kind string + + err = BindSimpleParam("kind", ParamLocationPath, r.PathValue("kind"), &kind) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "kind", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.RegisterWebhook(w, r, kind) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// 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)) + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + +// StdHTTPServerOptions configures the StdHTTP server. +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, + }) +} + +// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. +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) + } + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + m.HandleFunc("DELETE "+options.BaseURL+"/api/webhook/{id}", wrapper.DeregisterWebhook) + m.HandleFunc("POST "+options.BaseURL+"/api/webhook/{kind}", wrapper.RegisterWebhook) + return m +} + +// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. +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 +} + +// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. +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 +} + +// RequiredParamError is returned when a required parameter is missing. +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +// RequiredHeaderError is returned when a required header is missing. +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 +} + +// InvalidParamFormatError is returned when a parameter has an invalid format. +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 +} + +// TooManyValuesForParamError is returned when a parameter has too many values. +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) +} + +// ParamLocation indicates where a parameter is located in an HTTP request. +type ParamLocation int + +const ( + ParamLocationUndefined ParamLocation = iota + ParamLocationQuery + ParamLocationPath + ParamLocationHeader + ParamLocationCookie +) + +// Binder is an interface for types that can bind themselves from a string value. +type Binder interface { + Bind(value string) error +} + +// DateFormat is the format used for date (without time) parameters. +const DateFormat = "2006-01-02" + +// Date represents a date (without time) for OpenAPI date format. +type Date struct { + time.Time +} + +// UnmarshalText implements encoding.TextUnmarshaler for Date. +func (d *Date) UnmarshalText(data []byte) error { + t, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = t + return nil +} + +// MarshalText implements encoding.TextMarshaler for Date. +func (d Date) MarshalText() ([]byte, error) { + return []byte(d.Format(DateFormat)), nil +} + +// Format returns the date formatted according to layout. +func (d Date) Format(layout string) string { + return d.Time.Format(layout) +} + +// primitiveToString converts a primitive value to a string representation. +// It handles basic Go types, time.Time, types.Date, and types that implement +// json.Marshaler or fmt.Stringer. +func primitiveToString(value interface{}) (string, error) { + // Check for known types first (time, date, uuid) + if res, ok := marshalKnownTypes(value); ok { + return res, nil + } + + // Dereference pointers for optional values + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + kind := t.Kind() + + switch kind { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + case reflect.Float32: + return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil + case reflect.Bool: + if v.Bool() { + return "true", nil + } + return "false", nil + case reflect.String: + return v.String(), nil + case reflect.Struct: + // Check if it's a UUID + if u, ok := value.(uuid.UUID); ok { + return u.String(), nil + } + // Check if it implements json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + var i2 interface{} + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to decode JSON: %w", err) + } + return primitiveToString(i2) + } + fallthrough + default: + if s, ok := value.(fmt.Stringer); ok { + return s.String(), nil + } + return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) + } +} + +// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. +func marshalKnownTypes(value interface{}) (string, bool) { + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + tt := v.Convert(reflect.TypeOf(time.Time{})) + timeVal := tt.Interface().(time.Time) + return timeVal.Format(time.RFC3339Nano), true + } + + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + d := v.Convert(reflect.TypeOf(Date{})) + dateVal := d.Interface().(Date) + return dateVal.Format(DateFormat), true + } + + if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { + u := v.Convert(reflect.TypeOf(uuid.UUID{})) + uuidVal := u.Interface().(uuid.UUID) + return uuidVal.String(), true + } + + return "", false +} + +// escapeParameterString escapes a parameter value based on its location. +// Query and path parameters need URL escaping; headers and cookies do not. +func escapeParameterString(value string, paramLocation ParamLocation) string { + switch paramLocation { + case ParamLocationQuery: + return url.QueryEscape(value) + case ParamLocationPath: + return url.PathEscape(value) + default: + return value + } +} + +// unescapeParameterString unescapes a parameter value based on its location. +func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { + switch paramLocation { + case ParamLocationQuery, ParamLocationUndefined: + return url.QueryUnescape(value) + case ParamLocationPath: + return url.PathUnescape(value) + default: + return value, nil + } +} + +// sortedKeys returns the keys of a map in sorted order. +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// BindStringToObject binds a string value to a destination object. +// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. +func BindStringToObject(src string, dst interface{}) error { + // Check for TextUnmarshaler + if tu, ok := dst.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(src)) + } + + // Check for Binder interface + if b, ok := dst.(Binder); ok { + return b.Bind(src) + } + + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Ptr { + return fmt.Errorf("dst must be a pointer, got %T", dst) + } + v = v.Elem() + + switch v.Kind() { + case reflect.String: + v.SetString(src) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + v.SetInt(i) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + u, err := strconv.ParseUint(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse uint: %w", err) + } + v.SetUint(u) + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(src, 64) + if err != nil { + return fmt.Errorf("failed to parse float: %w", err) + } + v.SetFloat(f) + case reflect.Bool: + b, err := strconv.ParseBool(src) + if err != nil { + return fmt.Errorf("failed to parse bool: %w", err) + } + v.SetBool(b) + default: + // Try JSON unmarshal as a fallback + return json.Unmarshal([]byte(src), dst) + } + return nil +} + +// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. +func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + newArray := reflect.MakeSlice(t, len(parts), len(parts)) + for i, p := range parts { + err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) + if err != nil { + return fmt.Errorf("error setting array element: %w", err) + } + } + v.Set(newArray) + return nil +} + +// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. +func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { + var fields []string + if explode { + fields = make([]string, len(parts)) + for i, property := range parts { + propertyParts := strings.Split(property, "=") + if len(propertyParts) != 2 { + return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) + } + fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" + } + } else { + if len(parts)%2 != 0 { + return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) + } + fields = make([]string, len(parts)/2) + for i := 0; i < len(parts); i += 2 { + key := parts[i] + value := parts[i+1] + fields[i/2] = "\"" + key + "\":\"" + value + "\"" + } + } + jsonParam := "{" + strings.Join(fields, ",") + "}" + return json.Unmarshal([]byte(jsonParam), dest) +} + +// BindSimpleParam binds a simple-style parameter without explode to a destination. +// Simple style is the default for path and header parameters. +// Arrays: a,b,c -> []string{"a", "b", "c"} +// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} +func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + // Split on comma and bind as key,value pairs + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) + case reflect.Slice: + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} + +// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. +// Simple style is the default for path and header parameters. +// Arrays are comma-separated: a,b,c +// Objects are key,value pairs: key1,value1,key2,value2 +func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return escapeParameterString(string(b), paramLocation), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleSimpleSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleSimpleStruct(paramName, paramLocation, value) + case reflect.Map: + return styleSimpleMap(paramName, paramLocation, value) + default: + return styleSimplePrimitive(paramLocation, value) + } +} + +func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return escapeParameterString(strVal, paramLocation), nil +} + +func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return escapeParameterString(timeVal, paramLocation), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleSimpleParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +// structToFieldDict converts a struct to a map of field names to string values. +func structToFieldDict(value interface{}) (map[string]string, error) { + v := reflect.ValueOf(value) + t := reflect.TypeOf(value) + fieldDict := make(map[string]string) + + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + f := v.Field(i) + + // Skip nil optional fields + if f.Type().Kind() == reflect.Ptr && f.IsNil() { + continue + } + str, err := primitiveToString(f.Interface()) + if err != nil { + return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) + } + fieldDict[fieldName] = str + } + return fieldDict, nil +} + +type EnterEventJSONRequestBody = Person + +type ExitEventJSONRequestBody = Person + +// RequestEditorFn is the function signature for the RequestEditor callback function. +// It may already be defined if client code is also generated; this is a compatible redeclaration. +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// HttpRequestDoer performs HTTP requests. +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// WebhookInitiator sends webhook requests to target URLs. +// Unlike Client, it has no stored base URL — the full target URL is provided per-call. +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 request. This can be used to mutate the request. +func WithWebhookRequestEditorFn(fn RequestEditorFn) WebhookInitiatorOption { + return func(p *WebhookInitiator) error { + p.RequestEditors = append(p.RequestEditors, fn) + return nil + } +} + +func (p *WebhookInitiator) applyEditors(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 { + // EnterEventWithBody sends a POST webhook request + EnterEventWithBody(ctx context.Context, targetURL string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + EnterEvent(ctx context.Context, targetURL string, body EnterEventJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ExitEventWithBody sends a POST webhook request + ExitEventWithBody(ctx context.Context, targetURL string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + ExitEvent(ctx context.Context, targetURL string, body ExitEventJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +// EnterEventWithBody sends a POST webhook request +// Person entered the building +func (p *WebhookInitiator) EnterEventWithBody(ctx context.Context, targetURL string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewEnterEventWebhookRequestWithBody(targetURL, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := p.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return p.Client.Do(req) +} + +// EnterEvent sends a POST webhook request with JSON body +func (p *WebhookInitiator) EnterEvent(ctx context.Context, targetURL string, body EnterEventJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewEnterEventWebhookRequest(targetURL, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := p.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return p.Client.Do(req) +} + +// ExitEventWithBody sends a POST webhook request +// Person exited the building +func (p *WebhookInitiator) ExitEventWithBody(ctx context.Context, targetURL string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewExitEventWebhookRequestWithBody(targetURL, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := p.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return p.Client.Do(req) +} + +// ExitEvent sends a POST webhook request with JSON body +func (p *WebhookInitiator) ExitEvent(ctx context.Context, targetURL string, body ExitEventJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewExitEventWebhookRequest(targetURL, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := p.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return p.Client.Do(req) +} + +// NewEnterEventWebhookRequest creates a POST request for the webhook with application/json body +func NewEnterEventWebhookRequest(targetURL string, body EnterEventJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewEnterEventWebhookRequestWithBody(targetURL, "application/json", bodyReader) +} + +// NewEnterEventWebhookRequestWithBody creates a POST request for the webhook with any body +func NewEnterEventWebhookRequestWithBody(targetURL string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + parsedURL, err := url.Parse(targetURL) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", parsedURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewExitEventWebhookRequest creates a POST request for the webhook with application/json body +func NewExitEventWebhookRequest(targetURL string, body ExitEventJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewExitEventWebhookRequestWithBody(targetURL, "application/json", bodyReader) +} + +// NewExitEventWebhookRequestWithBody creates a POST request for the webhook with any body +func NewExitEventWebhookRequestWithBody(targetURL string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + parsedURL, err := url.Parse(targetURL) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", parsedURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// WebhookHttpError represents an HTTP error response. +// The type parameter E is the type of the parsed error body. +type WebhookHttpError[E any] struct { + StatusCode int + Body E + RawBody []byte +} + +func (e *WebhookHttpError[E]) Error() string { + return fmt.Sprintf("HTTP %d", e.StatusCode) +} + +// SimpleWebhookInitiator wraps WebhookInitiator with typed responses for operations that have +// unambiguous response types. Methods return the success type directly, +// and HTTP errors are returned as *WebhookHttpError[E] where E is the error type. +type SimpleWebhookInitiator struct { + *WebhookInitiator +} + +// NewSimpleWebhookInitiator creates a new SimpleWebhookInitiator which wraps a WebhookInitiator. +func NewSimpleWebhookInitiator(opts ...WebhookInitiatorOption) (*SimpleWebhookInitiator, error) { + initiator, err := NewWebhookInitiator(opts...) + if err != nil { + return nil, err + } + return &SimpleWebhookInitiator{WebhookInitiator: initiator}, nil +} + +// WebhookReceiverInterface represents handlers for receiving webhook requests. +type WebhookReceiverInterface interface { + // Person entered the building + // HandleEnterEventWebhook handles the POST webhook request. + HandleEnterEventWebhook(w http.ResponseWriter, r *http.Request) + // Person exited the building + // HandleExitEventWebhook handles the POST webhook request. + HandleExitEventWebhook(w http.ResponseWriter, r *http.Request) +} + +// WebhookReceiverMiddlewareFunc is a middleware function for webhook receiver handlers. +type WebhookReceiverMiddlewareFunc func(http.Handler) http.Handler + +// EnterEventWebhookHandler returns an http.Handler for the EnterEvent webhook. +// The caller is responsible for registering this handler at the appropriate path. +func EnterEventWebhookHandler(si WebhookReceiverInterface, errHandler func(w http.ResponseWriter, r *http.Request, err error), middlewares ...WebhookReceiverMiddlewareFunc) http.Handler { + if errHandler == nil { + errHandler = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + si.HandleEnterEventWebhook(w, r) + })) + + for _, middleware := range middlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) + }) +} + +// ExitEventWebhookHandler returns an http.Handler for the ExitEvent webhook. +// The caller is responsible for registering this handler at the appropriate path. +func ExitEventWebhookHandler(si WebhookReceiverInterface, errHandler func(w http.ResponseWriter, r *http.Request, err error), middlewares ...WebhookReceiverMiddlewareFunc) http.Handler { + if errHandler == nil { + errHandler = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + si.HandleExitEventWebhook(w, r) + })) + + for _, middleware := range middlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) + }) +} diff --git a/experimental/examples/webhook/server/main.go b/experimental/examples/webhook/server/main.go new file mode 100644 index 0000000000..8476aab7a8 --- /dev/null +++ b/experimental/examples/webhook/server/main.go @@ -0,0 +1,186 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "log" + "math/rand/v2" + "net" + "net/http" + "sync" + "time" + + "github.com/google/uuid" + + doorbadge "github.com/oapi-codegen/oapi-codegen/experimental/examples/webhook" +) + +var names = []string{ + "Alice", "Bob", "Charlie", "Diana", "Eve", + "Frank", "Grace", "Hank", "Iris", "Jack", +} + +type webhookEntry struct { + id uuid.UUID + url string + kind string +} + +// BadgeReader implements doorbadge.ServerInterface. +type BadgeReader struct { + initiator *doorbadge.WebhookInitiator + + mu sync.Mutex + webhooks map[uuid.UUID]webhookEntry +} + +var _ doorbadge.ServerInterface = (*BadgeReader)(nil) + +func NewBadgeReader() *BadgeReader { + initiator, err := doorbadge.NewWebhookInitiator() + if err != nil { + log.Fatalf("Failed to create webhook initiator: %v", err) + } + return &BadgeReader{ + initiator: initiator, + webhooks: make(map[uuid.UUID]webhookEntry), + } +} + +func sendError(w http.ResponseWriter, code int, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _ = json.NewEncoder(w).Encode(doorbadge.Error{ + Code: int32(code), + Message: message, + }) +} + +func (br *BadgeReader) RegisterWebhook(w http.ResponseWriter, r *http.Request, kind string) { + var req doorbadge.WebhookRegistration + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + sendError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + if kind != "enterEvent" && kind != "exitEvent" { + sendError(w, http.StatusBadRequest, "Invalid webhook kind: "+kind) + return + } + + id := uuid.New() + entry := webhookEntry{id: id, url: req.URL, kind: kind} + + br.mu.Lock() + br.webhooks[id] = entry + br.mu.Unlock() + + log.Printf("Registered webhook: id=%s kind=%s url=%s", id, kind, req.URL) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(doorbadge.WebhookRegistrationResponse{ID: id}) +} + +func (br *BadgeReader) DeregisterWebhook(w http.ResponseWriter, r *http.Request, id uuid.UUID) { + br.mu.Lock() + entry, ok := br.webhooks[id] + delete(br.webhooks, id) + br.mu.Unlock() + + if !ok { + sendError(w, http.StatusNotFound, "Webhook not found: "+id.String()) + return + } + + log.Printf("Deregistered webhook: id=%s kind=%s url=%s", id, entry.kind, entry.url) + w.WriteHeader(http.StatusNoContent) +} + +// generateEvents picks a random name and event kind every second and notifies webhooks. +func (br *BadgeReader) generateEvents(ctx context.Context) { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + name := names[rand.IntN(len(names))] + kind := "enterEvent" + if rand.IntN(2) == 0 { + kind = "exitEvent" + } + + person := doorbadge.Person{Name: name} + + br.mu.Lock() + targets := make([]webhookEntry, 0) + for _, entry := range br.webhooks { + if entry.kind == kind { + targets = append(targets, entry) + } + } + br.mu.Unlock() + + if len(targets) == 0 { + continue + } + + log.Printf("Event: %s %s (%d webhooks)", kind, name, len(targets)) + + for _, target := range targets { + var resp *http.Response + var err error + + switch kind { + case "enterEvent": + resp, err = br.initiator.EnterEvent(ctx, target.url, person) + case "exitEvent": + resp, err = br.initiator.ExitEvent(ctx, target.url, person) + } + + if err != nil { + log.Printf("Webhook %s failed: %v — removing", target.id, err) + br.mu.Lock() + delete(br.webhooks, target.id) + br.mu.Unlock() + continue + } + _ = resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + log.Printf("Webhook %s returned %d — removing", target.id, resp.StatusCode) + br.mu.Lock() + delete(br.webhooks, target.id) + br.mu.Unlock() + } + } + } + } +} + +func main() { + port := flag.String("port", "8080", "Port for HTTP server") + flag.Parse() + + reader := NewBadgeReader() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go reader.generateEvents(ctx) + + mux := http.NewServeMux() + doorbadge.HandlerFromMux(reader, mux) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) + mux.ServeHTTP(w, r) + }) + + addr := net.JoinHostPort("0.0.0.0", *port) + log.Printf("Door Badge Reader server listening on %s", addr) + log.Fatal(http.ListenAndServe(addr, handler)) +} diff --git a/experimental/go.mod b/experimental/go.mod new file mode 100644 index 0000000000..70784976bf --- /dev/null +++ b/experimental/go.mod @@ -0,0 +1,27 @@ +module github.com/oapi-codegen/oapi-codegen/experimental + +go 1.24.0 + +require ( + github.com/google/uuid v1.6.0 + github.com/pb33f/libopenapi v0.33.5 + github.com/stretchr/testify v1.11.1 + go.yaml.in/yaml/v4 v4.0.0-rc.4 // required by libopenapi for Extensions type + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + golang.org/x/text v0.33.0 + golang.org/x/tools v0.41.0 +) + +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pb33f/jsonpath v0.7.1 // indirect + github.com/pb33f/ordered-map/v2 v2.3.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/sync v0.19.0 // indirect +) diff --git a/experimental/go.sum b/experimental/go.sum new file mode 100644 index 0000000000..28075cb106 --- /dev/null +++ b/experimental/go.sum @@ -0,0 +1,41 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pb33f/jsonpath v0.7.1 h1:dEp6oIZuJbpDSyuHAl9m7GonoDW4M20BcD5vT0tPYRE= +github.com/pb33f/jsonpath v0.7.1/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo= +github.com/pb33f/libopenapi v0.33.5 h1:AzILVrOzMaawLFhQENmwmn7h/TIDH2QEgUd0PfxS2xE= +github.com/pb33f/libopenapi v0.33.5/go.mod h1:e/dmd2Pf1nkjqkI0r7guFSyt9T5V0IIQKgs0L6B/3b0= +github.com/pb33f/ordered-map/v2 v2.3.0 h1:k2OhVEQkhTCQMhAicQ3Z6iInzoZNQ7L9MVomwKBZ5WQ= +github.com/pb33f/ordered-map/v2 v2.3.0/go.mod h1:oe5ue+6ZNhy7QN9cPZvPA23Hx0vMHnNVeMg4fGdCANw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/experimental/internal/codegen/clientgen.go b/experimental/internal/codegen/clientgen.go new file mode 100644 index 0000000000..759d9e2da1 --- /dev/null +++ b/experimental/internal/codegen/clientgen.go @@ -0,0 +1,349 @@ +package codegen + +import ( + "bytes" + "fmt" + "strings" + "text/template" + + "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/templates" +) + +// ClientGenerator generates client code from operation descriptors. +type ClientGenerator struct { + tmpl *template.Template + schemaIndex map[string]*SchemaDescriptor + generateSimple bool + modelsPackage *ModelsPackage +} + +// NewClientGenerator creates a new client generator. +// modelsPackage can be nil if models are in the same package. +func NewClientGenerator(schemaIndex map[string]*SchemaDescriptor, generateSimple bool, modelsPackage *ModelsPackage) (*ClientGenerator, error) { + tmpl := template.New("client").Funcs(templates.Funcs()).Funcs(clientFuncs(schemaIndex, modelsPackage)) + + // Parse client templates + for _, ct := range templates.ClientTemplates { + content, err := templates.TemplateFS.ReadFile("files/" + ct.Template) + if err != nil { + return nil, err + } + _, err = tmpl.New(ct.Name).Parse(string(content)) + if err != nil { + return nil, err + } + } + + // Parse shared templates (param_types is shared with server) + for _, st := range templates.SharedServerTemplates { + content, err := templates.TemplateFS.ReadFile("files/" + st.Template) + if err != nil { + return nil, err + } + _, err = tmpl.New(st.Name).Parse(string(content)) + if err != nil { + return nil, err + } + } + + return &ClientGenerator{ + tmpl: tmpl, + schemaIndex: schemaIndex, + generateSimple: generateSimple, + modelsPackage: modelsPackage, + }, nil +} + +// clientFuncs returns template functions specific to client generation. +func clientFuncs(schemaIndex map[string]*SchemaDescriptor, modelsPackage *ModelsPackage) template.FuncMap { + return template.FuncMap{ + "pathFmt": pathFmt, + "isSimpleOperation": isSimpleOperation, + "simpleOperationSuccessResponse": simpleOperationSuccessResponse, + "errorResponseForOperation": errorResponseForOperation, + "goTypeForContent": func(content *ResponseContentDescriptor) string { + return goTypeForContent(content, schemaIndex, modelsPackage) + }, + "modelsPkg": func() string { + return modelsPackage.Prefix() + }, + } +} + +// pathFmt converts a path with {param} placeholders to a format string. +// Example: "/pets/{petId}" -> "/pets/%s" +func pathFmt(path string) string { + result := path + for { + start := strings.Index(result, "{") + if start == -1 { + break + } + end := strings.Index(result, "}") + if end == -1 { + break + } + result = result[:start] + "%s" + result[end+1:] + } + return result +} + +// isSimpleOperation returns true if an operation has a single JSON success response type. +// "Simple" operations can have typed wrapper methods in SimpleClient. +func isSimpleOperation(op *OperationDescriptor) bool { + // Must have responses + if len(op.Responses) == 0 { + return false + } + + // Count success responses (2xx or default that could be success) + var successResponses []*ResponseDescriptor + for _, r := range op.Responses { + if strings.HasPrefix(r.StatusCode, "2") { + successResponses = append(successResponses, r) + } + } + + // Must have exactly one success response + if len(successResponses) != 1 { + return false + } + + success := successResponses[0] + + // Must have at least one content type and exactly one JSON content type + // (i.e., if there are multiple content types, we can't have a simple typed response) + if len(success.Contents) == 0 { + return false + } + if len(success.Contents) != 1 { + return false + } + + // The single content type must be JSON + return success.Contents[0].IsJSON +} + +// simpleOperationSuccessResponse returns the single success response for a simple operation. +func simpleOperationSuccessResponse(op *OperationDescriptor) *ResponseDescriptor { + for _, r := range op.Responses { + if strings.HasPrefix(r.StatusCode, "2") { + return r + } + } + return nil +} + +// errorResponseForOperation returns the error response (default or 4xx/5xx) if one exists. +func errorResponseForOperation(op *OperationDescriptor) *ResponseDescriptor { + // First, look for a default response + for _, r := range op.Responses { + if r.StatusCode == "default" { + if len(r.Contents) > 0 && r.Contents[0].IsJSON { + return r + } + } + } + // Then look for a 4xx or 5xx response + for _, r := range op.Responses { + if strings.HasPrefix(r.StatusCode, "4") || strings.HasPrefix(r.StatusCode, "5") { + if len(r.Contents) > 0 && r.Contents[0].IsJSON { + return r + } + } + } + return nil +} + +// goTypeForContent returns the Go type for a response content descriptor. +// If modelsPackage is set, type names are prefixed with the package name. +func goTypeForContent(content *ResponseContentDescriptor, schemaIndex map[string]*SchemaDescriptor, modelsPackage *ModelsPackage) string { + if content == nil || content.Schema == nil { + return "interface{}" + } + + pkgPrefix := modelsPackage.Prefix() + + // If the schema has a reference, look it up + if content.Schema.Ref != "" { + if target, ok := schemaIndex[content.Schema.Ref]; ok { + return pkgPrefix + target.ShortName + } + } + + // Check if this is an array schema with items that have a reference + if content.Schema.Schema != nil && content.Schema.Schema.Items != nil { + itemProxy := content.Schema.Schema.Items.A + if itemProxy != nil && itemProxy.IsReference() { + ref := itemProxy.GetReference() + if target, ok := schemaIndex[ref]; ok { + return "[]" + pkgPrefix + target.ShortName + } + } + } + + // If the schema has a short name, use it + if content.Schema.ShortName != "" { + return pkgPrefix + content.Schema.ShortName + } + + // Fall back to the stable name + if content.Schema.StableName != "" { + return pkgPrefix + content.Schema.StableName + } + + // Try to derive from the schema itself + if content.Schema.Schema != nil { + return schemaToGoType(content.Schema.Schema) + } + + return "interface{}" +} + +// GenerateBase generates the base client types and helpers. +func (g *ClientGenerator) GenerateBase() (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "base", nil); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateInterface generates the ClientInterface. +func (g *ClientGenerator) GenerateInterface(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "interface", ops); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateMethods generates the Client methods. +func (g *ClientGenerator) GenerateMethods(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "methods", ops); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateRequestBuilders generates the request builder functions. +func (g *ClientGenerator) GenerateRequestBuilders(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "request_builders", ops); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateSimple generates the SimpleClient with typed responses. +func (g *ClientGenerator) GenerateSimple(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "simple", ops); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateParamTypes generates the parameter struct types. +func (g *ClientGenerator) GenerateParamTypes(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "param_types", ops); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateRequestBodyTypes generates type aliases for request bodies. +func (g *ClientGenerator) GenerateRequestBodyTypes(ops []*OperationDescriptor) string { + var buf bytes.Buffer + pkgPrefix := g.modelsPackage.Prefix() + + for _, op := range ops { + for _, body := range op.Bodies { + if !body.IsJSON { + continue + } + // Get the underlying type for this request body + var targetType string + if body.Schema != nil { + if body.Schema.Ref != "" { + // Reference to a component schema + if target, ok := g.schemaIndex[body.Schema.Ref]; ok { + targetType = pkgPrefix + target.ShortName + } + } else if body.Schema.ShortName != "" { + targetType = pkgPrefix + body.Schema.ShortName + } + } + if targetType == "" { + targetType = "interface{}" + } + + // Generate type alias: type addPetJSONRequestBody = models.NewPet + buf.WriteString(fmt.Sprintf("type %s = %s\n\n", body.GoTypeName, targetType)) + } + } + + return buf.String() +} + +// GenerateClient generates the complete client code. +func (g *ClientGenerator) GenerateClient(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + + // Generate request body type aliases first + bodyTypes := g.GenerateRequestBodyTypes(ops) + buf.WriteString(bodyTypes) + + // Generate base client + base, err := g.GenerateBase() + if err != nil { + return "", fmt.Errorf("generating base client: %w", err) + } + buf.WriteString(base) + buf.WriteString("\n") + + // Generate interface + iface, err := g.GenerateInterface(ops) + if err != nil { + return "", fmt.Errorf("generating client interface: %w", err) + } + buf.WriteString(iface) + buf.WriteString("\n") + + // Generate param types + paramTypes, err := g.GenerateParamTypes(ops) + if err != nil { + return "", fmt.Errorf("generating param types: %w", err) + } + buf.WriteString(paramTypes) + buf.WriteString("\n") + + // Generate methods + methods, err := g.GenerateMethods(ops) + if err != nil { + return "", fmt.Errorf("generating client methods: %w", err) + } + buf.WriteString(methods) + buf.WriteString("\n") + + // Generate request builders + builders, err := g.GenerateRequestBuilders(ops) + if err != nil { + return "", fmt.Errorf("generating request builders: %w", err) + } + buf.WriteString(builders) + buf.WriteString("\n") + + // Generate simple client if requested + if g.generateSimple { + simple, err := g.GenerateSimple(ops) + if err != nil { + return "", fmt.Errorf("generating simple client: %w", err) + } + buf.WriteString(simple) + } + + return buf.String(), nil +} diff --git a/experimental/internal/codegen/clientgen_test.go b/experimental/internal/codegen/clientgen_test.go new file mode 100644 index 0000000000..cb5d6961b5 --- /dev/null +++ b/experimental/internal/codegen/clientgen_test.go @@ -0,0 +1,188 @@ +package codegen + +import ( + "os" + "testing" + + "github.com/pb33f/libopenapi" + "github.com/stretchr/testify/require" +) + +func TestClientGenerator(t *testing.T) { + // Read the petstore spec + specPath := "../../examples/petstore-expanded/petstore-expanded.yaml" + specData, err := os.ReadFile(specPath) + require.NoError(t, err, "Failed to read petstore spec") + + // Parse the spec + doc, err := libopenapi.NewDocument(specData) + require.NoError(t, err, "Failed to parse petstore spec") + + // Gather schemas to build schema index + contentTypeMatcher := NewContentTypeMatcher(DefaultContentTypes()) + schemas, err := GatherSchemas(doc, contentTypeMatcher) + require.NoError(t, err, "Failed to gather schemas") + + // Compute names for schemas + converter := NewNameConverter(NameMangling{}, NameSubstitutions{}) + contentTypeNamer := NewContentTypeShortNamer(DefaultContentTypeShortNames()) + ComputeSchemaNames(schemas, converter, contentTypeNamer) + + // Build schema index - key by Path.String() for component schemas + schemaIndex := make(map[string]*SchemaDescriptor) + for _, s := range schemas { + schemaIndex[s.Path.String()] = s + } + + // Create param tracker + paramTracker := NewParamUsageTracker() + + // Gather operations + ops, err := GatherOperations(doc, paramTracker) + require.NoError(t, err, "Failed to gather operations") + require.Len(t, ops, 4, "Expected 4 operations") + + // Log operations for debugging + // Verify we have the expected operations + operationIDs := make([]string, 0, len(ops)) + for _, op := range ops { + operationIDs = append(operationIDs, op.GoOperationID) + } + t.Logf("Operations: %v", operationIDs) + + // Generate client code + gen, err := NewClientGenerator(schemaIndex, true, nil) + require.NoError(t, err, "Failed to create client generator") + + clientCode, err := gen.GenerateClient(ops) + require.NoError(t, err, "Failed to generate client code") + require.NotEmpty(t, clientCode, "Generated client code should not be empty") + + t.Logf("Generated client code:\n%s", clientCode) + + // Verify key components are present + require.Contains(t, clientCode, "type Client struct") + require.Contains(t, clientCode, "NewClient") + require.Contains(t, clientCode, "type ClientInterface interface") + require.Contains(t, clientCode, "FindPets") + require.Contains(t, clientCode, "AddPet") + require.Contains(t, clientCode, "DeletePet") + require.Contains(t, clientCode, "FindPetByID") + + // Verify request builders + require.Contains(t, clientCode, "NewFindPetsRequest") + require.Contains(t, clientCode, "NewAddPetRequest") + require.Contains(t, clientCode, "NewDeletePetRequest") + require.Contains(t, clientCode, "NewFindPetByIDRequest") + + // Verify SimpleClient + require.Contains(t, clientCode, "type SimpleClient struct") + require.Contains(t, clientCode, "NewSimpleClient") +} + +func TestIsSimpleOperation(t *testing.T) { + tests := []struct { + name string + op *OperationDescriptor + expected bool + }{ + { + name: "simple operation with single JSON 200 response", + op: &OperationDescriptor{ + Responses: []*ResponseDescriptor{ + { + StatusCode: "200", + Contents: []*ResponseContentDescriptor{ + {ContentType: "application/json", IsJSON: true}, + }, + }, + }, + }, + expected: true, + }, + { + name: "not simple - multiple success responses", + op: &OperationDescriptor{ + Responses: []*ResponseDescriptor{ + { + StatusCode: "200", + Contents: []*ResponseContentDescriptor{ + {ContentType: "application/json", IsJSON: true}, + }, + }, + { + StatusCode: "201", + Contents: []*ResponseContentDescriptor{ + {ContentType: "application/json", IsJSON: true}, + }, + }, + }, + }, + expected: false, + }, + { + name: "not simple - multiple content types", + op: &OperationDescriptor{ + Responses: []*ResponseDescriptor{ + { + StatusCode: "200", + Contents: []*ResponseContentDescriptor{ + {ContentType: "application/json", IsJSON: true}, + {ContentType: "application/xml", IsJSON: false}, + }, + }, + }, + }, + expected: false, + }, + { + name: "not simple - no JSON content", + op: &OperationDescriptor{ + Responses: []*ResponseDescriptor{ + { + StatusCode: "200", + Contents: []*ResponseContentDescriptor{ + {ContentType: "text/plain", IsJSON: false}, + }, + }, + }, + }, + expected: false, + }, + { + name: "not simple - no responses", + op: &OperationDescriptor{}, + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := isSimpleOperation(tc.op) + if result != tc.expected { + t.Errorf("isSimpleOperation() = %v, expected %v", result, tc.expected) + } + }) + } +} + +func TestPathFmt(t *testing.T) { + tests := []struct { + path string + expected string + }{ + {"/pets", "/pets"}, + {"/pets/{petId}", "/pets/%s"}, + {"/pets/{petId}/photos/{photoId}", "/pets/%s/photos/%s"}, + {"/users/{userId}/posts/{postId}/comments/{commentId}", "/users/%s/posts/%s/comments/%s"}, + } + + for _, tc := range tests { + t.Run(tc.path, func(t *testing.T) { + result := pathFmt(tc.path) + if result != tc.expected { + t.Errorf("pathFmt(%q) = %q, expected %q", tc.path, result, tc.expected) + } + }) + } +} diff --git a/experimental/internal/codegen/codegen.go b/experimental/internal/codegen/codegen.go new file mode 100644 index 0000000000..5b6fdff894 --- /dev/null +++ b/experimental/internal/codegen/codegen.go @@ -0,0 +1,1101 @@ +// Package codegen generates Go code from parsed OpenAPI specs. +package codegen + +import ( + "fmt" + "maps" + "slices" + "strings" + "text/template" + + "github.com/pb33f/libopenapi" + "github.com/pb33f/libopenapi/datamodel/high/base" + + "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/templates" +) + +// Generate produces Go code from the parsed OpenAPI document. +// specData is the raw spec bytes used to embed the spec in the generated code. +func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (string, error) { + cfg.ApplyDefaults() + + // Create content type matcher for filtering request/response bodies + contentTypeMatcher := NewContentTypeMatcher(cfg.ContentTypes) + + // Create content type short namer for friendly type names + contentTypeNamer := NewContentTypeShortNamer(cfg.ContentTypeShortNames) + + // Pass 1: Gather all schemas that need types + schemas, err := GatherSchemas(doc, contentTypeMatcher) + if err != nil { + return "", fmt.Errorf("gathering schemas: %w", err) + } + + // Pass 2: Compute names for all schemas + converter := NewNameConverter(cfg.NameMangling, cfg.NameSubstitutions) + ComputeSchemaNames(schemas, converter, contentTypeNamer) + + // Build schema index for type resolution + schemaIndex := make(map[string]*SchemaDescriptor) + for _, s := range schemas { + schemaIndex[s.Path.String()] = s + } + + // Pass 3: Generate Go code + importResolver := NewImportResolver(cfg.ImportMapping) + tagGenerator := NewStructTagGenerator(cfg.StructTags) + gen := NewTypeGenerator(cfg.TypeMapping, converter, importResolver, tagGenerator) + gen.IndexSchemas(schemas) + + output := NewOutput(cfg.PackageName) + // Note: encoding/json and fmt imports are added by generateType when needed + + // Generate models (types for schemas) unless using external models package + if cfg.Generation.ModelsPackage == nil { + for _, desc := range schemas { + code := generateType(gen, desc) + if code != "" { + output.AddType(code) + } + } + + // Add imports collected during generation + output.AddImports(gen.Imports()) + + // Add custom type templates (Date, Email, UUID, File, etc.) + // Sort template names for deterministic output ordering. + templateNames := slices.Sorted(maps.Keys(gen.RequiredTemplates())) + for _, templateName := range templateNames { + typeCode, typeImports := loadCustomType(templateName) + if typeCode != "" { + output.AddType(typeCode) + for path, alias := range typeImports { + output.AddImport(path, alias) + } + } + } + + // Embed the raw OpenAPI spec if specData was provided + if len(specData) > 0 { + embeddedCode, err := generateEmbeddedSpec(specData) + if err != nil { + return "", fmt.Errorf("generating embedded spec: %w", err) + } + output.AddType(embeddedCode) + output.AddImport("bytes", "") + output.AddImport("compress/gzip", "") + output.AddImport("encoding/base64", "") + output.AddImport("fmt", "") + output.AddImport("strings", "") + output.AddImport("sync", "") + } + } + + // Generate client code if requested + if cfg.Generation.Client { + // Create param tracker for tracking which param functions are needed + paramTracker := NewParamUsageTracker() + + // Gather operations + ops, err := GatherOperations(doc, paramTracker) + if err != nil { + return "", fmt.Errorf("gathering operations: %w", err) + } + + // Generate client + clientGen, err := NewClientGenerator(schemaIndex, cfg.Generation.SimpleClient, cfg.Generation.ModelsPackage) + if err != nil { + return "", fmt.Errorf("creating client generator: %w", err) + } + + clientCode, err := clientGen.GenerateClient(ops) + if err != nil { + return "", fmt.Errorf("generating client code: %w", err) + } + output.AddType(clientCode) + + // Add client imports + for _, ct := range templates.ClientTemplates { + for _, imp := range ct.Imports { + output.AddImport(imp.Path, imp.Alias) + } + } + + // Add models package import if using external models + if cfg.Generation.ModelsPackage != nil && cfg.Generation.ModelsPackage.Path != "" { + output.AddImport(cfg.Generation.ModelsPackage.Path, cfg.Generation.ModelsPackage.Alias) + } + + // Generate param functions + paramFuncs, err := generateParamFunctionsFromTracker(paramTracker) + if err != nil { + return "", fmt.Errorf("generating param functions: %w", err) + } + if paramFuncs != "" { + output.AddType(paramFuncs) + } + + // Add param function imports + for _, imp := range paramTracker.GetRequiredImports() { + output.AddImport(imp.Path, imp.Alias) + } + } + + // Track whether shared error types have been generated to avoid duplication. + // Both server and receiver generation emit the same error types. + generatedErrors := false + + // Generate server code for path operations if a server framework is set. + // Only generate if there are actual path operations — setting server solely + // for receiver use should not produce path-operation server code. + if cfg.Generation.Server != "" { + // Create param tracker for tracking which param functions are needed + paramTracker := NewParamUsageTracker() + + // Gather operations + ops, err := GatherOperations(doc, paramTracker) + if err != nil { + return "", fmt.Errorf("gathering operations: %w", err) + } + + if len(ops) > 0 { + // Generate server + serverGen, err := NewServerGenerator(cfg.Generation.Server) + if err != nil { + return "", fmt.Errorf("creating server generator: %w", err) + } + + serverCode, err := serverGen.GenerateServer(ops) + if err != nil { + return "", fmt.Errorf("generating server code: %w", err) + } + output.AddType(serverCode) + generatedErrors = true + + // Add server imports based on server type + serverTemplates, err := getServerTemplates(cfg.Generation.Server) + if err != nil { + return "", fmt.Errorf("getting server templates: %w", err) + } + for _, st := range serverTemplates { + for _, imp := range st.Imports { + output.AddImport(imp.Path, imp.Alias) + } + } + + // Note: Server interfaces don't use external models directly. + // Models are used in the hand-written implementation (petstore.go), + // not in the generated server interface code. + + // Generate param functions + paramFuncs, err := generateParamFunctionsFromTracker(paramTracker) + if err != nil { + return "", fmt.Errorf("generating param functions: %w", err) + } + if paramFuncs != "" { + output.AddType(paramFuncs) + } + + // Add param function imports + for _, imp := range paramTracker.GetRequiredImports() { + output.AddImport(imp.Path, imp.Alias) + } + } + } + + // Generate webhook initiator code if requested + if cfg.Generation.WebhookInitiator { + paramTracker := NewParamUsageTracker() + + webhookOps, err := GatherWebhookOperations(doc, paramTracker) + if err != nil { + return "", fmt.Errorf("gathering webhook operations: %w", err) + } + + if len(webhookOps) > 0 { + initiatorGen, err := NewInitiatorGenerator("Webhook", schemaIndex, true, cfg.Generation.ModelsPackage) + if err != nil { + return "", fmt.Errorf("creating webhook initiator generator: %w", err) + } + + initiatorCode, err := initiatorGen.GenerateInitiator(webhookOps) + if err != nil { + return "", fmt.Errorf("generating webhook initiator code: %w", err) + } + output.AddType(initiatorCode) + + for _, pt := range templates.InitiatorTemplates { + for _, imp := range pt.Imports { + output.AddImport(imp.Path, imp.Alias) + } + } + + if cfg.Generation.ModelsPackage != nil && cfg.Generation.ModelsPackage.Path != "" { + output.AddImport(cfg.Generation.ModelsPackage.Path, cfg.Generation.ModelsPackage.Alias) + } + + paramFuncs, err := generateParamFunctionsFromTracker(paramTracker) + if err != nil { + return "", fmt.Errorf("generating param functions: %w", err) + } + if paramFuncs != "" { + output.AddType(paramFuncs) + } + for _, imp := range paramTracker.GetRequiredImports() { + output.AddImport(imp.Path, imp.Alias) + } + } + } + + // Generate callback initiator code if requested + if cfg.Generation.CallbackInitiator { + paramTracker := NewParamUsageTracker() + + callbackOps, err := GatherCallbackOperations(doc, paramTracker) + if err != nil { + return "", fmt.Errorf("gathering callback operations: %w", err) + } + + if len(callbackOps) > 0 { + initiatorGen, err := NewInitiatorGenerator("Callback", schemaIndex, true, cfg.Generation.ModelsPackage) + if err != nil { + return "", fmt.Errorf("creating callback initiator generator: %w", err) + } + + initiatorCode, err := initiatorGen.GenerateInitiator(callbackOps) + if err != nil { + return "", fmt.Errorf("generating callback initiator code: %w", err) + } + output.AddType(initiatorCode) + + for _, pt := range templates.InitiatorTemplates { + for _, imp := range pt.Imports { + output.AddImport(imp.Path, imp.Alias) + } + } + + if cfg.Generation.ModelsPackage != nil && cfg.Generation.ModelsPackage.Path != "" { + output.AddImport(cfg.Generation.ModelsPackage.Path, cfg.Generation.ModelsPackage.Alias) + } + + paramFuncs, err := generateParamFunctionsFromTracker(paramTracker) + if err != nil { + return "", fmt.Errorf("generating param functions: %w", err) + } + if paramFuncs != "" { + output.AddType(paramFuncs) + } + for _, imp := range paramTracker.GetRequiredImports() { + output.AddImport(imp.Path, imp.Alias) + } + } + } + + // Generate webhook receiver code if requested + if cfg.Generation.WebhookReceiver { + if cfg.Generation.Server == "" { + return "", fmt.Errorf("webhook-receiver requires server to be set") + } + + paramTracker := NewParamUsageTracker() + + webhookOps, err := GatherWebhookOperations(doc, paramTracker) + if err != nil { + return "", fmt.Errorf("gathering webhook operations: %w", err) + } + + if len(webhookOps) > 0 { + receiverGen, err := NewReceiverGenerator("Webhook", cfg.Generation.Server) + if err != nil { + return "", fmt.Errorf("creating webhook receiver generator: %w", err) + } + + receiverCode, err := receiverGen.GenerateReceiver(webhookOps) + if err != nil { + return "", fmt.Errorf("generating webhook receiver code: %w", err) + } + output.AddType(receiverCode) + + // Add param types + paramTypes, err := receiverGen.GenerateParamTypes(webhookOps) + if err != nil { + return "", fmt.Errorf("generating webhook receiver param types: %w", err) + } + output.AddType(paramTypes) + + // Add error types (only if not already generated by server) + if !generatedErrors { + errors, err := receiverGen.GenerateErrors() + if err != nil { + return "", fmt.Errorf("generating webhook receiver errors: %w", err) + } + output.AddType(errors) + generatedErrors = true + } + + receiverTemplates, err := getReceiverTemplates(cfg.Generation.Server) + if err != nil { + return "", fmt.Errorf("getting receiver templates: %w", err) + } + for _, ct := range receiverTemplates { + for _, imp := range ct.Imports { + output.AddImport(imp.Path, imp.Alias) + } + } + for _, st := range templates.SharedServerTemplates { + for _, imp := range st.Imports { + output.AddImport(imp.Path, imp.Alias) + } + } + + paramFuncs, err := generateParamFunctionsFromTracker(paramTracker) + if err != nil { + return "", fmt.Errorf("generating param functions: %w", err) + } + if paramFuncs != "" { + output.AddType(paramFuncs) + } + for _, imp := range paramTracker.GetRequiredImports() { + output.AddImport(imp.Path, imp.Alias) + } + } + } + + // Generate callback receiver code if requested + if cfg.Generation.CallbackReceiver { + if cfg.Generation.Server == "" { + return "", fmt.Errorf("callback-receiver requires server to be set") + } + + paramTracker := NewParamUsageTracker() + + callbackOps, err := GatherCallbackOperations(doc, paramTracker) + if err != nil { + return "", fmt.Errorf("gathering callback operations: %w", err) + } + + if len(callbackOps) > 0 { + receiverGen, err := NewReceiverGenerator("Callback", cfg.Generation.Server) + if err != nil { + return "", fmt.Errorf("creating callback receiver generator: %w", err) + } + + receiverCode, err := receiverGen.GenerateReceiver(callbackOps) + if err != nil { + return "", fmt.Errorf("generating callback receiver code: %w", err) + } + output.AddType(receiverCode) + + paramTypes, err := receiverGen.GenerateParamTypes(callbackOps) + if err != nil { + return "", fmt.Errorf("generating callback receiver param types: %w", err) + } + output.AddType(paramTypes) + + // Add error types (only if not already generated by server or another receiver) + if !generatedErrors { + errors, err := receiverGen.GenerateErrors() + if err != nil { + return "", fmt.Errorf("generating callback receiver errors: %w", err) + } + output.AddType(errors) + generatedErrors = true //nolint:ineffassign // kept for symmetry with webhook loop below + } + + receiverTemplates, err := getReceiverTemplates(cfg.Generation.Server) + if err != nil { + return "", fmt.Errorf("getting receiver templates: %w", err) + } + for _, ct := range receiverTemplates { + for _, imp := range ct.Imports { + output.AddImport(imp.Path, imp.Alias) + } + } + for _, st := range templates.SharedServerTemplates { + for _, imp := range st.Imports { + output.AddImport(imp.Path, imp.Alias) + } + } + + paramFuncs, err := generateParamFunctionsFromTracker(paramTracker) + if err != nil { + return "", fmt.Errorf("generating param functions: %w", err) + } + if paramFuncs != "" { + output.AddType(paramFuncs) + } + for _, imp := range paramTracker.GetRequiredImports() { + output.AddImport(imp.Path, imp.Alias) + } + } + } + + return output.Format() +} + +// generateParamFunctionsFromTracker generates the parameter styling/binding functions based on usage. +func generateParamFunctionsFromTracker(tracker *ParamUsageTracker) (string, error) { + if !tracker.HasAnyUsage() { + return "", nil + } + + var result strings.Builder + + // Get required templates + requiredTemplates := tracker.GetRequiredTemplates() + + for _, tmplInfo := range requiredTemplates { + content, err := templates.TemplateFS.ReadFile("files/" + tmplInfo.Template) + if err != nil { + return "", fmt.Errorf("reading param template %s: %w", tmplInfo.Template, err) + } + + // Parse and execute as a Go template + tmpl, err := template.New(tmplInfo.Name).Parse(string(content)) + if err != nil { + return "", fmt.Errorf("parsing param template %s: %w", tmplInfo.Template, err) + } + + if err := tmpl.Execute(&result, nil); err != nil { + return "", fmt.Errorf("executing param template %s: %w", tmplInfo.Template, err) + } + result.WriteString("\n") + } + + return result.String(), nil +} + +// generateType generates Go code for a single schema descriptor. +func generateType(gen *TypeGenerator, desc *SchemaDescriptor) string { + kind := GetSchemaKind(desc) + + // If schema has TypeOverride extension, generate a type alias to the external type + // instead of generating the full type definition + if desc.Extensions != nil && desc.Extensions.TypeOverride != nil { + return generateTypeOverrideAlias(gen, desc) + } + + var code string + switch kind { + case KindReference: + // References don't generate new types; they use the referenced type's name + return "" + + case KindStruct: + code = generateStructType(gen, desc) + + case KindMap: + code = generateMapAlias(gen, desc) + + case KindEnum: + code = generateEnumType(gen, desc) + + case KindAllOf: + code = generateAllOfType(gen, desc) + + case KindAnyOf: + code = generateAnyOfType(gen, desc) + + case KindOneOf: + code = generateOneOfType(gen, desc) + + case KindAlias: + code = generateTypeAlias(gen, desc) + + default: + return "" + } + + if code == "" { + return "" + } + + // Prepend schema path comment + return schemaPathComment(desc.Path) + code +} + +// schemaPathComment returns a comment line showing the schema path. +func schemaPathComment(path SchemaPath) string { + return fmt.Sprintf("// %s\n", path.String()) +} + +// generateStructType generates a struct type for an object schema. +func generateStructType(gen *TypeGenerator, desc *SchemaDescriptor) string { + fields := gen.GenerateStructFields(desc) + doc := extractDescription(desc.Schema) + + // Check if we need additionalProperties handling + if gen.HasAdditionalProperties(desc) { + // Mixed properties need encoding/json for marshal/unmarshal (but not fmt) + gen.AddJSONImport() + + addPropsType := gen.AdditionalPropertiesType(desc) + structCode := GenerateStructWithAdditionalProps(desc.ShortName, fields, addPropsType, doc, gen.TagGenerator()) + + // Generate marshal/unmarshal methods + marshalCode := GenerateMixedPropertiesMarshal(desc.ShortName, fields) + unmarshalCode := GenerateMixedPropertiesUnmarshal(desc.ShortName, fields, addPropsType) + + code := structCode + "\n" + marshalCode + "\n" + unmarshalCode + + // Generate ApplyDefaults method if needed + if applyDefaults := GenerateApplyDefaults(desc.ShortName, fields); applyDefaults != "" { + code += "\n" + applyDefaults + } + + return code + } + + code := GenerateStruct(desc.ShortName, fields, doc, gen.TagGenerator()) + + // Generate ApplyDefaults method if needed + if applyDefaults := GenerateApplyDefaults(desc.ShortName, fields); applyDefaults != "" { + code += "\n" + applyDefaults + } + + return code +} + +// generateMapAlias generates a type alias for a pure map schema. +func generateMapAlias(gen *TypeGenerator, desc *SchemaDescriptor) string { + mapType := gen.GoTypeExpr(desc) + doc := extractDescription(desc.Schema) + return GenerateTypeAlias(desc.ShortName, mapType, doc) +} + +// generateEnumType generates an enum type with const values. +func generateEnumType(gen *TypeGenerator, desc *SchemaDescriptor) string { + schema := desc.Schema + if schema == nil { + return "" + } + + // Determine base type + baseType := "string" + primaryType := getPrimaryType(schema) + if primaryType == "integer" { + baseType = "int" + } + + // Extract enum values as strings + var values []string + for _, v := range schema.Enum { + values = append(values, fmt.Sprintf("%v", v.Value)) + } + + // Check for custom enum variable names from extensions + var customNames []string + if desc.Extensions != nil && len(desc.Extensions.EnumVarNames) > 0 { + customNames = desc.Extensions.EnumVarNames + } + + doc := extractDescription(schema) + return GenerateEnumWithConstPrefix(desc.ShortName, desc.ShortName, baseType, values, customNames, doc) +} + +// generateTypeAlias generates a simple type alias. +func generateTypeAlias(gen *TypeGenerator, desc *SchemaDescriptor) string { + goType := gen.GoTypeExpr(desc) + doc := extractDescription(desc.Schema) + return GenerateTypeAlias(desc.ShortName, goType, doc) +} + +// generateTypeOverrideAlias generates a type alias to an external type specified via x-oapi-codegen-type-override. +func generateTypeOverrideAlias(gen *TypeGenerator, desc *SchemaDescriptor) string { + override := desc.Extensions.TypeOverride + + // Register the import + if override.ImportPath != "" { + if override.ImportAlias != "" { + gen.AddImportAlias(override.ImportPath, override.ImportAlias) + } else { + gen.AddImport(override.ImportPath) + } + } + + doc := extractDescription(desc.Schema) + return GenerateTypeAlias(desc.ShortName, override.TypeName, doc) +} + +// AllOfMergeError represents a conflict when merging allOf schemas. +type AllOfMergeError struct { + SchemaName string + PropertyName string + Type1 string + Type2 string +} + +func (e AllOfMergeError) Error() string { + return fmt.Sprintf("allOf merge conflict in %s: property %q has conflicting types %s and %s", + e.SchemaName, e.PropertyName, e.Type1, e.Type2) +} + +// allOfMemberInfo holds information about an allOf member for merging. +type allOfMemberInfo struct { + fields []StructField // flattened fields from object schemas + unionType string // non-empty if this member is a oneOf/anyOf union + unionDesc *SchemaDescriptor + required []string // required fields from this allOf member +} + +// generateAllOfType generates a struct with flattened properties from all allOf members. +// Object schema properties are merged into flat fields. +// oneOf/anyOf members become union fields with json:"-" tag. +func generateAllOfType(gen *TypeGenerator, desc *SchemaDescriptor) string { + schema := desc.Schema + if schema == nil { + return "" + } + + // Merge all fields, checking for conflicts + mergedFields := make(map[string]StructField) // keyed by JSONName + var fieldOrder []string // preserve order + var unionFields []StructField + + // First, collect fields from properties defined directly on the schema + // (Issue 2102: properties at same level as allOf were being ignored) + if schema.Properties != nil && schema.Properties.Len() > 0 { + directFields := gen.GenerateStructFields(desc) + for _, field := range directFields { + mergedFields[field.JSONName] = field + fieldOrder = append(fieldOrder, field.JSONName) + } + } + + // Collect info about each allOf member + var members []allOfMemberInfo + for i, proxy := range schema.AllOf { + info := allOfMemberInfo{} + + memberSchema := proxy.Schema() + if memberSchema == nil { + continue + } + + // Check if this member is a oneOf/anyOf (union type) + if len(memberSchema.OneOf) > 0 || len(memberSchema.AnyOf) > 0 { + // This is a union - keep as a union field + if desc.AllOf != nil && i < len(desc.AllOf) { + info.unionType = desc.AllOf[i].ShortName + info.unionDesc = desc.AllOf[i] + } + } else if proxy.IsReference() { + // Reference to another schema - get its fields + ref := proxy.GetReference() + if target, ok := gen.schemaIndex[ref]; ok { + info.fields = gen.collectFieldsRecursive(target) + } + } else if memberSchema.Properties != nil && memberSchema.Properties.Len() > 0 { + // Inline object schema - get its fields + if desc.AllOf != nil && i < len(desc.AllOf) { + info.fields = gen.GenerateStructFields(desc.AllOf[i]) + } + } + + // Also check for required array in allOf members (may mark fields as required) + info.required = memberSchema.Required + + members = append(members, info) + } + + // Merge fields from allOf members + for _, member := range members { + if member.unionType != "" { + // Add union as a special field + unionFields = append(unionFields, StructField{ + Name: member.unionType, + Type: "*" + member.unionType, + JSONName: "-", // will use json:"-" + }) + continue + } + + for _, field := range member.fields { + if existing, ok := mergedFields[field.JSONName]; ok { + // Check for type conflict + if existing.Type != field.Type { + // Type conflict - generate error comment in output + // In a real implementation, this should be a proper error + // For now, we'll include a comment and use the first type + field.Doc = fmt.Sprintf("CONFLICT: type %s vs %s", existing.Type, field.Type) + } + // If same type, keep existing (first wins for required/nullable) + continue + } + mergedFields[field.JSONName] = field + fieldOrder = append(fieldOrder, field.JSONName) + } + + // Apply required array from this allOf member to update pointer/omitempty + for _, reqName := range member.required { + if field, ok := mergedFields[reqName]; ok { + if !field.Required { + field.Required = true + field.OmitEmpty = false + // Update pointer status - required non-nullable fields are not pointers + if !field.Nullable && !strings.HasPrefix(field.Type, "[]") && !strings.HasPrefix(field.Type, "map[") { + field.Type = strings.TrimPrefix(field.Type, "*") + field.Pointer = false + } + mergedFields[reqName] = field + } + } + } + } + + // Build final field list in order + var finalFields []StructField + for _, jsonName := range fieldOrder { + finalFields = append(finalFields, mergedFields[jsonName]) + } + + doc := extractDescription(schema) + + // Generate struct + var code string + if len(unionFields) > 0 { + // Has union members - need custom marshal/unmarshal + gen.AddJSONImport() + code = generateAllOfStructWithUnions(desc.ShortName, finalFields, unionFields, doc, gen.TagGenerator()) + } else { + // Simple case - just flattened fields + code = GenerateStruct(desc.ShortName, finalFields, doc, gen.TagGenerator()) + } + + // Generate ApplyDefaults method if needed + if applyDefaults := GenerateApplyDefaults(desc.ShortName, finalFields); applyDefaults != "" { + code += "\n" + applyDefaults + } + + return code +} + +// generateAllOfStructWithUnions generates an allOf struct that contains union fields. +func generateAllOfStructWithUnions(name string, fields []StructField, unionFields []StructField, doc string, tagGen *StructTagGenerator) string { + b := NewCodeBuilder() + + if doc != "" { + for _, line := range strings.Split(doc, "\n") { + b.Line("// %s", line) + } + } + + b.Line("type %s struct {", name) + b.Indent() + + // Regular fields + for _, f := range fields { + tag := generateFieldTag(f, tagGen) + b.Line("%s %s %s", f.Name, f.Type, tag) + } + + // Union fields with json:"-" + for _, f := range unionFields { + b.Line("%s %s `json:\"-\"`", f.Name, f.Type) + } + + b.Dedent() + b.Line("}") + + // Generate MarshalJSON + b.BlankLine() + b.Line("func (s %s) MarshalJSON() ([]byte, error) {", name) + b.Indent() + b.Line("result := make(map[string]any)") + b.BlankLine() + + // Marshal regular fields + for _, f := range fields { + if f.Pointer { + b.Line("if s.%s != nil {", f.Name) + b.Indent() + b.Line("result[%q] = s.%s", f.JSONName, f.Name) + b.Dedent() + b.Line("}") + } else if strings.HasPrefix(f.Type, "[]") || strings.HasPrefix(f.Type, "map[") { + // Slices and maps - only include if not nil + b.Line("if s.%s != nil {", f.Name) + b.Indent() + b.Line("result[%q] = s.%s", f.JSONName, f.Name) + b.Dedent() + b.Line("}") + } else { + b.Line("result[%q] = s.%s", f.JSONName, f.Name) + } + } + + // Marshal and merge union fields + for _, f := range unionFields { + b.BlankLine() + b.Line("if s.%s != nil {", f.Name) + b.Indent() + b.Line("unionData, err := json.Marshal(s.%s)", f.Name) + b.Line("if err != nil {") + b.Indent() + b.Line("return nil, err") + b.Dedent() + b.Line("}") + b.Line("var unionMap map[string]any") + b.Line("if err := json.Unmarshal(unionData, &unionMap); err == nil {") + b.Indent() + b.Line("for k, v := range unionMap {") + b.Indent() + b.Line("result[k] = v") + b.Dedent() + b.Line("}") + b.Dedent() + b.Line("}") + b.Dedent() + b.Line("}") + } + + b.BlankLine() + b.Line("return json.Marshal(result)") + b.Dedent() + b.Line("}") + + // Generate UnmarshalJSON + b.BlankLine() + b.Line("func (s *%s) UnmarshalJSON(data []byte) error {", name) + b.Indent() + + // Unmarshal into raw map for field extraction + b.Line("var raw map[string]json.RawMessage") + b.Line("if err := json.Unmarshal(data, &raw); err != nil {") + b.Indent() + b.Line("return err") + b.Dedent() + b.Line("}") + b.BlankLine() + + // Unmarshal known fields + for _, f := range fields { + b.Line("if v, ok := raw[%q]; ok {", f.JSONName) + b.Indent() + if f.Pointer { + baseType := strings.TrimPrefix(f.Type, "*") + b.Line("var val %s", baseType) + b.Line("if err := json.Unmarshal(v, &val); err != nil {") + b.Indent() + b.Line("return err") + b.Dedent() + b.Line("}") + b.Line("s.%s = &val", f.Name) + } else { + b.Line("if err := json.Unmarshal(v, &s.%s); err != nil {", f.Name) + b.Indent() + b.Line("return err") + b.Dedent() + b.Line("}") + } + b.Dedent() + b.Line("}") + } + + // Unmarshal union fields from the full data + for _, f := range unionFields { + b.BlankLine() + baseType := strings.TrimPrefix(f.Type, "*") + b.Line("var %sVal %s", f.Name, baseType) + b.Line("if err := json.Unmarshal(data, &%sVal); err != nil {", f.Name) + b.Indent() + b.Line("return err") + b.Dedent() + b.Line("}") + b.Line("s.%s = &%sVal", f.Name, f.Name) + } + + b.BlankLine() + b.Line("return nil") + b.Dedent() + b.Line("}") + + return b.String() +} + +// generateAnyOfType generates a union type for anyOf schemas. +func generateAnyOfType(gen *TypeGenerator, desc *SchemaDescriptor) string { + members := collectUnionMembers(gen, desc, desc.AnyOf, desc.Schema.AnyOf) + if len(members) == 0 { + return "" + } + + // anyOf types only need encoding/json (not fmt like oneOf) + gen.AddJSONImport() + + doc := extractDescription(desc.Schema) + code := GenerateUnionType(desc.ShortName, members, false, doc) + + marshalCode := GenerateUnionMarshalAnyOf(desc.ShortName, members) + unmarshalCode := GenerateUnionUnmarshalAnyOf(desc.ShortName, members) + applyDefaultsCode := GenerateUnionApplyDefaults(desc.ShortName, members) + + code += "\n" + marshalCode + "\n" + unmarshalCode + "\n" + applyDefaultsCode + + return code +} + +// generateOneOfType generates a union type for oneOf schemas. +func generateOneOfType(gen *TypeGenerator, desc *SchemaDescriptor) string { + members := collectUnionMembers(gen, desc, desc.OneOf, desc.Schema.OneOf) + if len(members) == 0 { + return "" + } + + // Union types need encoding/json and fmt for marshal/unmarshal + gen.AddJSONImports() + + doc := extractDescription(desc.Schema) + code := GenerateUnionType(desc.ShortName, members, true, doc) + + marshalCode := GenerateUnionMarshalOneOf(desc.ShortName, members) + unmarshalCode := GenerateUnionUnmarshalOneOf(desc.ShortName, members) + applyDefaultsCode := GenerateUnionApplyDefaults(desc.ShortName, members) + + code += "\n" + marshalCode + "\n" + unmarshalCode + "\n" + applyDefaultsCode + + return code +} + +// loadCustomType loads a custom type template and returns its code and imports. +func loadCustomType(templateName string) (string, map[string]string) { + // Lookup the type definition + typeName := strings.TrimSuffix(templateName, ".tmpl") + + // Map template name to type info from registry + var typeDef templates.TypeTemplate + var found bool + + for name, def := range templates.TypeTemplates { + if def.Template == "types/"+templateName || strings.ToLower(name) == typeName { + typeDef = def + found = true + break + } + } + + if !found { + return "", nil + } + + // Read the template file + content, err := templates.TemplateFS.ReadFile("files/" + typeDef.Template) + if err != nil { + return "", nil + } + + // Remove the template comment header + code := string(content) + if idx := strings.Index(code, "}}"); idx != -1 { + code = strings.TrimLeft(code[idx+2:], "\n") + } + + // Build imports map + imports := make(map[string]string) + for _, imp := range typeDef.Imports { + imports[imp.Path] = imp.Alias + } + + return code, imports +} + +// schemaHasApplyDefaults returns true if the schema will have an ApplyDefaults method generated. +// This is true for: +// - Object types with properties +// - Union types (oneOf/anyOf) +// - AllOf types (merged structs) +// This is false for: +// - Primitive types (string, integer, boolean, number) +// - Enum types (without object properties) +// - Arrays +// - Maps (additionalProperties only) +func schemaHasApplyDefaults(schema *base.Schema) bool { + if schema == nil { + return false + } + + // Has properties -> object type with ApplyDefaults + if schema.Properties != nil && schema.Properties.Len() > 0 { + return true + } + + // Has oneOf/anyOf -> union type with ApplyDefaults + if len(schema.OneOf) > 0 || len(schema.AnyOf) > 0 { + return true + } + + // Has allOf -> merged struct with ApplyDefaults + if len(schema.AllOf) > 0 { + return true + } + + return false +} + +// collectUnionMembers gathers union member information for anyOf/oneOf. +func collectUnionMembers(gen *TypeGenerator, parentDesc *SchemaDescriptor, memberDescs []*SchemaDescriptor, memberProxies []*base.SchemaProxy) []UnionMember { + var members []UnionMember + + // Build a map of schema paths to descriptors for lookup + descByPath := make(map[string]*SchemaDescriptor) + for _, desc := range memberDescs { + if desc != nil { + descByPath[desc.Path.String()] = desc + } + } + + for i, proxy := range memberProxies { + var memberType string + var fieldName string + var hasApplyDefaults bool + + if proxy.IsReference() { + ref := proxy.GetReference() + if target, ok := gen.schemaIndex[ref]; ok { + memberType = target.ShortName + fieldName = target.ShortName + hasApplyDefaults = schemaHasApplyDefaults(target.Schema) + } else { + continue + } + } else { + // Check if this inline schema has a descriptor + schema := proxy.Schema() + if schema == nil { + continue + } + + // Determine the path for this member to look up its descriptor + var memberPath SchemaPath + if parentDesc != nil { + // Try to find a descriptor by constructing the expected path + memberPath = parentDesc.Path.Append("anyOf", fmt.Sprintf("%d", i)) + if _, ok := descByPath[memberPath.String()]; !ok { + memberPath = parentDesc.Path.Append("oneOf", fmt.Sprintf("%d", i)) + } + } + + if desc, ok := descByPath[memberPath.String()]; ok && desc.ShortName != "" { + memberType = desc.ShortName + fieldName = desc.ShortName + hasApplyDefaults = schemaHasApplyDefaults(desc.Schema) + } else { + // This is a primitive type that doesn't have a named type + goType := gen.goTypeForSchema(schema, nil) + memberType = goType + // Create a field name based on the type + fieldName = gen.converter.ToTypeName(goType) + fmt.Sprintf("%d", i) + hasApplyDefaults = false // Primitive types don't have ApplyDefaults + } + } + + members = append(members, UnionMember{ + FieldName: fieldName, + TypeName: memberType, + Index: i, + HasApplyDefaults: hasApplyDefaults, + }) + } + + return members +} diff --git a/experimental/internal/codegen/configuration.go b/experimental/internal/codegen/configuration.go new file mode 100644 index 0000000000..c8d1b87388 --- /dev/null +++ b/experimental/internal/codegen/configuration.go @@ -0,0 +1,317 @@ +package codegen + +import ( + "crypto/sha256" + "encoding/hex" + "regexp" + "sort" + "strings" +) + +type Configuration struct { + // PackageName which will be used in all generated files + PackageName string `yaml:"package"` + // Output specifies the output file path + Output string `yaml:"output"` + // Generation controls which parts of the code are generated + Generation GenerationOptions `yaml:"generation,omitempty"` + // TypeMapping allows customizing OpenAPI type/format to Go type mappings + TypeMapping TypeMapping `yaml:"type-mapping,omitempty"` + // NameMangling configures how OpenAPI names are converted to Go identifiers + NameMangling NameMangling `yaml:"name-mangling,omitempty"` + // NameSubstitutions allows direct overrides of generated names + NameSubstitutions NameSubstitutions `yaml:"name-substitutions,omitempty"` + // ImportMapping maps external spec file paths to Go package import paths. + // Example: {"../common/api.yaml": "github.com/org/project/common"} + // Use "-" as the value to indicate types should be in the current package. + ImportMapping map[string]string `yaml:"import-mapping,omitempty"` + // ContentTypes is a list of regexp patterns for media types to generate models for. + // Only request/response bodies with matching content types will have types generated. + // Defaults to common JSON and YAML types if not specified. + ContentTypes []string `yaml:"content-types,omitempty"` + // ContentTypeShortNames maps content type regex patterns to short names for use in type names. + // Example: {"^application/json$": "JSON", "^application/xml$": "XML"} + // These are used when generating response/request type names like "FindPetsJSONResponse". + ContentTypeShortNames []ContentTypeShortName `yaml:"content-type-short-names,omitempty"` + // StructTags configures how struct tags are generated for fields. + // By default, only json tags are generated. + StructTags StructTagsConfig `yaml:"struct-tags,omitempty"` +} + +// ModelsPackage specifies an external package containing the model types. +type ModelsPackage struct { + // Path is the import path for the models package (e.g., "github.com/org/project/models") + Path string `yaml:"path"` + // Alias is an optional import alias. If empty, the last segment of the path is used. + Alias string `yaml:"alias,omitempty"` +} + +// Name returns the package name/alias to use for qualifying types. +// Returns the Alias if set, otherwise derives from the Path. +func (m *ModelsPackage) Name() string { + if m == nil || m.Path == "" { + return "" + } + if m.Alias != "" { + return m.Alias + } + // Derive from path - take last segment + parts := strings.Split(m.Path, "/") + return parts[len(parts)-1] +} + +// Prefix returns the package prefix for qualifying types (e.g., "models."). +// Returns empty string if models are in the same package. +func (m *ModelsPackage) Prefix() string { + name := m.Name() + if name == "" { + return "" + } + return name + "." +} + +// ContentTypeShortName maps a content type pattern to a short name. +type ContentTypeShortName struct { + // Pattern is a regex pattern to match against content types + Pattern string `yaml:"pattern"` + // ShortName is the short name to use in generated type names (e.g., "JSON", "XML") + ShortName string `yaml:"short-name"` +} + +// GenerationOptions controls which parts of the code are generated. +type GenerationOptions struct { + // Server specifies which server framework to generate code for. + // Supported values: "std-http" + // Empty string (default) means no server code is generated. + Server string `yaml:"server,omitempty"` + + // Client enables generation of the HTTP client. + // When true, generates a base Client that returns *http.Response. + Client bool `yaml:"client,omitempty"` + + // SimpleClient enables generation of the SimpleClient wrapper. + // SimpleClient wraps the base Client with typed responses for + // operations that have unambiguous response types. + // Requires Client to also be enabled. + SimpleClient bool `yaml:"simple-client,omitempty"` + + // WebhookInitiator enables generation of webhook initiator code (sends webhook requests). + // Generates a framework-agnostic client that takes the full target URL per-call. + WebhookInitiator bool `yaml:"webhook-initiator,omitempty"` + + // WebhookReceiver enables generation of webhook receiver code (receives webhook requests). + // Generates framework-specific handler functions. Requires Server to be set. + WebhookReceiver bool `yaml:"webhook-receiver,omitempty"` + + // CallbackInitiator enables generation of callback initiator code (sends callback requests). + // Generates a framework-agnostic client that takes the full target URL per-call. + CallbackInitiator bool `yaml:"callback-initiator,omitempty"` + + // CallbackReceiver enables generation of callback receiver code (receives callback requests). + // Generates framework-specific handler functions. Requires Server to be set. + CallbackReceiver bool `yaml:"callback-receiver,omitempty"` + + // ModelsPackage specifies an external package containing the model types. + // When set, models are NOT generated locally - instead, generated code + // imports and references types from this package. + // Example: {path: "github.com/org/project/models"} + ModelsPackage *ModelsPackage `yaml:"models-package,omitempty"` +} + +// ServerType constants for supported server frameworks. +const ( + ServerTypeStdHTTP = "std-http" + ServerTypeChi = "chi" + ServerTypeEcho = "echo" + ServerTypeEchoV4 = "echo/v4" + ServerTypeGin = "gin" + ServerTypeGorilla = "gorilla" + ServerTypeFiber = "fiber" + ServerTypeIris = "iris" +) + +// DefaultContentTypes returns the default list of content type patterns. +// These match common JSON and YAML media types. +func DefaultContentTypes() []string { + return []string{ + `^application/json$`, + `^application/.*\+json$`, + } +} + +// DefaultContentTypeShortNames returns the default content type to short name mappings. +func DefaultContentTypeShortNames() []ContentTypeShortName { + return []ContentTypeShortName{ + {Pattern: `^application/json$`, ShortName: "JSON"}, + {Pattern: `^application/.*\+json$`, ShortName: "JSON"}, + {Pattern: `^application/xml$`, ShortName: "XML"}, + {Pattern: `^application/.*\+xml$`, ShortName: "XML"}, + {Pattern: `^text/xml$`, ShortName: "XML"}, + {Pattern: `^text/plain$`, ShortName: "Text"}, + {Pattern: `^text/html$`, ShortName: "HTML"}, + {Pattern: `^application/octet-stream$`, ShortName: "Binary"}, + {Pattern: `^multipart/form-data$`, ShortName: "Multipart"}, + {Pattern: `^application/x-www-form-urlencoded$`, ShortName: "Form"}, + } +} + +// ApplyDefaults merges user configuration on top of default values. +func (c *Configuration) ApplyDefaults() { + c.TypeMapping = DefaultTypeMapping.Merge(c.TypeMapping) + c.NameMangling = DefaultNameMangling().Merge(c.NameMangling) + if len(c.ContentTypes) == 0 { + c.ContentTypes = DefaultContentTypes() + } + if len(c.ContentTypeShortNames) == 0 { + c.ContentTypeShortNames = DefaultContentTypeShortNames() + } + c.StructTags = DefaultStructTagsConfig().Merge(c.StructTags) +} + +// ContentTypeMatcher checks if content types match configured patterns. +type ContentTypeMatcher struct { + patterns []*regexp.Regexp +} + +// NewContentTypeMatcher creates a matcher from a list of regexp patterns. +// Invalid patterns are silently ignored. +func NewContentTypeMatcher(patterns []string) *ContentTypeMatcher { + m := &ContentTypeMatcher{ + patterns: make([]*regexp.Regexp, 0, len(patterns)), + } + for _, p := range patterns { + if re, err := regexp.Compile(p); err == nil { + m.patterns = append(m.patterns, re) + } + } + return m +} + +// Matches returns true if the content type matches any of the configured patterns. +func (m *ContentTypeMatcher) Matches(contentType string) bool { + for _, re := range m.patterns { + if re.MatchString(contentType) { + return true + } + } + return false +} + +// ContentTypeShortNamer resolves content types to short names for use in type names. +type ContentTypeShortNamer struct { + patterns []*regexp.Regexp + shortNames []string +} + +// NewContentTypeShortNamer creates a short namer from configuration. +func NewContentTypeShortNamer(mappings []ContentTypeShortName) *ContentTypeShortNamer { + n := &ContentTypeShortNamer{ + patterns: make([]*regexp.Regexp, 0, len(mappings)), + shortNames: make([]string, 0, len(mappings)), + } + for _, m := range mappings { + if re, err := regexp.Compile(m.Pattern); err == nil { + n.patterns = append(n.patterns, re) + n.shortNames = append(n.shortNames, m.ShortName) + } + } + return n +} + +// ShortName returns the short name for a content type, or a fallback derived from the content type. +func (n *ContentTypeShortNamer) ShortName(contentType string) string { + for i, re := range n.patterns { + if re.MatchString(contentType) { + return n.shortNames[i] + } + } + // Fallback: derive from content type (e.g., "application/vnd.api+json" -> "VndApiJson") + return deriveContentTypeShortName(contentType) +} + +// deriveContentTypeShortName creates a short name from an unmatched content type. +func deriveContentTypeShortName(contentType string) string { + // Remove "application/", "text/", etc. prefix + if idx := strings.Index(contentType, "/"); idx >= 0 { + contentType = contentType[idx+1:] + } + // Replace non-alphanumeric with spaces for word splitting + var result strings.Builder + capitalizeNext := true + for _, r := range contentType { + if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' { + if capitalizeNext { + if r >= 'a' && r <= 'z' { + r = r - 'a' + 'A' + } + capitalizeNext = false + } + result.WriteRune(r) + } else { + capitalizeNext = true + } + } + return result.String() +} + +// ExternalImport represents an external package import with its alias. +type ExternalImport struct { + Alias string // Short alias for use in generated code (e.g., "ext_a1b2c3") + Path string // Full import path (e.g., "github.com/org/project/common") +} + +// ImportResolver resolves external references to Go package imports. +type ImportResolver struct { + mapping map[string]ExternalImport // spec file path -> import info +} + +// NewImportResolver creates an ImportResolver from the configuration's import mapping. +func NewImportResolver(importMapping map[string]string) *ImportResolver { + resolver := &ImportResolver{ + mapping: make(map[string]ExternalImport), + } + + for specPath, pkgPath := range importMapping { + if pkgPath == "-" { + // "-" means current package, no import needed + resolver.mapping[specPath] = ExternalImport{Alias: "", Path: ""} + } else { + resolver.mapping[specPath] = ExternalImport{ + Alias: hashImportAlias(pkgPath), + Path: pkgPath, + } + } + } + + return resolver +} + +// Resolve looks up an external spec file path and returns its import info. +// Returns nil if the path is not in the mapping. +func (r *ImportResolver) Resolve(specPath string) *ExternalImport { + if imp, ok := r.mapping[specPath]; ok { + return &imp + } + return nil +} + +// AllImports returns all external imports sorted by alias. +func (r *ImportResolver) AllImports() []ExternalImport { + var imports []ExternalImport + for _, imp := range r.mapping { + if imp.Path != "" { // Skip current package markers + imports = append(imports, imp) + } + } + sort.Slice(imports, func(i, j int) bool { + return imports[i].Alias < imports[j].Alias + }) + return imports +} + +// hashImportAlias generates a short, deterministic alias from an import path. +// Uses first 8 characters of SHA256 hash prefixed with "ext_". +func hashImportAlias(importPath string) string { + h := sha256.Sum256([]byte(importPath)) + return "ext_" + hex.EncodeToString(h[:])[:8] +} diff --git a/experimental/internal/codegen/configuration_test.go b/experimental/internal/codegen/configuration_test.go new file mode 100644 index 0000000000..700c65505a --- /dev/null +++ b/experimental/internal/codegen/configuration_test.go @@ -0,0 +1,152 @@ +package codegen + +import ( + "testing" + + "gopkg.in/yaml.v3" +) + +func TestContentTypeMatcher(t *testing.T) { + tests := []struct { + name string + patterns []string + contentType string + want bool + }{ + // Default patterns - JSON only (YAML not supported without custom unmarshalers) + {"json exact", DefaultContentTypes(), "application/json", true}, + {"json+suffix", DefaultContentTypes(), "application/vnd.api+json", true}, + {"problem+json", DefaultContentTypes(), "application/problem+json", true}, + + // YAML not in defaults (would need custom unmarshalers) + {"yaml not default", DefaultContentTypes(), "application/yaml", false}, + {"text/yaml not default", DefaultContentTypes(), "text/yaml", false}, + + // Non-matching + {"text/plain", DefaultContentTypes(), "text/plain", false}, + {"text/html", DefaultContentTypes(), "text/html", false}, + {"application/xml", DefaultContentTypes(), "application/xml", false}, + {"application/octet-stream", DefaultContentTypes(), "application/octet-stream", false}, + {"multipart/form-data", DefaultContentTypes(), "multipart/form-data", false}, + {"image/png", DefaultContentTypes(), "image/png", false}, + + // Custom patterns + {"custom xml", []string{`^application/xml$`}, "application/xml", true}, + {"custom xml no match", []string{`^application/xml$`}, "application/json", false}, + {"custom wildcard", []string{`^text/.*`}, "text/plain", true}, + {"custom wildcard html", []string{`^text/.*`}, "text/html", true}, + {"custom yaml", []string{`^application/yaml$`}, "application/yaml", true}, + + // Empty patterns + {"empty patterns", []string{}, "application/json", false}, + + // Invalid pattern (silently ignored) + {"invalid pattern", []string{`[invalid`}, "application/json", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := NewContentTypeMatcher(tt.patterns) + got := m.Matches(tt.contentType) + if got != tt.want { + t.Errorf("Matches(%q) = %v, want %v", tt.contentType, got, tt.want) + } + }) + } +} + +func TestDefaultContentTypes(t *testing.T) { + defaults := DefaultContentTypes() + if len(defaults) == 0 { + t.Error("DefaultContentTypes() returned empty slice") + } + + // Verify all patterns are valid regexps + m := NewContentTypeMatcher(defaults) + if len(m.patterns) != len(defaults) { + t.Errorf("Some default patterns failed to compile: got %d patterns, want %d", + len(m.patterns), len(defaults)) + } +} + +func TestGenerationOptions_ServerYAML(t *testing.T) { + t.Run("unmarshal server field", func(t *testing.T) { + yamlContent := ` +generation: + server: std-http +` + var cfg Configuration + err := yaml.Unmarshal([]byte(yamlContent), &cfg) + if err != nil { + t.Fatalf("yaml.Unmarshal failed: %v", err) + } + if cfg.Generation.Server != ServerTypeStdHTTP { + t.Errorf("Server = %q, want %q", cfg.Generation.Server, ServerTypeStdHTTP) + } + }) + + t.Run("unmarshal empty server field", func(t *testing.T) { + yamlContent := ` +generation: + no-models: true +` + var cfg Configuration + err := yaml.Unmarshal([]byte(yamlContent), &cfg) + if err != nil { + t.Fatalf("yaml.Unmarshal failed: %v", err) + } + if cfg.Generation.Server != "" { + t.Errorf("Server = %q, want empty string", cfg.Generation.Server) + } + }) + + t.Run("marshal server field", func(t *testing.T) { + cfg := Configuration{ + PackageName: "test", + Generation: GenerationOptions{ + Server: ServerTypeStdHTTP, + }, + } + data, err := yaml.Marshal(&cfg) + if err != nil { + t.Fatalf("yaml.Marshal failed: %v", err) + } + if got := string(data); !contains(got, "server: std-http") { + t.Errorf("Marshaled YAML does not contain 'server: std-http':\n%s", got) + } + }) + + t.Run("omit empty server field", func(t *testing.T) { + cfg := Configuration{ + PackageName: "test", + Generation: GenerationOptions{}, + } + data, err := yaml.Marshal(&cfg) + if err != nil { + t.Fatalf("yaml.Marshal failed: %v", err) + } + if got := string(data); contains(got, "server:") { + t.Errorf("Marshaled YAML should not contain 'server:' when empty:\n%s", got) + } + }) +} + +func TestServerTypeConstants(t *testing.T) { + if ServerTypeStdHTTP != "std-http" { + t.Errorf("ServerTypeStdHTTP = %q, want %q", ServerTypeStdHTTP, "std-http") + } +} + +// contains is a simple helper for string containment check +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsAt(s, substr)) +} + +func containsAt(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/experimental/internal/codegen/extension.go b/experimental/internal/codegen/extension.go new file mode 100644 index 0000000000..f3f48dcbb3 --- /dev/null +++ b/experimental/internal/codegen/extension.go @@ -0,0 +1,337 @@ +// Package codegen provides extension handling for OpenAPI x- properties. +package codegen + +import ( + "fmt" + "strings" + + "github.com/pb33f/libopenapi/orderedmap" + "go.yaml.in/yaml/v4" +) + +// Extension names - new naming convention with x-oapi-codegen- prefix +const ( + // ExtTypeOverride specifies an external type to use instead of generating one. + // Format: "TypeName" or "TypeName;import/path" or "TypeName;alias import/path" + ExtTypeOverride = "x-oapi-codegen-type-override" + + // ExtNameOverride overrides the generated field name. + ExtNameOverride = "x-oapi-codegen-name-override" + + // ExtTypeNameOverride overrides the generated type name. + ExtTypeNameOverride = "x-oapi-codegen-type-name-override" + + // ExtSkipOptionalPointer skips pointer wrapping for optional fields. + ExtSkipOptionalPointer = "x-oapi-codegen-skip-optional-pointer" + + // ExtJSONIgnore excludes the field from JSON marshaling (json:"-"). + ExtJSONIgnore = "x-oapi-codegen-json-ignore" + + // ExtOmitEmpty explicitly controls the omitempty JSON tag. + ExtOmitEmpty = "x-oapi-codegen-omitempty" + + // ExtOmitZero adds omitzero to the JSON tag (Go 1.24+ encoding/json/v2). + ExtOmitZero = "x-oapi-codegen-omitzero" + + // ExtEnumVarNames overrides the generated enum constant names. + ExtEnumVarNames = "x-oapi-codegen-enum-varnames" + + // ExtDeprecatedReason provides a deprecation reason for documentation. + ExtDeprecatedReason = "x-oapi-codegen-deprecated-reason" + + // ExtOrder controls field ordering in generated structs. + ExtOrder = "x-oapi-codegen-order" +) + +// Legacy extension names for backwards compatibility +const ( + legacyExtGoType = "x-go-type" + legacyExtGoTypeImport = "x-go-type-import" + legacyExtGoName = "x-go-name" + legacyExtGoTypeName = "x-go-type-name" + legacyExtGoTypeSkipOptionalPtr = "x-go-type-skip-optional-pointer" + legacyExtGoJSONIgnore = "x-go-json-ignore" + legacyExtOmitEmpty = "x-omitempty" + legacyExtOmitZero = "x-omitzero" + legacyExtEnumVarNames = "x-enum-varnames" + legacyExtEnumNames = "x-enumNames" // Alternative name + legacyExtDeprecatedReason = "x-deprecated-reason" + legacyExtOrder = "x-order" +) + +// TypeOverride represents an external type override with optional import. +type TypeOverride struct { + TypeName string // The Go type name (e.g., "uuid.UUID") + ImportPath string // Import path (e.g., "github.com/google/uuid") + ImportAlias string // Optional import alias (e.g., "foo" for `import foo "..."`) +} + +// Extensions holds parsed extension values for a schema or property. +type Extensions struct { + TypeOverride *TypeOverride // External type to use + NameOverride string // Override field name + TypeNameOverride string // Override generated type name + SkipOptionalPointer *bool // Skip pointer for optional fields + JSONIgnore *bool // Exclude from JSON + OmitEmpty *bool // Control omitempty + OmitZero *bool // Control omitzero + EnumVarNames []string // Override enum constant names + DeprecatedReason string // Deprecation reason + Order *int // Field ordering +} + +// ParseExtensions extracts extension values from a schema's extensions map. +// It supports both new (x-oapi-codegen-*) and legacy (x-go-*) extension names, +// logging deprecation warnings for legacy names. +func ParseExtensions(extensions *orderedmap.Map[string, *yaml.Node], path string) (*Extensions, error) { + if extensions == nil { + return &Extensions{}, nil + } + + ext := &Extensions{} + + // Legacy type override needs special handling: x-go-type and x-go-type-import + // are separate extensions that must be combined + var legacyGoType string + var legacyGoTypeImport any + + for pair := extensions.First(); pair != nil; pair = pair.Next() { + key := pair.Key() + node := pair.Value() + if node == nil { + continue + } + + val := decodeYAMLNode(node) + + switch key { + case ExtTypeOverride: + override, err := parseTypeOverride(val) + if err != nil { + return nil, fmt.Errorf("parsing %s: %w", key, err) + } + ext.TypeOverride = override + + case legacyExtGoType: + if s, ok := val.(string); ok { + legacyGoType = s + } + + case legacyExtGoTypeImport: + legacyGoTypeImport = val + + case ExtNameOverride, legacyExtGoName: + s, err := asString(val, key) + if err != nil { + return nil, err + } + ext.NameOverride = s + + case ExtTypeNameOverride, legacyExtGoTypeName: + s, err := asString(val, key) + if err != nil { + return nil, err + } + ext.TypeNameOverride = s + + case ExtSkipOptionalPointer, legacyExtGoTypeSkipOptionalPtr: + b, err := asBool(val, key) + if err != nil { + return nil, err + } + ext.SkipOptionalPointer = &b + + case ExtJSONIgnore, legacyExtGoJSONIgnore: + b, err := asBool(val, key) + if err != nil { + return nil, err + } + ext.JSONIgnore = &b + + case ExtOmitEmpty, legacyExtOmitEmpty: + b, err := asBool(val, key) + if err != nil { + return nil, err + } + ext.OmitEmpty = &b + + case ExtOmitZero, legacyExtOmitZero: + b, err := asBool(val, key) + if err != nil { + return nil, err + } + ext.OmitZero = &b + + case ExtEnumVarNames, legacyExtEnumVarNames, legacyExtEnumNames: + s, err := asStringSlice(val, key) + if err != nil { + return nil, err + } + ext.EnumVarNames = s + + case ExtDeprecatedReason, legacyExtDeprecatedReason: + s, err := asString(val, key) + if err != nil { + return nil, err + } + ext.DeprecatedReason = s + + case ExtOrder, legacyExtOrder: + i, err := asInt(val, key) + if err != nil { + return nil, err + } + ext.Order = &i + + default: + // Unknown extension - ignore + } + } + + // Combine legacy x-go-type and x-go-type-import if no new-style override was set + if ext.TypeOverride == nil && legacyGoType != "" { + ext.TypeOverride = buildLegacyTypeOverride(legacyGoType, legacyGoTypeImport) + } + + return ext, nil +} + +// hasExtension checks if an extension exists by either the new or legacy name. +// This is used to check for extensions before fully parsing them. +func hasExtension(extensions *orderedmap.Map[string, *yaml.Node], newName, legacyName string) bool { + if extensions == nil { + return false + } + + for pair := extensions.First(); pair != nil; pair = pair.Next() { + key := pair.Key() + if key == newName || key == legacyName { + return true + } + } + return false +} + +// decodeYAMLNode converts a yaml.Node to a Go value. +func decodeYAMLNode(node *yaml.Node) any { + if node == nil { + return nil + } + + var result any + if err := node.Decode(&result); err != nil { + return nil + } + return result +} + +// parseTypeOverride parses the new combined type override format. +// Format: "TypeName" or "TypeName;import/path" or "TypeName;alias import/path" +func parseTypeOverride(val any) (*TypeOverride, error) { + str, ok := val.(string) + if !ok { + return nil, fmt.Errorf("expected string, got %T", val) + } + + override := &TypeOverride{} + parts := strings.SplitN(str, ";", 2) + override.TypeName = strings.TrimSpace(parts[0]) + + if len(parts) == 2 { + importPart := strings.TrimSpace(parts[1]) + importParts := strings.SplitN(importPart, " ", 2) + if len(importParts) == 2 { + override.ImportAlias = strings.TrimSpace(importParts[0]) + override.ImportPath = strings.TrimSpace(importParts[1]) + } else { + override.ImportPath = importPart + } + } + + return override, nil +} + +// buildLegacyTypeOverride combines legacy x-go-type and x-go-type-import values. +func buildLegacyTypeOverride(typeName string, importVal any) *TypeOverride { + override := &TypeOverride{ + TypeName: typeName, + } + + if importVal == nil { + return override + } + + // Legacy import can be a string or an object with path/name + switch v := importVal.(type) { + case string: + override.ImportPath = v + case map[string]any: + if p, ok := v["path"].(string); ok { + override.ImportPath = p + } + if name, ok := v["name"].(string); ok { + override.ImportAlias = name + } + } + + return override +} + +// Type conversion helpers that include the extension name in error messages + +func asString(val any, extName string) (string, error) { + if val == nil { + return "", nil + } + str, ok := val.(string) + if !ok { + return "", fmt.Errorf("parsing %s: expected string, got %T", extName, val) + } + return str, nil +} + +func asBool(val any, extName string) (bool, error) { + if val == nil { + return false, nil + } + b, ok := val.(bool) + if !ok { + return false, fmt.Errorf("parsing %s: expected bool, got %T", extName, val) + } + return b, nil +} + +func asInt(val any, extName string) (int, error) { + if val == nil { + return 0, nil + } + switch v := val.(type) { + case int: + return v, nil + case int64: + return int(v), nil + case float64: + return int(v), nil + default: + return 0, fmt.Errorf("parsing %s: expected int, got %T", extName, val) + } +} + +func asStringSlice(val any, extName string) ([]string, error) { + if val == nil { + return nil, nil + } + slice, ok := val.([]any) + if !ok { + return nil, fmt.Errorf("parsing %s: expected array, got %T", extName, val) + } + result := make([]string, len(slice)) + for i, v := range slice { + str, ok := v.(string) + if !ok { + return nil, fmt.Errorf("parsing %s: expected string at index %d, got %T", extName, i, v) + } + result[i] = str + } + return result, nil +} diff --git a/experimental/internal/codegen/extension_integration_test.go b/experimental/internal/codegen/extension_integration_test.go new file mode 100644 index 0000000000..c100af7333 --- /dev/null +++ b/experimental/internal/codegen/extension_integration_test.go @@ -0,0 +1,201 @@ +package codegen + +import ( + "strings" + "testing" + + "github.com/pb33f/libopenapi" +) + +func TestExtensionIntegration(t *testing.T) { + spec := ` +openapi: "3.1.0" +info: + title: Extension Test API + version: "1.0" +paths: {} +components: + schemas: + # Test type-name-override + MySchema: + type: object + x-oapi-codegen-type-name-override: CustomTypeName + properties: + id: + type: string + + # Test type-override at schema level + ExternalType: + type: string + x-oapi-codegen-type-override: "uuid.UUID;github.com/google/uuid" + + # Test field-level extensions + User: + type: object + properties: + # Test name override + user_id: + type: string + x-oapi-codegen-name-override: UserID + # Test type override + created_at: + type: string + x-oapi-codegen-type-override: "time.Time;time" + # Test skip optional pointer + description: + type: string + x-oapi-codegen-skip-optional-pointer: true + # Test omitempty control + status: + type: string + x-oapi-codegen-omitempty: false + # Test omitzero + count: + type: integer + x-oapi-codegen-omitzero: true + # Test order (count should come before status) + age: + type: integer + x-oapi-codegen-order: 1 + name: + type: string + x-oapi-codegen-order: 0 + # Test deprecated reason + old_field: + type: string + x-oapi-codegen-deprecated-reason: "Use new_field instead" + + # Test enum with custom var names + Status: + type: string + enum: + - active + - inactive + - pending + x-oapi-codegen-enum-varnames: + - Active + - Inactive + - Pending +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + if err != nil { + t.Fatalf("Failed to parse spec: %v", err) + } + + cfg := Configuration{ + PackageName: "output", + } + + code, err := Generate(doc, nil, cfg) + if err != nil { + t.Fatalf("Generate failed: %v", err) + } + + t.Logf("Generated code:\n%s", code) + + // Verify type-name-override + if !strings.Contains(code, "type CustomTypeName") { + t.Error("Expected CustomTypeName type from type-name-override") + } + + // Verify type-override at schema level creates alias + if !strings.Contains(code, "= uuid.UUID") { + t.Error("Expected type alias to uuid.UUID from type-override") + } + if !strings.Contains(code, `"github.com/google/uuid"`) { + t.Error("Expected uuid import") + } + + // Verify name override + if !strings.Contains(code, "UserID") { + t.Error("Expected UserID field from name-override") + } + + // Verify type override on field + if !strings.Contains(code, "time.Time") { + t.Error("Expected time.Time from field type-override") + } + if !strings.Contains(code, `"time"`) { + t.Error("Expected time import") + } + + // Verify skip-optional-pointer (description should be string, not *string) + // The field should appear as just "string", not "*string" + if strings.Contains(code, "Description *string") || strings.Contains(code, "Description *string") { + t.Error("Expected description to not be a pointer due to skip-optional-pointer") + } + if !strings.Contains(code, "Description string") && !strings.Contains(code, "Description string") { + t.Error("Expected description to be a non-pointer string") + } + + // Verify omitzero + if !strings.Contains(code, "omitzero") { + t.Error("Expected omitzero in struct tags") + } + + // Verify deprecated reason in doc + if !strings.Contains(code, "Deprecated:") { + t.Error("Expected Deprecated: in documentation") + } + + // Verify enum with custom var names + if !strings.Contains(code, "Status_Active") { + t.Error("Expected Status_Active from custom enum var names") + } + if !strings.Contains(code, "Status_Inactive") { + t.Error("Expected Status_Inactive from custom enum var names") + } + if !strings.Contains(code, "Status_Pending") { + t.Error("Expected Status_Pending from custom enum var names") + } +} + +func TestLegacyExtensionIntegration(t *testing.T) { + spec := ` +openapi: "3.1.0" +info: + title: Legacy Extension Test API + version: "1.0" +paths: {} +components: + schemas: + User: + type: object + properties: + # Test legacy x-go-type + id: + type: string + x-go-type: mypackage.ID + # Test legacy x-go-name + user_name: + type: string + x-go-name: Username +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + if err != nil { + t.Fatalf("Failed to parse spec: %v", err) + } + + cfg := Configuration{ + PackageName: "output", + } + + code, err := Generate(doc, nil, cfg) + if err != nil { + t.Fatalf("Generate failed: %v", err) + } + + t.Logf("Generated code:\n%s", code) + + // Verify legacy x-go-type works + if !strings.Contains(code, "mypackage.ID") { + t.Error("Expected mypackage.ID from legacy x-go-type") + } + + // Verify legacy x-go-name works + if !strings.Contains(code, "Username") { + t.Error("Expected Username from legacy x-go-name") + } +} diff --git a/experimental/internal/codegen/extension_test.go b/experimental/internal/codegen/extension_test.go new file mode 100644 index 0000000000..c0f615fc41 --- /dev/null +++ b/experimental/internal/codegen/extension_test.go @@ -0,0 +1,180 @@ +package codegen + +import ( + "testing" + + "github.com/pb33f/libopenapi/orderedmap" + "go.yaml.in/yaml/v4" +) + +func TestParseTypeOverride(t *testing.T) { + tests := []struct { + name string + input string + wantType string + wantPath string + wantAlias string + }{ + { + name: "simple type", + input: "int64", + wantType: "int64", + }, + { + name: "type with import", + input: "uuid.UUID;github.com/google/uuid", + wantType: "uuid.UUID", + wantPath: "github.com/google/uuid", + }, + { + name: "type with aliased import", + input: "foo.Type;foo github.com/bar/foo/v2", + wantType: "foo.Type", + wantPath: "github.com/bar/foo/v2", + wantAlias: "foo", + }, + { + name: "type with spaces", + input: " decimal.Decimal ; github.com/shopspring/decimal ", + wantType: "decimal.Decimal", + wantPath: "github.com/shopspring/decimal", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseTypeOverride(tt.input) + if err != nil { + t.Fatalf("parseTypeOverride() error = %v", err) + } + if got.TypeName != tt.wantType { + t.Errorf("TypeName = %q, want %q", got.TypeName, tt.wantType) + } + if got.ImportPath != tt.wantPath { + t.Errorf("ImportPath = %q, want %q", got.ImportPath, tt.wantPath) + } + if got.ImportAlias != tt.wantAlias { + t.Errorf("ImportAlias = %q, want %q", got.ImportAlias, tt.wantAlias) + } + }) + } +} + +func TestParseExtensions(t *testing.T) { + // Create a test extensions map + extensions := orderedmap.New[string, *yaml.Node]() + + // Add a type override extension + typeOverrideNode := &yaml.Node{} + typeOverrideNode.SetString("uuid.UUID;github.com/google/uuid") + extensions.Set(ExtTypeOverride, typeOverrideNode) + + // Add a name override extension + nameOverrideNode := &yaml.Node{} + nameOverrideNode.SetString("CustomFieldName") + extensions.Set(ExtNameOverride, nameOverrideNode) + + // Add omitempty extension + omitEmptyNode := &yaml.Node{} + if err := omitEmptyNode.Encode(true); err != nil { + t.Fatalf("Failed to encode omitEmptyNode: %v", err) + } + extensions.Set(ExtOmitEmpty, omitEmptyNode) + + ext, err := ParseExtensions(extensions, "#/test/path") + if err != nil { + t.Fatalf("ParseExtensions() error = %v", err) + } + + // Check type override + if ext.TypeOverride == nil { + t.Fatal("TypeOverride should not be nil") + } + if ext.TypeOverride.TypeName != "uuid.UUID" { + t.Errorf("TypeOverride.TypeName = %q, want %q", ext.TypeOverride.TypeName, "uuid.UUID") + } + if ext.TypeOverride.ImportPath != "github.com/google/uuid" { + t.Errorf("TypeOverride.ImportPath = %q, want %q", ext.TypeOverride.ImportPath, "github.com/google/uuid") + } + + // Check name override + if ext.NameOverride != "CustomFieldName" { + t.Errorf("NameOverride = %q, want %q", ext.NameOverride, "CustomFieldName") + } + + // Check omitempty + if ext.OmitEmpty == nil || *ext.OmitEmpty != true { + t.Errorf("OmitEmpty = %v, want true", ext.OmitEmpty) + } +} + +func TestParseExtensionsLegacy(t *testing.T) { + // Create a test extensions map with legacy names + extensions := orderedmap.New[string, *yaml.Node]() + + // Add legacy x-go-type extension + goTypeNode := &yaml.Node{} + goTypeNode.SetString("time.Time") + extensions.Set("x-go-type", goTypeNode) + + // Add legacy x-go-type-import extension + goImportNode := &yaml.Node{} + goImportNode.SetString("time") + extensions.Set("x-go-type-import", goImportNode) + + // Add legacy x-go-name extension + goNameNode := &yaml.Node{} + goNameNode.SetString("LegacyFieldName") + extensions.Set("x-go-name", goNameNode) + + ext, err := ParseExtensions(extensions, "#/test/path") + if err != nil { + t.Fatalf("ParseExtensions() error = %v", err) + } + + // Check type override (from legacy) + if ext.TypeOverride == nil { + t.Fatal("TypeOverride should not be nil") + } + if ext.TypeOverride.TypeName != "time.Time" { + t.Errorf("TypeOverride.TypeName = %q, want %q", ext.TypeOverride.TypeName, "time.Time") + } + if ext.TypeOverride.ImportPath != "time" { + t.Errorf("TypeOverride.ImportPath = %q, want %q", ext.TypeOverride.ImportPath, "time") + } + + // Check name override (from legacy) + if ext.NameOverride != "LegacyFieldName" { + t.Errorf("NameOverride = %q, want %q", ext.NameOverride, "LegacyFieldName") + } +} + +func TestParseExtensionsEnumVarNames(t *testing.T) { + extensions := orderedmap.New[string, *yaml.Node]() + + // Add enum var names as a sequence + enumNamesNode := &yaml.Node{ + Kind: yaml.SequenceNode, + Content: []*yaml.Node{ + {Kind: yaml.ScalarNode, Value: "Active"}, + {Kind: yaml.ScalarNode, Value: "Inactive"}, + {Kind: yaml.ScalarNode, Value: "Pending"}, + }, + } + extensions.Set(ExtEnumVarNames, enumNamesNode) + + ext, err := ParseExtensions(extensions, "#/test/path") + if err != nil { + t.Fatalf("ParseExtensions() error = %v", err) + } + + if len(ext.EnumVarNames) != 3 { + t.Fatalf("EnumVarNames length = %d, want 3", len(ext.EnumVarNames)) + } + expected := []string{"Active", "Inactive", "Pending"} + for i, name := range ext.EnumVarNames { + if name != expected[i] { + t.Errorf("EnumVarNames[%d] = %q, want %q", i, name, expected[i]) + } + } +} diff --git a/experimental/internal/codegen/gather.go b/experimental/internal/codegen/gather.go new file mode 100644 index 0000000000..0644ed7897 --- /dev/null +++ b/experimental/internal/codegen/gather.go @@ -0,0 +1,621 @@ +package codegen + +import ( + "fmt" + "log/slog" + + "github.com/pb33f/libopenapi" + "github.com/pb33f/libopenapi/datamodel/high/base" + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" +) + +// GatherResult contains the results of gathering from an OpenAPI document. +type GatherResult struct { + Schemas []*SchemaDescriptor + ParamTracker *ParamUsageTracker +} + +// GatherSchemas traverses an OpenAPI document and collects all schemas into a list. +func GatherSchemas(doc libopenapi.Document, contentTypeMatcher *ContentTypeMatcher) ([]*SchemaDescriptor, error) { + result, err := GatherAll(doc, contentTypeMatcher) + if err != nil { + return nil, err + } + return result.Schemas, nil +} + +// GatherAll traverses an OpenAPI document and collects all schemas and parameter usage. +func GatherAll(doc libopenapi.Document, contentTypeMatcher *ContentTypeMatcher) (*GatherResult, error) { + model, err := doc.BuildV3Model() + if err != nil { + return nil, fmt.Errorf("building v3 model: %w", err) + } + if model == nil { + return nil, fmt.Errorf("failed to build v3 model") + } + + g := &gatherer{ + schemas: make([]*SchemaDescriptor, 0), + contentTypeMatcher: contentTypeMatcher, + paramTracker: NewParamUsageTracker(), + } + + g.gatherFromDocument(&model.Model) + return &GatherResult{ + Schemas: g.schemas, + ParamTracker: g.paramTracker, + }, nil +} + +type gatherer struct { + schemas []*SchemaDescriptor + contentTypeMatcher *ContentTypeMatcher + paramTracker *ParamUsageTracker + // Context for the current operation being gathered (for nicer naming) + currentOperationID string + currentContentType string +} + +func (g *gatherer) gatherFromDocument(doc *v3.Document) { + // Gather from components/schemas + if doc.Components != nil && doc.Components.Schemas != nil { + for pair := doc.Components.Schemas.First(); pair != nil; pair = pair.Next() { + name := pair.Key() + schemaProxy := pair.Value() + path := SchemaPath{"components", "schemas", name} + g.gatherFromSchemaProxy(schemaProxy, path, nil) + } + } + + // Gather from paths + if doc.Paths != nil && doc.Paths.PathItems != nil { + for pair := doc.Paths.PathItems.First(); pair != nil; pair = pair.Next() { + pathStr := pair.Key() + pathItem := pair.Value() + g.gatherFromPathItem(pathItem, SchemaPath{"paths", pathStr}) + } + } + + // Gather from webhooks (3.1+) + if doc.Webhooks != nil { + for pair := doc.Webhooks.First(); pair != nil; pair = pair.Next() { + name := pair.Key() + pathItem := pair.Value() + g.gatherFromPathItem(pathItem, SchemaPath{"webhooks", name}) + } + } +} + +func (g *gatherer) gatherFromPathItem(pathItem *v3.PathItem, basePath SchemaPath) { + if pathItem == nil { + return + } + + // Path-level parameters + for i, param := range pathItem.Parameters { + g.gatherFromParameter(param, basePath.Append("parameters", fmt.Sprintf("%d", i))) + } + + // Operations + ops := pathItem.GetOperations() + if ops != nil { + for pair := ops.First(); pair != nil; pair = pair.Next() { + method := pair.Key() + op := pair.Value() + g.gatherFromOperation(op, basePath.Append(method)) + } + } +} + +func (g *gatherer) gatherFromOperation(op *v3.Operation, basePath SchemaPath) { + if op == nil { + return + } + + // Set operation context for nicer naming + prevOperationID := g.currentOperationID + if op.OperationId != "" { + g.currentOperationID = op.OperationId + } + + // Parameters + for i, param := range op.Parameters { + g.gatherFromParameter(param, basePath.Append("parameters", fmt.Sprintf("%d", i))) + } + + // Request body + if op.RequestBody != nil { + g.gatherFromRequestBody(op.RequestBody, basePath.Append("requestBody")) + } + + // Responses + if op.Responses != nil && op.Responses.Codes != nil { + for pair := op.Responses.Codes.First(); pair != nil; pair = pair.Next() { + code := pair.Key() + response := pair.Value() + g.gatherFromResponse(response, basePath.Append("responses", code)) + } + } + + // Callbacks + if op.Callbacks != nil { + for pair := op.Callbacks.First(); pair != nil; pair = pair.Next() { + name := pair.Key() + callback := pair.Value() + g.gatherFromCallback(callback, basePath.Append("callbacks", name)) + } + } + + // Restore previous operation context + g.currentOperationID = prevOperationID +} + +func (g *gatherer) gatherFromParameter(param *v3.Parameter, basePath SchemaPath) { + if param == nil { + return + } + + // Track parameter styling usage for code generation + if g.paramTracker != nil && param.Schema != nil { + // Determine style (with defaults based on location) + style := param.Style + if style == "" { + style = DefaultParamStyle(param.In) + } + + // Determine explode (with defaults based on location) + explode := DefaultParamExplode(param.In) + if param.Explode != nil { + explode = *param.Explode + } + + // Record both style (client) and bind (server) usage + g.paramTracker.RecordParam(style, explode) + } + + if param.Schema != nil { + g.gatherFromSchemaProxy(param.Schema, basePath.Append("schema"), nil) + } + + // Parameter can also have content with schemas + if param.Content != nil { + for pair := param.Content.First(); pair != nil; pair = pair.Next() { + contentType := pair.Key() + mediaType := pair.Value() + g.gatherFromMediaType(mediaType, basePath.Append("content", contentType)) + } + } +} + +func (g *gatherer) gatherFromRequestBody(rb *v3.RequestBody, basePath SchemaPath) { + if rb == nil || rb.Content == nil { + return + } + + for pair := rb.Content.First(); pair != nil; pair = pair.Next() { + contentType := pair.Key() + // Skip content types that don't match the configured patterns + if g.contentTypeMatcher != nil && !g.contentTypeMatcher.Matches(contentType) { + continue + } + // Set content type context + prevContentType := g.currentContentType + g.currentContentType = contentType + + mediaType := pair.Value() + g.gatherFromMediaType(mediaType, basePath.Append("content", contentType)) + + g.currentContentType = prevContentType + } +} + +func (g *gatherer) gatherFromResponse(response *v3.Response, basePath SchemaPath) { + if response == nil { + return + } + + if response.Content != nil { + for pair := response.Content.First(); pair != nil; pair = pair.Next() { + contentType := pair.Key() + // Skip content types that don't match the configured patterns + if g.contentTypeMatcher != nil && !g.contentTypeMatcher.Matches(contentType) { + continue + } + // Set content type context + prevContentType := g.currentContentType + g.currentContentType = contentType + + mediaType := pair.Value() + g.gatherFromMediaType(mediaType, basePath.Append("content", contentType)) + + g.currentContentType = prevContentType + } + } + + // Response headers can have schemas + if response.Headers != nil { + for pair := response.Headers.First(); pair != nil; pair = pair.Next() { + name := pair.Key() + header := pair.Value() + if header != nil && header.Schema != nil { + g.gatherFromSchemaProxy(header.Schema, basePath.Append("headers", name, "schema"), nil) + } + } + } +} + +func (g *gatherer) gatherFromMediaType(mt *v3.MediaType, basePath SchemaPath) { + if mt == nil || mt.Schema == nil { + return + } + g.gatherFromSchemaProxy(mt.Schema, basePath.Append("schema"), nil) +} + +func (g *gatherer) gatherFromCallback(callback *v3.Callback, basePath SchemaPath) { + if callback == nil || callback.Expression == nil { + return + } + + for pair := callback.Expression.First(); pair != nil; pair = pair.Next() { + expr := pair.Key() + pathItem := pair.Value() + g.gatherFromPathItem(pathItem, basePath.Append(expr)) + } +} + +func (g *gatherer) gatherFromSchemaProxy(proxy *base.SchemaProxy, path SchemaPath, parent *SchemaDescriptor) *SchemaDescriptor { + if proxy == nil { + return nil + } + + // Check if this is a reference + isRef := proxy.IsReference() + ref := "" + if isRef { + ref = proxy.GetReference() + } + + // Get the resolved schema + schema := proxy.Schema() + + // Check if schema has extensions that require type generation + hasTypeOverride := schema != nil && schema.Extensions != nil && hasExtension(schema.Extensions, ExtTypeOverride, legacyExtGoType) + hasTypeNameOverride := schema != nil && schema.Extensions != nil && hasExtension(schema.Extensions, ExtTypeNameOverride, legacyExtGoTypeName) + + // Only gather schemas that need a generated type + // References are always gathered (they point to real schemas) + // Simple types (primitives without enum) are skipped + // Inline nullable primitives (under properties/) don't need types - they use Nullable[T] directly + // Schemas with type-override or type-name-override extensions always need types + isInlineProperty := path.ContainsProperties() + skipInlineNullablePrimitive := isInlineProperty && isNullablePrimitive(schema) + needsType := isRef || needsGeneratedType(schema) || hasTypeOverride || hasTypeNameOverride + if needsType && !skipInlineNullablePrimitive { + desc := &SchemaDescriptor{ + Path: path, + Parent: parent, + Ref: ref, + Schema: schema, + OperationID: g.currentOperationID, + ContentType: g.currentContentType, + } + + // Parse extensions from the schema + if schema != nil && schema.Extensions != nil { + ext, err := ParseExtensions(schema.Extensions, path.String()) + if err != nil { + slog.Warn("failed to parse extensions", + "path", path.String(), + "error", err) + } else { + desc.Extensions = ext + } + } + + g.schemas = append(g.schemas, desc) + + // Don't recurse into references - they point to schemas we'll gather elsewhere + if isRef { + return desc + } + + // Recurse into schema structure + if schema != nil { + g.gatherFromSchema(schema, path, desc) + } + return desc + } else if schema != nil { + // Even if we don't gather this schema, we still need to recurse + // to find any nested complex schemas (e.g., array items that are objects) + g.gatherFromSchema(schema, path, nil) + } + return nil +} + +// gatherSchemaDescriptorOnly creates a descriptor for field extraction without adding it +// to the schemas list (i.e., no type will be generated for it). +// This is used for inline allOf members whose fields are flattened into the parent. +func (g *gatherer) gatherSchemaDescriptorOnly(proxy *base.SchemaProxy, path SchemaPath, parent *SchemaDescriptor) *SchemaDescriptor { + if proxy == nil { + return nil + } + + schema := proxy.Schema() + if schema == nil { + return nil + } + + desc := &SchemaDescriptor{ + Path: path, + Parent: parent, + Schema: schema, + } + + // Parse extensions from the schema + if schema.Extensions != nil { + ext, err := ParseExtensions(schema.Extensions, path.String()) + if err != nil { + slog.Warn("failed to parse extensions", + "path", path.String(), + "error", err) + } else { + desc.Extensions = ext + } + } + + // Still recurse to gather any nested complex schemas that DO need types + // (e.g., nested objects within properties) + g.gatherFromSchema(schema, path, desc) + + return desc +} + +// needsGeneratedType returns true if a schema requires a generated Go type. +// Simple primitive types (string, integer, number, boolean) without enums +// don't need generated types - they map directly to Go builtins. +// However, nullable primitives DO need generated types (Nullable[T]). +func needsGeneratedType(schema *base.Schema) bool { + if schema == nil { + return false + } + + // Nullable primitives need a generated type (Nullable[T]) + if isNullablePrimitive(schema) { + return true + } + + // Enums always need a generated type + if len(schema.Enum) > 0 { + return true + } + + // Objects need a generated type + if schema.Properties != nil && schema.Properties.Len() > 0 { + return true + } + + // Check explicit type + types := schema.Type + for _, t := range types { + if t == "object" { + return true + } + } + + // Composition types need generated types + if len(schema.AllOf) > 0 || len(schema.AnyOf) > 0 || len(schema.OneOf) > 0 { + return true + } + + // Arrays with complex items need generated types for the array type itself + // But we handle items separately in gatherFromSchema + if schema.Items != nil && schema.Items.A != nil { + itemSchema := schema.Items.A.Schema() + if needsGeneratedType(itemSchema) { + return true + } + } + + // AdditionalProperties with complex schema needs a type + if schema.AdditionalProperties != nil && schema.AdditionalProperties.A != nil { + addSchema := schema.AdditionalProperties.A.Schema() + if needsGeneratedType(addSchema) { + return true + } + } + + // Simple primitives (string, integer, number, boolean) without enum + // don't need generated types + return false +} + +// isNullablePrimitive returns true if the schema is a nullable primitive type. +// Nullable primitives need Nullable[T] wrapper types. +func isNullablePrimitive(schema *base.Schema) bool { + if schema == nil { + return false + } + + // Check for nullable + isNullable := false + // OpenAPI 3.1 style: type array includes "null" + for _, t := range schema.Type { + if t == "null" { + isNullable = true + break + } + } + // OpenAPI 3.0 style: nullable: true + if schema.Nullable != nil && *schema.Nullable { + isNullable = true + } + + if !isNullable { + return false + } + + // Check if it's a primitive type (not object, array, or composition) + if schema.Properties != nil && schema.Properties.Len() > 0 { + return false // object with properties + } + if len(schema.AllOf) > 0 || len(schema.AnyOf) > 0 || len(schema.OneOf) > 0 { + return false // composition type + } + if schema.Items != nil { + return false // array + } + + // Get the primary type + for _, t := range schema.Type { + switch t { + case "string", "integer", "number", "boolean": + return true + case "object": + return false + case "array": + return false + } + } + + return false +} + +func (g *gatherer) gatherFromSchema(schema *base.Schema, basePath SchemaPath, parent *SchemaDescriptor) { + if schema == nil { + return + } + + // Properties + if schema.Properties != nil { + if parent != nil { + parent.Properties = make(map[string]*SchemaDescriptor) + } + for pair := schema.Properties.First(); pair != nil; pair = pair.Next() { + propName := pair.Key() + propProxy := pair.Value() + propPath := basePath.Append("properties", propName) + propDesc := g.gatherFromSchemaProxy(propProxy, propPath, parent) + if parent != nil && propDesc != nil { + parent.Properties[propName] = propDesc + } + } + } + + // Items (array element schema) + if schema.Items != nil && schema.Items.A != nil { + itemsPath := basePath.Append("items") + itemsDesc := g.gatherFromSchemaProxy(schema.Items.A, itemsPath, parent) + if parent != nil && itemsDesc != nil { + parent.Items = itemsDesc + } + } + + // AllOf - inline object members don't need separate types since fields are flattened into parent + // However, inline oneOf/anyOf members DO need union types generated + for i, proxy := range schema.AllOf { + allOfPath := basePath.Append("allOf", fmt.Sprintf("%d", i)) + var allOfDesc *SchemaDescriptor + if proxy.IsReference() { + // References still need to be gathered normally + allOfDesc = g.gatherFromSchemaProxy(proxy, allOfPath, parent) + } else { + memberSchema := proxy.Schema() + // If the allOf member is itself a oneOf/anyOf, we need to generate a union type + if memberSchema != nil && (len(memberSchema.OneOf) > 0 || len(memberSchema.AnyOf) > 0) { + allOfDesc = g.gatherFromSchemaProxy(proxy, allOfPath, parent) + } else { + // Simple inline objects: create descriptor for field extraction but don't generate a type + allOfDesc = g.gatherSchemaDescriptorOnly(proxy, allOfPath, parent) + } + } + if parent != nil && allOfDesc != nil { + parent.AllOf = append(parent.AllOf, allOfDesc) + } + } + + // AnyOf + for i, proxy := range schema.AnyOf { + anyOfPath := basePath.Append("anyOf", fmt.Sprintf("%d", i)) + anyOfDesc := g.gatherFromSchemaProxy(proxy, anyOfPath, parent) + if parent != nil && anyOfDesc != nil { + parent.AnyOf = append(parent.AnyOf, anyOfDesc) + } + } + + // OneOf + for i, proxy := range schema.OneOf { + oneOfPath := basePath.Append("oneOf", fmt.Sprintf("%d", i)) + oneOfDesc := g.gatherFromSchemaProxy(proxy, oneOfPath, parent) + if parent != nil && oneOfDesc != nil { + parent.OneOf = append(parent.OneOf, oneOfDesc) + } + } + + // AdditionalProperties (if it's a schema, not a boolean) + if schema.AdditionalProperties != nil && schema.AdditionalProperties.A != nil { + addPropsPath := basePath.Append("additionalProperties") + addPropsDesc := g.gatherFromSchemaProxy(schema.AdditionalProperties.A, addPropsPath, parent) + if parent != nil && addPropsDesc != nil { + parent.AdditionalProps = addPropsDesc + } + } + + // Not + if schema.Not != nil { + g.gatherFromSchemaProxy(schema.Not, basePath.Append("not"), parent) + } + + // PrefixItems (3.1 tuple validation) + for i, proxy := range schema.PrefixItems { + g.gatherFromSchemaProxy(proxy, basePath.Append("prefixItems", fmt.Sprintf("%d", i)), parent) + } + + // Contains (3.1) + if schema.Contains != nil { + g.gatherFromSchemaProxy(schema.Contains, basePath.Append("contains"), parent) + } + + // If/Then/Else (3.1) + if schema.If != nil { + g.gatherFromSchemaProxy(schema.If, basePath.Append("if"), parent) + } + if schema.Then != nil { + g.gatherFromSchemaProxy(schema.Then, basePath.Append("then"), parent) + } + if schema.Else != nil { + g.gatherFromSchemaProxy(schema.Else, basePath.Append("else"), parent) + } + + // DependentSchemas (3.1) + if schema.DependentSchemas != nil { + for pair := schema.DependentSchemas.First(); pair != nil; pair = pair.Next() { + name := pair.Key() + proxy := pair.Value() + g.gatherFromSchemaProxy(proxy, basePath.Append("dependentSchemas", name), parent) + } + } + + // PatternProperties (3.1) + if schema.PatternProperties != nil { + for pair := schema.PatternProperties.First(); pair != nil; pair = pair.Next() { + pattern := pair.Key() + proxy := pair.Value() + g.gatherFromSchemaProxy(proxy, basePath.Append("patternProperties", pattern), parent) + } + } + + // PropertyNames (3.1) + if schema.PropertyNames != nil { + g.gatherFromSchemaProxy(schema.PropertyNames, basePath.Append("propertyNames"), parent) + } + + // UnevaluatedItems (3.1) + if schema.UnevaluatedItems != nil { + g.gatherFromSchemaProxy(schema.UnevaluatedItems, basePath.Append("unevaluatedItems"), parent) + } + + // UnevaluatedProperties (3.1) - can be schema or bool + if schema.UnevaluatedProperties != nil && schema.UnevaluatedProperties.A != nil { + g.gatherFromSchemaProxy(schema.UnevaluatedProperties.A, basePath.Append("unevaluatedProperties"), parent) + } +} diff --git a/experimental/internal/codegen/gather_operations.go b/experimental/internal/codegen/gather_operations.go new file mode 100644 index 0000000000..0492bbb433 --- /dev/null +++ b/experimental/internal/codegen/gather_operations.go @@ -0,0 +1,864 @@ +package codegen + +import ( + "fmt" + "sort" + "strings" + + "github.com/pb33f/libopenapi" + "github.com/pb33f/libopenapi/datamodel/high/base" + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" +) + +// GatherOperations traverses an OpenAPI document and collects all operations. +func GatherOperations(doc libopenapi.Document, paramTracker *ParamUsageTracker) ([]*OperationDescriptor, error) { + model, err := doc.BuildV3Model() + if err != nil { + return nil, fmt.Errorf("building v3 model: %w", err) + } + if model == nil { + return nil, fmt.Errorf("failed to build v3 model") + } + + g := &operationGatherer{ + paramTracker: paramTracker, + } + + return g.gatherFromDocument(&model.Model) +} + +type operationGatherer struct { + paramTracker *ParamUsageTracker +} + +func (g *operationGatherer) gatherFromDocument(doc *v3.Document) ([]*OperationDescriptor, error) { + var operations []*OperationDescriptor + + if doc.Paths == nil || doc.Paths.PathItems == nil { + return operations, nil + } + + // Collect paths in sorted order for deterministic output + var paths []string + for pair := doc.Paths.PathItems.First(); pair != nil; pair = pair.Next() { + paths = append(paths, pair.Key()) + } + sort.Strings(paths) + + for _, pathStr := range paths { + pathItem := doc.Paths.PathItems.GetOrZero(pathStr) + if pathItem == nil { + continue + } + + // Gather path-level parameters (shared by all operations on this path) + globalParams, err := g.gatherParameters(pathItem.Parameters) + if err != nil { + return nil, fmt.Errorf("error gathering path-level parameters for %s: %w", pathStr, err) + } + + // Process each operation on this path + ops := pathItem.GetOperations() + if ops == nil { + continue + } + + // Collect methods in sorted order + var methods []string + for pair := ops.First(); pair != nil; pair = pair.Next() { + methods = append(methods, pair.Key()) + } + sort.Strings(methods) + + for _, method := range methods { + op := ops.GetOrZero(method) + if op == nil { + continue + } + + opDesc, err := g.gatherOperation(method, pathStr, op, globalParams) + if err != nil { + return nil, fmt.Errorf("error gathering operation %s %s: %w", method, pathStr, err) + } + operations = append(operations, opDesc) + } + } + + return operations, nil +} + +func (g *operationGatherer) gatherOperation(method, path string, op *v3.Operation, globalParams []*ParameterDescriptor) (*OperationDescriptor, error) { + // Determine operation ID + operationID := op.OperationId + if operationID == "" { + operationID = generateOperationID(method, path) + } + goOperationID := ToGoIdentifier(operationID) + + // Gather operation-level parameters + localParams, err := g.gatherParameters(op.Parameters) + if err != nil { + return nil, fmt.Errorf("error gathering parameters: %w", err) + } + + // Combine global and local parameters (local overrides global) + allParams := combineParameters(globalParams, localParams) + + // Sort path params to match order in path + pathParams := filterParamsByLocation(allParams, "path") + pathParams, err = sortPathParamsByPath(path, pathParams) + if err != nil { + return nil, fmt.Errorf("error sorting path params: %w", err) + } + + // Gather request bodies + bodies, err := g.gatherRequestBodies(operationID, op.RequestBody) + if err != nil { + return nil, fmt.Errorf("error gathering request bodies: %w", err) + } + + // Gather responses + responses, err := g.gatherResponses(operationID, op.Responses) + if err != nil { + return nil, fmt.Errorf("error gathering responses: %w", err) + } + + // Gather security requirements + security := g.gatherSecurity(op.Security) + + queryParams := filterParamsByLocation(allParams, "query") + headerParams := filterParamsByLocation(allParams, "header") + cookieParams := filterParamsByLocation(allParams, "cookie") + + hasParams := len(queryParams)+len(headerParams)+len(cookieParams) > 0 + + desc := &OperationDescriptor{ + OperationID: operationID, + GoOperationID: goOperationID, + Method: strings.ToUpper(method), + Path: path, + Summary: op.Summary, + Description: op.Description, + + PathParams: pathParams, + QueryParams: queryParams, + HeaderParams: headerParams, + CookieParams: cookieParams, + + Bodies: bodies, + Responses: responses, + Security: security, + + HasBody: len(bodies) > 0, + HasParams: hasParams, + ParamsTypeName: goOperationID + "Params", + + Spec: op, + } + + return desc, nil +} + +func (g *operationGatherer) gatherParameters(params []*v3.Parameter) ([]*ParameterDescriptor, error) { + var result []*ParameterDescriptor + + for _, param := range params { + if param == nil { + continue + } + + desc, err := g.gatherParameter(param) + if err != nil { + return nil, fmt.Errorf("error gathering parameter %s: %w", param.Name, err) + } + result = append(result, desc) + } + + return result, nil +} + +func (g *operationGatherer) gatherParameter(param *v3.Parameter) (*ParameterDescriptor, error) { + // Determine style and explode (with defaults based on location) + style := param.Style + if style == "" { + style = DefaultParamStyle(param.In) + } + + explode := DefaultParamExplode(param.In) + if param.Explode != nil { + explode = *param.Explode + } + + // Record param usage for function generation + if g.paramTracker != nil { + g.paramTracker.RecordParam(style, explode) + } + + // Determine encoding mode + isStyled := param.Schema != nil + isJSON := false + isPassThrough := false + + if param.Content != nil && param.Content.Len() > 0 { + // Parameter uses content encoding + isStyled = false + for pair := param.Content.First(); pair != nil; pair = pair.Next() { + contentType := pair.Key() + if IsMediaTypeJSON(contentType) { + isJSON = true + break + } + } + if !isJSON { + isPassThrough = true + } + } + + // Get type declaration from schema + typeDecl := "string" // Default + var schemaDesc *SchemaDescriptor + if param.Schema != nil { + schema := param.Schema.Schema() + if schema != nil { + schemaDesc = &SchemaDescriptor{ + Schema: schema, + } + typeDecl = schemaToGoType(schema) + } + } + + goName := ToCamelCase(param.Name) + + // Handle *bool for Required + required := false + if param.Required != nil { + required = *param.Required + } + + desc := &ParameterDescriptor{ + Name: param.Name, + GoName: goName, + Location: param.In, + Required: required, + + Style: style, + Explode: explode, + + Schema: schemaDesc, + TypeDecl: typeDecl, + + StyleFunc: ComputeStyleFunc(style, explode), + BindFunc: ComputeBindFunc(style, explode), + + IsStyled: isStyled, + IsPassThrough: isPassThrough, + IsJSON: isJSON, + + Spec: param, + } + + return desc, nil +} + +func (g *operationGatherer) gatherRequestBodies(operationID string, bodyRef *v3.RequestBody) ([]*RequestBodyDescriptor, error) { + if bodyRef == nil { + return nil, nil + } + + var bodies []*RequestBodyDescriptor + + if bodyRef.Content == nil { + return bodies, nil + } + + // Collect content types in sorted order + var contentTypes []string + for pair := bodyRef.Content.First(); pair != nil; pair = pair.Next() { + contentTypes = append(contentTypes, pair.Key()) + } + sort.Strings(contentTypes) + + // Determine which is the default (application/json if present) + hasApplicationJSON := false + for _, ct := range contentTypes { + if ct == "application/json" { + hasApplicationJSON = true + break + } + } + + for _, contentType := range contentTypes { + mediaType := bodyRef.Content.GetOrZero(contentType) + if mediaType == nil { + continue + } + + nameTag := ComputeBodyNameTag(contentType) + isDefault := contentType == "application/json" || (!hasApplicationJSON && contentType == contentTypes[0]) + + var schemaDesc *SchemaDescriptor + if mediaType.Schema != nil { + schemaDesc = schemaProxyToDescriptor(mediaType.Schema) + } + + funcSuffix := "" + if !isDefault && nameTag != "" { + funcSuffix = "With" + nameTag + "Body" + } + + goTypeName := operationID + nameTag + "RequestBody" + if nameTag == "" { + goTypeName = operationID + "RequestBody" + } + + // Handle *bool for Required + bodyRequired := false + if bodyRef.Required != nil { + bodyRequired = *bodyRef.Required + } + + desc := &RequestBodyDescriptor{ + ContentType: contentType, + Required: bodyRequired, + Schema: schemaDesc, + + NameTag: nameTag, + GoTypeName: goTypeName, + FuncSuffix: funcSuffix, + IsDefault: isDefault, + IsJSON: IsMediaTypeJSON(contentType), + } + + // Gather encoding options for form data + if mediaType.Encoding != nil && mediaType.Encoding.Len() > 0 { + desc.Encoding = make(map[string]RequestBodyEncoding) + for pair := mediaType.Encoding.First(); pair != nil; pair = pair.Next() { + enc := pair.Value() + desc.Encoding[pair.Key()] = RequestBodyEncoding{ + ContentType: enc.ContentType, + Style: enc.Style, + Explode: enc.Explode, + } + } + } + + bodies = append(bodies, desc) + } + + return bodies, nil +} + +func (g *operationGatherer) gatherResponses(operationID string, responses *v3.Responses) ([]*ResponseDescriptor, error) { + if responses == nil { + return nil, nil + } + + var result []*ResponseDescriptor + + // Gather default response + if responses.Default != nil { + desc, err := g.gatherResponse(operationID, "default", responses.Default) + if err != nil { + return nil, err + } + if desc != nil { + result = append(result, desc) + } + } + + // Gather status code responses + if responses.Codes != nil { + var codes []string + for pair := responses.Codes.First(); pair != nil; pair = pair.Next() { + codes = append(codes, pair.Key()) + } + sort.Strings(codes) + + for _, code := range codes { + respRef := responses.Codes.GetOrZero(code) + if respRef == nil { + continue + } + + desc, err := g.gatherResponse(operationID, code, respRef) + if err != nil { + return nil, err + } + if desc != nil { + result = append(result, desc) + } + } + } + + return result, nil +} + +func (g *operationGatherer) gatherResponse(operationID, statusCode string, resp *v3.Response) (*ResponseDescriptor, error) { + if resp == nil { + return nil, nil + } + + var contents []*ResponseContentDescriptor + if resp.Content != nil { + var contentTypes []string + for pair := resp.Content.First(); pair != nil; pair = pair.Next() { + contentTypes = append(contentTypes, pair.Key()) + } + sort.Strings(contentTypes) + + for _, contentType := range contentTypes { + mediaType := resp.Content.GetOrZero(contentType) + if mediaType == nil { + continue + } + + var schemaDesc *SchemaDescriptor + if mediaType.Schema != nil { + schemaDesc = schemaProxyToDescriptor(mediaType.Schema) + } + + nameTag := ComputeBodyNameTag(contentType) + + contents = append(contents, &ResponseContentDescriptor{ + ContentType: contentType, + Schema: schemaDesc, + NameTag: nameTag, + IsJSON: IsMediaTypeJSON(contentType), + }) + } + } + + var headers []*ResponseHeaderDescriptor + if resp.Headers != nil { + var headerNames []string + for pair := resp.Headers.First(); pair != nil; pair = pair.Next() { + headerNames = append(headerNames, pair.Key()) + } + sort.Strings(headerNames) + + for _, name := range headerNames { + header := resp.Headers.GetOrZero(name) + if header == nil { + continue + } + + var schemaDesc *SchemaDescriptor + if header.Schema != nil { + schemaDesc = schemaProxyToDescriptor(header.Schema) + } + + headers = append(headers, &ResponseHeaderDescriptor{ + Name: name, + GoName: ToCamelCase(name), + Required: header.Required, + Schema: schemaDesc, + }) + } + } + + description := "" + if resp.Description != "" { + description = resp.Description + } + + return &ResponseDescriptor{ + StatusCode: statusCode, + Description: description, + Contents: contents, + Headers: headers, + }, nil +} + +func (g *operationGatherer) gatherSecurity(security []*base.SecurityRequirement) []SecurityRequirement { + if security == nil { + return nil + } + + var result []SecurityRequirement + for _, req := range security { + if req == nil || req.Requirements == nil { + continue + } + for pair := req.Requirements.First(); pair != nil; pair = pair.Next() { + result = append(result, SecurityRequirement{ + Name: pair.Key(), + Scopes: pair.Value(), + }) + } + } + return result +} + +// Helper functions + +func generateOperationID(method, path string) string { + // Generate operation ID from method and path + // GET /users/{id} -> GetUsersId + id := strings.ToLower(method) + for _, part := range strings.Split(path, "/") { + if part == "" { + continue + } + // Remove path parameter braces + part = strings.TrimPrefix(part, "{") + part = strings.TrimSuffix(part, "}") + id += "-" + part + } + return ToCamelCase(id) +} + +func combineParameters(global, local []*ParameterDescriptor) []*ParameterDescriptor { + // Local parameters override global parameters with the same name and location + seen := make(map[string]bool) + var result []*ParameterDescriptor + + for _, p := range local { + key := p.Location + ":" + p.Name + seen[key] = true + result = append(result, p) + } + + for _, p := range global { + key := p.Location + ":" + p.Name + if !seen[key] { + result = append(result, p) + } + } + + return result +} + +func filterParamsByLocation(params []*ParameterDescriptor, location string) []*ParameterDescriptor { + var result []*ParameterDescriptor + for _, p := range params { + if p.Location == location { + result = append(result, p) + } + } + return result +} + +func sortPathParamsByPath(path string, params []*ParameterDescriptor) ([]*ParameterDescriptor, error) { + // Extract parameter names from path in order + var pathParamNames []string + parts := strings.Split(path, "/") + for _, part := range parts { + if strings.HasPrefix(part, "{") && strings.HasSuffix(part, "}") { + name := strings.TrimPrefix(part, "{") + name = strings.TrimSuffix(name, "}") + pathParamNames = append(pathParamNames, name) + } + } + + // Build a map of params by name + paramMap := make(map[string]*ParameterDescriptor) + for _, p := range params { + paramMap[p.Name] = p + } + + // Sort params according to path order + var result []*ParameterDescriptor + for _, name := range pathParamNames { + if p, ok := paramMap[name]; ok { + result = append(result, p) + } + } + + return result, nil +} + +// GatherWebhookOperations traverses an OpenAPI document and collects operations from webhooks. +func GatherWebhookOperations(doc libopenapi.Document, paramTracker *ParamUsageTracker) ([]*OperationDescriptor, error) { + model, err := doc.BuildV3Model() + if err != nil { + return nil, fmt.Errorf("building v3 model: %w", err) + } + if model == nil { + return nil, fmt.Errorf("failed to build v3 model") + } + + g := &operationGatherer{ + paramTracker: paramTracker, + } + + return g.gatherWebhooks(&model.Model) +} + +// GatherCallbackOperations traverses an OpenAPI document and collects operations from callbacks. +func GatherCallbackOperations(doc libopenapi.Document, paramTracker *ParamUsageTracker) ([]*OperationDescriptor, error) { + model, err := doc.BuildV3Model() + if err != nil { + return nil, fmt.Errorf("building v3 model: %w", err) + } + if model == nil { + return nil, fmt.Errorf("failed to build v3 model") + } + + g := &operationGatherer{ + paramTracker: paramTracker, + } + + return g.gatherCallbacks(&model.Model) +} + +func (g *operationGatherer) gatherWebhooks(doc *v3.Document) ([]*OperationDescriptor, error) { + var operations []*OperationDescriptor + + if doc.Webhooks == nil || doc.Webhooks.Len() == 0 { + return operations, nil + } + + // Collect webhook names in sorted order for deterministic output + var webhookNames []string + for pair := doc.Webhooks.First(); pair != nil; pair = pair.Next() { + webhookNames = append(webhookNames, pair.Key()) + } + sort.Strings(webhookNames) + + for _, webhookName := range webhookNames { + pathItem := doc.Webhooks.GetOrZero(webhookName) + if pathItem == nil { + continue + } + + // Gather path-level parameters + globalParams, err := g.gatherParameters(pathItem.Parameters) + if err != nil { + return nil, fmt.Errorf("error gathering parameters for webhook %s: %w", webhookName, err) + } + + ops := pathItem.GetOperations() + if ops == nil { + continue + } + + var methods []string + for pair := ops.First(); pair != nil; pair = pair.Next() { + methods = append(methods, pair.Key()) + } + sort.Strings(methods) + + for _, method := range methods { + op := ops.GetOrZero(method) + if op == nil { + continue + } + + // For webhooks, Path is empty (no URL path in the spec) + opDesc, err := g.gatherOperation(method, "", op, globalParams) + if err != nil { + return nil, fmt.Errorf("error gathering webhook operation %s %s: %w", method, webhookName, err) + } + + // Override operation ID if not set - use webhook name + method + if op.OperationId == "" { + opDesc.OperationID = ToCamelCase(method + "-" + webhookName) + opDesc.GoOperationID = ToGoIdentifier(opDesc.OperationID) + opDesc.ParamsTypeName = opDesc.GoOperationID + "Params" + } + + opDesc.Source = OperationSourceWebhook + opDesc.WebhookName = webhookName + + operations = append(operations, opDesc) + } + } + + return operations, nil +} + +func (g *operationGatherer) gatherCallbacks(doc *v3.Document) ([]*OperationDescriptor, error) { + var operations []*OperationDescriptor + + if doc.Paths == nil || doc.Paths.PathItems == nil { + return operations, nil + } + + // Iterate all paths in sorted order + var paths []string + for pair := doc.Paths.PathItems.First(); pair != nil; pair = pair.Next() { + paths = append(paths, pair.Key()) + } + sort.Strings(paths) + + for _, pathStr := range paths { + pathItem := doc.Paths.PathItems.GetOrZero(pathStr) + if pathItem == nil { + continue + } + + pathOps := pathItem.GetOperations() + if pathOps == nil { + continue + } + + var methods []string + for pair := pathOps.First(); pair != nil; pair = pair.Next() { + methods = append(methods, pair.Key()) + } + sort.Strings(methods) + + for _, method := range methods { + parentOp := pathOps.GetOrZero(method) + if parentOp == nil || parentOp.Callbacks == nil || parentOp.Callbacks.Len() == 0 { + continue + } + + parentOpID := parentOp.OperationId + if parentOpID == "" { + parentOpID = generateOperationID(method, pathStr) + } + + // Collect callback names in sorted order + var callbackNames []string + for pair := parentOp.Callbacks.First(); pair != nil; pair = pair.Next() { + callbackNames = append(callbackNames, pair.Key()) + } + sort.Strings(callbackNames) + + for _, callbackName := range callbackNames { + callback := parentOp.Callbacks.GetOrZero(callbackName) + if callback == nil || callback.Expression == nil || callback.Expression.Len() == 0 { + continue + } + + // Iterate callback expressions in sorted order + var expressions []string + for pair := callback.Expression.First(); pair != nil; pair = pair.Next() { + expressions = append(expressions, pair.Key()) + } + sort.Strings(expressions) + + for _, expression := range expressions { + cbPathItem := callback.Expression.GetOrZero(expression) + if cbPathItem == nil { + continue + } + + cbOps := cbPathItem.GetOperations() + if cbOps == nil { + continue + } + + var cbMethods []string + for pair := cbOps.First(); pair != nil; pair = pair.Next() { + cbMethods = append(cbMethods, pair.Key()) + } + sort.Strings(cbMethods) + + for _, cbMethod := range cbMethods { + cbOp := cbOps.GetOrZero(cbMethod) + if cbOp == nil { + continue + } + + // URL expression is stored as path but params are not extracted + // (expressions are runtime-evaluated) + opDesc, err := g.gatherOperation(cbMethod, expression, cbOp, nil) + if err != nil { + return nil, fmt.Errorf("error gathering callback operation %s %s %s: %w", cbMethod, callbackName, expression, err) + } + + // Override operation ID if not set + if cbOp.OperationId == "" { + opDesc.OperationID = ToCamelCase(parentOpID + "-" + callbackName) + opDesc.GoOperationID = ToGoIdentifier(opDesc.OperationID) + opDesc.ParamsTypeName = opDesc.GoOperationID + "Params" + } + + // Clear path params since callback URLs are runtime expressions + opDesc.PathParams = nil + + opDesc.Source = OperationSourceCallback + opDesc.CallbackName = callbackName + opDesc.ParentOpID = parentOpID + + operations = append(operations, opDesc) + } + } + } + } + } + + return operations, nil +} + +// schemaProxyToDescriptor converts a schema proxy to a basic descriptor. +// This is a simplified version - for full type resolution, use the schema gatherer. +func schemaProxyToDescriptor(proxy *base.SchemaProxy) *SchemaDescriptor { + if proxy == nil { + return nil + } + + schema := proxy.Schema() + if schema == nil { + return nil + } + + desc := &SchemaDescriptor{ + Schema: schema, + } + + // Capture reference if this is a reference schema + if proxy.IsReference() { + desc.Ref = proxy.GetReference() + } + + return desc +} + +// schemaToGoType converts a schema to a Go type string. +// This is a simplified version for parameter types. +func schemaToGoType(schema *base.Schema) string { + if schema == nil { + return "interface{}" + } + + // Check for array + if schema.Items != nil && schema.Items.A != nil { + itemType := "interface{}" + if itemSchema := schema.Items.A.Schema(); itemSchema != nil { + itemType = schemaToGoType(itemSchema) + } + return "[]" + itemType + } + + // Check explicit type + for _, t := range schema.Type { + switch t { + case "string": + if schema.Format == "date-time" { + return "time.Time" + } + if schema.Format == "date" { + return "Date" + } + if schema.Format == "uuid" { + return "uuid.UUID" + } + return "string" + case "integer": + if schema.Format == "int64" { + return "int64" + } + if schema.Format == "int32" { + return "int32" + } + return "int" + case "number": + if schema.Format == "float" { + return "float32" + } + return "float64" + case "boolean": + return "bool" + case "array": + // Handled above + return "[]interface{}" + case "object": + return "map[string]interface{}" + } + } + + return "interface{}" +} diff --git a/experimental/internal/codegen/identifiers.go b/experimental/internal/codegen/identifiers.go new file mode 100644 index 0000000000..6a8f58f05b --- /dev/null +++ b/experimental/internal/codegen/identifiers.go @@ -0,0 +1,125 @@ +package codegen + +import ( + "strings" + "unicode" +) + +// Go keywords that can't be used as identifiers +var goKeywords = map[string]bool{ + "break": true, + "case": true, + "chan": true, + "const": true, + "continue": true, + "default": true, + "defer": true, + "else": true, + "fallthrough": true, + "for": true, + "func": true, + "go": true, + "goto": true, + "if": true, + "import": true, + "interface": true, + "map": true, + "package": true, + "range": true, + "return": true, + "select": true, + "struct": true, + "switch": true, + "type": true, + "var": true, +} + +// IsGoKeyword returns true if s is a Go keyword. +func IsGoKeyword(s string) bool { + return goKeywords[s] +} + +// ToCamelCase converts a string to CamelCase (PascalCase). +// It treats hyphens, underscores, spaces, and other non-alphanumeric characters as word separators. +// Example: "user-name" -> "UserName", "user_id" -> "UserId" +func ToCamelCase(s string) string { + if s == "" { + return "" + } + + var result strings.Builder + capitalizeNext := true + + for _, r := range s { + if isWordSeparator(r) { + capitalizeNext = true + continue + } + + if !unicode.IsLetter(r) && !unicode.IsDigit(r) { + capitalizeNext = true + continue + } + + if capitalizeNext { + result.WriteRune(unicode.ToUpper(r)) + capitalizeNext = false + } else { + result.WriteRune(r) + } + } + + return result.String() +} + +// LowercaseFirstCharacter lowercases only the first character of a string. +// Example: "UserName" -> "userName" +func LowercaseFirstCharacter(s string) string { + if s == "" { + return "" + } + runes := []rune(s) + runes[0] = unicode.ToLower(runes[0]) + return string(runes) +} + +// UppercaseFirstCharacter uppercases only the first character of a string. +// Example: "userName" -> "UserName" +func UppercaseFirstCharacter(s string) string { + if s == "" { + return "" + } + runes := []rune(s) + runes[0] = unicode.ToUpper(runes[0]) + return string(runes) +} + +// isWordSeparator returns true if the rune is a word separator. +func isWordSeparator(r rune) bool { + return r == '-' || r == '_' || r == ' ' || r == '.' || r == '/' +} + +// ToGoIdentifier converts a string to a valid Go identifier. +// It converts to CamelCase, handles leading digits, and avoids Go keywords. +func ToGoIdentifier(s string) string { + result := ToCamelCase(s) + + // Handle empty result + if result == "" { + return "Empty" + } + + // Handle leading digits + if result[0] >= '0' && result[0] <= '9' { + result = "N" + result + } + + // Handle Go keywords - check both the original input and lowercase result + // "type" -> "Type" but we still want to avoid "Type" being used as-is + // since user might write it as lowercase in code + if IsGoKeyword(s) || IsGoKeyword(strings.ToLower(result)) { + result = result + "_" + } + + return result +} diff --git a/experimental/internal/codegen/identifiers_test.go b/experimental/internal/codegen/identifiers_test.go new file mode 100644 index 0000000000..4d5eca6328 --- /dev/null +++ b/experimental/internal/codegen/identifiers_test.go @@ -0,0 +1,129 @@ +package codegen + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsGoKeyword(t *testing.T) { + keywords := []string{ + "break", "case", "chan", "const", "continue", + "default", "defer", "else", "fallthrough", "for", + "func", "go", "goto", "if", "import", + "interface", "map", "package", "range", "return", + "select", "struct", "switch", "type", "var", + } + + for _, kw := range keywords { + t.Run(kw, func(t *testing.T) { + assert.True(t, IsGoKeyword(kw), "%s should be a keyword", kw) + }) + } + + nonKeywords := []string{ + "user", "name", "id", "Type", "Interface", "Map", + "string", "int", "bool", "error", // predeclared but not keywords + } + + for _, nkw := range nonKeywords { + t.Run(nkw+"_not_keyword", func(t *testing.T) { + assert.False(t, IsGoKeyword(nkw), "%s should not be a keyword", nkw) + }) + } +} + +func TestToCamelCase(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"", ""}, + {"user", "User"}, + {"user_name", "UserName"}, + {"user-name", "UserName"}, + {"user.name", "UserName"}, + {"user name", "UserName"}, + {"USER", "USER"}, + {"USER_NAME", "USERNAME"}, + {"123", "123"}, + {"user123", "User123"}, + {"user123name", "User123name"}, + {"get-users-by-id", "GetUsersById"}, + {"__private", "Private"}, + {"a_b_c", "ABC"}, + {"already_CamelCase", "AlreadyCamelCase"}, + {"path/to/resource", "PathToResource"}, + } + + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + result := ToCamelCase(tc.input) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestLowercaseFirstCharacter(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"", ""}, + {"User", "user"}, + {"UserName", "userName"}, + {"user", "user"}, + {"ABC", "aBC"}, + {"123", "123"}, + } + + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + result := LowercaseFirstCharacter(tc.input) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestUppercaseFirstCharacter(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"", ""}, + {"user", "User"}, + {"userName", "UserName"}, + {"User", "User"}, + {"abc", "Abc"}, + } + + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + result := UppercaseFirstCharacter(tc.input) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestToGoIdentifier(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"user", "User"}, + {"user_name", "UserName"}, + {"123abc", "N123abc"}, + {"type", "Type_"}, + {"map", "Map_"}, + {"interface", "Interface_"}, + {"", "Empty"}, + {"get-users", "GetUsers"}, + } + + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + result := ToGoIdentifier(tc.input) + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/experimental/internal/codegen/initiatorgen.go b/experimental/internal/codegen/initiatorgen.go new file mode 100644 index 0000000000..67a9aa08fa --- /dev/null +++ b/experimental/internal/codegen/initiatorgen.go @@ -0,0 +1,216 @@ +package codegen + +import ( + "bytes" + "fmt" + "strings" + "text/template" + + "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/templates" +) + +// InitiatorTemplateData is passed to initiator templates. +type InitiatorTemplateData struct { + Prefix string // "Webhook" or "Callback" + PrefixLower string // "webhook" or "callback" + Operations []*OperationDescriptor // Operations to generate for +} + +// InitiatorGenerator generates initiator (sender) code from operation descriptors. +// It is parameterized by prefix to support both webhooks and callbacks. +type InitiatorGenerator struct { + tmpl *template.Template + prefix string // "Webhook" or "Callback" + schemaIndex map[string]*SchemaDescriptor + generateSimple bool + modelsPackage *ModelsPackage +} + +// NewInitiatorGenerator creates a new initiator generator. +func NewInitiatorGenerator(prefix string, schemaIndex map[string]*SchemaDescriptor, generateSimple bool, modelsPackage *ModelsPackage) (*InitiatorGenerator, error) { + tmpl := template.New("initiator").Funcs(templates.Funcs()).Funcs(clientFuncs(schemaIndex, modelsPackage)) + + // Parse initiator templates + for _, pt := range templates.InitiatorTemplates { + content, err := templates.TemplateFS.ReadFile("files/" + pt.Template) + if err != nil { + return nil, fmt.Errorf("failed to read initiator template %s: %w", pt.Template, err) + } + _, err = tmpl.New(pt.Name).Parse(string(content)) + if err != nil { + return nil, fmt.Errorf("failed to parse initiator template %s: %w", pt.Template, err) + } + } + + // Parse shared templates (param_types) + for _, st := range templates.SharedServerTemplates { + content, err := templates.TemplateFS.ReadFile("files/" + st.Template) + if err != nil { + return nil, fmt.Errorf("failed to read shared template %s: %w", st.Template, err) + } + _, err = tmpl.New(st.Name).Parse(string(content)) + if err != nil { + return nil, fmt.Errorf("failed to parse shared template %s: %w", st.Template, err) + } + } + + return &InitiatorGenerator{ + tmpl: tmpl, + prefix: prefix, + schemaIndex: schemaIndex, + generateSimple: generateSimple, + modelsPackage: modelsPackage, + }, nil +} + +func (g *InitiatorGenerator) templateData(ops []*OperationDescriptor) InitiatorTemplateData { + return InitiatorTemplateData{ + Prefix: g.prefix, + PrefixLower: strings.ToLower(g.prefix), + Operations: ops, + } +} + +// GenerateBase generates the base initiator types and helpers. +func (g *InitiatorGenerator) GenerateBase(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "initiator_base", g.templateData(ops)); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateInterface generates the InitiatorInterface. +func (g *InitiatorGenerator) GenerateInterface(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "initiator_interface", g.templateData(ops)); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateMethods generates the Initiator methods. +func (g *InitiatorGenerator) GenerateMethods(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "initiator_methods", g.templateData(ops)); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateRequestBuilders generates the request builder functions. +func (g *InitiatorGenerator) GenerateRequestBuilders(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "initiator_request_builders", g.templateData(ops)); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateSimple generates the SimpleInitiator with typed responses. +func (g *InitiatorGenerator) GenerateSimple(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "initiator_simple", g.templateData(ops)); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateParamTypes generates the parameter struct types. +func (g *InitiatorGenerator) GenerateParamTypes(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "param_types", ops); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateRequestBodyTypes generates type aliases for request bodies. +func (g *InitiatorGenerator) GenerateRequestBodyTypes(ops []*OperationDescriptor) string { + var buf bytes.Buffer + pkgPrefix := g.modelsPackage.Prefix() + + for _, op := range ops { + for _, body := range op.Bodies { + if !body.IsJSON { + continue + } + var targetType string + if body.Schema != nil { + if body.Schema.Ref != "" { + if target, ok := g.schemaIndex[body.Schema.Ref]; ok { + targetType = pkgPrefix + target.ShortName + } + } else if body.Schema.ShortName != "" { + targetType = pkgPrefix + body.Schema.ShortName + } + } + if targetType == "" { + targetType = "interface{}" + } + buf.WriteString(fmt.Sprintf("type %s = %s\n\n", body.GoTypeName, targetType)) + } + } + + return buf.String() +} + +// GenerateInitiator generates the complete initiator code. +func (g *InitiatorGenerator) GenerateInitiator(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + + // Generate request body type aliases first + bodyTypes := g.GenerateRequestBodyTypes(ops) + buf.WriteString(bodyTypes) + + // Generate base initiator + base, err := g.GenerateBase(ops) + if err != nil { + return "", fmt.Errorf("generating base initiator: %w", err) + } + buf.WriteString(base) + buf.WriteString("\n") + + // Generate interface + iface, err := g.GenerateInterface(ops) + if err != nil { + return "", fmt.Errorf("generating initiator interface: %w", err) + } + buf.WriteString(iface) + buf.WriteString("\n") + + // Generate param types + paramTypes, err := g.GenerateParamTypes(ops) + if err != nil { + return "", fmt.Errorf("generating param types: %w", err) + } + buf.WriteString(paramTypes) + buf.WriteString("\n") + + // Generate methods + methods, err := g.GenerateMethods(ops) + if err != nil { + return "", fmt.Errorf("generating initiator methods: %w", err) + } + buf.WriteString(methods) + buf.WriteString("\n") + + // Generate request builders + builders, err := g.GenerateRequestBuilders(ops) + if err != nil { + return "", fmt.Errorf("generating request builders: %w", err) + } + buf.WriteString(builders) + buf.WriteString("\n") + + // Generate simple initiator if requested + if g.generateSimple { + simple, err := g.GenerateSimple(ops) + if err != nil { + return "", fmt.Errorf("generating simple initiator: %w", err) + } + buf.WriteString(simple) + } + + return buf.String(), nil +} diff --git a/experimental/internal/codegen/inline.go b/experimental/internal/codegen/inline.go new file mode 100644 index 0000000000..25767cffa8 --- /dev/null +++ b/experimental/internal/codegen/inline.go @@ -0,0 +1,92 @@ +package codegen + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "strings" +) + +// generateEmbeddedSpec produces Go code that embeds the raw OpenAPI spec as +// gzip+base64 encoded data, with a public GetSwaggerSpecJSON() function to +// retrieve the decompressed JSON bytes. +func generateEmbeddedSpec(specData []byte) (string, error) { + // Gzip compress + var buf bytes.Buffer + gz, err := gzip.NewWriterLevel(&buf, gzip.BestCompression) + if err != nil { + return "", fmt.Errorf("creating gzip writer: %w", err) + } + if _, err := gz.Write(specData); err != nil { + return "", fmt.Errorf("gzip writing: %w", err) + } + if err := gz.Close(); err != nil { + return "", fmt.Errorf("gzip close: %w", err) + } + + // Base64 encode + encoded := base64.StdEncoding.EncodeToString(buf.Bytes()) + + // Split into 80-char chunks + var chunks []string + for len(encoded) > 0 { + end := 80 + if end > len(encoded) { + end = len(encoded) + } + chunks = append(chunks, encoded[:end]) + encoded = encoded[end:] + } + + // Build Go code + var b strings.Builder + + b.WriteString("// Base64-encoded, gzip-compressed OpenAPI spec.\n") + b.WriteString("var swaggerSpecJSON = []string{\n") + for _, chunk := range chunks { + fmt.Fprintf(&b, "\t%q,\n", chunk) + } + b.WriteString("}\n\n") + + b.WriteString("// decodeSwaggerSpec decodes and decompresses the embedded spec.\n") + b.WriteString("func decodeSwaggerSpec() ([]byte, error) {\n") + b.WriteString("\tjoined := strings.Join(swaggerSpecJSON, \"\")\n") + b.WriteString("\traw, err := base64.StdEncoding.DecodeString(joined)\n") + b.WriteString("\tif err != nil {\n") + b.WriteString("\t\treturn nil, fmt.Errorf(\"decoding base64: %w\", err)\n") + b.WriteString("\t}\n") + b.WriteString("\tr, err := gzip.NewReader(bytes.NewReader(raw))\n") + b.WriteString("\tif err != nil {\n") + b.WriteString("\t\treturn nil, fmt.Errorf(\"creating gzip reader: %w\", err)\n") + b.WriteString("\t}\n") + b.WriteString("\tdefer r.Close()\n") + b.WriteString("\tvar out bytes.Buffer\n") + b.WriteString("\tif _, err := out.ReadFrom(r); err != nil {\n") + b.WriteString("\t\treturn nil, fmt.Errorf(\"decompressing: %w\", err)\n") + b.WriteString("\t}\n") + b.WriteString("\treturn out.Bytes(), nil\n") + b.WriteString("}\n\n") + + b.WriteString("// decodeSwaggerSpecCached returns a closure that caches the decoded spec.\n") + b.WriteString("func decodeSwaggerSpecCached() func() ([]byte, error) {\n") + b.WriteString("\tvar cached []byte\n") + b.WriteString("\tvar cachedErr error\n") + b.WriteString("\tvar once sync.Once\n") + b.WriteString("\treturn func() ([]byte, error) {\n") + b.WriteString("\t\tonce.Do(func() {\n") + b.WriteString("\t\t\tcached, cachedErr = decodeSwaggerSpec()\n") + b.WriteString("\t\t})\n") + b.WriteString("\t\treturn cached, cachedErr\n") + b.WriteString("\t}\n") + b.WriteString("}\n\n") + + b.WriteString("var swaggerSpec = decodeSwaggerSpecCached()\n\n") + + b.WriteString("// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes.\n") + b.WriteString("func GetSwaggerSpecJSON() ([]byte, error) {\n") + b.WriteString("\treturn swaggerSpec()\n") + b.WriteString("}\n") + + return b.String(), nil +} diff --git a/experimental/internal/codegen/inline_test.go b/experimental/internal/codegen/inline_test.go new file mode 100644 index 0000000000..ae35938493 --- /dev/null +++ b/experimental/internal/codegen/inline_test.go @@ -0,0 +1,139 @@ +package codegen + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "strings" + "testing" + + "github.com/pb33f/libopenapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateEmbeddedSpec(t *testing.T) { + specData := []byte(`{"openapi":"3.0.0","info":{"title":"Test","version":"1.0"}}`) + + code, err := generateEmbeddedSpec(specData) + require.NoError(t, err) + + // Should contain the chunked base64 variable + assert.Contains(t, code, "var swaggerSpecJSON = []string{") + + // Should contain the decode function + assert.Contains(t, code, "func decodeSwaggerSpec() ([]byte, error)") + + // Should contain the cached decode function + assert.Contains(t, code, "func decodeSwaggerSpecCached() func() ([]byte, error)") + + // Should contain the public API + assert.Contains(t, code, "func GetSwaggerSpecJSON() ([]byte, error)") + + // Should contain the cached var + assert.Contains(t, code, "var swaggerSpec = decodeSwaggerSpecCached()") +} + +func TestGenerateEmbeddedSpecRoundTrip(t *testing.T) { + specData := []byte(`{"openapi":"3.0.0","info":{"title":"Test API","version":"1.0"},"paths":{}}`) + + code, err := generateEmbeddedSpec(specData) + require.NoError(t, err) + + // Extract the base64 chunks from the generated code + var chunks []string + for _, line := range strings.Split(code, "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, `"`) && strings.HasSuffix(line, `",`) { + // Remove quotes and trailing comma + chunk := line[1 : len(line)-2] + chunks = append(chunks, chunk) + } + } + require.NotEmpty(t, chunks, "should have extracted base64 chunks") + + // Decode base64 + joined := strings.Join(chunks, "") + raw, err := base64.StdEncoding.DecodeString(joined) + require.NoError(t, err) + + // Decompress gzip + r, err := gzip.NewReader(bytes.NewReader(raw)) + require.NoError(t, err) + defer func() { _ = r.Close() }() + + var out bytes.Buffer + _, err = out.ReadFrom(r) + require.NoError(t, err) + + // Should match original spec + assert.Equal(t, specData, out.Bytes()) +} + +func TestGenerateEmbeddedSpecInGenerate(t *testing.T) { + spec := `openapi: "3.0.0" +info: + title: Test API + version: "1.0" +paths: {} +components: + schemas: + Pet: + type: object + properties: + name: + type: string +` + + specBytes := []byte(spec) + + doc, err := libopenapi.NewDocument(specBytes) + require.NoError(t, err) + + cfg := Configuration{ + PackageName: "testpkg", + } + + code, err := Generate(doc, specBytes, cfg) + require.NoError(t, err) + + // Should contain the model type + assert.Contains(t, code, "type Pet struct") + + // Should contain the embedded spec + assert.Contains(t, code, "GetSwaggerSpecJSON") + assert.Contains(t, code, "swaggerSpecJSON") +} + +func TestGenerateWithNilSpecData(t *testing.T) { + spec := `openapi: "3.0.0" +info: + title: Test API + version: "1.0" +paths: {} +components: + schemas: + Pet: + type: object + properties: + name: + type: string +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + cfg := Configuration{ + PackageName: "testpkg", + } + + code, err := Generate(doc, nil, cfg) + require.NoError(t, err) + + // Should contain the model type + assert.Contains(t, code, "type Pet struct") + + // Should NOT contain the embedded spec + assert.NotContains(t, code, "GetSwaggerSpecJSON") + assert.NotContains(t, code, "swaggerSpecJSON") +} diff --git a/experimental/internal/codegen/namemangling.go b/experimental/internal/codegen/namemangling.go new file mode 100644 index 0000000000..9dff27a642 --- /dev/null +++ b/experimental/internal/codegen/namemangling.go @@ -0,0 +1,420 @@ +package codegen + +import ( + "strings" + "unicode" +) + +// NameMangling configures how OpenAPI names are converted to valid Go identifiers. +type NameMangling struct { + // CharacterSubstitutions maps characters to their word replacements. + // Used when these characters appear at the start of a name. + // Example: '$' -> "DollarSign", '-' -> "Minus" + CharacterSubstitutions map[string]string `yaml:"character-substitutions,omitempty"` + + // WordSeparators is a string of characters that mark word boundaries. + // When encountered, the next letter is capitalized. + // Example: "-_. " means "foo-bar" becomes "FooBar" + WordSeparators string `yaml:"word-separators,omitempty"` + + // NumericPrefix is prepended when a name starts with a digit. + // Example: "N" means "123foo" becomes "N123foo" + NumericPrefix string `yaml:"numeric-prefix,omitempty"` + + // KeywordPrefix is prepended when a name conflicts with a Go keyword. + // Example: "_" means "type" becomes "_type" + KeywordPrefix string `yaml:"keyword-prefix,omitempty"` + + // Initialisms is a list of words that should be all-uppercase. + // Example: ["ID", "HTTP", "URL"] means "userId" becomes "UserID" + Initialisms []string `yaml:"initialisms,omitempty"` +} + +// DefaultNameMangling returns sensible defaults for name mangling. +func DefaultNameMangling() NameMangling { + return NameMangling{ + CharacterSubstitutions: map[string]string{ + "$": "DollarSign", + "-": "Minus", + "+": "Plus", + "&": "And", + "|": "Or", + "~": "Tilde", + "=": "Equal", + ">": "GreaterThan", + "<": "LessThan", + "#": "Hash", + ".": "Dot", + "*": "Asterisk", + "^": "Caret", + "%": "Percent", + "_": "Underscore", + "@": "At", + "!": "Bang", + "?": "Question", + "/": "Slash", + "\\": "Backslash", + ":": "Colon", + ";": "Semicolon", + "'": "Apos", + "\"": "Quote", + "`": "Backtick", + "(": "LParen", + ")": "RParen", + "[": "LBracket", + "]": "RBracket", + "{": "LBrace", + "}": "RBrace", + }, + WordSeparators: "-#@!$&=.+:;_~ (){}[]|<>?/\\", + NumericPrefix: "N", + KeywordPrefix: "_", + Initialisms: []string{ + "ACL", "API", "ASCII", "CPU", "CSS", "DB", "DNS", "EOF", + "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", + "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", + "TLS", "TTL", "UDP", "UI", "UID", "GID", "URI", "URL", + "UTF8", "UUID", "VM", "XML", "XMPP", "XSRF", "XSS", + "SIP", "RTP", "AMQP", "TS", + }, + } +} + +// Merge returns a new NameMangling with user values overlaid on defaults. +// Non-zero user values override defaults. +func (n NameMangling) Merge(user NameMangling) NameMangling { + result := n + + // Merge character substitutions (user overrides/adds to defaults) + if len(user.CharacterSubstitutions) > 0 { + merged := make(map[string]string, len(n.CharacterSubstitutions)) + for k, v := range n.CharacterSubstitutions { + merged[k] = v + } + for k, v := range user.CharacterSubstitutions { + if v == "" { + // Empty string means "remove this substitution" + delete(merged, k) + } else { + merged[k] = v + } + } + result.CharacterSubstitutions = merged + } + + if user.WordSeparators != "" { + result.WordSeparators = user.WordSeparators + } + if user.NumericPrefix != "" { + result.NumericPrefix = user.NumericPrefix + } + if user.KeywordPrefix != "" { + result.KeywordPrefix = user.KeywordPrefix + } + if len(user.Initialisms) > 0 { + result.Initialisms = user.Initialisms + } + + return result +} + +// NameSubstitutions holds direct name overrides for generated identifiers. +type NameSubstitutions struct { + // TypeNames maps generated type names to user-preferred names. + // Example: {"MyGeneratedType": "MyPreferredName"} + TypeNames map[string]string `yaml:"type-names,omitempty"` + + // PropertyNames maps generated property/field names to user-preferred names. + // Example: {"GeneratedField": "PreferredField"} + PropertyNames map[string]string `yaml:"property-names,omitempty"` +} + +// NameConverter handles converting OpenAPI names to Go identifiers. +type NameConverter struct { + mangling NameMangling + substitutions NameSubstitutions + initialismSet map[string]bool +} + +// NewNameConverter creates a NameConverter with the given configuration. +func NewNameConverter(mangling NameMangling, substitutions NameSubstitutions) *NameConverter { + initialismSet := make(map[string]bool, len(mangling.Initialisms)) + for _, init := range mangling.Initialisms { + initialismSet[strings.ToUpper(init)] = true + } + return &NameConverter{ + mangling: mangling, + substitutions: substitutions, + initialismSet: initialismSet, + } +} + +// ToTypeName converts an OpenAPI schema name to a Go type name. +func (c *NameConverter) ToTypeName(name string) string { + // Check for direct substitution first + if sub, ok := c.substitutions.TypeNames[name]; ok { + return sub + } + return c.toGoIdentifier(name, true) +} + +// ToTypeNamePart converts a name to a type name component that will be joined with others. +// Unlike ToTypeName, it doesn't add a numeric prefix since the result won't be the start of an identifier. +func (c *NameConverter) ToTypeNamePart(name string) string { + // Check for direct substitution first + if sub, ok := c.substitutions.TypeNames[name]; ok { + return sub + } + return c.toGoIdentifierPart(name) +} + +// ToPropertyName converts an OpenAPI property name to a Go field name. +func (c *NameConverter) ToPropertyName(name string) string { + // Check for direct substitution first + if sub, ok := c.substitutions.PropertyNames[name]; ok { + return sub + } + return c.toGoIdentifier(name, true) +} + +// ToVariableName converts an OpenAPI name to a Go variable name (unexported). +func (c *NameConverter) ToVariableName(name string) string { + id := c.toGoIdentifier(name, false) + if id == "" { + return id + } + // Make first letter lowercase + runes := []rune(id) + runes[0] = unicode.ToLower(runes[0]) + return string(runes) +} + +// toGoIdentifier converts a name to a valid Go identifier. +func (c *NameConverter) toGoIdentifier(name string, exported bool) string { + if name == "" { + return "Empty" + } + + // Build the identifier with prefix handling + var result strings.Builder + prefix := c.getPrefix(name) + result.WriteString(prefix) + + // Convert the rest using word boundaries + capitalizeNext := exported || prefix != "" + prevWasDigit := false + for _, r := range name { + if c.isWordSeparator(r) { + capitalizeNext = true + prevWasDigit = false + continue + } + + if !unicode.IsLetter(r) && !unicode.IsDigit(r) { + // Skip invalid characters (already handled by prefix if at start) + capitalizeNext = true + prevWasDigit = false + continue + } + + // Capitalize after digits + if prevWasDigit && unicode.IsLetter(r) { + capitalizeNext = true + } + + if capitalizeNext && unicode.IsLetter(r) { + result.WriteRune(unicode.ToUpper(r)) + capitalizeNext = false + } else { + result.WriteRune(r) + } + + prevWasDigit = unicode.IsDigit(r) + } + + id := result.String() + if id == "" { + return "Empty" + } + + // Apply initialism fixes + id = c.applyInitialisms(id) + + return id +} + +// toGoIdentifierPart converts a name to a Go identifier component (for joining with others). +// It doesn't add a numeric prefix since the result won't necessarily be at the start of an identifier. +func (c *NameConverter) toGoIdentifierPart(name string) string { + if name == "" { + return "" + } + + // Build the identifier without numeric prefix (but still handle special characters at start) + var result strings.Builder + + // Only add prefix for non-digit special characters at the start + firstRune := []rune(name)[0] + if !unicode.IsLetter(firstRune) && !unicode.IsDigit(firstRune) { + firstChar := string(firstRune) + if sub, ok := c.mangling.CharacterSubstitutions[firstChar]; ok { + result.WriteString(sub) + } else { + result.WriteString("X") + } + } + + // Convert the rest using word boundaries (always capitalize since this is a part) + capitalizeNext := true + prevWasDigit := false + for _, r := range name { + if c.isWordSeparator(r) { + capitalizeNext = true + prevWasDigit = false + continue + } + + if !unicode.IsLetter(r) && !unicode.IsDigit(r) { + // Skip invalid characters (already handled by prefix if at start) + capitalizeNext = true + prevWasDigit = false + continue + } + + // Capitalize after digits + if prevWasDigit && unicode.IsLetter(r) { + capitalizeNext = true + } + + if capitalizeNext && unicode.IsLetter(r) { + result.WriteRune(unicode.ToUpper(r)) + capitalizeNext = false + } else { + result.WriteRune(r) + } + + prevWasDigit = unicode.IsDigit(r) + } + + id := result.String() + + // Apply initialism fixes + id = c.applyInitialisms(id) + + return id +} + +// getPrefix returns the prefix needed for names starting with invalid characters. +func (c *NameConverter) getPrefix(name string) string { + if name == "" { + return "" + } + + firstRune := []rune(name)[0] + + // Check if starts with digit + if unicode.IsDigit(firstRune) { + return c.mangling.NumericPrefix + } + + // Check if starts with letter (valid, no prefix needed) + if unicode.IsLetter(firstRune) { + return "" + } + + // Check character substitutions + firstChar := string(firstRune) + if sub, ok := c.mangling.CharacterSubstitutions[firstChar]; ok { + return sub + } + + // Unknown special character, use generic prefix + return "X" +} + +// isWordSeparator returns true if the rune is a word separator. +func (c *NameConverter) isWordSeparator(r rune) bool { + return strings.ContainsRune(c.mangling.WordSeparators, r) +} + +// applyInitialisms uppercases known initialisms in the identifier. +// It detects initialisms at word boundaries in PascalCase identifiers. +func (c *NameConverter) applyInitialisms(name string) string { + if len(name) == 0 { + return name + } + + // Split the identifier into "words" based on case transitions + // e.g., "UserId" -> ["User", "Id"], "HTTPUrl" -> ["HTTP", "Url"] + words := splitPascalCase(name) + + // Check each word against initialisms + for i, word := range words { + upper := strings.ToUpper(word) + if c.initialismSet[upper] { + words[i] = upper + } + } + + return strings.Join(words, "") +} + +// splitPascalCase splits a PascalCase identifier into words. +// e.g., "UserId" -> ["User", "Id"], "HTTPServer" -> ["HTTP", "Server"] +func splitPascalCase(s string) []string { + if len(s) == 0 { + return nil + } + + var words []string + var currentWord strings.Builder + + runes := []rune(s) + for i := 0; i < len(runes); i++ { + r := runes[i] + + if i == 0 { + currentWord.WriteRune(r) + continue + } + + prevUpper := unicode.IsUpper(runes[i-1]) + currUpper := unicode.IsUpper(r) + currDigit := unicode.IsDigit(r) + + // Start new word on: + // 1. Lowercase to uppercase transition (e.g., "userId" -> "user" | "Id") + // 2. Multiple uppercase followed by lowercase (e.g., "HTTPServer" -> "HTTP" | "Server") + if currUpper && !prevUpper { + // Lowercase to uppercase: start new word + words = append(words, currentWord.String()) + currentWord.Reset() + currentWord.WriteRune(r) + } else if currUpper && prevUpper && i+1 < len(runes) && unicode.IsLower(runes[i+1]) { + // Uppercase followed by lowercase, and previous was uppercase + // This is the start of a new word after an acronym + // e.g., in "HTTPServer", 'S' starts a new word + words = append(words, currentWord.String()) + currentWord.Reset() + currentWord.WriteRune(r) + } else if currDigit && !unicode.IsDigit(runes[i-1]) { + // Transition to digit: start new word + words = append(words, currentWord.String()) + currentWord.Reset() + currentWord.WriteRune(r) + } else if !currDigit && unicode.IsDigit(runes[i-1]) { + // Transition from digit: start new word + words = append(words, currentWord.String()) + currentWord.Reset() + currentWord.WriteRune(r) + } else { + currentWord.WriteRune(r) + } + } + + if currentWord.Len() > 0 { + words = append(words, currentWord.String()) + } + + return words +} diff --git a/experimental/internal/codegen/namemangling_test.go b/experimental/internal/codegen/namemangling_test.go new file mode 100644 index 0000000000..6d36cc0749 --- /dev/null +++ b/experimental/internal/codegen/namemangling_test.go @@ -0,0 +1,195 @@ +package codegen + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToTypeName(t *testing.T) { + c := NewNameConverter(DefaultNameMangling(), NameSubstitutions{}) + + tests := []struct { + input string + expected string + }{ + // Basic conversions + {"foo", "Foo"}, + {"fooBar", "FooBar"}, + {"foo_bar", "FooBar"}, + {"foo-bar", "FooBar"}, + {"foo.bar", "FooBar"}, + + // Names starting with numbers + {"123", "N123"}, + {"123foo", "N123Foo"}, + {"1param", "N1Param"}, + + // Names starting with special characters + {"$ref", "DollarSignRef"}, + {"$", "DollarSign"}, + {"-1", "Minus1"}, + {"+1", "Plus1"}, + {"&now", "AndNow"}, + {"#tag", "HashTag"}, + {".hidden", "DotHidden"}, + {"@timestamp", "AtTimestamp"}, + {"_private", "UnderscorePrivate"}, + + // Initialisms + {"userId", "UserID"}, + {"httpUrl", "HTTPURL"}, + {"apiId", "APIID"}, + {"jsonData", "JSONData"}, + {"xmlParser", "XMLParser"}, + {"getHttpResponse", "GetHTTPResponse"}, + + // Go keywords - PascalCase doesn't conflict with lowercase keywords + {"type", "Type"}, + {"interface", "Interface"}, + {"map", "Map"}, + {"chan", "Chan"}, + + // Predeclared identifiers - PascalCase doesn't conflict with lowercase identifiers + {"string", "String"}, + {"int", "Int"}, + {"error", "Error"}, + {"nil", "Nil"}, + + // Edge cases + {"", "Empty"}, + {"a", "A"}, + {"A", "A"}, + {"ABC", "ABC"}, + {"myXMLParser", "MyXMLParser"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result := c.ToTypeName(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestToPropertyName(t *testing.T) { + c := NewNameConverter(DefaultNameMangling(), NameSubstitutions{}) + + tests := []struct { + input string + expected string + }{ + {"user_id", "UserID"}, + {"created_at", "CreatedAt"}, + {"is_active", "IsActive"}, + {"123field", "N123Field"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result := c.ToPropertyName(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestToVariableName(t *testing.T) { + c := NewNameConverter(DefaultNameMangling(), NameSubstitutions{}) + + tests := []struct { + input string + expected string + }{ + {"Foo", "foo"}, + {"FooBar", "fooBar"}, + {"user_id", "userID"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result := c.ToVariableName(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestNameSubstitutions(t *testing.T) { + c := NewNameConverter(DefaultNameMangling(), NameSubstitutions{ + TypeNames: map[string]string{ + "foo": "MyCustomFoo", + }, + PropertyNames: map[string]string{ + "bar": "MyCustomBar", + }, + }) + + assert.Equal(t, "MyCustomFoo", c.ToTypeName("foo")) + assert.Equal(t, "MyCustomBar", c.ToPropertyName("bar")) + + // Non-substituted names still work normally + assert.Equal(t, "Baz", c.ToTypeName("baz")) +} + +func TestCustomNameMangling(t *testing.T) { + // Custom config that changes numeric prefix + mangling := DefaultNameMangling() + mangling.NumericPrefix = "Num" + + c := NewNameConverter(mangling, NameSubstitutions{}) + + assert.Equal(t, "Num123", c.ToTypeName("123")) + assert.Equal(t, "Num1Foo", c.ToTypeName("1foo")) +} + +func TestMergeNameMangling(t *testing.T) { + defaults := DefaultNameMangling() + + // User wants to change just the numeric prefix + user := NameMangling{ + NumericPrefix: "Number", + } + + merged := defaults.Merge(user) + + // User value overrides + assert.Equal(t, "Number", merged.NumericPrefix) + + // Defaults preserved + assert.Equal(t, defaults.KeywordPrefix, merged.KeywordPrefix) + assert.Equal(t, defaults.WordSeparators, merged.WordSeparators) + assert.Equal(t, len(defaults.CharacterSubstitutions), len(merged.CharacterSubstitutions)) +} + +func TestMergeCharacterSubstitutions(t *testing.T) { + defaults := DefaultNameMangling() + + // User wants to override $ and add a new one + user := NameMangling{ + CharacterSubstitutions: map[string]string{ + "$": "Dollar", // Override default "DollarSign" + "€": "Euro", // Add new + }, + } + + merged := defaults.Merge(user) + + assert.Equal(t, "Dollar", merged.CharacterSubstitutions["$"]) + assert.Equal(t, "Euro", merged.CharacterSubstitutions["€"]) + assert.Equal(t, "Minus", merged.CharacterSubstitutions["-"]) // Default preserved +} + +func TestRemoveCharacterSubstitution(t *testing.T) { + defaults := DefaultNameMangling() + + // User wants to remove $ substitution (empty string = remove) + user := NameMangling{ + CharacterSubstitutions: map[string]string{ + "$": "", // Remove + }, + } + + merged := defaults.Merge(user) + + _, exists := merged.CharacterSubstitutions["$"] + assert.False(t, exists) +} diff --git a/experimental/internal/codegen/operation.go b/experimental/internal/codegen/operation.go new file mode 100644 index 0000000000..88cb4fe27a --- /dev/null +++ b/experimental/internal/codegen/operation.go @@ -0,0 +1,287 @@ +package codegen + +import ( + "strings" + + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" +) + +// OperationSource indicates where an operation was defined in the spec. +type OperationSource string + +const ( + OperationSourcePath OperationSource = "path" + OperationSourceWebhook OperationSource = "webhook" + OperationSourceCallback OperationSource = "callback" +) + +// OperationDescriptor describes a single API operation from an OpenAPI spec. +type OperationDescriptor struct { + OperationID string // Normalized operation ID for function names + GoOperationID string // Go-safe identifier (handles leading digits, keywords) + Method string // HTTP method: GET, POST, PUT, DELETE, etc. + Path string // Original path: /users/{id} + Summary string // For generating comments + Description string // Longer description + + // Source indicates where this operation was defined (path, webhook, or callback) + Source OperationSource + WebhookName string // Webhook name (for Source=webhook) + CallbackName string // Callback key (for Source=callback) + ParentOpID string // Parent operation ID (for Source=callback) + + PathParams []*ParameterDescriptor + QueryParams []*ParameterDescriptor + HeaderParams []*ParameterDescriptor + CookieParams []*ParameterDescriptor + + Bodies []*RequestBodyDescriptor + Responses []*ResponseDescriptor + + Security []SecurityRequirement + + // Precomputed for templates + HasBody bool // Has at least one request body + HasParams bool // Has non-path params (needs Params struct) + ParamsTypeName string // "{OperationID}Params" + + // Reference to the underlying spec + Spec *v3.Operation +} + +// Params returns all non-path parameters (query, header, cookie). +// These are bundled into a Params struct. +func (o *OperationDescriptor) Params() []*ParameterDescriptor { + result := make([]*ParameterDescriptor, 0, len(o.QueryParams)+len(o.HeaderParams)+len(o.CookieParams)) + result = append(result, o.QueryParams...) + result = append(result, o.HeaderParams...) + result = append(result, o.CookieParams...) + return result +} + +// AllParams returns all parameters including path params. +func (o *OperationDescriptor) AllParams() []*ParameterDescriptor { + result := make([]*ParameterDescriptor, 0, len(o.PathParams)+len(o.QueryParams)+len(o.HeaderParams)+len(o.CookieParams)) + result = append(result, o.PathParams...) + result = append(result, o.QueryParams...) + result = append(result, o.HeaderParams...) + result = append(result, o.CookieParams...) + return result +} + +// SummaryAsComment returns the summary formatted as a Go comment. +func (o *OperationDescriptor) SummaryAsComment() string { + if o.Summary == "" { + return "" + } + trimmed := strings.TrimSuffix(o.Summary, "\n") + parts := strings.Split(trimmed, "\n") + for i, p := range parts { + parts[i] = "// " + p + } + return strings.Join(parts, "\n") +} + +// DefaultBody returns the default request body (typically application/json), or nil. +func (o *OperationDescriptor) DefaultBody() *RequestBodyDescriptor { + for _, b := range o.Bodies { + if b.IsDefault { + return b + } + } + if len(o.Bodies) > 0 { + return o.Bodies[0] + } + return nil +} + +// ParameterDescriptor describes a parameter in any location. +type ParameterDescriptor struct { + Name string // Original name from spec (e.g., "user_id") + GoName string // Go-safe name for struct fields (e.g., "UserId") + Location string // "path", "query", "header", "cookie" + Required bool + + // Serialization style + Style string // "simple", "form", "label", "matrix", etc. + Explode bool + + // Type information + Schema *SchemaDescriptor + TypeDecl string // Go type declaration (e.g., "string", "[]int", "*MyType") + + // Precomputed function names for templates + StyleFunc string // "StyleSimpleParam", "StyleFormExplodeParam", etc. + BindFunc string // "BindSimpleParam", "BindFormExplodeParam", etc. + + // Encoding modes + IsStyled bool // Uses style/explode serialization (most common) + IsPassThrough bool // No styling, just pass the string through + IsJSON bool // Parameter uses JSON content encoding + + Spec *v3.Parameter +} + +// GoVariableName returns a Go-safe variable name for this parameter. +// Used for local variables in generated code. +func (p *ParameterDescriptor) GoVariableName() string { + name := LowercaseFirstCharacter(p.GoName) + if IsGoKeyword(name) { + name = "p" + p.GoName + } + // Handle leading digits + if len(name) > 0 && name[0] >= '0' && name[0] <= '9' { + name = "n" + name + } + return name +} + +// HasOptionalPointer returns true if this parameter should be a pointer +// (optional parameters that aren't required). +func (p *ParameterDescriptor) HasOptionalPointer() bool { + if p.Required { + return false + } + // Check if schema has skip-optional-pointer extension + if p.Schema != nil && p.Schema.Extensions != nil && + p.Schema.Extensions.SkipOptionalPointer != nil && *p.Schema.Extensions.SkipOptionalPointer { + return false + } + return true +} + +// RequestBodyDescriptor describes a request body for a specific content type. +type RequestBodyDescriptor struct { + ContentType string // "application/json", "multipart/form-data", etc. + Required bool + Schema *SchemaDescriptor + + // Precomputed for templates + NameTag string // "JSON", "Formdata", "Multipart", "Text", etc. + GoTypeName string // "{OperationID}JSONBody", etc. + FuncSuffix string // "", "WithJSONBody", "WithFormBody" (empty for default) + IsDefault bool // Is this the default body type? + IsJSON bool // Is this a JSON content type? + + // Encoding options for form data + Encoding map[string]RequestBodyEncoding +} + +// RequestBodyEncoding describes encoding options for a form field. +type RequestBodyEncoding struct { + ContentType string + Style string + Explode *bool +} + +// ResponseDescriptor describes a response for a status code. +type ResponseDescriptor struct { + StatusCode string // "200", "404", "default", "2XX" + Description string + Contents []*ResponseContentDescriptor + Headers []*ResponseHeaderDescriptor + Ref string // If this is a reference to a named response +} + +// GoName returns a Go-safe name for this response (e.g., "200" -> "200", "default" -> "Default"). +func (r *ResponseDescriptor) GoName() string { + return ToCamelCase(r.StatusCode) +} + +// HasFixedStatusCode returns true if the status code is a specific number (not "default" or "2XX"). +func (r *ResponseDescriptor) HasFixedStatusCode() bool { + if r.StatusCode == "default" { + return false + } + // Check for wildcard patterns like "2XX" + if strings.HasSuffix(strings.ToUpper(r.StatusCode), "XX") { + return false + } + return true +} + +// ResponseContentDescriptor describes response content for a content type. +type ResponseContentDescriptor struct { + ContentType string + Schema *SchemaDescriptor + NameTag string // "JSON", "XML", etc. + IsJSON bool +} + +// ResponseHeaderDescriptor describes a response header. +type ResponseHeaderDescriptor struct { + Name string + GoName string + Required bool + Schema *SchemaDescriptor +} + +// SecurityRequirement describes a security requirement for an operation. +type SecurityRequirement struct { + Name string // Security scheme name + Scopes []string // Required scopes (for OAuth2) +} + +// Helper functions for computing descriptor fields + +// ComputeStyleFunc returns the style function name for a parameter. +func ComputeStyleFunc(style string, explode bool) string { + base := "Style" + ToCamelCase(style) + if explode { + return base + "ExplodeParam" + } + return base + "Param" +} + +// ComputeBindFunc returns the bind function name for a parameter. +func ComputeBindFunc(style string, explode bool) string { + base := "Bind" + ToCamelCase(style) + if explode { + return base + "ExplodeParam" + } + return base + "Param" +} + +// ComputeBodyNameTag returns the name tag for a content type. +func ComputeBodyNameTag(contentType string) string { + switch { + case contentType == "application/json": + return "JSON" + case IsMediaTypeJSON(contentType): + return MediaTypeToCamelCase(contentType) + case strings.HasPrefix(contentType, "multipart/"): + return "Multipart" + case contentType == "application/x-www-form-urlencoded": + return "Formdata" + case contentType == "text/plain": + return "Text" + case strings.HasPrefix(contentType, "application/xml") || strings.HasSuffix(contentType, "+xml"): + return "XML" + default: + return "" + } +} + +// IsMediaTypeJSON returns true if the content type is a JSON media type. +func IsMediaTypeJSON(contentType string) bool { + if contentType == "application/json" { + return true + } + if strings.HasSuffix(contentType, "+json") { + return true + } + if strings.Contains(contentType, "json") { + return true + } + return false +} + +// MediaTypeToCamelCase converts a media type to a CamelCase identifier. +func MediaTypeToCamelCase(mediaType string) string { + // application/vnd.api+json -> ApplicationVndApiJson + mediaType = strings.ReplaceAll(mediaType, "/", " ") + mediaType = strings.ReplaceAll(mediaType, "+", " ") + mediaType = strings.ReplaceAll(mediaType, ".", " ") + mediaType = strings.ReplaceAll(mediaType, "-", " ") + return ToCamelCase(mediaType) +} diff --git a/experimental/internal/codegen/output.go b/experimental/internal/codegen/output.go new file mode 100644 index 0000000000..0793124223 --- /dev/null +++ b/experimental/internal/codegen/output.go @@ -0,0 +1,764 @@ +package codegen + +import ( + "bytes" + "fmt" + "sort" + "strings" + + "golang.org/x/tools/imports" +) + +// Output collects generated Go code and formats it. +type Output struct { + packageName string + imports map[string]string // path -> alias + types []string // type definitions in order +} + +// NewOutput creates a new output collector. +func NewOutput(packageName string) *Output { + return &Output{ + packageName: packageName, + imports: make(map[string]string), + } +} + +// AddImport adds an import path with optional alias. +func (o *Output) AddImport(path, alias string) { + if path == "" { + return + } + o.imports[path] = alias +} + +// AddImports adds multiple imports from a map. +func (o *Output) AddImports(imports map[string]string) { + for path, alias := range imports { + o.AddImport(path, alias) + } +} + +// AddType adds a type definition to the output. +func (o *Output) AddType(code string) { + if code != "" { + o.types = append(o.types, code) + } +} + +// String generates the complete Go source file. +func (o *Output) String() string { + var buf bytes.Buffer + + // Generated code header (tells linters to skip this file) + buf.WriteString("// Code generated by oapi-codegen; DO NOT EDIT.\n\n") + + // Package declaration + fmt.Fprintf(&buf, "package %s\n\n", o.packageName) + + // Imports + if len(o.imports) > 0 { + buf.WriteString("import (\n") + paths := make([]string, 0, len(o.imports)) + for path := range o.imports { + paths = append(paths, path) + } + sort.Strings(paths) + + for _, path := range paths { + alias := o.imports[path] + if alias != "" { + fmt.Fprintf(&buf, "\t%s %q\n", alias, path) + } else { + fmt.Fprintf(&buf, "\t%q\n", path) + } + } + buf.WriteString(")\n\n") + } + + // Types + for _, t := range o.types { + buf.WriteString(t) + buf.WriteString("\n\n") + } + + return buf.String() +} + +// Format returns the formatted Go source code with imports organized. +func (o *Output) Format() (string, error) { + src := o.String() + formatted, err := imports.Process("", []byte(src), nil) + if err != nil { + return src, fmt.Errorf("formatting output: %w (source:\n%s)", err, src) + } + return string(formatted), nil +} + +// CodeBuilder helps construct Go code fragments. +type CodeBuilder struct { + buf bytes.Buffer + indent int +} + +// NewCodeBuilder creates a new code builder. +func NewCodeBuilder() *CodeBuilder { + return &CodeBuilder{} +} + +// Indent increases indentation. +func (b *CodeBuilder) Indent() { + b.indent++ +} + +// Dedent decreases indentation. +func (b *CodeBuilder) Dedent() { + if b.indent > 0 { + b.indent-- + } +} + +// Line writes a line with current indentation. +func (b *CodeBuilder) Line(format string, args ...any) { + for i := 0; i < b.indent; i++ { + b.buf.WriteByte('\t') + } + if len(args) > 0 { + fmt.Fprintf(&b.buf, format, args...) + } else { + b.buf.WriteString(format) + } + b.buf.WriteByte('\n') +} + +// BlankLine writes an empty line. +func (b *CodeBuilder) BlankLine() { + b.buf.WriteByte('\n') +} + +// Raw writes raw text without indentation or newline. +func (b *CodeBuilder) Raw(s string) { + b.buf.WriteString(s) +} + +// String returns the built code. +func (b *CodeBuilder) String() string { + return b.buf.String() +} + +// GenerateStruct generates a struct type definition. +func GenerateStruct(name string, fields []StructField, doc string, tagGen *StructTagGenerator) string { + b := NewCodeBuilder() + + // Type documentation + if doc != "" { + for _, line := range strings.Split(doc, "\n") { + b.Line("// %s", line) + } + } + + b.Line("type %s struct {", name) + b.Indent() + + for _, f := range fields { + tag := generateFieldTag(f, tagGen) + if f.Doc != "" { + // Single line comment for field + b.Line("%s %s %s // %s", f.Name, f.Type, tag, f.Doc) + } else { + b.Line("%s %s %s", f.Name, f.Type, tag) + } + } + + b.Dedent() + b.Line("}") + + return b.String() +} + +// generateFieldTag generates the struct tag for a field. +func generateFieldTag(f StructField, tagGen *StructTagGenerator) string { + if tagGen == nil { + if f.JSONIgnore { + return "`json:\"-\"`" + } + return FormatJSONTag(f.JSONName, f.OmitEmpty) + } + info := StructTagInfo{ + FieldName: f.JSONName, + GoFieldName: f.Name, + IsOptional: !f.Required, + IsNullable: f.Nullable, + IsPointer: f.Pointer, + OmitEmpty: f.OmitEmpty, + OmitZero: f.OmitZero, + JSONIgnore: f.JSONIgnore, + } + return tagGen.GenerateTags(info) +} + +// GenerateStructWithAdditionalProps generates a struct with AdditionalProperties field +// and custom marshal/unmarshal methods. +func GenerateStructWithAdditionalProps(name string, fields []StructField, addPropsType string, doc string, tagGen *StructTagGenerator) string { + b := NewCodeBuilder() + + // Type documentation + if doc != "" { + for _, line := range strings.Split(doc, "\n") { + b.Line("// %s", line) + } + } + + b.Line("type %s struct {", name) + b.Indent() + + // Regular fields + for _, f := range fields { + tag := generateFieldTag(f, tagGen) + b.Line("%s %s %s", f.Name, f.Type, tag) + } + + // AdditionalProperties field + b.Line("AdditionalProperties map[string]%s `json:\"-\"`", addPropsType) + + b.Dedent() + b.Line("}") + + return b.String() +} + +// GenerateTypeAlias generates a type alias definition. +func GenerateTypeAlias(name, targetType, doc string) string { + b := NewCodeBuilder() + + if doc != "" { + for _, line := range strings.Split(doc, "\n") { + b.Line("// %s", line) + } + } + + b.Line("type %s = %s", name, targetType) + + return b.String() +} + +// GenerateEnum generates an enum type with const values. +// If customNames is provided and has the same length as values, those names will be used +// as the constant names instead of auto-generated ones. +func GenerateEnum(name, baseType string, values []string, customNames []string, doc string) string { + return GenerateEnumWithConstPrefix(name, name, baseType, values, customNames, doc) +} + +// GenerateEnumWithConstPrefix generates an enum type with const values. +// typeName is used for the type definition, constPrefix is used for constant names. +// This allows the type to be defined with a stable name while constants use a friendly name. +func GenerateEnumWithConstPrefix(typeName, constPrefix, baseType string, values []string, customNames []string, doc string) string { + b := NewCodeBuilder() + + if doc != "" { + for _, line := range strings.Split(doc, "\n") { + b.Line("// %s", line) + } + } + + b.Line("type %s %s", typeName, baseType) + b.BlankLine() + + if len(values) > 0 { + b.Line("const (") + b.Indent() + + // Track used names to handle duplicates + usedNames := make(map[string]int) + + for i, v := range values { + var constName string + + // Use custom name if provided, otherwise auto-generate + if len(customNames) > i && customNames[i] != "" { + constName = constPrefix + "_" + customNames[i] + } else { + constSuffix := sanitizeEnumValue(v, baseType) + constName = constPrefix + "_" + constSuffix + } + + // Handle duplicate names by adding a numeric suffix + if count, exists := usedNames[constName]; exists { + constName = fmt.Sprintf("%s_%d", constName, count) + usedNames[constName] = count + 1 + } else { + usedNames[constName] = 1 + } + + if baseType == "string" { + b.Line("%s %s = %q", constName, typeName, v) + } else { + b.Line("%s %s = %s", constName, typeName, v) + } + } + + b.Dedent() + b.Line(")") + } + + return b.String() +} + +// sanitizeEnumValue converts an enum value to a valid Go identifier suffix. +func sanitizeEnumValue(v string, baseType string) string { + // For integer enums, prefix with N for readability + if baseType == "int" || baseType == "int32" || baseType == "int64" { + // Check if it's a numeric value + if len(v) > 0 { + firstChar := v[0] + if firstChar >= '0' && firstChar <= '9' || firstChar == '-' { + // Negative numbers + if firstChar == '-' { + return "Minus" + v[1:] + } + return "N" + v + } + } + } + + // Replace common special characters + v = strings.ReplaceAll(v, "-", "_") + v = strings.ReplaceAll(v, " ", "_") + v = strings.ReplaceAll(v, ".", "_") + + // Remove any remaining invalid characters + var result strings.Builder + for i, r := range v { + if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r == '_' { + result.WriteRune(r) + } else if r >= '0' && r <= '9' && i > 0 { + result.WriteRune(r) + } + } + + if result.Len() == 0 { + return "Value" + } + return result.String() +} + +// GenerateUnionType generates a union struct for anyOf/oneOf with marshal/unmarshal. +func GenerateUnionType(name string, members []UnionMember, isOneOf bool, doc string) string { + b := NewCodeBuilder() + + if doc != "" { + for _, line := range strings.Split(doc, "\n") { + b.Line("// %s", line) + } + } + + // Generate struct with pointer field for each member + b.Line("type %s struct {", name) + b.Indent() + + for _, m := range members { + b.Line("%s *%s", m.FieldName, m.TypeName) + } + + b.Dedent() + b.Line("}") + + return b.String() +} + +// UnionMember represents a member of a union type (anyOf/oneOf). +type UnionMember struct { + FieldName string // Go field name + TypeName string // Go type name + Index int // Position in anyOf/oneOf array + HasApplyDefaults bool // Whether this type has an ApplyDefaults method +} + +// isPrimitiveType returns true if the type is a Go primitive or collection +// that doesn't have an ApplyDefaults method. +func isPrimitiveType(typeName string) bool { + switch typeName { + case "string", "int", "int8", "int16", "int32", "int64", + "uint", "uint8", "uint16", "uint32", "uint64", + "float32", "float64", "bool", "any": + return true + default: + // Slices and maps don't have ApplyDefaults + if strings.HasPrefix(typeName, "[]") || strings.HasPrefix(typeName, "map[") { + return true + } + return false + } +} + +// GenerateUnionApplyDefaults generates ApplyDefaults for a union type. +// It recurses into non-nil members that have ApplyDefaults. +func GenerateUnionApplyDefaults(name string, members []UnionMember) string { + b := NewCodeBuilder() + + b.Line("// ApplyDefaults sets default values for fields that are nil.") + b.Line("func (u *%s) ApplyDefaults() {", name) + b.Indent() + + for _, m := range members { + // Only recurse into types that have ApplyDefaults + if m.HasApplyDefaults { + b.Line("if u.%s != nil {", m.FieldName) + b.Indent() + b.Line("u.%s.ApplyDefaults()", m.FieldName) + b.Dedent() + b.Line("}") + } + } + + b.Dedent() + b.Line("}") + + return b.String() +} + +// GenerateUnionMarshalOneOf generates MarshalJSON for a oneOf type. +func GenerateUnionMarshalOneOf(name string, members []UnionMember) string { + b := NewCodeBuilder() + + b.Line("func (u %s) MarshalJSON() ([]byte, error) {", name) + b.Indent() + + b.Line("var count int") + b.Line("var data []byte") + b.Line("var err error") + b.BlankLine() + + for _, m := range members { + b.Line("if u.%s != nil {", m.FieldName) + b.Indent() + b.Line("count++") + b.Line("data, err = json.Marshal(u.%s)", m.FieldName) + b.Line("if err != nil {") + b.Indent() + b.Line("return nil, err") + b.Dedent() + b.Line("}") + b.Dedent() + b.Line("}") + } + + b.BlankLine() + b.Line("if count != 1 {") + b.Indent() + b.Line("return nil, fmt.Errorf(\"%s: exactly one member must be set, got %%d\", count)", name) + b.Dedent() + b.Line("}") + b.BlankLine() + b.Line("return data, nil") + + b.Dedent() + b.Line("}") + + return b.String() +} + +// GenerateUnionUnmarshalOneOf generates UnmarshalJSON for a oneOf type. +func GenerateUnionUnmarshalOneOf(name string, members []UnionMember) string { + b := NewCodeBuilder() + + b.Line("func (u *%s) UnmarshalJSON(data []byte) error {", name) + b.Indent() + + b.Line("var successCount int") + b.BlankLine() + + for _, m := range members { + b.Line("var v%d %s", m.Index, m.TypeName) + b.Line("if err := json.Unmarshal(data, &v%d); err == nil {", m.Index) + b.Indent() + b.Line("u.%s = &v%d", m.FieldName, m.Index) + b.Line("successCount++") + b.Dedent() + b.Line("}") + b.BlankLine() + } + + b.Line("if successCount != 1 {") + b.Indent() + b.Line("return fmt.Errorf(\"%s: expected exactly one type to match, got %%d\", successCount)", name) + b.Dedent() + b.Line("}") + b.BlankLine() + b.Line("return nil") + + b.Dedent() + b.Line("}") + + return b.String() +} + +// GenerateUnionMarshalAnyOf generates MarshalJSON for an anyOf type. +func GenerateUnionMarshalAnyOf(name string, members []UnionMember) string { + b := NewCodeBuilder() + + b.Line("func (u %s) MarshalJSON() ([]byte, error) {", name) + b.Indent() + + // Check if any members are objects (need field merging) + hasObjects := false + for _, m := range members { + if !isPrimitiveType(m.TypeName) { + hasObjects = true + break + } + } + + if hasObjects { + // Merge object fields + b.Line("result := make(map[string]any)") + b.BlankLine() + + for _, m := range members { + b.Line("if u.%s != nil {", m.FieldName) + b.Indent() + if isPrimitiveType(m.TypeName) { + // For primitives, we can't merge - just return the value + b.Line("return json.Marshal(u.%s)", m.FieldName) + } else { + b.Line("data, err := json.Marshal(u.%s)", m.FieldName) + b.Line("if err != nil {") + b.Indent() + b.Line("return nil, err") + b.Dedent() + b.Line("}") + b.Line("var m map[string]any") + b.Line("if err := json.Unmarshal(data, &m); err == nil {") + b.Indent() + b.Line("for k, v := range m {") + b.Indent() + b.Line("result[k] = v") + b.Dedent() + b.Line("}") + b.Dedent() + b.Line("}") + } + b.Dedent() + b.Line("}") + } + + b.BlankLine() + b.Line("return json.Marshal(result)") + } else { + // All primitives - marshal the first non-nil one + for _, m := range members { + b.Line("if u.%s != nil {", m.FieldName) + b.Indent() + b.Line("return json.Marshal(u.%s)", m.FieldName) + b.Dedent() + b.Line("}") + } + b.Line("return []byte(\"null\"), nil") + } + + b.Dedent() + b.Line("}") + + return b.String() +} + +// GenerateUnionUnmarshalAnyOf generates UnmarshalJSON for an anyOf type. +func GenerateUnionUnmarshalAnyOf(name string, members []UnionMember) string { + b := NewCodeBuilder() + + b.Line("func (u *%s) UnmarshalJSON(data []byte) error {", name) + b.Indent() + + for _, m := range members { + b.Line("var v%d %s", m.Index, m.TypeName) + b.Line("if err := json.Unmarshal(data, &v%d); err == nil {", m.Index) + b.Indent() + b.Line("u.%s = &v%d", m.FieldName, m.Index) + b.Dedent() + b.Line("}") + b.BlankLine() + } + + b.Line("return nil") + + b.Dedent() + b.Line("}") + + return b.String() +} + +// GenerateMixedPropertiesMarshal generates MarshalJSON for structs with additionalProperties. +func GenerateMixedPropertiesMarshal(name string, fields []StructField) string { + b := NewCodeBuilder() + + b.Line("func (s %s) MarshalJSON() ([]byte, error) {", name) + b.Indent() + + b.Line("result := make(map[string]any)") + b.BlankLine() + + // Copy known fields + for _, f := range fields { + if f.Pointer { + b.Line("if s.%s != nil {", f.Name) + b.Indent() + b.Line("result[%q] = s.%s", f.JSONName, f.Name) + b.Dedent() + b.Line("}") + } else { + b.Line("result[%q] = s.%s", f.JSONName, f.Name) + } + } + + b.BlankLine() + b.Line("// Add additional properties") + b.Line("for k, v := range s.AdditionalProperties {") + b.Indent() + b.Line("result[k] = v") + b.Dedent() + b.Line("}") + b.BlankLine() + b.Line("return json.Marshal(result)") + + b.Dedent() + b.Line("}") + + return b.String() +} + +// GenerateApplyDefaults generates an ApplyDefaults method for a struct. +// It sets default values for fields that are nil and have defaults defined, +// and recursively calls ApplyDefaults on nested struct fields. +// Always generates the method (even if empty) so it can be called uniformly. +func GenerateApplyDefaults(name string, fields []StructField) string { + b := NewCodeBuilder() + + b.Line("// ApplyDefaults sets default values for fields that are nil.") + b.Line("func (s *%s) ApplyDefaults() {", name) + b.Indent() + + for _, f := range fields { + // Apply defaults to nil pointer fields + if f.Default != "" && f.Pointer { + b.Line("if s.%s == nil {", f.Name) + b.Indent() + // Get the base type (without *) + baseType := strings.TrimPrefix(f.Type, "*") + // Check if we need an explicit type conversion for numeric types + // This is needed because Go infers float64 for floating point literals + // and int for integer literals, which may not match the target type + if needsTypeConversion(baseType) { + b.Line("v := %s(%s)", baseType, f.Default) + } else { + b.Line("v := %s", f.Default) + } + b.Line("s.%s = &v", f.Name) + b.Dedent() + b.Line("}") + } + + // Recursively apply defaults to struct fields + if f.IsStruct && f.Pointer { + b.Line("if s.%s != nil {", f.Name) + b.Indent() + b.Line("s.%s.ApplyDefaults()", f.Name) + b.Dedent() + b.Line("}") + } + } + + b.Dedent() + b.Line("}") + + return b.String() +} + +// needsTypeConversion returns true if a numeric type needs an explicit conversion +// from the default Go literal type (int for integers, float64 for floats). +func needsTypeConversion(goType string) bool { + switch goType { + case "int8", "int16", "int32", "int64", + "uint", "uint8", "uint16", "uint32", "uint64", + "float32": + return true + default: + return false + } +} + +// GenerateMixedPropertiesUnmarshal generates UnmarshalJSON for structs with additionalProperties. +func GenerateMixedPropertiesUnmarshal(name string, fields []StructField, addPropsType string) string { + b := NewCodeBuilder() + + b.Line("func (s *%s) UnmarshalJSON(data []byte) error {", name) + b.Indent() + + // Build set of known field names + b.Line("// Known fields") + b.Line("knownFields := map[string]bool{") + b.Indent() + for _, f := range fields { + b.Line("%q: true,", f.JSONName) + } + b.Dedent() + b.Line("}") + b.BlankLine() + + // Unmarshal into a map first + b.Line("var raw map[string]json.RawMessage") + b.Line("if err := json.Unmarshal(data, &raw); err != nil {") + b.Indent() + b.Line("return err") + b.Dedent() + b.Line("}") + b.BlankLine() + + // Unmarshal known fields + for _, f := range fields { + b.Line("if v, ok := raw[%q]; ok {", f.JSONName) + b.Indent() + if f.Pointer { + b.Line("var val %s", strings.TrimPrefix(f.Type, "*")) + b.Line("if err := json.Unmarshal(v, &val); err != nil {") + b.Indent() + b.Line("return err") + b.Dedent() + b.Line("}") + b.Line("s.%s = &val", f.Name) + } else { + b.Line("if err := json.Unmarshal(v, &s.%s); err != nil {", f.Name) + b.Indent() + b.Line("return err") + b.Dedent() + b.Line("}") + } + b.Dedent() + b.Line("}") + } + + b.BlankLine() + b.Line("// Collect additional properties") + b.Line("s.AdditionalProperties = make(map[string]%s)", addPropsType) + b.Line("for k, v := range raw {") + b.Indent() + b.Line("if !knownFields[k] {") + b.Indent() + b.Line("var val %s", addPropsType) + b.Line("if err := json.Unmarshal(v, &val); err != nil {") + b.Indent() + b.Line("return err") + b.Dedent() + b.Line("}") + b.Line("s.AdditionalProperties[k] = val") + b.Dedent() + b.Line("}") + b.Dedent() + b.Line("}") + b.BlankLine() + b.Line("return nil") + + b.Dedent() + b.Line("}") + + return b.String() +} diff --git a/experimental/internal/codegen/paramgen.go b/experimental/internal/codegen/paramgen.go new file mode 100644 index 0000000000..19221ac0c0 --- /dev/null +++ b/experimental/internal/codegen/paramgen.go @@ -0,0 +1,178 @@ +package codegen + +import ( + "fmt" + "sort" + + "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/templates" +) + +// ParamUsageTracker tracks which parameter styling and binding functions +// are needed based on the OpenAPI spec being processed. +type ParamUsageTracker struct { + // usedStyles tracks which style/explode combinations are used. + // Keys are formatted as "style_{style}" or "style_{style}_explode" for serialization, + // and "bind_{style}" or "bind_{style}_explode" for binding. + usedStyles map[string]bool +} + +// NewParamUsageTracker creates a new ParamUsageTracker. +func NewParamUsageTracker() *ParamUsageTracker { + return &ParamUsageTracker{ + usedStyles: make(map[string]bool), + } +} + +// RecordStyleParam records that a style/explode combination is used for serialization. +// This is typically called when processing client parameters. +func (t *ParamUsageTracker) RecordStyleParam(style string, explode bool) { + key := templates.ParamStyleKey("style_", style, explode) + t.usedStyles[key] = true +} + +// RecordBindParam records that a style/explode combination is used for binding. +// This is typically called when processing server parameters. +func (t *ParamUsageTracker) RecordBindParam(style string, explode bool) { + key := templates.ParamStyleKey("bind_", style, explode) + t.usedStyles[key] = true +} + +// RecordParam records both style and bind usage for a parameter. +// Use this when generating both client and server code. +func (t *ParamUsageTracker) RecordParam(style string, explode bool) { + t.RecordStyleParam(style, explode) + t.RecordBindParam(style, explode) +} + +// HasAnyUsage returns true if any parameter functions are needed. +func (t *ParamUsageTracker) HasAnyUsage() bool { + return len(t.usedStyles) > 0 +} + +// GetRequiredTemplates returns the list of templates needed based on usage. +// The helpers template is always included first if any functions are needed. +func (t *ParamUsageTracker) GetRequiredTemplates() []templates.ParamTemplate { + if !t.HasAnyUsage() { + return nil + } + + var result []templates.ParamTemplate + + // Always include helpers first + result = append(result, templates.ParamHelpersTemplate) + + // Get all used style keys and sort them for deterministic output + keys := make([]string, 0, len(t.usedStyles)) + for key := range t.usedStyles { + keys = append(keys, key) + } + sort.Strings(keys) + + // Add each required template + for _, key := range keys { + tmpl, ok := templates.ParamTemplates[key] + if !ok { + // This shouldn't happen if keys are properly validated + continue + } + result = append(result, tmpl) + } + + return result +} + +// GetRequiredImports returns all imports needed for the used parameter functions. +// This aggregates imports from the helpers template and all used templates. +func (t *ParamUsageTracker) GetRequiredImports() []templates.Import { + if !t.HasAnyUsage() { + return nil + } + + // Use a map to deduplicate imports + importSet := make(map[string]templates.Import) + + // Add helpers imports + for _, imp := range templates.ParamHelpersTemplate.Imports { + importSet[imp.Path] = imp + } + + // Add imports from each used template + for key := range t.usedStyles { + tmpl, ok := templates.ParamTemplates[key] + if !ok { + continue + } + for _, imp := range tmpl.Imports { + importSet[imp.Path] = imp + } + } + + // Convert to sorted slice + result := make([]templates.Import, 0, len(importSet)) + for _, imp := range importSet { + result = append(result, imp) + } + sort.Slice(result, func(i, j int) bool { + return result[i].Path < result[j].Path + }) + + return result +} + +// GetUsedStyleKeys returns the sorted list of used style keys for debugging. +func (t *ParamUsageTracker) GetUsedStyleKeys() []string { + keys := make([]string, 0, len(t.usedStyles)) + for key := range t.usedStyles { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +// DefaultParamStyle returns the default style for a parameter location. +func DefaultParamStyle(location string) string { + switch location { + case "path", "header": + return "simple" + case "query", "cookie": + return "form" + default: + return "form" + } +} + +// DefaultParamExplode returns the default explode value for a parameter location. +func DefaultParamExplode(location string) bool { + switch location { + case "path", "header": + return false + case "query", "cookie": + return true + default: + return false + } +} + +// ValidateParamStyle validates that a style is supported for a location. +// Returns an error if the combination is invalid. +func ValidateParamStyle(style, location string) error { + validStyles := map[string][]string{ + "path": {"simple", "label", "matrix"}, + "query": {"form", "spaceDelimited", "pipeDelimited", "deepObject"}, + "header": {"simple"}, + "cookie": {"form"}, + } + + allowed, ok := validStyles[location] + if !ok { + return fmt.Errorf("unknown parameter location: %s", location) + } + + for _, s := range allowed { + if s == style { + return nil + } + } + + return fmt.Errorf("style '%s' is not valid for location '%s'; valid styles are: %v", style, location, allowed) +} diff --git a/experimental/internal/codegen/paramgen_test.go b/experimental/internal/codegen/paramgen_test.go new file mode 100644 index 0000000000..a80de1e40d --- /dev/null +++ b/experimental/internal/codegen/paramgen_test.go @@ -0,0 +1,161 @@ +package codegen + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParamUsageTracker(t *testing.T) { + t.Run("empty tracker has no usage", func(t *testing.T) { + tracker := NewParamUsageTracker() + assert.False(t, tracker.HasAnyUsage()) + assert.Empty(t, tracker.GetRequiredTemplates()) + assert.Empty(t, tracker.GetRequiredImports()) + }) + + t.Run("records style param", func(t *testing.T) { + tracker := NewParamUsageTracker() + tracker.RecordStyleParam("simple", false) + + assert.True(t, tracker.HasAnyUsage()) + keys := tracker.GetUsedStyleKeys() + assert.Contains(t, keys, "style_simple") + }) + + t.Run("records style param with explode", func(t *testing.T) { + tracker := NewParamUsageTracker() + tracker.RecordStyleParam("form", true) + + keys := tracker.GetUsedStyleKeys() + assert.Contains(t, keys, "style_form_explode") + }) + + t.Run("records bind param", func(t *testing.T) { + tracker := NewParamUsageTracker() + tracker.RecordBindParam("label", false) + + keys := tracker.GetUsedStyleKeys() + assert.Contains(t, keys, "bind_label") + }) + + t.Run("records both style and bind", func(t *testing.T) { + tracker := NewParamUsageTracker() + tracker.RecordParam("matrix", true) + + keys := tracker.GetUsedStyleKeys() + assert.Contains(t, keys, "style_matrix_explode") + assert.Contains(t, keys, "bind_matrix_explode") + }) + + t.Run("returns helpers template first", func(t *testing.T) { + tracker := NewParamUsageTracker() + tracker.RecordStyleParam("simple", false) + + templates := tracker.GetRequiredTemplates() + require.NotEmpty(t, templates) + assert.Equal(t, "helpers", templates[0].Name) + }) + + t.Run("aggregates imports", func(t *testing.T) { + tracker := NewParamUsageTracker() + tracker.RecordStyleParam("simple", false) + tracker.RecordStyleParam("form", true) + + imports := tracker.GetRequiredImports() + assert.NotEmpty(t, imports) + + // Check that common imports are included + paths := make([]string, len(imports)) + for i, imp := range imports { + paths[i] = imp.Path + } + assert.Contains(t, paths, "reflect") + assert.Contains(t, paths, "strings") + }) +} + +func TestDefaultParamStyle(t *testing.T) { + tests := []struct { + location string + expected string + }{ + {"path", "simple"}, + {"header", "simple"}, + {"query", "form"}, + {"cookie", "form"}, + {"unknown", "form"}, + } + + for _, tc := range tests { + t.Run(tc.location, func(t *testing.T) { + assert.Equal(t, tc.expected, DefaultParamStyle(tc.location)) + }) + } +} + +func TestDefaultParamExplode(t *testing.T) { + tests := []struct { + location string + expected bool + }{ + {"path", false}, + {"header", false}, + {"query", true}, + {"cookie", true}, + {"unknown", false}, + } + + for _, tc := range tests { + t.Run(tc.location, func(t *testing.T) { + assert.Equal(t, tc.expected, DefaultParamExplode(tc.location)) + }) + } +} + +func TestValidateParamStyle(t *testing.T) { + validCases := []struct { + style string + location string + }{ + {"simple", "path"}, + {"label", "path"}, + {"matrix", "path"}, + {"form", "query"}, + {"spaceDelimited", "query"}, + {"pipeDelimited", "query"}, + {"deepObject", "query"}, + {"simple", "header"}, + {"form", "cookie"}, + } + + for _, tc := range validCases { + t.Run(tc.style+"_in_"+tc.location, func(t *testing.T) { + err := ValidateParamStyle(tc.style, tc.location) + assert.NoError(t, err) + }) + } + + invalidCases := []struct { + style string + location string + }{ + {"deepObject", "path"}, + {"matrix", "query"}, + {"label", "header"}, + {"simple", "cookie"}, + } + + for _, tc := range invalidCases { + t.Run(tc.style+"_in_"+tc.location+"_invalid", func(t *testing.T) { + err := ValidateParamStyle(tc.style, tc.location) + assert.Error(t, err) + }) + } + + t.Run("unknown location", func(t *testing.T) { + err := ValidateParamStyle("simple", "body") + assert.Error(t, err) + }) +} diff --git a/experimental/internal/codegen/receivergen.go b/experimental/internal/codegen/receivergen.go new file mode 100644 index 0000000000..e57fef3929 --- /dev/null +++ b/experimental/internal/codegen/receivergen.go @@ -0,0 +1,131 @@ +package codegen + +import ( + "bytes" + "fmt" + "strings" + "text/template" + + "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/templates" +) + +// ReceiverTemplateData is passed to receiver templates. +type ReceiverTemplateData struct { + Prefix string // "Webhook" or "Callback" + PrefixLower string // "webhook" or "callback" + Operations []*OperationDescriptor // Operations to generate for +} + +// ReceiverGenerator generates receiver code from operation descriptors. +// It is parameterized by prefix to support both webhooks and callbacks. +type ReceiverGenerator struct { + tmpl *template.Template + prefix string // "Webhook" or "Callback" + serverType string +} + +// NewReceiverGenerator creates a new receiver generator for the specified server type. +func NewReceiverGenerator(prefix string, serverType string) (*ReceiverGenerator, error) { + if serverType == "" { + return nil, fmt.Errorf("%s receiver requires a server type to be set", prefix) + } + + tmpl := template.New("receiver").Funcs(templates.Funcs()) + + // Get receiver templates for the specified server type + receiverTemplates, err := getReceiverTemplates(serverType) + if err != nil { + return nil, err + } + + // Parse receiver-specific templates + for _, ct := range receiverTemplates { + content, err := templates.TemplateFS.ReadFile("files/" + ct.Template) + if err != nil { + return nil, fmt.Errorf("failed to read receiver template %s: %w", ct.Template, err) + } + _, err = tmpl.New(ct.Name).Parse(string(content)) + if err != nil { + return nil, fmt.Errorf("failed to parse receiver template %s: %w", ct.Template, err) + } + } + + // Parse shared templates (errors, param_types) + for _, st := range templates.SharedServerTemplates { + content, err := templates.TemplateFS.ReadFile("files/" + st.Template) + if err != nil { + return nil, fmt.Errorf("failed to read shared template %s: %w", st.Template, err) + } + _, err = tmpl.New(st.Name).Parse(string(content)) + if err != nil { + return nil, fmt.Errorf("failed to parse shared template %s: %w", st.Template, err) + } + } + + return &ReceiverGenerator{ + tmpl: tmpl, + prefix: prefix, + serverType: serverType, + }, nil +} + +// getReceiverTemplates returns the receiver templates for the specified server type. +func getReceiverTemplates(serverType string) (map[string]templates.ReceiverTemplate, error) { + switch serverType { + case ServerTypeStdHTTP: + return templates.StdHTTPReceiverTemplates, nil + case ServerTypeChi: + return templates.ChiReceiverTemplates, nil + case ServerTypeEcho: + return templates.EchoReceiverTemplates, nil + case ServerTypeEchoV4: + return templates.EchoV4ReceiverTemplates, nil + case ServerTypeGin: + return templates.GinReceiverTemplates, nil + case ServerTypeGorilla: + return templates.GorillaReceiverTemplates, nil + case ServerTypeFiber: + return templates.FiberReceiverTemplates, nil + case ServerTypeIris: + return templates.IrisReceiverTemplates, nil + default: + return nil, fmt.Errorf("unsupported server type for receiver: %q", serverType) + } +} + +func (g *ReceiverGenerator) templateData(ops []*OperationDescriptor) ReceiverTemplateData { + return ReceiverTemplateData{ + Prefix: g.prefix, + PrefixLower: strings.ToLower(g.prefix), + Operations: ops, + } +} + +// GenerateReceiver generates the receiver interface and handler functions. +func (g *ReceiverGenerator) GenerateReceiver(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + + if err := g.tmpl.ExecuteTemplate(&buf, "receiver", g.templateData(ops)); err != nil { + return "", fmt.Errorf("generating receiver code: %w", err) + } + + return buf.String(), nil +} + +// GenerateParamTypes generates the parameter struct types. +func (g *ReceiverGenerator) GenerateParamTypes(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "param_types", ops); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateErrors generates error types (shared with server). +func (g *ReceiverGenerator) GenerateErrors() (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "errors", nil); err != nil { + return "", err + } + return buf.String(), nil +} diff --git a/experimental/internal/codegen/schema.go b/experimental/internal/codegen/schema.go new file mode 100644 index 0000000000..bb0770bb65 --- /dev/null +++ b/experimental/internal/codegen/schema.go @@ -0,0 +1,117 @@ +package codegen + +import ( + "strings" + + "github.com/pb33f/libopenapi/datamodel/high/base" +) + +// SchemaPath represents the location of a schema in the OpenAPI document. +// Used for deriving type names and disambiguating collisions. +// Example: ["components", "schemas", "Pet", "properties", "address"] +type SchemaPath []string + +// String returns the path as a JSON pointer-style string. +func (p SchemaPath) String() string { + return "#/" + strings.Join(p, "/") +} + +// Append returns a new SchemaPath with the given elements appended. +// This creates a fresh slice to avoid aliasing issues with append. +func (p SchemaPath) Append(elements ...string) SchemaPath { + result := make(SchemaPath, len(p)+len(elements)) + copy(result, p) + copy(result[len(p):], elements) + return result +} + +// ContainsProperties returns true if this path contains "properties" anywhere. +// This indicates it's an inline property schema rather than a component schema. +func (p SchemaPath) ContainsProperties() bool { + for _, element := range p { + if element == "properties" { + return true + } + } + return false +} + +// SchemaDescriptor represents a schema found during the first pass through the spec. +type SchemaDescriptor struct { + // Path is where this schema appears in the document + Path SchemaPath + + // Ref is the $ref string if this is a reference (e.g., "#/components/schemas/Pet") + // Empty if this is an inline schema definition + Ref string + + // Schema is the underlying schema from libopenapi + // nil for unresolved external references + Schema *base.Schema + + // Parent points to the containing schema (nil for top-level schemas) + Parent *SchemaDescriptor + + // StableName is the deterministic Go type name derived from the full path. + // This name is stable across spec changes and should be used for type definitions. + // Example: #/components/schemas/Cat -> CatSchemaComponent + StableName string + + // ShortName is a friendly alias that may change due to deduplication. + // Generated as a type alias pointing to StableName. + ShortName string + + // OperationID is the operationId from the path operation, if this schema + // comes from a path's request body or response. Used for friendlier naming. + OperationID string + + // ContentType is the media type (e.g., "application/json") if this schema + // comes from a request body or response content. Used for naming. + ContentType string + + // Extensions holds parsed x- extension values for this schema. + // These control code generation behavior (type overrides, field names, etc.) + Extensions *Extensions + + // Recursive structure: + Properties map[string]*SchemaDescriptor + Items *SchemaDescriptor + AllOf []*SchemaDescriptor + AnyOf []*SchemaDescriptor + OneOf []*SchemaDescriptor + AdditionalProps *SchemaDescriptor +} + +// IsReference returns true if this schema is a $ref to another schema +func (d *SchemaDescriptor) IsReference() bool { + return d.Ref != "" +} + +// IsExternalReference returns true if this is a reference to an external file. +// External refs have the format: file.yaml#/path/to/schema +func (d *SchemaDescriptor) IsExternalReference() bool { + if d.Ref == "" { + return false + } + // External refs contain # but don't start with it + return !strings.HasPrefix(d.Ref, "#") && strings.Contains(d.Ref, "#") +} + +// ParseExternalRef splits an external reference into its file path and internal path. +// For "common/api.yaml#/components/schemas/Pet", returns ("common/api.yaml", "#/components/schemas/Pet"). +// Returns empty strings if not an external ref. +func (d *SchemaDescriptor) ParseExternalRef() (filePath, internalPath string) { + if !d.IsExternalReference() { + return "", "" + } + parts := strings.SplitN(d.Ref, "#", 2) + if len(parts) != 2 { + return "", "" + } + return parts[0], "#" + parts[1] +} + +// IsComponentSchema returns true if this schema is defined in #/components/schemas +func (d *SchemaDescriptor) IsComponentSchema() bool { + return len(d.Path) >= 2 && d.Path[0] == "components" && d.Path[1] == "schemas" +} diff --git a/experimental/internal/codegen/schemanames.go b/experimental/internal/codegen/schemanames.go new file mode 100644 index 0000000000..c41e8b6cef --- /dev/null +++ b/experimental/internal/codegen/schemanames.go @@ -0,0 +1,812 @@ +package codegen + +import ( + "fmt" + "strings" +) + +// SchemaContext identifies what kind of schema this is based on its location. +type SchemaContext int + +const ( + ContextUnknown SchemaContext = iota + ContextComponentSchema + ContextParameter + ContextRequestBody + ContextResponse + ContextHeader + ContextCallback + ContextWebhook + ContextProperty + ContextItems + ContextAllOf + ContextAnyOf + ContextOneOf + ContextAdditionalProperties +) + +// ComputeSchemaNames assigns StableName and ShortName to each schema descriptor. +// StableName is deterministic from the path; ShortName is a friendly alias. +// If a schema has a TypeNameOverride extension, that takes precedence over computed names. +func ComputeSchemaNames(schemas []*SchemaDescriptor, converter *NameConverter, contentTypeNamer *ContentTypeShortNamer) { + // First: compute stable names from full paths + for _, s := range schemas { + // Check for TypeNameOverride extension + if s.Extensions != nil && s.Extensions.TypeNameOverride != "" { + s.StableName = s.Extensions.TypeNameOverride + } else { + s.StableName = computeStableName(s.Path, converter) + } + } + + // Second: generate candidate short names + candidates := make(map[*SchemaDescriptor]string) + for _, s := range schemas { + // TypeNameOverride also applies to short names + if s.Extensions != nil && s.Extensions.TypeNameOverride != "" { + candidates[s] = s.Extensions.TypeNameOverride + } else { + candidates[s] = generateCandidateName(s, converter, contentTypeNamer) + } + } + + // Third: detect collisions and resolve them for short names + resolveCollisions(schemas, candidates, converter) + + // Assign final short names + for _, s := range schemas { + s.ShortName = candidates[s] + } +} + +// computeStableName generates a deterministic type name from the full path. +// The format is: {meaningful_names}{reversed_context_suffix} +// Example: #/components/schemas/Cat -> CatSchemaComponent +func computeStableName(path SchemaPath, converter *NameConverter) string { + if len(path) == 0 { + return "Schema" + } + + // Separate path into name parts and context parts + var nameParts []string + var contextParts []string + + for i := 0; i < len(path); i++ { + part := path[i] + // Strip leading slash from API paths (e.g., "/pets" -> "pets") + part = strings.TrimPrefix(part, "/") + if part == "" { + continue + } + if isContextKeyword(part) { + contextParts = append(contextParts, part) + } else { + nameParts = append(nameParts, part) + } + } + + // Build the name: names first, then reversed context as suffix + var result strings.Builder + + // Add name parts + // First part uses ToTypeName (adds numeric prefix if needed) + // Subsequent parts use ToTypeNamePart (no numeric prefix since they're not at the start) + for i, name := range nameParts { + if i == 0 { + result.WriteString(converter.ToTypeName(name)) + } else { + result.WriteString(converter.ToTypeNamePart(name)) + } + } + + // Add reversed context as suffix (singularized) + for i := len(contextParts) - 1; i >= 0; i-- { + suffix := contextToSuffix(contextParts[i]) + result.WriteString(suffix) + } + + name := result.String() + if name == "" { + return "Schema" + } + return name +} + +// isContextKeyword returns true if the path segment is a structural keyword +// rather than a user-defined name. +func isContextKeyword(s string) bool { + switch s { + case "components", "schemas", "parameters", "responses", "requestBodies", + "headers", "callbacks", "paths", "webhooks", + "properties", "items", "additionalProperties", + "allOf", "anyOf", "oneOf", "not", + "prefixItems", "contains", "if", "then", "else", + "dependentSchemas", "patternProperties", "propertyNames", + "unevaluatedItems", "unevaluatedProperties", + "content", "schema", "requestBody": + return true + default: + return false + } +} + +// contextToSuffix converts a context keyword to its singular suffix form. +func contextToSuffix(context string) string { + switch context { + case "components": + return "Component" + case "schemas": + return "Schema" + case "parameters": + return "Parameter" + case "responses": + return "Response" + case "requestBodies", "requestBody": + return "Request" + case "headers": + return "Header" + case "callbacks": + return "Callback" + case "paths": + return "Path" + case "webhooks": + return "Webhook" + case "properties": + return "Property" + case "items": + return "Item" + case "additionalProperties": + return "Value" + case "allOf": + return "AllOf" + case "anyOf": + return "AnyOf" + case "oneOf": + return "OneOf" + case "not": + return "Not" + case "prefixItems": + return "PrefixItem" + case "contains": + return "Contains" + case "if": + return "If" + case "then": + return "Then" + case "else": + return "Else" + case "content": + return "Content" + case "schema": + return "" // Skip redundant "Schema" suffix from content/schema + default: + return "" + } +} + +// generateCandidateName creates a candidate short name based on the schema's path. +func generateCandidateName(s *SchemaDescriptor, converter *NameConverter, contentTypeNamer *ContentTypeShortNamer) string { + path := s.Path + if len(path) == 0 { + return "Schema" + } + + ctx, parts := parsePathContext(path) + + switch ctx { + case ContextComponentSchema: + // #/components/schemas/Cat -> "Cat" + // #/components/schemas/Cat/properties/name -> "CatName" + return buildComponentSchemaName(parts, converter) + + case ContextParameter: + // Always suffix with Parameter + return buildParameterName(parts, converter) + + case ContextRequestBody: + // Use operationId if available for nicer names, but only for the direct request body schema + // (not nested items, properties, etc.) + if s.OperationID != "" && isDirectBodySchema(path) { + return buildOperationRequestName(s.OperationID, s.ContentType, converter, contentTypeNamer) + } + return buildRequestBodyName(parts, converter) + + case ContextResponse: + // Use operationId if available for nicer names, but only for the direct response schema + // (not nested items, properties, etc.) + if s.OperationID != "" && isDirectBodySchema(path) { + // Extract status code from path + statusCode := extractStatusCode(parts) + return buildOperationResponseName(s.OperationID, statusCode, s.ContentType, converter, contentTypeNamer) + } + return buildResponseName(parts, converter) + + case ContextHeader: + return buildHeaderName(parts, converter) + + case ContextCallback: + return buildCallbackName(parts, converter) + + case ContextWebhook: + return buildWebhookName(parts, converter) + + default: + // Fallback: join all meaningful parts + return buildFallbackName(path, converter) + } +} + +// parsePathContext determines the schema context and extracts relevant path parts. +func parsePathContext(path SchemaPath) (SchemaContext, []string) { + if len(path) == 0 { + return ContextUnknown, nil + } + + switch path[0] { + case "components": + if len(path) >= 3 && path[1] == "schemas" { + return ContextComponentSchema, path[2:] + } + if len(path) >= 3 && path[1] == "parameters" { + return ContextParameter, path[2:] + } + if len(path) >= 3 && path[1] == "requestBodies" { + return ContextRequestBody, path[2:] + } + if len(path) >= 3 && path[1] == "responses" { + return ContextResponse, path[2:] + } + if len(path) >= 3 && path[1] == "headers" { + return ContextHeader, path[2:] + } + if len(path) >= 3 && path[1] == "callbacks" { + return ContextCallback, path[2:] + } + + case "paths": + // paths/{path}/{method}/... + if len(path) >= 3 { + remaining := path[3:] // skip paths, {path}, {method} + return detectPathsContext(remaining), path[1:] // include path and method + } + + case "webhooks": + return ContextWebhook, path[1:] + } + + return ContextUnknown, path +} + +// detectPathsContext determines context from within a path item. +func detectPathsContext(remaining SchemaPath) SchemaContext { + if len(remaining) == 0 { + return ContextUnknown + } + + switch remaining[0] { + case "parameters": + return ContextParameter + case "requestBody": + return ContextRequestBody + case "responses": + return ContextResponse + case "callbacks": + return ContextCallback + } + + return ContextUnknown +} + +// buildComponentSchemaName builds a name for a component schema. +// e.g., ["Cat"] -> "Cat", ["Cat", "properties", "name"] -> "CatName" +func buildComponentSchemaName(parts []string, converter *NameConverter) string { + if len(parts) == 0 { + return "Schema" + } + + var nameParts []string + nameParts = append(nameParts, parts[0]) // schema name + + // Track trailing structural elements (only add suffix if they're at the end) + trailingSuffix := "" + + // Process nested parts + for i := 1; i < len(parts); i++ { + part := parts[i] + switch part { + case "properties": + // Skip, but next part (property name) will be added + // Clear trailing suffix since we're going deeper + trailingSuffix = "" + continue + case "items": + // Accumulate Item suffix (for nested arrays) + trailingSuffix += "Item" + continue + case "additionalProperties": + // Set Value suffix + trailingSuffix = "Value" + continue + case "allOf", "anyOf", "oneOf": + // Include the composition type and index + trailingSuffix = "" // Clear since we're adding meaningful content + if i+1 < len(parts) { + nameParts = append(nameParts, part+parts[i+1]) + i++ // Skip the index + } else { + nameParts = append(nameParts, part) + } + case "not", "prefixItems", "contains", "if", "then", "else": + // Include these structural keywords + trailingSuffix = "" + nameParts = append(nameParts, part) + default: + // Include meaningful parts (property names, indices) + // Clear trailing suffix since we have a meaningful name part + trailingSuffix = "" + nameParts = append(nameParts, part) + } + } + + name := converter.ToTypeName(strings.Join(nameParts, "_")) + + // Add trailing structural suffix if still present + if trailingSuffix != "" { + name += trailingSuffix + } + + return name +} + +// buildParameterName builds a name for a parameter schema. +func buildParameterName(parts []string, converter *NameConverter) string { + // parts could be: + // - from components/parameters: [paramName, "schema"] + // - from paths: [path, method, "parameters", index, "schema"] + + var baseName string + if len(parts) >= 2 && parts[0] != "" { + // Try to extract operation-style name + baseName = buildOperationName(parts, converter) + } + if baseName == "" && len(parts) > 0 { + baseName = converter.ToTypeName(parts[0]) + } + if baseName == "" { + baseName = "Param" + } + + // Always add Parameter suffix + if !strings.HasSuffix(baseName, "Parameter") { + baseName += "Parameter" + } + return baseName +} + +// buildRequestBodyName builds a name for a request body schema. +func buildRequestBodyName(parts []string, converter *NameConverter) string { + var baseName string + if len(parts) >= 2 { + baseName = buildOperationName(parts, converter) + } + if baseName == "" && len(parts) > 0 { + baseName = converter.ToTypeName(parts[0]) + } + if baseName == "" { + baseName = "Request" + } + + // Always add Request suffix + if !strings.HasSuffix(baseName, "Request") { + baseName += "Request" + } + return baseName +} + +// buildResponseName builds a name for a response schema. +func buildResponseName(parts []string, converter *NameConverter) string { + var baseName string + var statusCode string + + if len(parts) >= 4 { + // paths: [path, method, "responses", code, ...] + baseName = buildOperationName(parts[:2], converter) + // Find status code + for i, p := range parts { + if p == "responses" && i+1 < len(parts) { + statusCode = parts[i+1] + break + } + } + } + if baseName == "" && len(parts) > 0 { + baseName = converter.ToTypeName(parts[0]) + } + if baseName == "" { + baseName = "Response" + } + + // Add status code if present + if statusCode != "" && statusCode != "default" { + baseName += statusCode + } + + // Always add Response suffix + if !strings.HasSuffix(baseName, "Response") { + baseName += "Response" + } + return baseName +} + +// buildHeaderName builds a name for a header schema. +func buildHeaderName(parts []string, converter *NameConverter) string { + if len(parts) == 0 { + return "Header" + } + baseName := converter.ToTypeName(parts[0]) + if !strings.HasSuffix(baseName, "Header") { + baseName += "Header" + } + return baseName +} + +// buildCallbackName builds a name for a callback schema. +func buildCallbackName(parts []string, converter *NameConverter) string { + if len(parts) == 0 { + return "Callback" + } + return converter.ToTypeName(parts[0]) + "Callback" +} + +// buildWebhookName builds a name for a webhook schema. +func buildWebhookName(parts []string, converter *NameConverter) string { + if len(parts) == 0 { + return "Webhook" + } + return converter.ToTypeName(parts[0]) + "Webhook" +} + +// buildOperationName builds a name from path and method. +// e.g., ["/pets", "get"] -> "GetPets" +func buildOperationName(parts []string, converter *NameConverter) string { + if len(parts) < 2 { + return "" + } + + pathStr := parts[0] + method := parts[1] + + // Convert method to title case + methodName := converter.ToTypeName(method) + + // Convert path to name parts + // /pets/{petId}/toys -> PetsPetIdToys + pathName := pathToName(pathStr, converter) + + return methodName + pathName +} + +// pathToName converts an API path to a name component. +// e.g., "/pets/{petId}" -> "PetsPetId" +func pathToName(path string, converter *NameConverter) string { + // Remove leading slash + path = strings.TrimPrefix(path, "/") + + // Split by slash + segments := strings.Split(path, "/") + + var parts []string + for _, seg := range segments { + if seg == "" { + continue + } + // Remove braces from path parameters + seg = strings.TrimPrefix(seg, "{") + seg = strings.TrimSuffix(seg, "}") + parts = append(parts, seg) + } + + return converter.ToTypeName(strings.Join(parts, "_")) +} + +// buildFallbackName creates a name from the full path as a last resort. +func buildFallbackName(path SchemaPath, converter *NameConverter) string { + var parts []string + for _, p := range path { + // Skip common structural elements + switch p { + case "components", "schemas", "paths", "properties", + "items", "schema", "content", "application/json": + continue + default: + parts = append(parts, p) + } + } + + if len(parts) == 0 { + return "Schema" + } + + return converter.ToTypeName(strings.Join(parts, "_")) +} + +// schemaContextSuffix maps a SchemaContext to a disambiguation suffix. +func schemaContextSuffix(ctx SchemaContext) string { + switch ctx { + case ContextComponentSchema: + return "Schema" + case ContextParameter: + return "Parameter" + case ContextRequestBody: + return "Request" + case ContextResponse: + return "Response" + case ContextHeader: + return "Header" + case ContextCallback: + return "Callback" + case ContextWebhook: + return "Webhook" + default: + return "" + } +} + +// resolveCollisions detects name collisions and makes them unique. +// Reference schemas are excluded from collision detection because they don't +// generate types — their names are only used for type resolution lookups. +// +// Resolution proceeds in phases: +// 1. Context suffix: append a suffix derived from the schema's location +// (e.g. "Request", "Response"). If exactly one collider lives under +// components/schemas it keeps the bare name. +// 2. Existing disambiguateName logic (content type, status code, composition). +// 3. Numeric fallback as a last resort. +func resolveCollisions(schemas []*SchemaDescriptor, candidates map[*SchemaDescriptor]string, converter *NameConverter) { + // Filter out reference schemas — they don't generate types so their + // short names can safely shadow non-ref names without causing a collision. + var nonRefSchemas []*SchemaDescriptor + for _, s := range schemas { + if s.Ref == "" { + nonRefSchemas = append(nonRefSchemas, s) + } + } + + maxIterations := 10 // Prevent infinite loops + + for iteration := range maxIterations { + // Group non-ref schemas by candidate name + byName := make(map[string][]*SchemaDescriptor) + for _, s := range nonRefSchemas { + name := candidates[s] + byName[name] = append(byName[name], s) + } + + // Check if there are any collisions + hasCollisions := false + for _, group := range byName { + if len(group) > 1 { + hasCollisions = true + break + } + } + + if !hasCollisions { + return // All names are unique + } + + // Resolve collisions + for _, group := range byName { + if len(group) <= 1 { + continue // No collision + } + + // On last iteration, just add numeric suffixes + if iteration == maxIterations-1 { + for i, s := range group { + candidates[s] = fmt.Sprintf("%s%d", candidates[s], i+1) + } + continue + } + + // First iteration: try context suffix disambiguation + if iteration == 0 { + resolveWithContextSuffix(group, candidates) + continue + } + + // Subsequent iterations: existing disambiguateName logic + for i, s := range group { + newName := disambiguateName(s, candidates[s], i, converter) + candidates[s] = newName + } + } + } +} + +// resolveWithContextSuffix attempts to disambiguate colliding schemas by +// appending a suffix derived from their path context (e.g. "Request", +// "Response"). If exactly one member is a component schema, it keeps the +// bare name and only the others are suffixed. +func resolveWithContextSuffix(group []*SchemaDescriptor, candidates map[*SchemaDescriptor]string) { + // Count how many are from components/schemas + var componentSchemaCount int + for _, s := range group { + ctx, _ := parsePathContext(s.Path) + if ctx == ContextComponentSchema { + componentSchemaCount++ + } + } + + // If exactly one is from components/schemas, it is "privileged" and keeps + // the bare name. + privileged := componentSchemaCount == 1 + + for _, s := range group { + ctx, _ := parsePathContext(s.Path) + + // Privileged component schema keeps the bare name + if privileged && ctx == ContextComponentSchema { + continue + } + + suffix := schemaContextSuffix(ctx) + if suffix != "" { + name := candidates[s] + if !strings.HasSuffix(name, suffix) { + candidates[s] = name + suffix + } + } + // If suffix is empty (unknown context), leave unchanged for later + // iterations to handle via disambiguateName. + } +} + +// disambiguateName adds more context to make a name unique. +func disambiguateName(s *SchemaDescriptor, currentName string, index int, converter *NameConverter) string { + path := s.Path + + // Try to add more path context based on what's in the path + // but not already in the name + + // Check for content type differentiation + for i, part := range path { + if part == "content" && i+1 < len(path) { + contentType := path[i+1] + var suffix string + switch { + case strings.Contains(contentType, "json"): + suffix = "JSON" + case strings.Contains(contentType, "xml"): + suffix = "XML" + case strings.Contains(contentType, "form"): + suffix = "Form" + case strings.Contains(contentType, "text"): + suffix = "Text" + case strings.Contains(contentType, "binary"): + suffix = "Binary" + default: + suffix = converter.ToTypeName(strings.ReplaceAll(contentType, "/", "_")) + } + // Use Contains since the suffix might be embedded before "Response" or "Request" + if !strings.Contains(currentName, suffix) { + return currentName + suffix + } + } + } + + // Check for status code differentiation (for responses) + for i, part := range path { + if part == "responses" && i+1 < len(path) { + code := path[i+1] + if !strings.Contains(currentName, code) { + return currentName + code + } + } + } + + // Check for parameter index differentiation + for i, part := range path { + if part == "parameters" && i+1 < len(path) { + idx := path[i+1] + if !strings.HasSuffix(currentName, idx) { + return currentName + idx + } + } + } + + // Check for composition type differentiation + for i := len(path) - 1; i >= 0; i-- { + part := path[i] + switch part { + case "allOf", "anyOf", "oneOf": + suffix := converter.ToTypeName(part) + if i+1 < len(path) { + suffix += path[i+1] // Add index + } + if !strings.Contains(currentName, suffix) { + return currentName + suffix + } + } + } + + // Last resort: use numeric suffix + return fmt.Sprintf("%s%d", currentName, index+1) +} + +// buildOperationRequestName builds a name for a request body using the operationId. +// e.g., operationId="addPet" -> "AddPetJSONRequest" +func buildOperationRequestName(operationID, contentType string, converter *NameConverter, contentTypeNamer *ContentTypeShortNamer) string { + baseName := converter.ToTypeName(operationID) + + // Add content type short name if available + if contentType != "" && contentTypeNamer != nil { + baseName += contentTypeNamer.ShortName(contentType) + } + + return baseName + "Request" +} + +// buildOperationResponseName builds a name for a response using the operationId. +// e.g., operationId="findPets", statusCode="200" -> "FindPetsJSONResponse" +// e.g., operationId="findPets", statusCode="404" -> "FindPets404JSONResponse" +// e.g., operationId="findPets", statusCode="default" -> "FindPetsDefaultJSONResponse" +func buildOperationResponseName(operationID, statusCode, contentType string, converter *NameConverter, contentTypeNamer *ContentTypeShortNamer) string { + baseName := converter.ToTypeName(operationID) + + // Add status code, skipping only for 200 (the common success case) + if statusCode != "" && statusCode != "200" { + if statusCode == "default" { + baseName += "Default" + } else { + baseName += statusCode + } + } + + // Add content type short name if available + if contentType != "" && contentTypeNamer != nil { + baseName += contentTypeNamer.ShortName(contentType) + } + + return baseName + "Response" +} + +// extractStatusCode extracts the HTTP status code from path parts. +// Looks for "responses" followed by the status code. +func extractStatusCode(parts []string) string { + for i, p := range parts { + if p == "responses" && i+1 < len(parts) { + return parts[i+1] + } + } + return "" +} + +// isDirectBodySchema returns true if the schema path represents the direct +// schema of a request body or response (i.e., content/{type}/schema), +// not a nested schema (items, properties, allOf members, etc.). +func isDirectBodySchema(path SchemaPath) bool { + // Find the position of "schema" after "content" + schemaIdx := -1 + for i := len(path) - 1; i >= 0; i-- { + if path[i] == "schema" { + schemaIdx = i + break + } + } + if schemaIdx == -1 { + return false + } + + // Check that "schema" is directly after a content type (content/{type}/schema) + // and there are no structural elements after it + if schemaIdx < 2 { + return false + } + if path[schemaIdx-2] != "content" { + return false + } + + // If schema is at the end of the path, it's a direct body schema + return schemaIdx == len(path)-1 +} diff --git a/experimental/internal/codegen/servergen.go b/experimental/internal/codegen/servergen.go new file mode 100644 index 0000000000..86177c4167 --- /dev/null +++ b/experimental/internal/codegen/servergen.go @@ -0,0 +1,180 @@ +package codegen + +import ( + "bytes" + "fmt" + "text/template" + + "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/templates" +) + +// ServerGenerator generates server code from operation descriptors. +type ServerGenerator struct { + tmpl *template.Template + serverType string +} + +// NewServerGenerator creates a new server generator for the specified server type. +func NewServerGenerator(serverType string) (*ServerGenerator, error) { + if serverType == "" { + // No server generation requested + return &ServerGenerator{serverType: ""}, nil + } + + tmpl := template.New("server").Funcs(templates.Funcs()) + + // Get templates for the specified server type + serverTemplates, err := getServerTemplates(serverType) + if err != nil { + return nil, err + } + + // Parse server-specific templates + for _, st := range serverTemplates { + content, err := templates.TemplateFS.ReadFile("files/" + st.Template) + if err != nil { + return nil, fmt.Errorf("failed to read template %s: %w", st.Template, err) + } + _, err = tmpl.New(st.Name).Parse(string(content)) + if err != nil { + return nil, fmt.Errorf("failed to parse template %s: %w", st.Template, err) + } + } + + // Parse shared templates + for _, st := range templates.SharedServerTemplates { + content, err := templates.TemplateFS.ReadFile("files/" + st.Template) + if err != nil { + return nil, fmt.Errorf("failed to read shared template %s: %w", st.Template, err) + } + _, err = tmpl.New(st.Name).Parse(string(content)) + if err != nil { + return nil, fmt.Errorf("failed to parse shared template %s: %w", st.Template, err) + } + } + + return &ServerGenerator{tmpl: tmpl, serverType: serverType}, nil +} + +// getServerTemplates returns the templates for the specified server type. +func getServerTemplates(serverType string) (map[string]templates.ServerTemplate, error) { + switch serverType { + case ServerTypeStdHTTP: + return templates.StdHTTPServerTemplates, nil + case ServerTypeChi: + return templates.ChiServerTemplates, nil + case ServerTypeEcho: + return templates.EchoServerTemplates, nil + case ServerTypeEchoV4: + return templates.EchoV4ServerTemplates, nil + case ServerTypeGin: + return templates.GinServerTemplates, nil + case ServerTypeGorilla: + return templates.GorillaServerTemplates, nil + case ServerTypeFiber: + return templates.FiberServerTemplates, nil + case ServerTypeIris: + return templates.IrisServerTemplates, nil + default: + return nil, fmt.Errorf("unsupported server type: %q (supported: %q, %q, %q, %q, %q, %q, %q, %q)", + serverType, + ServerTypeStdHTTP, ServerTypeChi, ServerTypeEcho, ServerTypeEchoV4, ServerTypeGin, + ServerTypeGorilla, ServerTypeFiber, ServerTypeIris) + } +} + +// GenerateInterface generates the ServerInterface. +func (g *ServerGenerator) GenerateInterface(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "interface", ops); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateHandler generates the HTTP handler and routing code. +func (g *ServerGenerator) GenerateHandler(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "handler", ops); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateWrapper generates the ServerInterfaceWrapper. +func (g *ServerGenerator) GenerateWrapper(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "wrapper", ops); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateErrors generates the error types. +func (g *ServerGenerator) GenerateErrors() (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "errors", nil); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateParamTypes generates the parameter struct types. +func (g *ServerGenerator) GenerateParamTypes(ops []*OperationDescriptor) (string, error) { + var buf bytes.Buffer + if err := g.tmpl.ExecuteTemplate(&buf, "param_types", ops); err != nil { + return "", err + } + return buf.String(), nil +} + +// GenerateServer generates all server code components. +// Returns empty string if no server type was configured. +func (g *ServerGenerator) GenerateServer(ops []*OperationDescriptor) (string, error) { + if g.serverType == "" || g.tmpl == nil { + return "", nil + } + + var buf bytes.Buffer + + // Generate interface + iface, err := g.GenerateInterface(ops) + if err != nil { + return "", err + } + buf.WriteString(iface) + buf.WriteString("\n") + + // Generate param types + paramTypes, err := g.GenerateParamTypes(ops) + if err != nil { + return "", err + } + buf.WriteString(paramTypes) + buf.WriteString("\n") + + // Generate wrapper + wrapper, err := g.GenerateWrapper(ops) + if err != nil { + return "", err + } + buf.WriteString(wrapper) + buf.WriteString("\n") + + // Generate handler + handler, err := g.GenerateHandler(ops) + if err != nil { + return "", err + } + buf.WriteString(handler) + buf.WriteString("\n") + + // Generate errors + errors, err := g.GenerateErrors() + if err != nil { + return "", err + } + buf.WriteString(errors) + + return buf.String(), nil +} diff --git a/experimental/internal/codegen/skip_external_ref_test.go b/experimental/internal/codegen/skip_external_ref_test.go new file mode 100644 index 0000000000..6983852a73 --- /dev/null +++ b/experimental/internal/codegen/skip_external_ref_test.go @@ -0,0 +1,68 @@ +package codegen + +import ( + "os" + "strings" + "testing" + + "github.com/pb33f/libopenapi" + "github.com/pb33f/libopenapi/datamodel" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSkipExternalRefResolution verifies that we can parse a spec containing +// external $ref references without resolving them, and still generate correct +// code using import mappings. This uses libopenapi's SkipExternalRefResolution +// flag (added in v0.33.4, pb33f/libopenapi#519). +func TestSkipExternalRefResolution(t *testing.T) { + specData, err := os.ReadFile("test/external_ref/spec.yaml") + require.NoError(t, err) + + // Parse WITHOUT BasePath or AllowFileReferences — the external spec files + // won't be read. Instead, we rely on SkipExternalRefResolution to leave + // external $refs unresolved while still building an iterable model. + docConfig := datamodel.NewDocumentConfiguration() + docConfig.SkipExternalRefResolution = true + + doc, err := libopenapi.NewDocumentWithConfiguration(specData, docConfig) + require.NoError(t, err) + + cfg := Configuration{ + PackageName: "externalref", + ImportMapping: map[string]string{ + "./packagea/spec.yaml": "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packagea", + "./packageb/spec.yaml": "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packageb", + }, + } + + code, err := Generate(doc, nil, cfg) + require.NoError(t, err) + + // The generated code should contain the Container struct with external type references + assert.Contains(t, code, "type Container struct") + assert.Contains(t, code, "ObjectA") + assert.Contains(t, code, "ObjectB") + + // Should reference the external packages via hashed aliases + assert.Contains(t, code, "ext_95d82e90") + assert.Contains(t, code, "ext_a5fddf6c") + + // Should contain the import declarations + assert.Contains(t, code, `"github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packagea"`) + assert.Contains(t, code, `"github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packageb"`) + + // Should NOT contain "any" as a fallback type for the external refs + // (which would indicate the refs weren't properly detected) + lines := strings.Split(code, "\n") + for _, line := range lines { + if strings.Contains(line, "ObjectA") && strings.Contains(line, "any") { + t.Errorf("ObjectA resolved to 'any' instead of external type: %s", line) + } + if strings.Contains(line, "ObjectB") && strings.Contains(line, "any") { + t.Errorf("ObjectB resolved to 'any' instead of external type: %s", line) + } + } + + t.Logf("Generated code:\n%s", code) +} diff --git a/experimental/internal/codegen/structtags.go b/experimental/internal/codegen/structtags.go new file mode 100644 index 0000000000..36279956b7 --- /dev/null +++ b/experimental/internal/codegen/structtags.go @@ -0,0 +1,172 @@ +package codegen + +import ( + "bytes" + "sort" + "strings" + "text/template" +) + +// StructTagInfo contains the data available to struct tag templates. +type StructTagInfo struct { + // FieldName is the JSON/YAML field name (from the OpenAPI property name) + FieldName string + // GoFieldName is the Go struct field name + GoFieldName string + // IsOptional is true if the field is optional (not required) + IsOptional bool + // IsNullable is true if the field can be null + IsNullable bool + // IsPointer is true if the Go type is a pointer + IsPointer bool + // OmitEmpty is true if the omitempty tag option should be used + // (derived from IsOptional but can be overridden via extensions) + OmitEmpty bool + // OmitZero is true if the omitzero tag option should be used (Go 1.24+) + OmitZero bool + // JSONIgnore is true if the field should be excluded from JSON (json:"-") + JSONIgnore bool +} + +// StructTagTemplate defines a single struct tag with a name and template. +type StructTagTemplate struct { + // Name is the tag name (e.g., "json", "yaml", "form") + Name string `yaml:"name"` + // Template is a Go text/template that produces the tag value. + // Available fields: .FieldName, .GoFieldName, .IsOptional, .IsNullable, .IsPointer + // Example: `{{ .FieldName }}{{if .IsOptional}},omitempty{{end}}` + Template string `yaml:"template"` +} + +// StructTagsConfig configures struct tag generation. +type StructTagsConfig struct { + // Tags is the list of tags to generate for struct fields. + // Order is preserved in the generated output. + Tags []StructTagTemplate `yaml:"tags,omitempty"` +} + +// DefaultStructTagsConfig returns the default struct tag configuration. +// By default, json and form tags are generated. +func DefaultStructTagsConfig() StructTagsConfig { + return StructTagsConfig{ + Tags: []StructTagTemplate{ + { + Name: "json", + Template: `{{if .JSONIgnore}}-{{else}}{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}{{if .OmitZero}},omitzero{{end}}{{end}}`, + }, + { + Name: "form", + Template: `{{if .JSONIgnore}}-{{else}}{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}{{end}}`, + }, + }, + } +} + +// Merge merges user config on top of this config. +// If user specifies any tags, they completely replace the defaults. +func (c StructTagsConfig) Merge(other StructTagsConfig) StructTagsConfig { + if len(other.Tags) > 0 { + return other + } + return c +} + +// StructTagGenerator generates struct tags from templates. +type StructTagGenerator struct { + templates []*tagTemplate +} + +type tagTemplate struct { + name string + tmpl *template.Template +} + +// NewStructTagGenerator creates a generator from the configuration. +// Invalid templates are silently skipped. +func NewStructTagGenerator(config StructTagsConfig) *StructTagGenerator { + g := &StructTagGenerator{ + templates: make([]*tagTemplate, 0, len(config.Tags)), + } + + for _, tag := range config.Tags { + tmpl, err := template.New(tag.Name).Parse(tag.Template) + if err != nil { + // Skip invalid templates + continue + } + g.templates = append(g.templates, &tagTemplate{ + name: tag.Name, + tmpl: tmpl, + }) + } + + return g +} + +// GenerateTags generates the complete struct tag string for a field. +// Returns a string like `json:"name,omitempty" yaml:"name,omitempty"`. +func (g *StructTagGenerator) GenerateTags(info StructTagInfo) string { + if len(g.templates) == 0 { + return "" + } + + var tags []string + for _, t := range g.templates { + var buf bytes.Buffer + if err := t.tmpl.Execute(&buf, info); err != nil { + // Skip tags that fail to render + continue + } + value := buf.String() + if value != "" { + tags = append(tags, t.name+`:`+`"`+value+`"`) + } + } + + if len(tags) == 0 { + return "" + } + + return "`" + strings.Join(tags, " ") + "`" +} + +// GenerateTagsMap generates tags as a map for cases where we need to add extra tags. +// Returns a map of tag name -> tag value (without quotes). +func (g *StructTagGenerator) GenerateTagsMap(info StructTagInfo) map[string]string { + result := make(map[string]string) + + for _, t := range g.templates { + var buf bytes.Buffer + if err := t.tmpl.Execute(&buf, info); err != nil { + continue + } + value := buf.String() + if value != "" { + result[t.name] = value + } + } + + return result +} + +// FormatTagsMap formats a tag map into a struct tag string. +// Tags are sorted alphabetically by name for deterministic output. +func FormatTagsMap(tags map[string]string) string { + if len(tags) == 0 { + return "" + } + + // Sort tag names for deterministic output + names := make([]string, 0, len(tags)) + for name := range tags { + names = append(names, name) + } + sort.Strings(names) + + var parts []string + for _, name := range names { + parts = append(parts, name+`:`+`"`+tags[name]+`"`) + } + + return "`" + strings.Join(parts, " ") + "`" +} diff --git a/experimental/internal/codegen/templates/embed.go b/experimental/internal/codegen/templates/embed.go new file mode 100644 index 0000000000..ed91bb95e7 --- /dev/null +++ b/experimental/internal/codegen/templates/embed.go @@ -0,0 +1,9 @@ +package templates + +import "embed" + +// TemplateFS contains all embedded template files. +// The files/* pattern recursively includes all files in subdirectories. +// +//go:embed files/* +var TemplateFS embed.FS diff --git a/experimental/internal/codegen/templates/files/client/base.go.tmpl b/experimental/internal/codegen/templates/files/client/base.go.tmpl new file mode 100644 index 0000000000..96d32997f4 --- /dev/null +++ b/experimental/internal/codegen/templates/files/client/base.go.tmpl @@ -0,0 +1,95 @@ +{{/* Base client template - returns raw *http.Response */}} + +// RequestEditorFn is the function signature for the RequestEditor callback function. +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// HttpRequestDoer performs HTTP requests. +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // 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 +} + +// ClientOption allows setting custom parameters during construction. +type ClientOption func(*Client) error + +// NewClient creates a new Client with reasonable defaults. +func NewClient(server string, opts ...ClientOption) (*Client, error) { + client := Client{ + Server: server, + } + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // Ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // Create httpClient if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.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 +} diff --git a/experimental/internal/codegen/templates/files/client/interface.go.tmpl b/experimental/internal/codegen/templates/files/client/interface.go.tmpl new file mode 100644 index 0000000000..458e4867dc --- /dev/null +++ b/experimental/internal/codegen/templates/files/client/interface.go.tmpl @@ -0,0 +1,17 @@ +{{/* Client interface template */}} + +// ClientInterface is the interface specification for the client. +type ClientInterface interface { +{{- range . }} +{{- $opid := .GoOperationID }} +{{- $hasParams := .HasParams }} +{{- $pathParams := .PathParams }} + // {{ $opid }}{{ if .HasBody }}WithBody{{ end }} makes a {{ .Method }} request to {{ .Path }} + {{ $opid }}{{ if .HasBody }}WithBody{{ end }}(ctx context.Context{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ .ParamsTypeName }}{{ end }}{{ if .HasBody }}, contentType string, body io.Reader{{ end }}, reqEditors ...RequestEditorFn) (*http.Response, error) +{{- range .Bodies }} +{{- if .IsJSON }} + {{ $opid }}{{ .FuncSuffix }}(ctx context.Context{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ $.ParamsTypeName }}{{ end }}, body {{ .GoTypeName }}, reqEditors ...RequestEditorFn) (*http.Response, error) +{{- end }} +{{- end }} +{{- end }} +} diff --git a/experimental/internal/codegen/templates/files/client/methods.go.tmpl b/experimental/internal/codegen/templates/files/client/methods.go.tmpl new file mode 100644 index 0000000000..a7a6dfd7f1 --- /dev/null +++ b/experimental/internal/codegen/templates/files/client/methods.go.tmpl @@ -0,0 +1,40 @@ +{{/* Client methods template - implements ClientInterface */}} + +{{- range . }} +{{- $op := . }} +{{- $opid := .GoOperationID }} +{{- $hasParams := .HasParams }} +{{- $pathParams := .PathParams }} +{{- $paramsTypeName := .ParamsTypeName }} + +// {{ $opid }}{{ if .HasBody }}WithBody{{ end }} makes a {{ .Method }} request to {{ .Path }} +{{ if .Summary }}// {{ .Summary }}{{ end }} +func (c *Client) {{ $opid }}{{ if .HasBody }}WithBody{{ end }}(ctx context.Context{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ 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{{ range $pathParams }}, {{ .GoVariableName }}{{ end }}{{ 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) +} +{{- range .Bodies }} +{{- if .IsJSON }} + +// {{ $opid }}{{ .FuncSuffix }} makes a {{ $op.Method }} request to {{ $op.Path }} with JSON body +func (c *Client) {{ $opid }}{{ .FuncSuffix }}(ctx context.Context{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}, body {{ .GoTypeName }}, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := New{{ $opid }}Request{{ .FuncSuffix }}(c.Server{{ range $pathParams }}, {{ .GoVariableName }}{{ end }}{{ if $hasParams }}, params{{ end }}, body) + 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) +} +{{- end }} +{{- end }} +{{- end }} diff --git a/experimental/internal/codegen/templates/files/client/request_builders.go.tmpl b/experimental/internal/codegen/templates/files/client/request_builders.go.tmpl new file mode 100644 index 0000000000..6461c14de9 --- /dev/null +++ b/experimental/internal/codegen/templates/files/client/request_builders.go.tmpl @@ -0,0 +1,177 @@ +{{/* Request builder functions template */}} + +{{- range . }} +{{- $op := . }} +{{- $opid := .GoOperationID }} +{{- $hasParams := .HasParams }} +{{- $pathParams := .PathParams }} +{{- $paramsTypeName := .ParamsTypeName }} +{{- $queryParams := .QueryParams }} +{{- $headerParams := .HeaderParams }} +{{- $cookieParams := .CookieParams }} + +{{- /* Generate typed body request builders for JSON bodies */ -}} +{{- range .Bodies }} +{{- if .IsJSON }} + +// New{{ $opid }}Request{{ .FuncSuffix }} creates a {{ $op.Method }} request for {{ $op.Path }} with {{ .ContentType }} body +func New{{ $opid }}Request{{ .FuncSuffix }}(server string{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}, body {{ .GoTypeName }}) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return New{{ $opid }}RequestWithBody(server{{ range $pathParams }}, {{ .GoVariableName }}{{ end }}{{ if $hasParams }}, params{{ end }}, "{{ .ContentType }}", bodyReader) +} +{{- end }} +{{- end }} + +// New{{ $opid }}Request{{ if .HasBody }}WithBody{{ end }} creates a {{ .Method }} request for {{ .Path }}{{ if .HasBody }} with any body{{ end }} +func New{{ $opid }}Request{{ if .HasBody }}WithBody{{ end }}(server string{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}{{ if .HasBody }}, contentType string, body io.Reader{{ end }}) (*http.Request, error) { + var err error +{{- range $idx, $param := $pathParams }} + + var pathParam{{ $idx }} string + {{- if .IsPassThrough }} + pathParam{{ $idx }} = {{ .GoVariableName }} + {{- else if .IsJSON }} + var pathParamBuf{{ $idx }} []byte + pathParamBuf{{ $idx }}, err = json.Marshal({{ .GoVariableName }}) + if err != nil { + return nil, err + } + pathParam{{ $idx }} = string(pathParamBuf{{ $idx }}) + {{- else if .IsStyled }} + pathParam{{ $idx }}, err = {{ .StyleFunc }}("{{ .Name }}", ParamLocationPath, {{ .GoVariableName }}) + if err != nil { + return nil, err + } + {{- end }} +{{- end }} + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("{{ pathFmt .Path }}"{{ range $idx, $_ := $pathParams }}, pathParam{{ $idx }}{{ end }}) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } +{{- if $queryParams }} + + if params != nil { + queryValues := queryURL.Query() +{{- range $idx, $param := $queryParams }} + {{- if .HasOptionalPointer }} + if params.{{ .GoName }} != nil { + {{- end }} + {{- if .IsPassThrough }} + queryValues.Add("{{ .Name }}", {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) + {{- else if .IsJSON }} + if queryParamBuf, err := json.Marshal({{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}); err != nil { + return nil, err + } else { + queryValues.Add("{{ .Name }}", string(queryParamBuf)) + } + {{- else if .IsStyled }} + if queryFrag, err := {{ .StyleFunc }}("{{ .Name }}", ParamLocationQuery, {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + {{- end }} + {{- if .HasOptionalPointer }} + } + {{- end }} +{{- end }} + queryURL.RawQuery = queryValues.Encode() + } +{{- end }} + + req, err := http.NewRequest("{{ .Method }}", queryURL.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 $idx, $param := $headerParams }} + {{- if .HasOptionalPointer }} + if params.{{ .GoName }} != nil { + {{- end }} + var headerParam{{ $idx }} string + {{- if .IsPassThrough }} + headerParam{{ $idx }} = {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }} + {{- else if .IsJSON }} + var headerParamBuf{{ $idx }} []byte + headerParamBuf{{ $idx }}, err = json.Marshal({{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) + if err != nil { + return nil, err + } + headerParam{{ $idx }} = string(headerParamBuf{{ $idx }}) + {{- else if .IsStyled }} + headerParam{{ $idx }}, err = {{ .StyleFunc }}("{{ .Name }}", ParamLocationHeader, {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) + if err != nil { + return nil, err + } + {{- end }} + req.Header.Set("{{ .Name }}", headerParam{{ $idx }}) + {{- if .HasOptionalPointer }} + } + {{- end }} +{{- end }} + } +{{- end }} +{{- if $cookieParams }} + + if params != nil { +{{- range $idx, $param := $cookieParams }} + {{- if .HasOptionalPointer }} + if params.{{ .GoName }} != nil { + {{- end }} + var cookieParam{{ $idx }} string + {{- if .IsPassThrough }} + cookieParam{{ $idx }} = {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }} + {{- else if .IsJSON }} + var cookieParamBuf{{ $idx }} []byte + cookieParamBuf{{ $idx }}, err = json.Marshal({{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) + if err != nil { + return nil, err + } + cookieParam{{ $idx }} = url.QueryEscape(string(cookieParamBuf{{ $idx }})) + {{- else if .IsStyled }} + cookieParam{{ $idx }}, err = StyleSimpleParam("{{ .Name }}", ParamLocationCookie, {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) + if err != nil { + return nil, err + } + {{- end }} + cookie{{ $idx }} := &http.Cookie{ + Name: "{{ .Name }}", + Value: cookieParam{{ $idx }}, + } + req.AddCookie(cookie{{ $idx }}) + {{- if .HasOptionalPointer }} + } + {{- end }} +{{- end }} + } +{{- end }} + + return req, nil +} +{{- end }} diff --git a/experimental/internal/codegen/templates/files/client/simple.go.tmpl b/experimental/internal/codegen/templates/files/client/simple.go.tmpl new file mode 100644 index 0000000000..988cf73182 --- /dev/null +++ b/experimental/internal/codegen/templates/files/client/simple.go.tmpl @@ -0,0 +1,121 @@ +{{/* SimpleClient template - wraps Client with typed responses for simple operations */}} + +// ClientHttpError represents an HTTP error response from the server. +// The type parameter E is the type of the parsed error body. +type ClientHttpError[E any] struct { + StatusCode int + Body E + RawBody []byte +} + +func (e *ClientHttpError[E]) Error() string { + return fmt.Sprintf("HTTP %d", e.StatusCode) +} + +// SimpleClient wraps Client with typed responses for operations that have +// unambiguous response types. Methods return the success type directly, +// and HTTP errors are returned as *ClientHttpError[E] where E is the error type. +type SimpleClient struct { + *Client +} + +// NewSimpleClient creates a new SimpleClient which wraps a Client. +func NewSimpleClient(server string, opts ...ClientOption) (*SimpleClient, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &SimpleClient{Client: client}, nil +} + +{{- range . }} +{{- $op := . }} +{{- $opid := .GoOperationID }} +{{- $hasParams := .HasParams }} +{{- $pathParams := .PathParams }} +{{- $paramsTypeName := .ParamsTypeName }} + +{{- /* Determine if this operation is "simple" - single success content type, single JSON success response */}} +{{- $simpleOp := isSimpleOperation . }} +{{- if $simpleOp }} +{{- $successResponse := simpleOperationSuccessResponse . }} +{{- $successContent := index $successResponse.Contents 0 }} +{{- $successType := goTypeForContent $successContent }} +{{- $errorResponse := errorResponseForOperation . }} + +// {{ $opid }} makes a {{ .Method }} request to {{ .Path }} and returns the parsed response. +{{ if .Summary }}// {{ .Summary }}{{ end }} +{{- if $errorResponse }} +{{- $errorContent := index $errorResponse.Contents 0 }} +{{- $errorType := goTypeForContent $errorContent }} +// On success, returns the response body. On HTTP error, returns *ClientHttpError[{{ $errorType }}]. +func (c *SimpleClient) {{ $opid }}(ctx context.Context{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}{{ if .HasBody }}, body {{ (index .Bodies 0).GoTypeName }}{{ end }}, reqEditors ...RequestEditorFn) ({{ $successType }}, error) { + var result {{ $successType }} +{{- if .HasBody }} +{{- $defaultBody := index .Bodies 0 }} + resp, err := c.Client.{{ $opid }}{{ $defaultBody.FuncSuffix }}(ctx{{ range $pathParams }}, {{ .GoVariableName }}{{ end }}{{ if $hasParams }}, params{{ end }}, body, reqEditors...) +{{- else }} + resp, err := c.Client.{{ $opid }}(ctx{{ range $pathParams }}, {{ .GoVariableName }}{{ end }}{{ if $hasParams }}, params{{ end }}, reqEditors...) +{{- end }} + if err != nil { + return result, err + } + defer resp.Body.Close() + + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return result, err + } + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + if err := json.Unmarshal(rawBody, &result); err != nil { + return result, err + } + return result, nil + } + + // Parse error response + var errBody {{ $errorType }} + _ = json.Unmarshal(rawBody, &errBody) // Best effort parse + return result, &ClientHttpError[{{ $errorType }}]{ + StatusCode: resp.StatusCode, + Body: errBody, + RawBody: rawBody, + } +} +{{- else }} +// On success, returns the response body. On HTTP error, returns *ClientHttpError[struct{}]. +func (c *SimpleClient) {{ $opid }}(ctx context.Context{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}{{ if .HasBody }}, body {{ (index .Bodies 0).GoTypeName }}{{ end }}, reqEditors ...RequestEditorFn) ({{ $successType }}, error) { + var result {{ $successType }} +{{- if .HasBody }} +{{- $defaultBody := index .Bodies 0 }} + resp, err := c.Client.{{ $opid }}{{ $defaultBody.FuncSuffix }}(ctx{{ range $pathParams }}, {{ .GoVariableName }}{{ end }}{{ if $hasParams }}, params{{ end }}, body, reqEditors...) +{{- else }} + resp, err := c.Client.{{ $opid }}(ctx{{ range $pathParams }}, {{ .GoVariableName }}{{ end }}{{ if $hasParams }}, params{{ end }}, reqEditors...) +{{- end }} + if err != nil { + return result, err + } + defer resp.Body.Close() + + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return result, err + } + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + if err := json.Unmarshal(rawBody, &result); err != nil { + return result, err + } + return result, nil + } + + // No typed error response defined + return result, &ClientHttpError[struct{}]{ + StatusCode: resp.StatusCode, + RawBody: rawBody, + } +} +{{- end }} +{{- end }} +{{- end }} diff --git a/experimental/internal/codegen/templates/files/initiator/base.go.tmpl b/experimental/internal/codegen/templates/files/initiator/base.go.tmpl new file mode 100644 index 0000000000..99e885136d --- /dev/null +++ b/experimental/internal/codegen/templates/files/initiator/base.go.tmpl @@ -0,0 +1,73 @@ +{{/* Initiator base template - framework-agnostic HTTP client for webhooks/callbacks */}} +{{/* Input: InitiatorTemplateData */}} + +// RequestEditorFn is the function signature for the RequestEditor callback function. +// It may already be defined if client code is also generated; this is a compatible redeclaration. +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// HttpRequestDoer performs HTTP requests. +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// {{ .Prefix }}Initiator sends {{ .PrefixLower }} requests to target URLs. +// Unlike Client, it has no stored base URL — the full target URL is provided per-call. +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 request. This can be used to mutate the request. +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) applyEditors(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 +} diff --git a/experimental/internal/codegen/templates/files/initiator/interface.go.tmpl b/experimental/internal/codegen/templates/files/initiator/interface.go.tmpl new file mode 100644 index 0000000000..a6ad431733 --- /dev/null +++ b/experimental/internal/codegen/templates/files/initiator/interface.go.tmpl @@ -0,0 +1,17 @@ +{{/* Initiator interface template */}} +{{/* Input: InitiatorTemplateData */}} + +// {{ .Prefix }}InitiatorInterface is the interface specification for the {{ .PrefixLower }} initiator. +type {{ .Prefix }}InitiatorInterface interface { +{{- range .Operations }} +{{- $opid := .GoOperationID }} +{{- $hasParams := .HasParams }} + // {{ $opid }}{{ if .HasBody }}WithBody{{ end }} sends a {{ .Method }} {{ $.PrefixLower }} request + {{ $opid }}{{ if .HasBody }}WithBody{{ end }}(ctx context.Context, targetURL string{{ if $hasParams }}, params *{{ .ParamsTypeName }}{{ end }}{{ if .HasBody }}, contentType string, body io.Reader{{ end }}, reqEditors ...RequestEditorFn) (*http.Response, error) +{{- range .Bodies }} +{{- if .IsJSON }} + {{ $opid }}{{ .FuncSuffix }}(ctx context.Context, targetURL string{{ if $hasParams }}, params *{{ $.ParamsTypeName }}{{ end }}, body {{ .GoTypeName }}, reqEditors ...RequestEditorFn) (*http.Response, error) +{{- end }} +{{- end }} +{{- end }} +} diff --git a/experimental/internal/codegen/templates/files/initiator/methods.go.tmpl b/experimental/internal/codegen/templates/files/initiator/methods.go.tmpl new file mode 100644 index 0000000000..ff079042ed --- /dev/null +++ b/experimental/internal/codegen/templates/files/initiator/methods.go.tmpl @@ -0,0 +1,41 @@ +{{/* Initiator methods template - implements InitiatorInterface */}} +{{/* Input: InitiatorTemplateData */}} + +{{- range .Operations }} +{{- $op := . }} +{{- $opid := .GoOperationID }} +{{- $hasParams := .HasParams }} +{{- $paramsTypeName := .ParamsTypeName }} +{{- $prefix := $.Prefix }} + +// {{ $opid }}{{ if .HasBody }}WithBody{{ end }} sends a {{ .Method }} {{ $.PrefixLower }} request +{{ if .Summary }}// {{ .Summary }}{{ end }} +func (p *{{ $prefix }}Initiator) {{ $opid }}{{ if .HasBody }}WithBody{{ end }}(ctx context.Context, targetURL string{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}{{ if .HasBody }}, contentType string, body io.Reader{{ end }}, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := New{{ $opid }}{{ $.Prefix }}Request{{ 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.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return p.Client.Do(req) +} +{{- range .Bodies }} +{{- if .IsJSON }} + +// {{ $opid }}{{ .FuncSuffix }} sends a {{ $op.Method }} {{ $.PrefixLower }} request with JSON body +func (p *{{ $prefix }}Initiator) {{ $opid }}{{ .FuncSuffix }}(ctx context.Context, targetURL string{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}, body {{ .GoTypeName }}, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := New{{ $opid }}{{ $prefix }}Request{{ .FuncSuffix }}(targetURL{{ if $hasParams }}, params{{ end }}, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := p.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return p.Client.Do(req) +} +{{- end }} +{{- end }} +{{- end }} diff --git a/experimental/internal/codegen/templates/files/initiator/request_builders.go.tmpl b/experimental/internal/codegen/templates/files/initiator/request_builders.go.tmpl new file mode 100644 index 0000000000..d651403492 --- /dev/null +++ b/experimental/internal/codegen/templates/files/initiator/request_builders.go.tmpl @@ -0,0 +1,149 @@ +{{/* Initiator request builder functions template */}} +{{/* Input: InitiatorTemplateData */}} + +{{- range .Operations }} +{{- $op := . }} +{{- $opid := .GoOperationID }} +{{- $hasParams := .HasParams }} +{{- $paramsTypeName := .ParamsTypeName }} +{{- $queryParams := .QueryParams }} +{{- $headerParams := .HeaderParams }} +{{- $cookieParams := .CookieParams }} +{{- $prefix := $.Prefix }} + +{{- /* Generate typed body request builders for JSON bodies */ -}} +{{- range .Bodies }} +{{- if .IsJSON }} + +// New{{ $opid }}{{ $prefix }}Request{{ .FuncSuffix }} creates a {{ $op.Method }} request for the {{ $.PrefixLower }} with {{ .ContentType }} body +func New{{ $opid }}{{ $prefix }}Request{{ .FuncSuffix }}(targetURL string{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}, body {{ .GoTypeName }}) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return New{{ $opid }}{{ $prefix }}RequestWithBody(targetURL{{ if $hasParams }}, params{{ end }}, "{{ .ContentType }}", bodyReader) +} +{{- end }} +{{- end }} + +// New{{ $opid }}{{ $prefix }}Request{{ if .HasBody }}WithBody{{ end }} creates a {{ .Method }} request for the {{ $.PrefixLower }}{{ if .HasBody }} with any body{{ end }} +func New{{ $opid }}{{ $prefix }}Request{{ if .HasBody }}WithBody{{ end }}(targetURL string{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}{{ if .HasBody }}, contentType string, body io.Reader{{ end }}) (*http.Request, error) { + var err error + + parsedURL, err := url.Parse(targetURL) + if err != nil { + return nil, err + } +{{- if $queryParams }} + + if params != nil { + queryValues := parsedURL.Query() +{{- range $idx, $param := $queryParams }} + {{- if .HasOptionalPointer }} + if params.{{ .GoName }} != nil { + {{- end }} + {{- if .IsPassThrough }} + queryValues.Add("{{ .Name }}", {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) + {{- else if .IsJSON }} + if queryParamBuf, err := json.Marshal({{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}); err != nil { + return nil, err + } else { + queryValues.Add("{{ .Name }}", string(queryParamBuf)) + } + {{- else if .IsStyled }} + if queryFrag, err := {{ .StyleFunc }}("{{ .Name }}", ParamLocationQuery, {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + {{- end }} + {{- if .HasOptionalPointer }} + } + {{- end }} +{{- end }} + parsedURL.RawQuery = queryValues.Encode() + } +{{- end }} + + req, err := http.NewRequest("{{ .Method }}", parsedURL.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 $idx, $param := $headerParams }} + {{- if .HasOptionalPointer }} + if params.{{ .GoName }} != nil { + {{- end }} + var headerParam{{ $idx }} string + {{- if .IsPassThrough }} + headerParam{{ $idx }} = {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }} + {{- else if .IsJSON }} + var headerParamBuf{{ $idx }} []byte + headerParamBuf{{ $idx }}, err = json.Marshal({{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) + if err != nil { + return nil, err + } + headerParam{{ $idx }} = string(headerParamBuf{{ $idx }}) + {{- else if .IsStyled }} + headerParam{{ $idx }}, err = {{ .StyleFunc }}("{{ .Name }}", ParamLocationHeader, {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) + if err != nil { + return nil, err + } + {{- end }} + req.Header.Set("{{ .Name }}", headerParam{{ $idx }}) + {{- if .HasOptionalPointer }} + } + {{- end }} +{{- end }} + } +{{- end }} +{{- if $cookieParams }} + + if params != nil { +{{- range $idx, $param := $cookieParams }} + {{- if .HasOptionalPointer }} + if params.{{ .GoName }} != nil { + {{- end }} + var cookieParam{{ $idx }} string + {{- if .IsPassThrough }} + cookieParam{{ $idx }} = {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }} + {{- else if .IsJSON }} + var cookieParamBuf{{ $idx }} []byte + cookieParamBuf{{ $idx }}, err = json.Marshal({{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) + if err != nil { + return nil, err + } + cookieParam{{ $idx }} = url.QueryEscape(string(cookieParamBuf{{ $idx }})) + {{- else if .IsStyled }} + cookieParam{{ $idx }}, err = StyleSimpleParam("{{ .Name }}", ParamLocationCookie, {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) + if err != nil { + return nil, err + } + {{- end }} + cookie{{ $idx }} := &http.Cookie{ + Name: "{{ .Name }}", + Value: cookieParam{{ $idx }}, + } + req.AddCookie(cookie{{ $idx }}) + {{- if .HasOptionalPointer }} + } + {{- end }} +{{- end }} + } +{{- end }} + + return req, nil +} +{{- end }} diff --git a/experimental/internal/codegen/templates/files/initiator/simple.go.tmpl b/experimental/internal/codegen/templates/files/initiator/simple.go.tmpl new file mode 100644 index 0000000000..008d4095ec --- /dev/null +++ b/experimental/internal/codegen/templates/files/initiator/simple.go.tmpl @@ -0,0 +1,121 @@ +{{/* Simple initiator template - wraps Initiator with typed responses for simple operations */}} +{{/* Input: InitiatorTemplateData */}} + +// {{ .Prefix }}HttpError represents an HTTP error response. +// The type parameter E is the type of the parsed error body. +type {{ .Prefix }}HttpError[E any] struct { + StatusCode int + Body E + RawBody []byte +} + +func (e *{{ .Prefix }}HttpError[E]) Error() string { + return fmt.Sprintf("HTTP %d", e.StatusCode) +} + +// Simple{{ .Prefix }}Initiator wraps {{ .Prefix }}Initiator with typed responses for operations that have +// unambiguous response types. Methods return the success type directly, +// and HTTP errors are returned as *{{ .Prefix }}HttpError[E] where E is the error type. +type Simple{{ .Prefix }}Initiator struct { + *{{ .Prefix }}Initiator +} + +// NewSimple{{ .Prefix }}Initiator creates a new Simple{{ .Prefix }}Initiator which wraps a {{ .Prefix }}Initiator. +func NewSimple{{ .Prefix }}Initiator(opts ...{{ .Prefix }}InitiatorOption) (*Simple{{ .Prefix }}Initiator, error) { + initiator, err := New{{ .Prefix }}Initiator(opts...) + if err != nil { + return nil, err + } + return &Simple{{ .Prefix }}Initiator{ {{ .Prefix }}Initiator: initiator}, nil +} + +{{- range .Operations }} +{{- $op := . }} +{{- $opid := .GoOperationID }} +{{- $hasParams := .HasParams }} +{{- $paramsTypeName := .ParamsTypeName }} + +{{- /* Determine if this operation is "simple" - single success content type, single JSON success response */}} +{{- $simpleOp := isSimpleOperation . }} +{{- if $simpleOp }} +{{- $successResponse := simpleOperationSuccessResponse . }} +{{- $successContent := index $successResponse.Contents 0 }} +{{- $successType := goTypeForContent $successContent }} +{{- $errorResponse := errorResponseForOperation . }} + +// {{ $opid }} sends a {{ .Method }} {{ $.PrefixLower }} request and returns the parsed response. +{{ if .Summary }}// {{ .Summary }}{{ end }} +{{- if $errorResponse }} +{{- $errorContent := index $errorResponse.Contents 0 }} +{{- $errorType := goTypeForContent $errorContent }} +// On success, returns the response body. On HTTP error, returns *{{ $.Prefix }}HttpError[{{ $errorType }}]. +func (p *Simple{{ $.Prefix }}Initiator) {{ $opid }}(ctx context.Context, targetURL string{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}{{ if .HasBody }}, body {{ (index .Bodies 0).GoTypeName }}{{ end }}, reqEditors ...RequestEditorFn) ({{ $successType }}, error) { + var result {{ $successType }} +{{- if .HasBody }} +{{- $defaultBody := index .Bodies 0 }} + resp, err := p.{{ $.Prefix }}Initiator.{{ $opid }}{{ $defaultBody.FuncSuffix }}(ctx, targetURL{{ if $hasParams }}, params{{ end }}, body, reqEditors...) +{{- else }} + resp, err := p.{{ $.Prefix }}Initiator.{{ $opid }}(ctx, targetURL{{ if $hasParams }}, params{{ end }}, reqEditors...) +{{- end }} + if err != nil { + return result, err + } + defer resp.Body.Close() + + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return result, err + } + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + if err := json.Unmarshal(rawBody, &result); err != nil { + return result, err + } + return result, nil + } + + // Parse error response + var errBody {{ $errorType }} + _ = json.Unmarshal(rawBody, &errBody) // Best effort parse + return result, &{{ $.Prefix }}HttpError[{{ $errorType }}]{ + StatusCode: resp.StatusCode, + Body: errBody, + RawBody: rawBody, + } +} +{{- else }} +// On success, returns the response body. On HTTP error, returns *{{ $.Prefix }}HttpError[struct{}]. +func (p *Simple{{ $.Prefix }}Initiator) {{ $opid }}(ctx context.Context, targetURL string{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}{{ if .HasBody }}, body {{ (index .Bodies 0).GoTypeName }}{{ end }}, reqEditors ...RequestEditorFn) ({{ $successType }}, error) { + var result {{ $successType }} +{{- if .HasBody }} +{{- $defaultBody := index .Bodies 0 }} + resp, err := p.{{ $.Prefix }}Initiator.{{ $opid }}{{ $defaultBody.FuncSuffix }}(ctx, targetURL{{ if $hasParams }}, params{{ end }}, body, reqEditors...) +{{- else }} + resp, err := p.{{ $.Prefix }}Initiator.{{ $opid }}(ctx, targetURL{{ if $hasParams }}, params{{ end }}, reqEditors...) +{{- end }} + if err != nil { + return result, err + } + defer resp.Body.Close() + + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return result, err + } + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + if err := json.Unmarshal(rawBody, &result); err != nil { + return result, err + } + return result, nil + } + + // No typed error response defined + return result, &{{ $.Prefix }}HttpError[struct{}]{ + StatusCode: resp.StatusCode, + RawBody: rawBody, + } +} +{{- end }} +{{- end }} +{{- end }} diff --git a/experimental/internal/codegen/templates/files/params/bind_deep_object.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_deep_object.go.tmpl new file mode 100644 index 0000000000..403eb5c13d --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/bind_deep_object.go.tmpl @@ -0,0 +1,271 @@ +{{/* BindDeepObjectParam - deepObject style (always exploded) */}} + +// BindDeepObjectParam binds a deepObject-style parameter to a destination. +// DeepObject style is only valid for query parameters and must be exploded. +// Objects: ?paramName[key1]=value1¶mName[key2]=value2 -> struct{Key1, Key2} +// Nested: ?paramName[outer][inner]=value -> struct{Outer: {Inner: value}} +func BindDeepObjectParam(paramName string, queryParams url.Values, dest interface{}) error { + return UnmarshalDeepObject(dest, paramName, queryParams) +} + +// UnmarshalDeepObject unmarshals deepObject-style query parameters to a destination. +func UnmarshalDeepObject(dst interface{}, paramName string, params url.Values) error { + // Find all params that look like "paramName[..." + var fieldNames []string + var fieldValues []string + searchStr := paramName + "[" + + for pName, pValues := range params { + if strings.HasPrefix(pName, searchStr) { + // Trim the parameter name prefix + pName = pName[len(paramName):] + fieldNames = append(fieldNames, pName) + if len(pValues) != 1 { + return fmt.Errorf("%s has multiple values", pName) + } + fieldValues = append(fieldValues, pValues[0]) + } + } + + // Reconstruct subscript paths + paths := make([][]string, len(fieldNames)) + for i, path := range fieldNames { + path = strings.TrimLeft(path, "[") + path = strings.TrimRight(path, "]") + paths[i] = strings.Split(path, "][") + } + + fieldPaths := makeFieldOrValue(paths, fieldValues) + return assignPathValues(dst, fieldPaths) +} + +type fieldOrValue struct { + fields map[string]fieldOrValue + value string +} + +func (f *fieldOrValue) appendPathValue(path []string, value string) { + fieldName := path[0] + if len(path) == 1 { + f.fields[fieldName] = fieldOrValue{value: value} + return + } + + pv, found := f.fields[fieldName] + if !found { + pv = fieldOrValue{ + fields: make(map[string]fieldOrValue), + } + f.fields[fieldName] = pv + } + pv.appendPathValue(path[1:], value) +} + +func makeFieldOrValue(paths [][]string, values []string) fieldOrValue { + f := fieldOrValue{ + fields: make(map[string]fieldOrValue), + } + for i := range paths { + f.appendPathValue(paths[i], values[i]) + } + return f +} + +func getFieldName(f reflect.StructField) string { + n := f.Name + tag, found := f.Tag.Lookup("json") + if found { + parts := strings.Split(tag, ",") + if parts[0] != "" { + n = parts[0] + } + } + return n +} + +func fieldIndicesByJsonTag(i interface{}) (map[string]int, error) { + t := reflect.TypeOf(i) + if t.Kind() != reflect.Struct { + return nil, errors.New("expected a struct as input") + } + + n := t.NumField() + fieldMap := make(map[string]int) + for i := 0; i < n; i++ { + field := t.Field(i) + fieldName := getFieldName(field) + fieldMap[fieldName] = i + } + return fieldMap, nil +} + +func assignPathValues(dst interface{}, pathValues fieldOrValue) error { + v := reflect.ValueOf(dst) + iv := reflect.Indirect(v) + it := iv.Type() + + switch it.Kind() { + case reflect.Map: + dstMap := reflect.MakeMap(iv.Type()) + for key, value := range pathValues.fields { + dstKey := reflect.ValueOf(key) + dstVal := reflect.New(iv.Type().Elem()) + err := assignPathValues(dstVal.Interface(), value) + if err != nil { + return fmt.Errorf("error binding map: %w", err) + } + dstMap.SetMapIndex(dstKey, dstVal.Elem()) + } + iv.Set(dstMap) + return nil + + case reflect.Slice: + sliceLength := len(pathValues.fields) + dstSlice := reflect.MakeSlice(it, sliceLength, sliceLength) + err := assignDeepObjectSlice(dstSlice, pathValues) + if err != nil { + return fmt.Errorf("error assigning slice: %w", err) + } + iv.Set(dstSlice) + return nil + + case reflect.Struct: + // Check for Binder interface + if dst, isBinder := v.Interface().(Binder); isBinder { + return dst.Bind(pathValues.value) + } + + // Handle Date type + if it.ConvertibleTo(reflect.TypeOf(Date{})) { + var date Date + var err error + date.Time, err = time.Parse(DateFormat, pathValues.value) + if err != nil { + return fmt.Errorf("invalid date format: %w", err) + } + dst := iv + if it != reflect.TypeOf(Date{}) { + ivPtr := iv.Addr() + aPtr := ivPtr.Convert(reflect.TypeOf(&Date{})) + dst = reflect.Indirect(aPtr) + } + dst.Set(reflect.ValueOf(date)) + return nil + } + + // Handle time.Time type + if it.ConvertibleTo(reflect.TypeOf(time.Time{})) { + tm, err := time.Parse(time.RFC3339Nano, pathValues.value) + if err != nil { + tm, err = time.Parse(DateFormat, pathValues.value) + if err != nil { + return fmt.Errorf("error parsing '%s' as RFC3339 or date: %w", pathValues.value, err) + } + } + dst := iv + if it != reflect.TypeOf(time.Time{}) { + ivPtr := iv.Addr() + aPtr := ivPtr.Convert(reflect.TypeOf(&time.Time{})) + dst = reflect.Indirect(aPtr) + } + dst.Set(reflect.ValueOf(tm)) + return nil + } + + // Regular struct + fieldMap, err := fieldIndicesByJsonTag(iv.Interface()) + if err != nil { + return fmt.Errorf("failed enumerating fields: %w", err) + } + for _, fieldName := range sortedFieldOrValueKeys(pathValues.fields) { + fieldValue := pathValues.fields[fieldName] + fieldIndex, found := fieldMap[fieldName] + if !found { + return fmt.Errorf("field [%s] is not present in destination object", fieldName) + } + field := iv.Field(fieldIndex) + err = assignPathValues(field.Addr().Interface(), fieldValue) + if err != nil { + return fmt.Errorf("error assigning field [%s]: %w", fieldName, err) + } + } + return nil + + case reflect.Ptr: + dstVal := reflect.New(it.Elem()) + dstPtr := dstVal.Interface() + err := assignPathValues(dstPtr, pathValues) + iv.Set(dstVal) + return err + + case reflect.Bool: + val, err := strconv.ParseBool(pathValues.value) + if err != nil { + return fmt.Errorf("expected a valid bool, got %s", pathValues.value) + } + iv.SetBool(val) + return nil + + case reflect.Float32: + val, err := strconv.ParseFloat(pathValues.value, 32) + if err != nil { + return fmt.Errorf("expected a valid float, got %s", pathValues.value) + } + iv.SetFloat(val) + return nil + + case reflect.Float64: + val, err := strconv.ParseFloat(pathValues.value, 64) + if err != nil { + return fmt.Errorf("expected a valid float, got %s", pathValues.value) + } + iv.SetFloat(val) + return nil + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + val, err := strconv.ParseInt(pathValues.value, 10, 64) + if err != nil { + return fmt.Errorf("expected a valid int, got %s", pathValues.value) + } + iv.SetInt(val) + return nil + + case reflect.String: + iv.SetString(pathValues.value) + return nil + + default: + return errors.New("unhandled type: " + it.String()) + } +} + +func assignDeepObjectSlice(dst reflect.Value, pathValues fieldOrValue) error { + nValues := len(pathValues.fields) + values := make([]string, nValues) + for i := 0; i < nValues; i++ { + indexStr := strconv.Itoa(i) + fv, found := pathValues.fields[indexStr] + if !found { + return errors.New("array deepObjects must have consecutive indices") + } + values[i] = fv.value + } + + for i := 0; i < nValues; i++ { + dstElem := dst.Index(i).Addr() + err := assignPathValues(dstElem.Interface(), fieldOrValue{value: values[i]}) + if err != nil { + return fmt.Errorf("error binding array: %w", err) + } + } + return nil +} + +func sortedFieldOrValueKeys(m map[string]fieldOrValue) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} diff --git a/experimental/internal/codegen/templates/files/params/bind_form.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_form.go.tmpl new file mode 100644 index 0000000000..6b6b6d63de --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/bind_form.go.tmpl @@ -0,0 +1,38 @@ +{{/* BindFormParam - form style without explode */}} + +// BindFormParam binds a form-style parameter without explode to a destination. +// Form style is the default for query and cookie parameters. +// This function handles a single query parameter value (not url.Values). +// Arrays: a,b,c -> []string{"a", "b", "c"} +// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} +func BindFormParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) + case reflect.Slice: + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} diff --git a/experimental/internal/codegen/templates/files/params/bind_form_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_form_explode.go.tmpl new file mode 100644 index 0000000000..ec0f66d692 --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/bind_form_explode.go.tmpl @@ -0,0 +1,145 @@ +{{/* BindFormExplodeParam - form style with explode */}} + +// BindFormExplodeParam binds a form-style parameter with explode to a destination. +// Form style is the default for query and cookie parameters. +// This handles the exploded case where arrays come as multiple query params. +// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) +// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) +func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { + dv := reflect.Indirect(reflect.ValueOf(dest)) + v := dv + var output interface{} + + if required { + output = dest + } else { + // For optional parameters, allocate if nil + if v.IsNil() { + t := v.Type() + newValue := reflect.New(t.Elem()) + output = newValue.Interface() + } else { + output = v.Interface() + } + v = reflect.Indirect(reflect.ValueOf(output)) + } + + t := v.Type() + k := t.Kind() + + values, found := queryParams[paramName] + + switch k { + case reflect.Slice: + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := bindSplitPartsToDestinationArray(values, output) + if err != nil { + return err + } + case reflect.Struct: + // For exploded objects, fields are spread across query params + fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) + if err != nil { + return err + } + if !fieldsPresent { + return nil + } + default: + // Primitive + if len(values) == 0 { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + if len(values) != 1 { + return fmt.Errorf("multiple values for single value parameter '%s'", paramName) + } + if !found { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } + return nil + } + err := BindStringToObject(values[0], output) + if err != nil { + return err + } + } + + if !required { + dv.Set(reflect.ValueOf(output)) + } + return nil +} + +// bindParamsToExplodedObject binds query params to struct fields for exploded objects. +func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { + binder, v, t := indirectBinder(dest) + if binder != nil { + _, found := values[paramName] + if !found { + return false, nil + } + return true, BindStringToObject(values.Get(paramName), dest) + } + if t.Kind() != reflect.Struct { + return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) + } + + fieldsPresent := false + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + if !v.Field(i).CanSet() { + continue + } + + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + + fieldVal, found := values[fieldName] + if found { + if len(fieldVal) != 1 { + return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) + } + err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) + if err != nil { + return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) + } + fieldsPresent = true + } + } + return fieldsPresent, nil +} + +// indirectBinder checks if dest implements Binder and returns reflect values. +func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { + v := reflect.ValueOf(dest) + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Binder); ok { + return u, reflect.Value{}, nil + } + } + v = reflect.Indirect(v) + t := v.Type() + // Handle special types like time.Time and Date + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return dest, reflect.Value{}, nil + } + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + return dest, reflect.Value{}, nil + } + return nil, v, t +} diff --git a/experimental/internal/codegen/templates/files/params/bind_label.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_label.go.tmpl new file mode 100644 index 0000000000..a9bff4ab18 --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/bind_label.go.tmpl @@ -0,0 +1,46 @@ +{{/* BindLabelParam - label style without explode */}} + +// BindLabelParam binds a label-style parameter without explode to a destination. +// Label style values are prefixed with a dot. +// Primitives: .value -> "value" +// Arrays: .a,b,c -> []string{"a", "b", "c"} +// Objects: .key1,value1,key2,value2 -> struct{Key1, Key2} +func BindLabelParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Label style requires leading dot + if value[0] != '.' { + return fmt.Errorf("invalid format for label parameter '%s', should start with '.'", paramName) + } + + // Strip the leading dot and split on comma + stripped := value[1:] + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(stripped)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + parts := strings.Split(stripped, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) + case reflect.Slice: + parts := strings.Split(stripped, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(stripped, dest) + } +} diff --git a/experimental/internal/codegen/templates/files/params/bind_label_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_label_explode.go.tmpl new file mode 100644 index 0000000000..766d29e3ea --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/bind_label_explode.go.tmpl @@ -0,0 +1,51 @@ +{{/* BindLabelExplodeParam - label style with explode */}} + +// BindLabelExplodeParam binds a label-style parameter with explode to a destination. +// Label style values are prefixed with a dot. +// Primitives: .value -> "value" +// Arrays: .a.b.c -> []string{"a", "b", "c"} +// Objects: .key1=value1.key2=value2 -> struct{Key1, Key2} +func BindLabelExplodeParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Label style requires leading dot + if value[0] != '.' { + return fmt.Errorf("invalid format for label parameter '%s', should start with '.'", paramName) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value[1:])) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + // Split on dot (skip first empty part) + parts := strings.Split(value, ".") + if parts[0] != "" { + return fmt.Errorf("invalid format for label parameter '%s', should start with '.'", paramName) + } + return bindSplitPartsToDestinationStruct(paramName, parts[1:], true, dest) + case reflect.Slice: + // Split on dot (skip first empty part) + parts := strings.Split(value, ".") + if parts[0] != "" { + return fmt.Errorf("invalid format for label parameter '%s', should start with '.'", paramName) + } + return bindSplitPartsToDestinationArray(parts[1:], dest) + default: + return BindStringToObject(value[1:], dest) + } +} diff --git a/experimental/internal/codegen/templates/files/params/bind_matrix.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_matrix.go.tmpl new file mode 100644 index 0000000000..0702dfa755 --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/bind_matrix.go.tmpl @@ -0,0 +1,47 @@ +{{/* BindMatrixParam - matrix style without explode */}} + +// BindMatrixParam binds a matrix-style parameter without explode to a destination. +// Matrix style values are prefixed with ;paramName=. +// Primitives: ;paramName=value -> "value" +// Arrays: ;paramName=a,b,c -> []string{"a", "b", "c"} +// Objects: ;paramName=key1,value1,key2,value2 -> struct{Key1, Key2} +func BindMatrixParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Matrix style requires ;paramName= prefix + prefix := ";" + paramName + "=" + if !strings.HasPrefix(value, prefix) { + return fmt.Errorf("expected parameter '%s' to start with %s", paramName, prefix) + } + + // Strip the prefix + stripped := strings.TrimPrefix(value, prefix) + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(stripped)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + parts := strings.Split(stripped, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) + case reflect.Slice: + parts := strings.Split(stripped, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(stripped, dest) + } +} diff --git a/experimental/internal/codegen/templates/files/params/bind_matrix_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_matrix_explode.go.tmpl new file mode 100644 index 0000000000..0b2f7ffd9a --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/bind_matrix_explode.go.tmpl @@ -0,0 +1,65 @@ +{{/* BindMatrixExplodeParam - matrix style with explode */}} + +// BindMatrixExplodeParam binds a matrix-style parameter with explode to a destination. +// Matrix style values are prefixed with semicolons. +// Primitives: ;paramName=value -> "value" +// Arrays: ;paramName=a;paramName=b;paramName=c -> []string{"a", "b", "c"} +// Objects: ;key1=value1;key2=value2 -> struct{Key1, Key2} +func BindMatrixExplodeParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Break up on semicolon + parts := strings.Split(value, ";") + // First part should be empty since we start with ; + if parts[0] != "" { + return fmt.Errorf("invalid format for matrix parameter '%s', should start with ';'", paramName) + } + parts = parts[1:] + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + // For primitives, should be ;paramName=value + if len(parts) == 1 { + kv := strings.SplitN(parts[0], "=", 2) + if len(kv) == 2 && kv[0] == paramName { + return tu.UnmarshalText([]byte(kv[1])) + } + } + return fmt.Errorf("invalid format for matrix parameter '%s'", paramName) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + // For objects, we have key1=value1, key2=value2 + return bindSplitPartsToDestinationStruct(paramName, parts, true, dest) + case reflect.Slice: + // For arrays, strip paramName= prefix from each part + prefix := paramName + "=" + values := make([]string, len(parts)) + for i, part := range parts { + values[i] = strings.TrimPrefix(part, prefix) + } + return bindSplitPartsToDestinationArray(values, dest) + default: + // Primitive: ;paramName=value + if len(parts) == 1 { + kv := strings.SplitN(parts[0], "=", 2) + if len(kv) == 2 && kv[0] == paramName { + return BindStringToObject(kv[1], dest) + } + } + return fmt.Errorf("invalid format for matrix parameter '%s'", paramName) + } +} diff --git a/experimental/internal/codegen/templates/files/params/bind_pipe_delimited.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_pipe_delimited.go.tmpl new file mode 100644 index 0000000000..1adfd9ef3e --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/bind_pipe_delimited.go.tmpl @@ -0,0 +1,33 @@ +{{/* BindPipeDelimitedParam - pipeDelimited style without explode */}} + +// BindPipeDelimitedParam binds a pipeDelimited-style parameter without explode. +// Pipe-delimited style uses pipe as the separator. +// Arrays: a|b|c -> []string{"a", "b", "c"} +func BindPipeDelimitedParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Slice: + parts := strings.Split(value, "|") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} diff --git a/experimental/internal/codegen/templates/files/params/bind_pipe_delimited_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_pipe_delimited_explode.go.tmpl new file mode 100644 index 0000000000..9af6d4a5f3 --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/bind_pipe_delimited_explode.go.tmpl @@ -0,0 +1,9 @@ +{{/* BindPipeDelimitedExplodeParam - pipeDelimited style with explode */}} + +// BindPipeDelimitedExplodeParam binds a pipeDelimited-style parameter with explode. +// When exploded, arrays come as multiple query params (same as form explode). +// Arrays: ?param=a¶m=b -> []string{"a", "b"} +func BindPipeDelimitedExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { + // Exploded pipe-delimited is same as exploded form + return BindFormExplodeParam(paramName, required, queryParams, dest) +} diff --git a/experimental/internal/codegen/templates/files/params/bind_simple.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_simple.go.tmpl new file mode 100644 index 0000000000..3b48a81c15 --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/bind_simple.go.tmpl @@ -0,0 +1,38 @@ +{{/* BindSimpleParam - simple style without explode */}} + +// BindSimpleParam binds a simple-style parameter without explode to a destination. +// Simple style is the default for path and header parameters. +// Arrays: a,b,c -> []string{"a", "b", "c"} +// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} +func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + // Split on comma and bind as key,value pairs + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) + case reflect.Slice: + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} diff --git a/experimental/internal/codegen/templates/files/params/bind_simple_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_simple_explode.go.tmpl new file mode 100644 index 0000000000..3d9468f26a --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/bind_simple_explode.go.tmpl @@ -0,0 +1,38 @@ +{{/* BindSimpleExplodeParam - simple style with explode */}} + +// BindSimpleExplodeParam binds a simple-style parameter with explode to a destination. +// Simple style is the default for path and header parameters. +// Arrays: a,b,c -> []string{"a", "b", "c"} (same as non-explode) +// Objects: key1=value1,key2=value2 -> struct{Key1, Key2} +func BindSimpleExplodeParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Struct: + // Split on comma and bind as key=value pairs + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationStruct(paramName, parts, true, dest) + case reflect.Slice: + parts := strings.Split(value, ",") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} diff --git a/experimental/internal/codegen/templates/files/params/bind_space_delimited.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_space_delimited.go.tmpl new file mode 100644 index 0000000000..b7b83685df --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/bind_space_delimited.go.tmpl @@ -0,0 +1,33 @@ +{{/* BindSpaceDelimitedParam - spaceDelimited style without explode */}} + +// BindSpaceDelimitedParam binds a spaceDelimited-style parameter without explode. +// Space-delimited style uses space as the separator. +// Arrays: a b c -> []string{"a", "b", "c"} +func BindSpaceDelimitedParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { + if value == "" { + return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) + } + + // Unescape based on location + var err error + value, err = unescapeParameterString(value, paramLocation) + if err != nil { + return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) + } + + // Check for TextUnmarshaler + if tu, ok := dest.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(value)) + } + + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + switch t.Kind() { + case reflect.Slice: + parts := strings.Split(value, " ") + return bindSplitPartsToDestinationArray(parts, dest) + default: + return BindStringToObject(value, dest) + } +} diff --git a/experimental/internal/codegen/templates/files/params/bind_space_delimited_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_space_delimited_explode.go.tmpl new file mode 100644 index 0000000000..f79f097eff --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/bind_space_delimited_explode.go.tmpl @@ -0,0 +1,9 @@ +{{/* BindSpaceDelimitedExplodeParam - spaceDelimited style with explode */}} + +// BindSpaceDelimitedExplodeParam binds a spaceDelimited-style parameter with explode. +// When exploded, arrays come as multiple query params (same as form explode). +// Arrays: ?param=a¶m=b -> []string{"a", "b"} +func BindSpaceDelimitedExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { + // Exploded space-delimited is same as exploded form + return BindFormExplodeParam(paramName, required, queryParams, dest) +} diff --git a/experimental/internal/codegen/templates/files/params/helpers.go.tmpl b/experimental/internal/codegen/templates/files/params/helpers.go.tmpl new file mode 100644 index 0000000000..b3f650f211 --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/helpers.go.tmpl @@ -0,0 +1,260 @@ +{{/* Parameter styling and binding helper functions - included when any param function is used */}} + +// ParamLocation indicates where a parameter is located in an HTTP request. +type ParamLocation int + +const ( + ParamLocationUndefined ParamLocation = iota + ParamLocationQuery + ParamLocationPath + ParamLocationHeader + ParamLocationCookie +) + +// Binder is an interface for types that can bind themselves from a string value. +type Binder interface { + Bind(value string) error +} + +// DateFormat is the format used for date (without time) parameters. +const DateFormat = "2006-01-02" + +// Date represents a date (without time) for OpenAPI date format. +type Date struct { + time.Time +} + +// UnmarshalText implements encoding.TextUnmarshaler for Date. +func (d *Date) UnmarshalText(data []byte) error { + t, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = t + return nil +} + +// MarshalText implements encoding.TextMarshaler for Date. +func (d Date) MarshalText() ([]byte, error) { + return []byte(d.Format(DateFormat)), nil +} + +// Format returns the date formatted according to layout. +func (d Date) Format(layout string) string { + return d.Time.Format(layout) +} + +// primitiveToString converts a primitive value to a string representation. +// It handles basic Go types, time.Time, types.Date, and types that implement +// json.Marshaler or fmt.Stringer. +func primitiveToString(value interface{}) (string, error) { + // Check for known types first (time, date, uuid) + if res, ok := marshalKnownTypes(value); ok { + return res, nil + } + + // Dereference pointers for optional values + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + kind := t.Kind() + + switch kind { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + case reflect.Float32: + return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil + case reflect.Bool: + if v.Bool() { + return "true", nil + } + return "false", nil + case reflect.String: + return v.String(), nil + case reflect.Struct: + // Check if it's a UUID + if u, ok := value.(uuid.UUID); ok { + return u.String(), nil + } + // Check if it implements json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + var i2 interface{} + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to decode JSON: %w", err) + } + return primitiveToString(i2) + } + fallthrough + default: + if s, ok := value.(fmt.Stringer); ok { + return s.String(), nil + } + return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) + } +} + +// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. +func marshalKnownTypes(value interface{}) (string, bool) { + v := reflect.Indirect(reflect.ValueOf(value)) + t := v.Type() + + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + tt := v.Convert(reflect.TypeOf(time.Time{})) + timeVal := tt.Interface().(time.Time) + return timeVal.Format(time.RFC3339Nano), true + } + + if t.ConvertibleTo(reflect.TypeOf(Date{})) { + d := v.Convert(reflect.TypeOf(Date{})) + dateVal := d.Interface().(Date) + return dateVal.Format(DateFormat), true + } + + if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { + u := v.Convert(reflect.TypeOf(uuid.UUID{})) + uuidVal := u.Interface().(uuid.UUID) + return uuidVal.String(), true + } + + return "", false +} + +// escapeParameterString escapes a parameter value based on its location. +// Query and path parameters need URL escaping; headers and cookies do not. +func escapeParameterString(value string, paramLocation ParamLocation) string { + switch paramLocation { + case ParamLocationQuery: + return url.QueryEscape(value) + case ParamLocationPath: + return url.PathEscape(value) + default: + return value + } +} + +// unescapeParameterString unescapes a parameter value based on its location. +func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { + switch paramLocation { + case ParamLocationQuery, ParamLocationUndefined: + return url.QueryUnescape(value) + case ParamLocationPath: + return url.PathUnescape(value) + default: + return value, nil + } +} + +// sortedKeys returns the keys of a map in sorted order. +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// BindStringToObject binds a string value to a destination object. +// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. +func BindStringToObject(src string, dst interface{}) error { + // Check for TextUnmarshaler + if tu, ok := dst.(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(src)) + } + + // Check for Binder interface + if b, ok := dst.(Binder); ok { + return b.Bind(src) + } + + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Ptr { + return fmt.Errorf("dst must be a pointer, got %T", dst) + } + v = v.Elem() + + switch v.Kind() { + case reflect.String: + v.SetString(src) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + v.SetInt(i) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + u, err := strconv.ParseUint(src, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse uint: %w", err) + } + v.SetUint(u) + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(src, 64) + if err != nil { + return fmt.Errorf("failed to parse float: %w", err) + } + v.SetFloat(f) + case reflect.Bool: + b, err := strconv.ParseBool(src) + if err != nil { + return fmt.Errorf("failed to parse bool: %w", err) + } + v.SetBool(b) + default: + // Try JSON unmarshal as a fallback + return json.Unmarshal([]byte(src), dst) + } + return nil +} + +// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. +func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { + v := reflect.Indirect(reflect.ValueOf(dest)) + t := v.Type() + + newArray := reflect.MakeSlice(t, len(parts), len(parts)) + for i, p := range parts { + err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) + if err != nil { + return fmt.Errorf("error setting array element: %w", err) + } + } + v.Set(newArray) + return nil +} + +// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. +func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { + var fields []string + if explode { + fields = make([]string, len(parts)) + for i, property := range parts { + propertyParts := strings.Split(property, "=") + if len(propertyParts) != 2 { + return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) + } + fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" + } + } else { + if len(parts)%2 != 0 { + return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) + } + fields = make([]string, len(parts)/2) + for i := 0; i < len(parts); i += 2 { + key := parts[i] + value := parts[i+1] + fields[i/2] = "\"" + key + "\":\"" + value + "\"" + } + } + jsonParam := "{" + strings.Join(fields, ",") + "}" + return json.Unmarshal([]byte(jsonParam), dest) +} diff --git a/experimental/internal/codegen/templates/files/params/style_deep_object.go.tmpl b/experimental/internal/codegen/templates/files/params/style_deep_object.go.tmpl new file mode 100644 index 0000000000..753ff4433a --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/style_deep_object.go.tmpl @@ -0,0 +1,74 @@ +{{/* StyleDeepObjectParam - deepObject style (always exploded) */}} + +// StyleDeepObjectParam serializes a value using deepObject style. +// DeepObject style is only valid for query parameters with object values and must be exploded. +// Objects: paramName[key1]=value1¶mName[key2]=value2 +// Nested: paramName[outer][inner]=value +func StyleDeepObjectParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // deepObject always requires explode=true + return MarshalDeepObject(value, paramName) +} + +// MarshalDeepObject marshals an object to deepObject style query parameters. +func MarshalDeepObject(i interface{}, paramName string) (string, error) { + // Marshal to JSON first to handle all field annotations + buf, err := json.Marshal(i) + if err != nil { + return "", fmt.Errorf("failed to marshal input to JSON: %w", err) + } + var i2 interface{} + err = json.Unmarshal(buf, &i2) + if err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + fields, err := marshalDeepObjectRecursive(i2, nil) + if err != nil { + return "", fmt.Errorf("error traversing JSON structure: %w", err) + } + + // Prefix the param name to each subscripted field + for idx := range fields { + fields[idx] = paramName + fields[idx] + } + return strings.Join(fields, "&"), nil +} + +func marshalDeepObjectRecursive(in interface{}, path []string) ([]string, error) { + var result []string + + switch t := in.(type) { + case []interface{}: + // Arrays use numerical subscripts [0], [1], etc. + for i, iface := range t { + newPath := append(path, strconv.Itoa(i)) + fields, err := marshalDeepObjectRecursive(iface, newPath) + if err != nil { + return nil, fmt.Errorf("error traversing array: %w", err) + } + result = append(result, fields...) + } + case map[string]interface{}: + // Maps use key subscripts [key1], [key2], etc. + keys := make([]string, 0, len(t)) + for k := range t { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + newPath := append(path, k) + fields, err := marshalDeepObjectRecursive(t[k], newPath) + if err != nil { + return nil, fmt.Errorf("error traversing map: %w", err) + } + result = append(result, fields...) + } + default: + // Concrete value: turn path into [a][b][c] format + prefix := "[" + strings.Join(path, "][") + "]" + result = []string{ + prefix + fmt.Sprintf("=%v", t), + } + } + return result, nil +} diff --git a/experimental/internal/codegen/templates/files/params/style_form.go.tmpl b/experimental/internal/codegen/templates/files/params/style_form.go.tmpl new file mode 100644 index 0000000000..91df97e6e0 --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/style_form.go.tmpl @@ -0,0 +1,132 @@ +{{/* StyleFormParam - form style without explode */}} + +// StyleFormParam serializes a value using form style (RFC 6570) without exploding. +// Form style is the default for query and cookie parameters. +// Primitives: paramName=value +// Arrays: paramName=a,b,c +// Objects: paramName=key1,value1,key2,value2 +func StyleFormParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleFormSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleFormStruct(paramName, paramLocation, value) + case reflect.Map: + return styleFormMap(paramName, paramLocation, value) + default: + return styleFormPrimitive(paramName, paramLocation, value) + } +} + +func styleFormPrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleFormSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Form without explode: paramName=a,b,c + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, ","), nil +} + +func styleFormStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleFormParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Form style without explode: paramName=key1,value1,key2,value2 + prefix := fmt.Sprintf("%s=", paramName) + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return prefix + strings.Join(parts, ","), nil +} + +func styleFormMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Form style without explode: paramName=key1,value1,key2,value2 + prefix := fmt.Sprintf("%s=", paramName) + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return prefix + strings.Join(parts, ","), nil +} diff --git a/experimental/internal/codegen/templates/files/params/style_form_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/style_form_explode.go.tmpl new file mode 100644 index 0000000000..a39c67c21b --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/style_form_explode.go.tmpl @@ -0,0 +1,130 @@ +{{/* StyleFormExplodeParam - form style with explode */}} + +// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. +// Form style is the default for query and cookie parameters. +// Primitives: paramName=value +// Arrays: paramName=a¶mName=b¶mName=c +// Objects: key1=value1&key2=value2 +func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleFormExplodeSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleFormExplodeStruct(paramName, paramLocation, value) + case reflect.Map: + return styleFormExplodeMap(paramName, paramLocation, value) + default: + return styleFormExplodePrimitive(paramName, paramLocation, value) + } +} + +func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Form with explode: paramName=a¶mName=b¶mName=c + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, "&"+prefix), nil +} + +func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleFormExplodeParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} + +func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Form style with explode: key1=value1&key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, "&"), nil +} diff --git a/experimental/internal/codegen/templates/files/params/style_label.go.tmpl b/experimental/internal/codegen/templates/files/params/style_label.go.tmpl new file mode 100644 index 0000000000..def8c74ee0 --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/style_label.go.tmpl @@ -0,0 +1,129 @@ +{{/* StyleLabelParam - label style without explode */}} + +// StyleLabelParam serializes a value using label style (RFC 6570) without exploding. +// Label style prefixes values with a dot. +// Primitives: .value +// Arrays: .a,b,c +// Objects: .key1,value1,key2,value2 +func StyleLabelParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return "." + escapeParameterString(string(b), paramLocation), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleLabelSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleLabelStruct(paramName, paramLocation, value) + case reflect.Map: + return styleLabelMap(paramName, paramLocation, value) + default: + return styleLabelPrimitive(paramLocation, value) + } +} + +func styleLabelPrimitive(paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return "." + escapeParameterString(strVal, paramLocation), nil +} + +func styleLabelSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Label without explode: .a,b,c + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return "." + strings.Join(parts, ","), nil +} + +func styleLabelStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return "." + escapeParameterString(timeVal, paramLocation), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleLabelParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Label style without explode: .key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return "." + strings.Join(parts, ","), nil +} + +func styleLabelMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Label style without explode: .key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return "." + strings.Join(parts, ","), nil +} diff --git a/experimental/internal/codegen/templates/files/params/style_label_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/style_label_explode.go.tmpl new file mode 100644 index 0000000000..c770480811 --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/style_label_explode.go.tmpl @@ -0,0 +1,129 @@ +{{/* StyleLabelExplodeParam - label style with explode */}} + +// StyleLabelExplodeParam serializes a value using label style (RFC 6570) with exploding. +// Label style prefixes values with a dot. +// Primitives: .value +// Arrays: .a.b.c +// Objects: .key1=value1.key2=value2 +func StyleLabelExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return "." + escapeParameterString(string(b), paramLocation), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleLabelExplodeSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleLabelExplodeStruct(paramName, paramLocation, value) + case reflect.Map: + return styleLabelExplodeMap(paramName, paramLocation, value) + default: + return styleLabelExplodePrimitive(paramLocation, value) + } +} + +func styleLabelExplodePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return "." + escapeParameterString(strVal, paramLocation), nil +} + +func styleLabelExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Label with explode: .a.b.c + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return "." + strings.Join(parts, "."), nil +} + +func styleLabelExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return "." + escapeParameterString(timeVal, paramLocation), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleLabelExplodeParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Label style with explode: .key1=value1.key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return "." + strings.Join(parts, "."), nil +} + +func styleLabelExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Label style with explode: .key1=value1.key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return "." + strings.Join(parts, "."), nil +} diff --git a/experimental/internal/codegen/templates/files/params/style_matrix.go.tmpl b/experimental/internal/codegen/templates/files/params/style_matrix.go.tmpl new file mode 100644 index 0000000000..49c001f9dc --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/style_matrix.go.tmpl @@ -0,0 +1,132 @@ +{{/* StyleMatrixParam - matrix style without explode */}} + +// StyleMatrixParam serializes a value using matrix style (RFC 6570) without exploding. +// Matrix style prefixes values with ;paramName=. +// Primitives: ;paramName=value +// Arrays: ;paramName=a,b,c +// Objects: ;paramName=key1,value1,key2,value2 +func StyleMatrixParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf(";%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleMatrixSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleMatrixStruct(paramName, paramLocation, value) + case reflect.Map: + return styleMatrixMap(paramName, paramLocation, value) + default: + return styleMatrixPrimitive(paramName, paramLocation, value) + } +} + +func styleMatrixPrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf(";%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleMatrixSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Matrix without explode: ;paramName=a,b,c + prefix := fmt.Sprintf(";%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, ","), nil +} + +func styleMatrixStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return fmt.Sprintf(";%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleMatrixParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Matrix style without explode: ;paramName=key1,value1,key2,value2 + prefix := fmt.Sprintf(";%s=", paramName) + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return prefix + strings.Join(parts, ","), nil +} + +func styleMatrixMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Matrix style without explode: ;paramName=key1,value1,key2,value2 + prefix := fmt.Sprintf(";%s=", paramName) + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return prefix + strings.Join(parts, ","), nil +} diff --git a/experimental/internal/codegen/templates/files/params/style_matrix_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/style_matrix_explode.go.tmpl new file mode 100644 index 0000000000..63790426f4 --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/style_matrix_explode.go.tmpl @@ -0,0 +1,130 @@ +{{/* StyleMatrixExplodeParam - matrix style with explode */}} + +// StyleMatrixExplodeParam serializes a value using matrix style (RFC 6570) with exploding. +// Matrix style prefixes values with ;paramName=. +// Primitives: ;paramName=value +// Arrays: ;paramName=a;paramName=b;paramName=c +// Objects: ;key1=value1;key2=value2 +func StyleMatrixExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf(";%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleMatrixExplodeSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleMatrixExplodeStruct(paramName, paramLocation, value) + case reflect.Map: + return styleMatrixExplodeMap(paramName, paramLocation, value) + default: + return styleMatrixExplodePrimitive(paramName, paramLocation, value) + } +} + +func styleMatrixExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf(";%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleMatrixExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Matrix with explode: ;paramName=a;paramName=b;paramName=c + prefix := fmt.Sprintf(";%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, prefix), nil +} + +func styleMatrixExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return fmt.Sprintf(";%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleMatrixExplodeParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Matrix style with explode: ;key1=value1;key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return ";" + strings.Join(parts, ";"), nil +} + +func styleMatrixExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Matrix style with explode: ;key1=value1;key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return ";" + strings.Join(parts, ";"), nil +} diff --git a/experimental/internal/codegen/templates/files/params/style_pipe_delimited.go.tmpl b/experimental/internal/codegen/templates/files/params/style_pipe_delimited.go.tmpl new file mode 100644 index 0000000000..de7022ecda --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/style_pipe_delimited.go.tmpl @@ -0,0 +1,66 @@ +{{/* StylePipeDelimitedParam - pipeDelimited style without explode */}} + +// StylePipeDelimitedParam serializes a value using pipeDelimited style without exploding. +// Pipe-delimited style is used for query parameters with array values. +// Arrays: paramName=a|b|c +// Note: Only valid for arrays; objects should use other styles. +func StylePipeDelimitedParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return stylePipeDelimitedSlice(paramName, paramLocation, sliceVal) + default: + // For primitives, fall back to form style + return stylePipeDelimitedPrimitive(paramName, paramLocation, value) + } +} + +func stylePipeDelimitedPrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func stylePipeDelimitedSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Pipe-delimited without explode: paramName=a|b|c + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, "|"), nil +} diff --git a/experimental/internal/codegen/templates/files/params/style_pipe_delimited_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/style_pipe_delimited_explode.go.tmpl new file mode 100644 index 0000000000..fa156dea7e --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/style_pipe_delimited_explode.go.tmpl @@ -0,0 +1,66 @@ +{{/* StylePipeDelimitedExplodeParam - pipeDelimited style with explode */}} + +// StylePipeDelimitedExplodeParam serializes a value using pipeDelimited style with exploding. +// Pipe-delimited style is used for query parameters with array values. +// Arrays: paramName=a¶mName=b¶mName=c (same as form explode) +// Note: Only valid for arrays; objects should use other styles. +func StylePipeDelimitedExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return stylePipeDelimitedExplodeSlice(paramName, paramLocation, sliceVal) + default: + // For primitives, fall back to form style + return stylePipeDelimitedExplodePrimitive(paramName, paramLocation, value) + } +} + +func stylePipeDelimitedExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func stylePipeDelimitedExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Pipe-delimited with explode: paramName=a¶mName=b¶mName=c + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, "&"+prefix), nil +} diff --git a/experimental/internal/codegen/templates/files/params/style_simple.go.tmpl b/experimental/internal/codegen/templates/files/params/style_simple.go.tmpl new file mode 100644 index 0000000000..7476b2d4a4 --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/style_simple.go.tmpl @@ -0,0 +1,158 @@ +{{/* StyleSimpleParam - simple style without explode */}} + +// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. +// Simple style is the default for path and header parameters. +// Arrays are comma-separated: a,b,c +// Objects are key,value pairs: key1,value1,key2,value2 +func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return escapeParameterString(string(b), paramLocation), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleSimpleSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleSimpleStruct(paramName, paramLocation, value) + case reflect.Map: + return styleSimpleMap(paramName, paramLocation, value) + default: + return styleSimplePrimitive(paramLocation, value) + } +} + +func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return escapeParameterString(strVal, paramLocation), nil +} + +func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return escapeParameterString(timeVal, paramLocation), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleSimpleParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Simple style without explode: key1,value1,key2,value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k, v) + } + return strings.Join(parts, ","), nil +} + +// structToFieldDict converts a struct to a map of field names to string values. +func structToFieldDict(value interface{}) (map[string]string, error) { + v := reflect.ValueOf(value) + t := reflect.TypeOf(value) + fieldDict := make(map[string]string) + + for i := 0; i < t.NumField(); i++ { + fieldT := t.Field(i) + tag := fieldT.Tag.Get("json") + fieldName := fieldT.Name + if tag != "" { + tagParts := strings.Split(tag, ",") + if tagParts[0] != "" { + fieldName = tagParts[0] + } + } + f := v.Field(i) + + // Skip nil optional fields + if f.Type().Kind() == reflect.Ptr && f.IsNil() { + continue + } + str, err := primitiveToString(f.Interface()) + if err != nil { + return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) + } + fieldDict[fieldName] = str + } + return fieldDict, nil +} diff --git a/experimental/internal/codegen/templates/files/params/style_simple_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/style_simple_explode.go.tmpl new file mode 100644 index 0000000000..38dc6d3983 --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/style_simple_explode.go.tmpl @@ -0,0 +1,128 @@ +{{/* StyleSimpleExplodeParam - simple style with explode */}} + +// StyleSimpleExplodeParam serializes a value using simple style (RFC 6570) with exploding. +// Simple style is the default for path and header parameters. +// Arrays are comma-separated: a,b,c (same as non-explode) +// Objects are key=value pairs: key1=value1,key2=value2 +func StyleSimpleExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return escapeParameterString(string(b), paramLocation), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleSimpleExplodeSlice(paramName, paramLocation, sliceVal) + case reflect.Struct: + return styleSimpleExplodeStruct(paramName, paramLocation, value) + case reflect.Map: + return styleSimpleExplodeMap(paramName, paramLocation, value) + default: + return styleSimpleExplodePrimitive(paramLocation, value) + } +} + +func styleSimpleExplodePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return escapeParameterString(strVal, paramLocation), nil +} + +func styleSimpleExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Exploded simple array is same as non-exploded: comma-separated + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + // Check for known types first + if timeVal, ok := marshalKnownTypes(value); ok { + return escapeParameterString(timeVal, paramLocation), nil + } + + // Check for json.Marshaler + if m, ok := value.(json.Marshaler); ok { + buf, err := m.MarshalJSON() + if err != nil { + return "", fmt.Errorf("failed to marshal to JSON: %w", err) + } + var i2 interface{} + e := json.NewDecoder(bytes.NewReader(buf)) + e.UseNumber() + if err = e.Decode(&i2); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return StyleSimpleExplodeParam(paramName, paramLocation, i2) + } + + // Build field dictionary + fieldDict, err := structToFieldDict(value) + if err != nil { + return "", err + } + + // Simple style with explode: key1=value1,key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, ","), nil +} + +func styleSimpleExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + dict, ok := value.(map[string]interface{}) + if !ok { + return "", errors.New("map not of type map[string]interface{}") + } + + fieldDict := make(map[string]string) + for fieldName, val := range dict { + str, err := primitiveToString(val) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + fieldDict[fieldName] = str + } + + // Simple style with explode: key1=value1,key2=value2 + var parts []string + for _, k := range sortedKeys(fieldDict) { + v := escapeParameterString(fieldDict[k], paramLocation) + parts = append(parts, k+"="+v) + } + return strings.Join(parts, ","), nil +} diff --git a/experimental/internal/codegen/templates/files/params/style_space_delimited.go.tmpl b/experimental/internal/codegen/templates/files/params/style_space_delimited.go.tmpl new file mode 100644 index 0000000000..7256e3a43c --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/style_space_delimited.go.tmpl @@ -0,0 +1,66 @@ +{{/* StyleSpaceDelimitedParam - spaceDelimited style without explode */}} + +// StyleSpaceDelimitedParam serializes a value using spaceDelimited style without exploding. +// Space-delimited style is used for query parameters with array values. +// Arrays: paramName=a b c (space-separated) +// Note: Only valid for arrays; objects should use other styles. +func StyleSpaceDelimitedParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleSpaceDelimitedSlice(paramName, paramLocation, sliceVal) + default: + // For primitives, fall back to form style + return styleSpaceDelimitedPrimitive(paramName, paramLocation, value) + } +} + +func styleSpaceDelimitedPrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleSpaceDelimitedSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Space-delimited without explode: paramName=a b c (space becomes %20 when encoded) + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, " "), nil +} diff --git a/experimental/internal/codegen/templates/files/params/style_space_delimited_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/style_space_delimited_explode.go.tmpl new file mode 100644 index 0000000000..e32813ba91 --- /dev/null +++ b/experimental/internal/codegen/templates/files/params/style_space_delimited_explode.go.tmpl @@ -0,0 +1,66 @@ +{{/* StyleSpaceDelimitedExplodeParam - spaceDelimited style with explode */}} + +// StyleSpaceDelimitedExplodeParam serializes a value using spaceDelimited style with exploding. +// Space-delimited style is used for query parameters with array values. +// Arrays: paramName=a¶mName=b¶mName=c (same as form explode) +// Note: Only valid for arrays; objects should use other styles. +func StyleSpaceDelimitedExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + t := reflect.TypeOf(value) + v := reflect.ValueOf(value) + + // Dereference pointers + if t.Kind() == reflect.Ptr { + if v.IsNil() { + return "", fmt.Errorf("value is a nil pointer") + } + v = reflect.Indirect(v) + t = v.Type() + } + + // Check for TextMarshaler (but not time.Time or Date) + if tu, ok := value.(encoding.TextMarshaler); ok { + innerT := reflect.Indirect(reflect.ValueOf(value)).Type() + if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { + b, err := tu.MarshalText() + if err != nil { + return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil + } + } + + switch t.Kind() { + case reflect.Slice: + n := v.Len() + sliceVal := make([]interface{}, n) + for i := 0; i < n; i++ { + sliceVal[i] = v.Index(i).Interface() + } + return styleSpaceDelimitedExplodeSlice(paramName, paramLocation, sliceVal) + default: + // For primitives, fall back to form style + return styleSpaceDelimitedExplodePrimitive(paramName, paramLocation, value) + } +} + +func styleSpaceDelimitedExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { + strVal, err := primitiveToString(value) + if err != nil { + return "", err + } + return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil +} + +func styleSpaceDelimitedExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { + // Space-delimited with explode: paramName=a¶mName=b¶mName=c + prefix := fmt.Sprintf("%s=", paramName) + parts := make([]string, len(values)) + for i, v := range values { + part, err := primitiveToString(v) + if err != nil { + return "", fmt.Errorf("error formatting '%s': %w", paramName, err) + } + parts[i] = escapeParameterString(part, paramLocation) + } + return prefix + strings.Join(parts, "&"+prefix), nil +} diff --git a/experimental/internal/codegen/templates/files/server/chi/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/chi/handler.go.tmpl new file mode 100644 index 0000000000..ccd9653b40 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/chi/handler.go.tmpl @@ -0,0 +1,59 @@ +{{- /* + This template generates the HTTP handler and routing for Chi servers. + Input: []OperationDescriptor +*/ -}} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{}) +} + +// ChiServerOptions configures the Chi server. +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, + }) +} + +// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. +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+"{{ pathToChiPattern .Path }}", wrapper.{{ .GoOperationID }}) + }) +{{- end }} + return r +} diff --git a/experimental/internal/codegen/templates/files/server/chi/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/chi/interface.go.tmpl new file mode 100644 index 0000000000..010ed1b157 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/chi/interface.go.tmpl @@ -0,0 +1,24 @@ +{{- /* + This template generates the ServerInterface for Chi servers. + Input: []OperationDescriptor +*/ -}} + +// ServerInterface represents all server handlers. +type ServerInterface interface { +{{- range . }} +{{ .SummaryAsComment }} + // ({{ .Method }} {{ .Path }}) + {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) +{{- end }} +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. +type Unimplemented struct{} + +{{- range . }} +{{ .SummaryAsComment }} +// ({{ .Method }} {{ .Path }}) +func (_ Unimplemented) {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) { + w.WriteHeader(http.StatusNotImplemented) +} +{{- end }} diff --git a/experimental/internal/codegen/templates/files/server/chi/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/chi/receiver.go.tmpl new file mode 100644 index 0000000000..dce41f1d14 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/chi/receiver.go.tmpl @@ -0,0 +1,95 @@ +{{- /* + This template generates the receiver interface and handler functions for Chi. + Input: ReceiverTemplateData +*/ -}} + +// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. +type {{ .Prefix }}ReceiverInterface interface { +{{- range .Operations }} +{{ .SummaryAsComment }} + // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. + Handle{{ .GoOperationID }}{{ $.Prefix }}(w http.ResponseWriter, r *http.Request{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) +{{- end }} +} + +// {{ .Prefix }}ReceiverMiddlewareFunc is a middleware function for {{ $.PrefixLower }} receiver handlers. +type {{ .Prefix }}ReceiverMiddlewareFunc func(http.Handler) http.Handler + +{{ range .Operations }} +// {{ .GoOperationID }}{{ $.Prefix }}Handler returns an http.Handler for the {{ .GoOperationID }} {{ $.PrefixLower }}. +// The caller is responsible for registering this handler at the appropriate path. +func {{ .GoOperationID }}{{ $.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) + } + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +{{- if .HasParams }} + var err error + _ = err + + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} +{{- if or .Required .IsPassThrough .IsJSON }} + if paramValue := r.URL.Query().Get("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + errHandler(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + errHandler(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) + return + }{{ end }} +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, r.URL.Query(), ¶ms.{{ .GoName }}) + if err != nil { + errHandler(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{ end }} +{{ range .HeaderParams }} + if valueList, found := r.Header[http.CanonicalHeaderKey("{{ .Name }}")]; found { + var {{ .GoVariableName }} {{ .TypeDecl }} + n := len(valueList) + if n != 1 { + errHandler(w, r, &TooManyValuesForParamError{ParamName: "{{ .Name }}", Count: n}) + return + } +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) + if err != nil { + errHandler(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + errHandler(w, r, &RequiredHeaderError{ParamName: "{{ .Name }}", Err: fmt.Errorf("header parameter {{ .Name }} is required, but not found")}) + return + }{{ end }} +{{ end }} +{{- end }} + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + si.Handle{{ .GoOperationID }}{{ $.Prefix }}(w, r{{ if .HasParams }}, params{{ end }}) + })) + + for _, middleware := range middlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) + }) +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/chi/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/chi/wrapper.go.tmpl new file mode 100644 index 0000000000..91d346b682 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/chi/wrapper.go.tmpl @@ -0,0 +1,166 @@ +{{- /* + This template generates the ServerInterfaceWrapper that extracts parameters + from HTTP requests and calls the ServerInterface methods for Chi. + Input: []OperationDescriptor +*/ -}} + +// ServerInterfaceWrapper converts HTTP requests to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// MiddlewareFunc is a middleware function type. +type MiddlewareFunc func(http.Handler) http.Handler + +{{ range . }} +// {{ .GoOperationID }} operation middleware +func (siw *ServerInterfaceWrapper) {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request) { +{{- if or .PathParams .HasParams }} + var err error +{{- end }} +{{ range .PathParams }} + // ------------- Path parameter "{{ .Name }}" ------------- + var {{ .GoVariableName }} {{ .TypeDecl }} +{{ if .IsPassThrough }} + {{ .GoVariableName }} = chi.URLParam(r, "{{ .Name }}") +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(chi.URLParam(r, "{{ .Name }}")), &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, chi.URLParam(r, "{{ .Name }}"), &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{ end }} +{{- if .Security }} + ctx := r.Context() +{{- range .Security }} + ctx = context.WithValue(ctx, {{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) +{{- end }} + r = r.WithContext(ctx) +{{- end }} +{{ if .HasParams }} + // Parameter object where we will unmarshal all parameters from the context + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- +{{- if or .Required .IsPassThrough .IsJSON }} + if paramValue := r.URL.Query().Get("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) + return + }{{ end }} +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, r.URL.Query(), ¶ms.{{ .GoName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{ end }} +{{ if .HeaderParams }} + headers := r.Header +{{ range .HeaderParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found { + var {{ .GoVariableName }} {{ .TypeDecl }} + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "{{ .Name }}", Count: n}) + return + } +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + err := fmt.Errorf("Header parameter {{ .Name }} is required, but not found") + siw.ErrorHandlerFunc(w, r, &RequiredHeaderError{ParamName: "{{ .Name }}", Err: err}) + return + }{{ end }} +{{ end }} +{{ end }} +{{ range .CookieParams }} + { + var cookie *http.Cookie + if cookie, err = r.Cookie("{{ .Name }}"); err == nil { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie.Value +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + decoded, err := url.QueryUnescape(cookie.Value) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnescapedCookieParamError{ParamName: "{{ .Name }}", Err: err}) + return + } + err = json.Unmarshal([]byte(decoded), &value) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} +{{- if .IsStyled }} + var value {{ .TypeDecl }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie.Value, &value) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) + return + }{{ end }} + } +{{ end }} +{{ end }} + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.{{ .GoOperationID }}(w, r{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/echo-v4/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/echo-v4/handler.go.tmpl new file mode 100644 index 0000000000..fcbb54b60d --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/echo-v4/handler.go.tmpl @@ -0,0 +1,34 @@ +{{- /* + This template generates the HTTP handler and routing for Echo servers. + Input: []OperationDescriptor +*/ -}} + +// EchoRouter is an interface for echo.Echo and echo.Group. +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 +} + +// RegisterHandlers adds each server route to the EchoRouter. +func RegisterHandlers(router EchoRouter, si ServerInterface) { + RegisterHandlersWithBaseURL(router, si, "") +} + +// RegisterHandlersWithBaseURL adds each server route to the EchoRouter with a base URL prefix. +func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { +{{ if . }} + wrapper := ServerInterfaceWrapper{ + Handler: si, + } +{{ end }} +{{- range . }} + router.{{ .Method }}(baseURL+"{{ pathToEchoPattern .Path }}", wrapper.{{ .GoOperationID }}) +{{- end }} +} diff --git a/experimental/internal/codegen/templates/files/server/echo-v4/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/echo-v4/interface.go.tmpl new file mode 100644 index 0000000000..22cb0ebe68 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/echo-v4/interface.go.tmpl @@ -0,0 +1,24 @@ +{{- /* + This template generates the ServerInterface for Echo servers. + Input: []OperationDescriptor +*/ -}} + +// ServerInterface represents all server handlers. +type ServerInterface interface { +{{- range . }} +{{ .SummaryAsComment }} + // ({{ .Method }} {{ .Path }}) + {{ .GoOperationID }}(ctx echo.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error +{{- end }} +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. +type Unimplemented struct{} + +{{- range . }} +{{ .SummaryAsComment }} +// ({{ .Method }} {{ .Path }}) +func (_ Unimplemented) {{ .GoOperationID }}(ctx echo.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error { + return ctx.NoContent(http.StatusNotImplemented) +} +{{- end }} diff --git a/experimental/internal/codegen/templates/files/server/echo-v4/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/echo-v4/receiver.go.tmpl new file mode 100644 index 0000000000..0fa1b97708 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/echo-v4/receiver.go.tmpl @@ -0,0 +1,72 @@ +{{- /* + This template generates the receiver interface and handler functions for Echo v4. + Input: ReceiverTemplateData +*/ -}} + +// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. +type {{ .Prefix }}ReceiverInterface interface { +{{- range .Operations }} +{{ .SummaryAsComment }} + // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. + Handle{{ .GoOperationID }}{{ $.Prefix }}(ctx echo.Context{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error +{{- end }} +} + +{{ range .Operations }} +// {{ .GoOperationID }}{{ $.Prefix }}Handler returns an echo.HandlerFunc for the {{ .GoOperationID }} {{ $.PrefixLower }}. +// The caller is responsible for registering this handler at the appropriate path. +func {{ .GoOperationID }}{{ $.Prefix }}Handler(si {{ $.Prefix }}ReceiverInterface) echo.HandlerFunc { + return func(ctx echo.Context) error { +{{- if .HasParams }} + var err error + _ = err + + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} +{{- if or .Required .IsPassThrough .IsJSON }} + if paramValue := ctx.QueryParam("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query parameter {{ .Name }} is required")) + }{{ end }} +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, ctx.QueryParams(), ¶ms.{{ .GoName }}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- end }} +{{ end }} +{{ range .HeaderParams }} + if valueList := ctx.Request().Header[http.CanonicalHeaderKey("{{ .Name }}")]; len(valueList) > 0 { + var {{ .GoVariableName }} {{ .TypeDecl }} + n := len(valueList) + if n != 1 { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for {{ .Name }}, got %d", n)) + } +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter {{ .Name }} is required")) + }{{ end }} +{{ end }} +{{- end }} + return si.Handle{{ .GoOperationID }}{{ $.Prefix }}(ctx{{ if .HasParams }}, params{{ end }}) + } +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/echo-v4/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/echo-v4/wrapper.go.tmpl new file mode 100644 index 0000000000..6847388a7e --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/echo-v4/wrapper.go.tmpl @@ -0,0 +1,134 @@ +{{- /* + This template generates the ServerInterfaceWrapper that extracts parameters + from HTTP requests and calls the ServerInterface methods for Echo. + Input: []OperationDescriptor +*/ -}} + +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +{{ range . }} +// {{ .GoOperationID }} converts echo context to params. +func (w *ServerInterfaceWrapper) {{ .GoOperationID }}(ctx echo.Context) error { + var err error + +{{ range .PathParams }} + // ------------- Path parameter "{{ .Name }}" ------------- + var {{ .GoVariableName }} {{ .TypeDecl }} +{{ if .IsPassThrough }} + {{ .GoVariableName }} = ctx.Param("{{ .Name }}") +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(ctx.Param("{{ .Name }}")), &{{ .GoVariableName }}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, ctx.Param("{{ .Name }}"), &{{ .GoVariableName }}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- end }} +{{ end }} +{{- if .Security }} +{{- range .Security }} + ctx.Set({{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) +{{- end }} +{{- end }} +{{ if .HasParams }} + // Parameter object where we will unmarshal all parameters from the context + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, ctx.QueryParams(), ¶ms.{{ .GoName }}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- else }} + if paramValue := ctx.QueryParam("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{ .Name }} is required, but not found")) + }{{ end }} +{{- end }} +{{ end }} +{{ if .HeaderParams }} + headers := ctx.Request().Header +{{ range .HeaderParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found { + var {{ .GoVariableName }} {{ .TypeDecl }} + n := len(valueList) + if n != 1 { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for {{ .Name }}, got %d", n)) + } +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter {{ .Name }} is required, but not found")) + }{{ end }} +{{ end }} +{{ end }} +{{ range .CookieParams }} + if cookie, err := ctx.Cookie("{{ .Name }}"); err == nil { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie.Value +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + decoded, err := url.QueryUnescape(cookie.Value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unescaping cookie parameter '%s'", "{{ .Name }}")) + } + err = json.Unmarshal([]byte(decoded), &value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} +{{- if .IsStyled }} + var value {{ .TypeDecl }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie.Value, &value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{ .Name }} is required, but not found")) + }{{ end }} +{{ end }} +{{ end }} + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.{{ .GoOperationID }}(ctx{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) + return err +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/echo/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/echo/handler.go.tmpl new file mode 100644 index 0000000000..0e6d2dbd01 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/echo/handler.go.tmpl @@ -0,0 +1,35 @@ +{{- /* + This template generates the HTTP handler and routing for Echo v5 servers. + Input: []OperationDescriptor +*/ -}} + +// EchoRouter is an interface for echo.Echo and echo.Group. +type EchoRouter interface { + Add(method string, path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + 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 +} + +// RegisterHandlers adds each server route to the EchoRouter. +func RegisterHandlers(router EchoRouter, si ServerInterface) { + RegisterHandlersWithBaseURL(router, si, "") +} + +// RegisterHandlersWithBaseURL adds each server route to the EchoRouter with a base URL prefix. +func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { +{{ if . }} + wrapper := ServerInterfaceWrapper{ + Handler: si, + } +{{ end }} +{{- range . }} + router.{{ .Method }}(baseURL+"{{ pathToEchoPattern .Path }}", wrapper.{{ .GoOperationID }}) +{{- end }} +} diff --git a/experimental/internal/codegen/templates/files/server/echo/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/echo/interface.go.tmpl new file mode 100644 index 0000000000..e1c4ac779a --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/echo/interface.go.tmpl @@ -0,0 +1,24 @@ +{{- /* + This template generates the ServerInterface for Echo v5 servers. + Input: []OperationDescriptor +*/ -}} + +// ServerInterface represents all server handlers. +type ServerInterface interface { +{{- range . }} +{{ .SummaryAsComment }} + // ({{ .Method }} {{ .Path }}) + {{ .GoOperationID }}(ctx *echo.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error +{{- end }} +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. +type Unimplemented struct{} + +{{- range . }} +{{ .SummaryAsComment }} +// ({{ .Method }} {{ .Path }}) +func (_ Unimplemented) {{ .GoOperationID }}(ctx *echo.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error { + return ctx.NoContent(http.StatusNotImplemented) +} +{{- end }} diff --git a/experimental/internal/codegen/templates/files/server/echo/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/echo/receiver.go.tmpl new file mode 100644 index 0000000000..8a336c5e23 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/echo/receiver.go.tmpl @@ -0,0 +1,72 @@ +{{- /* + This template generates the receiver interface and handler functions for Echo v5. + Input: ReceiverTemplateData +*/ -}} + +// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. +type {{ .Prefix }}ReceiverInterface interface { +{{- range .Operations }} +{{ .SummaryAsComment }} + // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. + Handle{{ .GoOperationID }}{{ $.Prefix }}(ctx *echo.Context{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error +{{- end }} +} + +{{ range .Operations }} +// {{ .GoOperationID }}{{ $.Prefix }}Handler returns an echo.HandlerFunc for the {{ .GoOperationID }} {{ $.PrefixLower }}. +// The caller is responsible for registering this handler at the appropriate path. +func {{ .GoOperationID }}{{ $.Prefix }}Handler(si {{ $.Prefix }}ReceiverInterface) echo.HandlerFunc { + return func(ctx *echo.Context) error { +{{- if .HasParams }} + var err error + _ = err + + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} +{{- if or .Required .IsPassThrough .IsJSON }} + if paramValue := ctx.QueryParam("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query parameter {{ .Name }} is required")) + }{{ end }} +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, ctx.QueryParams(), ¶ms.{{ .GoName }}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- end }} +{{ end }} +{{ range .HeaderParams }} + if valueList := ctx.Request().Header[http.CanonicalHeaderKey("{{ .Name }}")]; len(valueList) > 0 { + var {{ .GoVariableName }} {{ .TypeDecl }} + n := len(valueList) + if n != 1 { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for {{ .Name }}, got %d", n)) + } +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter {{ .Name }} is required")) + }{{ end }} +{{ end }} +{{- end }} + return si.Handle{{ .GoOperationID }}{{ $.Prefix }}(ctx{{ if .HasParams }}, params{{ end }}) + } +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/echo/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/echo/wrapper.go.tmpl new file mode 100644 index 0000000000..ae92e02879 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/echo/wrapper.go.tmpl @@ -0,0 +1,134 @@ +{{- /* + This template generates the ServerInterfaceWrapper that extracts parameters + from HTTP requests and calls the ServerInterface methods for Echo v5. + Input: []OperationDescriptor +*/ -}} + +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +{{ range . }} +// {{ .GoOperationID }} converts echo context to params. +func (w *ServerInterfaceWrapper) {{ .GoOperationID }}(ctx *echo.Context) error { + var err error + +{{ range .PathParams }} + // ------------- Path parameter "{{ .Name }}" ------------- + var {{ .GoVariableName }} {{ .TypeDecl }} +{{ if .IsPassThrough }} + {{ .GoVariableName }} = ctx.Param("{{ .Name }}") +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(ctx.Param("{{ .Name }}")), &{{ .GoVariableName }}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, ctx.Param("{{ .Name }}"), &{{ .GoVariableName }}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- end }} +{{ end }} +{{- if .Security }} +{{- range .Security }} + ctx.Set({{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) +{{- end }} +{{- end }} +{{ if .HasParams }} + // Parameter object where we will unmarshal all parameters from the context + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, ctx.QueryParams(), ¶ms.{{ .GoName }}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- else }} + if paramValue := ctx.QueryParam("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{ .Name }} is required, but not found")) + }{{ end }} +{{- end }} +{{ end }} +{{ if .HeaderParams }} + headers := ctx.Request().Header +{{ range .HeaderParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found { + var {{ .GoVariableName }} {{ .TypeDecl }} + n := len(valueList) + if n != 1 { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for {{ .Name }}, got %d", n)) + } +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter {{ .Name }} is required, but not found")) + }{{ end }} +{{ end }} +{{ end }} +{{ range .CookieParams }} + if cookie, err := ctx.Cookie("{{ .Name }}"); err == nil { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie.Value +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + decoded, err := url.QueryUnescape(cookie.Value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unescaping cookie parameter '%s'", "{{ .Name }}")) + } + err = json.Unmarshal([]byte(decoded), &value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} +{{- if .IsStyled }} + var value {{ .TypeDecl }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie.Value, &value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{ .Name }} is required, but not found")) + }{{ end }} +{{ end }} +{{ end }} + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.{{ .GoOperationID }}(ctx{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) + return err +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/errors.go.tmpl b/experimental/internal/codegen/templates/files/server/errors.go.tmpl new file mode 100644 index 0000000000..c4727ef976 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/errors.go.tmpl @@ -0,0 +1,79 @@ +{{- /* + This template generates error types for server parameter handling. + These are shared across all router implementations. +*/ -}} + +// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. +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 +} + +// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. +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 +} + +// RequiredParamError is returned when a required parameter is missing. +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +// RequiredHeaderError is returned when a required header is missing. +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 +} + +// InvalidParamFormatError is returned when a parameter has an invalid format. +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 +} + +// TooManyValuesForParamError is returned when a parameter has too many values. +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/experimental/internal/codegen/templates/files/server/fiber/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/fiber/handler.go.tmpl new file mode 100644 index 0000000000..b6e59cfb75 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/fiber/handler.go.tmpl @@ -0,0 +1,31 @@ +{{- /* + This template generates the HTTP handler and routing for Fiber servers. + Input: []OperationDescriptor +*/ -}} + +// FiberServerOptions provides options for the Fiber server. +type FiberServerOptions struct { + BaseURL string + Middlewares []fiber.Handler +} + +// 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, + } + + for _, m := range options.Middlewares { + router.Use(m) + } +{{ end }} +{{- range . }} + router.{{ .Method | lower | title }}(options.BaseURL+"{{ pathToFiberPattern .Path }}", wrapper.{{ .GoOperationID }}) +{{- end }} +} diff --git a/experimental/internal/codegen/templates/files/server/fiber/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/fiber/interface.go.tmpl new file mode 100644 index 0000000000..dddefc81c6 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/fiber/interface.go.tmpl @@ -0,0 +1,24 @@ +{{- /* + This template generates the ServerInterface for Fiber servers. + Input: []OperationDescriptor +*/ -}} + +// ServerInterface represents all server handlers. +type ServerInterface interface { +{{- range . }} +{{ .SummaryAsComment }} + // ({{ .Method }} {{ .Path }}) + {{ .GoOperationID }}(c fiber.Ctx{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error +{{- end }} +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. +type Unimplemented struct{} + +{{- range . }} +{{ .SummaryAsComment }} +// ({{ .Method }} {{ .Path }}) +func (_ Unimplemented) {{ .GoOperationID }}(c fiber.Ctx{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error { + return c.SendStatus(fiber.StatusNotImplemented) +} +{{- end }} diff --git a/experimental/internal/codegen/templates/files/server/fiber/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/fiber/receiver.go.tmpl new file mode 100644 index 0000000000..5775f45ed1 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/fiber/receiver.go.tmpl @@ -0,0 +1,71 @@ +{{- /* + This template generates the receiver interface and handler functions for Fiber. + Input: ReceiverTemplateData +*/ -}} + +// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. +type {{ .Prefix }}ReceiverInterface interface { +{{- range .Operations }} +{{ .SummaryAsComment }} + // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. + Handle{{ .GoOperationID }}{{ $.Prefix }}(c fiber.Ctx{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error +{{- end }} +} + +{{ range .Operations }} +// {{ .GoOperationID }}{{ $.Prefix }}Handler returns a fiber.Handler for the {{ .GoOperationID }} {{ $.PrefixLower }}. +// The caller is responsible for registering this handler at the appropriate path. +func {{ .GoOperationID }}{{ $.Prefix }}Handler(si {{ $.Prefix }}ReceiverInterface) fiber.Handler { + return func(c fiber.Ctx) error { +{{- if .HasParams }} + var err error + _ = err + + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} +{{- if or .Required .IsPassThrough .IsJSON }} + if paramValue := c.Query("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + return fiber.NewError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + return fiber.NewError(http.StatusBadRequest, "Query parameter {{ .Name }} is required") + }{{ end }} +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, c.Queries(), ¶ms.{{ .GoName }}) + if err != nil { + return fiber.NewError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- end }} +{{ end }} +{{ range .HeaderParams }} + { + headerValue := c.Get("{{ .Name }}") + if headerValue != "" { + var {{ .GoVariableName }} {{ .TypeDecl }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, headerValue, &{{ .GoVariableName }}) + if err != nil { + return fiber.NewError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + return fiber.NewError(http.StatusBadRequest, "Header parameter {{ .Name }} is required") + }{{ end }} + } +{{ end }} +{{- end }} + return si.Handle{{ .GoOperationID }}{{ $.Prefix }}(c{{ if .HasParams }}, params{{ end }}) + } +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/fiber/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/fiber/wrapper.go.tmpl new file mode 100644 index 0000000000..7abbd2da7a --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/fiber/wrapper.go.tmpl @@ -0,0 +1,137 @@ +{{- /* + This template generates the ServerInterfaceWrapper that extracts parameters + from HTTP requests and calls the ServerInterface methods for Fiber. + Input: []OperationDescriptor +*/ -}} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +{{ range . }} +// {{ .GoOperationID }} operation middleware +func (siw *ServerInterfaceWrapper) {{ .GoOperationID }}(c fiber.Ctx) error { +{{- if or .PathParams .HasParams }} + var err error +{{- end }} +{{ range .PathParams }} + // ------------- Path parameter "{{ .Name }}" ------------- + var {{ .GoVariableName }} {{ .TypeDecl }} +{{ if .IsPassThrough }} + {{ .GoVariableName }} = c.Params("{{ .Name }}") +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(c.Params("{{ .Name }}")), &{{ .GoVariableName }}) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON: %s", "{{ .Name }}", err)) + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, c.Params("{{ .Name }}"), &{{ .GoVariableName }}) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- end }} +{{ end }} +{{- if .Security }} +{{- range .Security }} + c.Locals({{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) +{{- end }} +{{- end }} +{{ if .HasParams }} + // Parameter object where we will unmarshal all parameters from the context + var params {{ .ParamsTypeName }} +{{ if .QueryParams }} + var query url.Values + query, err = url.ParseQuery(string(c.Request().URI().QueryString())) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for query string: %s", err)) + } +{{ end }} +{{ range .QueryParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, query, ¶ms.{{ .GoName }}) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- else if or .Required .IsPassThrough .IsJSON }} + if paramValue := c.Query("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON: %s", "{{ .Name }}", err)) + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + return fiber.NewError(fiber.StatusBadRequest, "Query argument {{ .Name }} is required, but not found") + }{{ end }} +{{- end }} +{{ end }} +{{ if .HeaderParams }} + headers := c.GetReqHeaders() +{{ range .HeaderParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found && len(valueList) > 0 { + var {{ .GoVariableName }} {{ .TypeDecl }} + value := valueList[0] +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(value), &{{ .GoVariableName }}) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON: %s", "{{ .Name }}", err)) + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, value, &{{ .GoVariableName }}) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + return fiber.NewError(fiber.StatusBadRequest, "Header parameter {{ .Name }} is required, but not found") + }{{ end }} +{{ end }} +{{ end }} +{{ range .CookieParams }} + if cookie := c.Cookies("{{ .Name }}"); cookie != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + decoded, err := url.QueryUnescape(cookie) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error unescaping cookie parameter '%s': %s", "{{ .Name }}", err)) + } + err = json.Unmarshal([]byte(decoded), &value) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON: %s", "{{ .Name }}", err)) + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} +{{- if .IsStyled }} + var value {{ .TypeDecl }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie, &value) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + return fiber.NewError(fiber.StatusBadRequest, "Query argument {{ .Name }} is required, but not found") + }{{ end }} +{{ end }} +{{ end }} + return siw.Handler.{{ .GoOperationID }}(c{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/gin/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/gin/handler.go.tmpl new file mode 100644 index 0000000000..3ba848a34a --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/gin/handler.go.tmpl @@ -0,0 +1,37 @@ +{{- /* + This template generates the HTTP handler and routing for Gin servers. + Input: []OperationDescriptor +*/ -}} + +// GinServerOptions provides options for the Gin server. +type GinServerOptions struct { + BaseURL string + Middlewares []MiddlewareFunc + ErrorHandler func(*gin.Context, error, int) +} + +// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. +func RegisterHandlers(router gin.IRouter, si ServerInterface) { + RegisterHandlersWithOptions(router, si, GinServerOptions{}) +} + +// RegisterHandlersWithOptions creates http.Handler with additional options. +func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options GinServerOptions) { +{{ if . }} + errorHandler := options.ErrorHandler + if errorHandler == nil { + errorHandler = func(c *gin.Context, err error, statusCode int) { + c.JSON(statusCode, gin.H{"msg": err.Error()}) + } + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandler: errorHandler, + } +{{ end }} +{{- range . }} + router.{{ .Method }}(options.BaseURL+"{{ pathToGinPattern .Path }}", wrapper.{{ .GoOperationID }}) +{{- end }} +} diff --git a/experimental/internal/codegen/templates/files/server/gin/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/gin/interface.go.tmpl new file mode 100644 index 0000000000..a2b22df6d6 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/gin/interface.go.tmpl @@ -0,0 +1,24 @@ +{{- /* + This template generates the ServerInterface for Gin servers. + Input: []OperationDescriptor +*/ -}} + +// ServerInterface represents all server handlers. +type ServerInterface interface { +{{- range . }} +{{ .SummaryAsComment }} + // ({{ .Method }} {{ .Path }}) + {{ .GoOperationID }}(c *gin.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) +{{- end }} +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. +type Unimplemented struct{} + +{{- range . }} +{{ .SummaryAsComment }} +// ({{ .Method }} {{ .Path }}) +func (_ Unimplemented) {{ .GoOperationID }}(c *gin.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) { + c.Status(http.StatusNotImplemented) +} +{{- end }} diff --git a/experimental/internal/codegen/templates/files/server/gin/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/gin/receiver.go.tmpl new file mode 100644 index 0000000000..1b150abc7b --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/gin/receiver.go.tmpl @@ -0,0 +1,78 @@ +{{- /* + This template generates the receiver interface and handler functions for Gin. + Input: ReceiverTemplateData +*/ -}} + +// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. +type {{ .Prefix }}ReceiverInterface interface { +{{- range .Operations }} +{{ .SummaryAsComment }} + // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. + Handle{{ .GoOperationID }}{{ $.Prefix }}(c *gin.Context{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) +{{- end }} +} + +{{ range .Operations }} +// {{ .GoOperationID }}{{ $.Prefix }}Handler returns a gin.HandlerFunc for the {{ .GoOperationID }} {{ $.PrefixLower }}. +// The caller is responsible for registering this handler at the appropriate path. +func {{ .GoOperationID }}{{ $.Prefix }}Handler(si {{ $.Prefix }}ReceiverInterface) gin.HandlerFunc { + return func(c *gin.Context) { +{{- if .HasParams }} + var err error + _ = err + + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} +{{- if or .Required .IsPassThrough .IsJSON }} + if paramValue := c.Query("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)}) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + c.JSON(http.StatusBadRequest, gin.H{"error": "Query parameter {{ .Name }} is required"}) + return + }{{ end }} +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, c.Request.URL.Query(), ¶ms.{{ .GoName }}) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)}) + return + } +{{- end }} +{{ end }} +{{ range .HeaderParams }} + if valueList := c.Request.Header[http.CanonicalHeaderKey("{{ .Name }}")]; len(valueList) > 0 { + var {{ .GoVariableName }} {{ .TypeDecl }} + n := len(valueList) + if n != 1 { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Expected one value for {{ .Name }}, got %d", n)}) + return + } +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)}) + return + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + c.JSON(http.StatusBadRequest, gin.H{"error": "Header parameter {{ .Name }} is required"}) + return + }{{ end }} +{{ end }} +{{- end }} + si.Handle{{ .GoOperationID }}{{ $.Prefix }}(c{{ if .HasParams }}, params{{ end }}) + } +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/gin/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/gin/wrapper.go.tmpl new file mode 100644 index 0000000000..1fb5792ee5 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/gin/wrapper.go.tmpl @@ -0,0 +1,161 @@ +{{- /* + This template generates the ServerInterfaceWrapper that extracts parameters + from HTTP requests and calls the ServerInterface methods for Gin. + Input: []OperationDescriptor +*/ -}} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandler func(*gin.Context, error, int) +} + +// MiddlewareFunc is a middleware function type. +type MiddlewareFunc func(c *gin.Context) + +{{ range . }} +// {{ .GoOperationID }} operation middleware +func (siw *ServerInterfaceWrapper) {{ .GoOperationID }}(c *gin.Context) { +{{- if or .PathParams .HasParams }} + var err error +{{- end }} +{{ range .PathParams }} + // ------------- Path parameter "{{ .Name }}" ------------- + var {{ .GoVariableName }} {{ .TypeDecl }} +{{ if .IsPassThrough }} + {{ .GoVariableName }} = c.Param("{{ .Name }}") +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(c.Param("{{ .Name }}")), &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Error unmarshaling parameter '{{ .Name }}' as JSON"), http.StatusBadRequest) + return + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, c.Param("{{ .Name }}"), &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter {{ .Name }}: %w", err), http.StatusBadRequest) + return + } +{{- end }} +{{ end }} +{{- if .Security }} +{{- range .Security }} + c.Set({{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) +{{- end }} +{{- end }} +{{ if .HasParams }} + // Parameter object where we will unmarshal all parameters from the context + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, c.Request.URL.Query(), ¶ms.{{ .GoName }}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter {{ .Name }}: %w", err), http.StatusBadRequest) + return + } +{{- else if or .Required .IsPassThrough .IsJSON }} + if paramValue := c.Query("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Error unmarshaling parameter '{{ .Name }}' as JSON: %w", err), http.StatusBadRequest) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + siw.ErrorHandler(c, fmt.Errorf("Query argument {{ .Name }} is required, but not found"), http.StatusBadRequest) + return + }{{ end }} +{{- end }} +{{ end }} +{{ if .HeaderParams }} + headers := c.Request.Header +{{ range .HeaderParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found { + var {{ .GoVariableName }} {{ .TypeDecl }} + n := len(valueList) + if n != 1 { + siw.ErrorHandler(c, fmt.Errorf("Expected one value for {{ .Name }}, got %d", n), http.StatusBadRequest) + return + } +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Error unmarshaling parameter '{{ .Name }}' as JSON"), http.StatusBadRequest) + return + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter {{ .Name }}: %w", err), http.StatusBadRequest) + return + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + siw.ErrorHandler(c, fmt.Errorf("Header parameter {{ .Name }} is required, but not found"), http.StatusBadRequest) + return + }{{ end }} +{{ end }} +{{ end }} +{{ range .CookieParams }} + { + var cookie string + if cookie, err = c.Cookie("{{ .Name }}"); err == nil { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + decoded, err := url.QueryUnescape(cookie) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Error unescaping cookie parameter '{{ .Name }}'"), http.StatusBadRequest) + return + } + err = json.Unmarshal([]byte(decoded), &value) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Error unmarshaling parameter '{{ .Name }}' as JSON"), http.StatusBadRequest) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} +{{- if .IsStyled }} + var value {{ .TypeDecl }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie, &value) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter {{ .Name }}: %w", err), http.StatusBadRequest) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + siw.ErrorHandler(c, fmt.Errorf("Query argument {{ .Name }} is required, but not found"), http.StatusBadRequest) + return + }{{ end }} + } +{{ end }} +{{ end }} + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.{{ .GoOperationID }}(c{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/gorilla/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/gorilla/handler.go.tmpl new file mode 100644 index 0000000000..9d7bcf2121 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/gorilla/handler.go.tmpl @@ -0,0 +1,57 @@ +{{- /* + This template generates the HTTP handler and routing for Gorilla servers. + Input: []OperationDescriptor +*/ -}} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, GorillaServerOptions{}) +} + +// GorillaServerOptions configures the Gorilla server. +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, + }) +} + +// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. +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+"{{ pathToGorillaPattern .Path }}", wrapper.{{ .GoOperationID }}).Methods("{{ .Method }}") +{{- end }} + return r +} diff --git a/experimental/internal/codegen/templates/files/server/gorilla/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/gorilla/interface.go.tmpl new file mode 100644 index 0000000000..7a9855b515 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/gorilla/interface.go.tmpl @@ -0,0 +1,24 @@ +{{- /* + This template generates the ServerInterface for Gorilla servers. + Input: []OperationDescriptor +*/ -}} + +// ServerInterface represents all server handlers. +type ServerInterface interface { +{{- range . }} +{{ .SummaryAsComment }} + // ({{ .Method }} {{ .Path }}) + {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) +{{- end }} +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. +type Unimplemented struct{} + +{{- range . }} +{{ .SummaryAsComment }} +// ({{ .Method }} {{ .Path }}) +func (_ Unimplemented) {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) { + w.WriteHeader(http.StatusNotImplemented) +} +{{- end }} diff --git a/experimental/internal/codegen/templates/files/server/gorilla/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/gorilla/receiver.go.tmpl new file mode 100644 index 0000000000..b0a24f0fdc --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/gorilla/receiver.go.tmpl @@ -0,0 +1,95 @@ +{{- /* + This template generates the receiver interface and handler functions for Gorilla. + Input: ReceiverTemplateData +*/ -}} + +// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. +type {{ .Prefix }}ReceiverInterface interface { +{{- range .Operations }} +{{ .SummaryAsComment }} + // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. + Handle{{ .GoOperationID }}{{ $.Prefix }}(w http.ResponseWriter, r *http.Request{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) +{{- end }} +} + +// {{ .Prefix }}ReceiverMiddlewareFunc is a middleware function for {{ $.PrefixLower }} receiver handlers. +type {{ .Prefix }}ReceiverMiddlewareFunc func(http.Handler) http.Handler + +{{ range .Operations }} +// {{ .GoOperationID }}{{ $.Prefix }}Handler returns an http.Handler for the {{ .GoOperationID }} {{ $.PrefixLower }}. +// The caller is responsible for registering this handler at the appropriate path. +func {{ .GoOperationID }}{{ $.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) + } + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +{{- if .HasParams }} + var err error + _ = err + + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} +{{- if or .Required .IsPassThrough .IsJSON }} + if paramValue := r.URL.Query().Get("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + errHandler(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + errHandler(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) + return + }{{ end }} +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, r.URL.Query(), ¶ms.{{ .GoName }}) + if err != nil { + errHandler(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{ end }} +{{ range .HeaderParams }} + if valueList, found := r.Header[http.CanonicalHeaderKey("{{ .Name }}")]; found { + var {{ .GoVariableName }} {{ .TypeDecl }} + n := len(valueList) + if n != 1 { + errHandler(w, r, &TooManyValuesForParamError{ParamName: "{{ .Name }}", Count: n}) + return + } +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) + if err != nil { + errHandler(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + errHandler(w, r, &RequiredHeaderError{ParamName: "{{ .Name }}", Err: fmt.Errorf("header parameter {{ .Name }} is required, but not found")}) + return + }{{ end }} +{{ end }} +{{- end }} + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + si.Handle{{ .GoOperationID }}{{ $.Prefix }}(w, r{{ if .HasParams }}, params{{ end }}) + })) + + for _, middleware := range middlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) + }) +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/gorilla/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/gorilla/wrapper.go.tmpl new file mode 100644 index 0000000000..395fea4d7b --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/gorilla/wrapper.go.tmpl @@ -0,0 +1,169 @@ +{{- /* + This template generates the ServerInterfaceWrapper that extracts parameters + from HTTP requests and calls the ServerInterface methods for Gorilla. + Input: []OperationDescriptor +*/ -}} + +// ServerInterfaceWrapper converts HTTP requests to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// MiddlewareFunc is a middleware function type. +type MiddlewareFunc func(http.Handler) http.Handler + +{{ range . }} +// {{ .GoOperationID }} operation middleware +func (siw *ServerInterfaceWrapper) {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request) { +{{- if or .PathParams .HasParams }} + var err error +{{- end }} +{{ if .PathParams }} + pathParams := mux.Vars(r) +{{ end }} +{{ range .PathParams }} + // ------------- Path parameter "{{ .Name }}" ------------- + var {{ .GoVariableName }} {{ .TypeDecl }} +{{ if .IsPassThrough }} + {{ .GoVariableName }} = pathParams["{{ .Name }}"] +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(pathParams["{{ .Name }}"]), &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, pathParams["{{ .Name }}"], &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{ end }} +{{- if .Security }} + ctx := r.Context() +{{- range .Security }} + ctx = context.WithValue(ctx, {{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) +{{- end }} + r = r.WithContext(ctx) +{{- end }} +{{ if .HasParams }} + // Parameter object where we will unmarshal all parameters from the context + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- +{{- if or .Required .IsPassThrough .IsJSON }} + if paramValue := r.URL.Query().Get("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) + return + }{{ end }} +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, r.URL.Query(), ¶ms.{{ .GoName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{ end }} +{{ if .HeaderParams }} + headers := r.Header +{{ range .HeaderParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found { + var {{ .GoVariableName }} {{ .TypeDecl }} + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "{{ .Name }}", Count: n}) + return + } +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + err := fmt.Errorf("Header parameter {{ .Name }} is required, but not found") + siw.ErrorHandlerFunc(w, r, &RequiredHeaderError{ParamName: "{{ .Name }}", Err: err}) + return + }{{ end }} +{{ end }} +{{ end }} +{{ range .CookieParams }} + { + var cookie *http.Cookie + if cookie, err = r.Cookie("{{ .Name }}"); err == nil { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie.Value +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + decoded, err := url.QueryUnescape(cookie.Value) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnescapedCookieParamError{ParamName: "{{ .Name }}", Err: err}) + return + } + err = json.Unmarshal([]byte(decoded), &value) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} +{{- if .IsStyled }} + var value {{ .TypeDecl }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie.Value, &value) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) + return + }{{ end }} + } +{{ end }} +{{ end }} + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.{{ .GoOperationID }}(w, r{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/iris/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/iris/handler.go.tmpl new file mode 100644 index 0000000000..2fa5813b49 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/iris/handler.go.tmpl @@ -0,0 +1,28 @@ +{{- /* + This template generates the HTTP handler and routing for Iris servers. + Input: []OperationDescriptor +*/ -}} + +// IrisServerOptions is the option for iris server. +type IrisServerOptions struct { + BaseURL string + Middlewares []iris.Handler +} + +// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. +func RegisterHandlers(router *iris.Application, si ServerInterface) { + RegisterHandlersWithOptions(router, si, IrisServerOptions{}) +} + +// RegisterHandlersWithOptions creates http.Handler with additional options. +func RegisterHandlersWithOptions(router *iris.Application, si ServerInterface, options IrisServerOptions) { +{{ if . }} + wrapper := ServerInterfaceWrapper{ + Handler: si, + } +{{ end }} +{{- range . }} + router.{{ .Method | lower | title }}(options.BaseURL+"{{ pathToIrisPattern .Path }}", wrapper.{{ .GoOperationID }}) +{{- end }} + router.Build() +} diff --git a/experimental/internal/codegen/templates/files/server/iris/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/iris/interface.go.tmpl new file mode 100644 index 0000000000..5b80fa550c --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/iris/interface.go.tmpl @@ -0,0 +1,24 @@ +{{- /* + This template generates the ServerInterface for Iris servers. + Input: []OperationDescriptor +*/ -}} + +// ServerInterface represents all server handlers. +type ServerInterface interface { +{{- range . }} +{{ .SummaryAsComment }} + // ({{ .Method }} {{ .Path }}) + {{ .GoOperationID }}(ctx iris.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) +{{- end }} +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. +type Unimplemented struct{} + +{{- range . }} +{{ .SummaryAsComment }} +// ({{ .Method }} {{ .Path }}) +func (_ Unimplemented) {{ .GoOperationID }}(ctx iris.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) { + ctx.StatusCode(http.StatusNotImplemented) +} +{{- end }} diff --git a/experimental/internal/codegen/templates/files/server/iris/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/iris/receiver.go.tmpl new file mode 100644 index 0000000000..050076b4a4 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/iris/receiver.go.tmpl @@ -0,0 +1,84 @@ +{{- /* + This template generates the receiver interface and handler functions for Iris. + Input: ReceiverTemplateData +*/ -}} + +// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. +type {{ .Prefix }}ReceiverInterface interface { +{{- range .Operations }} +{{ .SummaryAsComment }} + // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. + Handle{{ .GoOperationID }}{{ $.Prefix }}(ctx iris.Context{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) +{{- end }} +} + +{{ range .Operations }} +// {{ .GoOperationID }}{{ $.Prefix }}Handler returns an iris.Handler for the {{ .GoOperationID }} {{ $.PrefixLower }}. +// The caller is responsible for registering this handler at the appropriate path. +func {{ .GoOperationID }}{{ $.Prefix }}Handler(si {{ $.Prefix }}ReceiverInterface) iris.Handler { + return func(ctx iris.Context) { +{{- if .HasParams }} + var err error + _ = err + + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} +{{- if or .Required .IsPassThrough .IsJSON }} + if paramValue := ctx.URLParam("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + _, _ = ctx.WriteString(fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + ctx.StatusCode(http.StatusBadRequest) + _, _ = ctx.WriteString("Query parameter {{ .Name }} is required") + return + }{{ end }} +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, ctx.Request().URL.Query(), ¶ms.{{ .GoName }}) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + _, _ = ctx.WriteString(fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + return + } +{{- end }} +{{ end }} +{{ range .HeaderParams }} + if valueList := ctx.Request().Header[http.CanonicalHeaderKey("{{ .Name }}")]; len(valueList) > 0 { + var {{ .GoVariableName }} {{ .TypeDecl }} + n := len(valueList) + if n != 1 { + ctx.StatusCode(http.StatusBadRequest) + _, _ = ctx.WriteString(fmt.Sprintf("Expected one value for {{ .Name }}, got %d", n)) + return + } +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + _, _ = ctx.WriteString(fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + return + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + ctx.StatusCode(http.StatusBadRequest) + _, _ = ctx.WriteString("Header parameter {{ .Name }} is required") + return + }{{ end }} +{{ end }} +{{- end }} + si.Handle{{ .GoOperationID }}{{ $.Prefix }}(ctx{{ if .HasParams }}, params{{ end }}) + } +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/iris/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/iris/wrapper.go.tmpl new file mode 100644 index 0000000000..8c44f3dd10 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/iris/wrapper.go.tmpl @@ -0,0 +1,160 @@ +{{- /* + This template generates the ServerInterfaceWrapper that extracts parameters + from HTTP requests and calls the ServerInterface methods for Iris. + Input: []OperationDescriptor +*/ -}} + +// ServerInterfaceWrapper converts iris contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +{{ range . }} +// {{ .GoOperationID }} converts iris context to params. +func (w *ServerInterfaceWrapper) {{ .GoOperationID }}(ctx iris.Context) { +{{- if or .PathParams .HasParams }} + var err error +{{- end }} +{{ range .PathParams }} + // ------------- Path parameter "{{ .Name }}" ------------- + var {{ .GoVariableName }} {{ .TypeDecl }} +{{ if .IsPassThrough }} + {{ .GoVariableName }} = ctx.Params().Get("{{ .Name }}") +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(ctx.Params().Get("{{ .Name }}")), &{{ .GoVariableName }}) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) + return + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, ctx.Params().Get("{{ .Name }}"), &{{ .GoVariableName }}) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + return + } +{{- end }} +{{ end }} +{{- if .Security }} +{{- range .Security }} + ctx.Values().Set({{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) +{{- end }} +{{- end }} +{{ if .HasParams }} + // Parameter object where we will unmarshal all parameters from the context + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, ctx.Request().URL.Query(), ¶ms.{{ .GoName }}) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + return + } +{{- else }} + if paramValue := ctx.URLParam("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString("Query argument {{ .Name }} is required, but not found") + return + }{{ end }} +{{- end }} +{{ end }} +{{ if .HeaderParams }} + headers := ctx.Request().Header +{{ range .HeaderParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found { + var {{ .GoVariableName }} {{ .TypeDecl }} + n := len(valueList) + if n != 1 { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(fmt.Sprintf("Expected one value for {{ .Name }}, got %d", n)) + return + } +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) + return + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + return + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString("Header {{ .Name }} is required, but not found") + return + }{{ end }} +{{ end }} +{{ end }} +{{ range .CookieParams }} + if cookie := ctx.GetCookie("{{ .Name }}"); cookie != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + decoded, err := url.QueryUnescape(cookie) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(fmt.Sprintf("Error unescaping cookie parameter '%s'", "{{ .Name }}")) + return + } + err = json.Unmarshal([]byte(decoded), &value) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} +{{- if .IsStyled }} + var value {{ .TypeDecl }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie, &value) + if err != nil { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString("Cookie {{ .Name }} is required, but not found") + return + }{{ end }} +{{ end }} +{{ end }} + // Invoke the callback with all the unmarshaled arguments + w.Handler.{{ .GoOperationID }}(ctx{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/param_types.go.tmpl b/experimental/internal/codegen/templates/files/server/param_types.go.tmpl new file mode 100644 index 0000000000..7b0eb914de --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/param_types.go.tmpl @@ -0,0 +1,24 @@ +{{- /* + This template generates parameter struct types for server operations. + Input: []OperationDescriptor +*/ -}} + +{{ range . }} +{{- if .HasParams }} +// {{ .ParamsTypeName }} defines parameters for {{ .GoOperationID }}. +type {{ .ParamsTypeName }} struct { +{{- range .QueryParams }} + // {{ .Name }} {{ if .Required }}(required){{ else }}(optional){{ end }} + {{ .GoName }} {{ if .HasOptionalPointer }}*{{ end }}{{ .TypeDecl }} `form:"{{ .Name }}" json:"{{ .Name }}"` +{{- end }} +{{- range .HeaderParams }} + // {{ .Name }} (header{{ if .Required }}, required{{ end }}) + {{ .GoName }} {{ if .HasOptionalPointer }}*{{ end }}{{ .TypeDecl }} +{{- end }} +{{- range .CookieParams }} + // {{ .Name }} (cookie{{ if .Required }}, required{{ end }}) + {{ .GoName }} {{ if .HasOptionalPointer }}*{{ end }}{{ .TypeDecl }} +{{- end }} +} +{{ end }} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/stdhttp/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/stdhttp/handler.go.tmpl new file mode 100644 index 0000000000..47d50ec8bc --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/stdhttp/handler.go.tmpl @@ -0,0 +1,63 @@ +{{- /* + This template generates the HTTP handler and routing for StdHTTP servers. + Input: []OperationDescriptor +*/ -}} + +// 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)) + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + +// StdHTTPServerOptions configures the StdHTTP server. +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, + }) +} + +// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. +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 }} "+options.BaseURL+"{{ pathToStdHTTPPattern .Path }}", wrapper.{{ .GoOperationID }}) +{{- end }} + return m +} diff --git a/experimental/internal/codegen/templates/files/server/stdhttp/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/stdhttp/interface.go.tmpl new file mode 100644 index 0000000000..6dc320f8f7 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/stdhttp/interface.go.tmpl @@ -0,0 +1,13 @@ +{{- /* + This template generates the ServerInterface for StdHTTP servers. + Input: []OperationDescriptor +*/ -}} + +// ServerInterface represents all server handlers. +type ServerInterface interface { +{{- range . }} +{{ .SummaryAsComment }} + // ({{ .Method }} {{ .Path }}) + {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) +{{- end }} +} diff --git a/experimental/internal/codegen/templates/files/server/stdhttp/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/stdhttp/receiver.go.tmpl new file mode 100644 index 0000000000..5f6d1fb5d0 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/stdhttp/receiver.go.tmpl @@ -0,0 +1,108 @@ +{{- /* + This template generates the receiver interface and handler functions for StdHTTP. + Receiver handlers receive webhook/callback requests — the caller registers them at chosen paths. + Input: ReceiverTemplateData +*/ -}} + +// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. +type {{ .Prefix }}ReceiverInterface interface { +{{- range .Operations }} +{{ .SummaryAsComment }} + // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. + Handle{{ .GoOperationID }}{{ $.Prefix }}(w http.ResponseWriter, r *http.Request{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) +{{- end }} +} + +// {{ .Prefix }}ReceiverMiddlewareFunc is a middleware function for {{ $.PrefixLower }} receiver handlers. +type {{ .Prefix }}ReceiverMiddlewareFunc func(http.Handler) http.Handler + +{{ range .Operations }} +// {{ .GoOperationID }}{{ $.Prefix }}Handler returns an http.Handler for the {{ .GoOperationID }} {{ $.PrefixLower }}. +// The caller is responsible for registering this handler at the appropriate path. +func {{ .GoOperationID }}{{ $.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) + } + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +{{- if .HasParams }} + var err error + _ = err + + // Parameter object where we will unmarshal all parameters from the context + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- +{{- if or .Required .IsPassThrough .IsJSON }} + if paramValue := r.URL.Query().Get("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + errHandler(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + errHandler(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) + return + }{{ end }} +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, r.URL.Query(), ¶ms.{{ .GoName }}) + if err != nil { + errHandler(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{ end }} +{{ range .HeaderParams }} + if valueList, found := r.Header[http.CanonicalHeaderKey("{{ .Name }}")]; found { + var {{ .GoVariableName }} {{ .TypeDecl }} + n := len(valueList) + if n != 1 { + errHandler(w, r, &TooManyValuesForParamError{ParamName: "{{ .Name }}", Count: n}) + return + } +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) + if err != nil { + errHandler(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) + if err != nil { + errHandler(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + errHandler(w, r, &RequiredHeaderError{ParamName: "{{ .Name }}", Err: fmt.Errorf("header parameter {{ .Name }} is required, but not found")}) + return + }{{ end }} +{{ end }} +{{- end }} + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + si.Handle{{ .GoOperationID }}{{ $.Prefix }}(w, r{{ if .HasParams }}, params{{ end }}) + })) + + for _, middleware := range middlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) + }) +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/stdhttp/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/stdhttp/wrapper.go.tmpl new file mode 100644 index 0000000000..6c74e9c980 --- /dev/null +++ b/experimental/internal/codegen/templates/files/server/stdhttp/wrapper.go.tmpl @@ -0,0 +1,166 @@ +{{- /* + This template generates the ServerInterfaceWrapper that extracts parameters + from HTTP requests and calls the ServerInterface methods. + Input: []OperationDescriptor +*/ -}} + +// ServerInterfaceWrapper converts HTTP requests to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// MiddlewareFunc is a middleware function type. +type MiddlewareFunc func(http.Handler) http.Handler + +{{ range . }} +// {{ .GoOperationID }} operation middleware +func (siw *ServerInterfaceWrapper) {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request) { +{{- if or .PathParams .HasParams }} + var err error +{{- end }} +{{ range .PathParams }} + // ------------- Path parameter "{{ .Name }}" ------------- + var {{ .GoVariableName }} {{ .TypeDecl }} +{{ if .IsPassThrough }} + {{ .GoVariableName }} = r.PathValue("{{ .Name }}") +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(r.PathValue("{{ .Name }}")), &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, r.PathValue("{{ .Name }}"), &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{ end }} +{{- if .Security }} + ctx := r.Context() +{{- range .Security }} + ctx = context.WithValue(ctx, {{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) +{{- end }} + r = r.WithContext(ctx) +{{- end }} +{{ if .HasParams }} + // Parameter object where we will unmarshal all parameters from the context + var params {{ .ParamsTypeName }} +{{ range .QueryParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- +{{- if or .Required .IsPassThrough .IsJSON }} + if paramValue := r.URL.Query().Get("{{ .Name }}"); paramValue != "" { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) + return + }{{ end }} +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, r.URL.Query(), ¶ms.{{ .GoName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{ end }} +{{ if .HeaderParams }} + headers := r.Header +{{ range .HeaderParams }} + // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found { + var {{ .GoVariableName }} {{ .TypeDecl }} + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "{{ .Name }}", Count: n}) + return + } +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] +{{- end }} +{{- if .IsJSON }} + err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} +{{- if .IsStyled }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } +{{- end }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} + }{{ if .Required }} else { + err := fmt.Errorf("Header parameter {{ .Name }} is required, but not found") + siw.ErrorHandlerFunc(w, r, &RequiredHeaderError{ParamName: "{{ .Name }}", Err: err}) + return + }{{ end }} +{{ end }} +{{ end }} +{{ range .CookieParams }} + { + var cookie *http.Cookie + if cookie, err = r.Cookie("{{ .Name }}"); err == nil { +{{- if .IsPassThrough }} + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie.Value +{{- end }} +{{- if .IsJSON }} + var value {{ .TypeDecl }} + decoded, err := url.QueryUnescape(cookie.Value) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnescapedCookieParamError{ParamName: "{{ .Name }}", Err: err}) + return + } + err = json.Unmarshal([]byte(decoded), &value) + if err != nil { + siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} +{{- if .IsStyled }} + var value {{ .TypeDecl }} + err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie.Value, &value) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) + return + } + params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value +{{- end }} + }{{ if .Required }} else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) + return + }{{ end }} + } +{{ end }} +{{ end }} + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.{{ .GoOperationID }}(w, r{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} +{{ end }} diff --git a/experimental/internal/codegen/templates/files/types/date.tmpl b/experimental/internal/codegen/templates/files/types/date.tmpl new file mode 100644 index 0000000000..1698b09320 --- /dev/null +++ b/experimental/internal/codegen/templates/files/types/date.tmpl @@ -0,0 +1,38 @@ +{{/* Date type for OpenAPI format: date */}} + +const DateFormat = "2006-01-02" + +type Date struct { + time.Time +} + +func (d Date) MarshalJSON() ([]byte, error) { + return json.Marshal(d.Format(DateFormat)) +} + +func (d *Date) UnmarshalJSON(data []byte) error { + var dateStr string + err := json.Unmarshal(data, &dateStr) + if err != nil { + return err + } + parsed, err := time.Parse(DateFormat, dateStr) + if err != nil { + return err + } + d.Time = parsed + return nil +} + +func (d Date) String() string { + return d.Format(DateFormat) +} + +func (d *Date) UnmarshalText(data []byte) error { + parsed, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = parsed + return nil +} diff --git a/experimental/internal/codegen/templates/files/types/email.tmpl b/experimental/internal/codegen/templates/files/types/email.tmpl new file mode 100644 index 0000000000..1f1e5d100a --- /dev/null +++ b/experimental/internal/codegen/templates/files/types/email.tmpl @@ -0,0 +1,43 @@ +{{/* Email type for OpenAPI format: email */}} + +const ( + emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" +) + +var ( + emailRegex = regexp.MustCompile(emailRegexString) +) + +// ErrValidationEmail is the sentinel error returned when an email fails validation +var ErrValidationEmail = errors.New("email: failed to pass regex validation") + +// Email represents an email address. +// It is a string type that must pass regex validation before being marshalled +// to JSON or unmarshalled from JSON. +type Email string + +func (e Email) MarshalJSON() ([]byte, error) { + if !emailRegex.MatchString(string(e)) { + return nil, ErrValidationEmail + } + + return json.Marshal(string(e)) +} + +func (e *Email) UnmarshalJSON(data []byte) error { + if e == nil { + return nil + } + + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + *e = Email(s) + if !emailRegex.MatchString(s) { + return ErrValidationEmail + } + + return nil +} diff --git a/experimental/internal/codegen/templates/files/types/file.tmpl b/experimental/internal/codegen/templates/files/types/file.tmpl new file mode 100644 index 0000000000..3b853101bb --- /dev/null +++ b/experimental/internal/codegen/templates/files/types/file.tmpl @@ -0,0 +1,64 @@ +{{/* File type for OpenAPI format: binary */}} + +type File struct { + multipart *multipart.FileHeader + data []byte + filename string +} + +func (file *File) InitFromMultipart(header *multipart.FileHeader) { + file.multipart = header + file.data = nil + file.filename = "" +} + +func (file *File) InitFromBytes(data []byte, filename string) { + file.data = data + file.filename = filename + file.multipart = nil +} + +func (file File) MarshalJSON() ([]byte, error) { + b, err := file.Bytes() + if err != nil { + return nil, err + } + return json.Marshal(b) +} + +func (file *File) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &file.data) +} + +func (file File) Bytes() ([]byte, error) { + if file.multipart != nil { + f, err := file.multipart.Open() + if err != nil { + return nil, err + } + defer func() { _ = f.Close() }() + return io.ReadAll(f) + } + return file.data, nil +} + +func (file File) Reader() (io.ReadCloser, error) { + if file.multipart != nil { + return file.multipart.Open() + } + return io.NopCloser(bytes.NewReader(file.data)), nil +} + +func (file File) Filename() string { + if file.multipart != nil { + return file.multipart.Filename + } + return file.filename +} + +func (file File) FileSize() int64 { + if file.multipart != nil { + return file.multipart.Size + } + return int64(len(file.data)) +} diff --git a/experimental/internal/codegen/templates/files/types/nullable.tmpl b/experimental/internal/codegen/templates/files/types/nullable.tmpl new file mode 100644 index 0000000000..cf75b0910c --- /dev/null +++ b/experimental/internal/codegen/templates/files/types/nullable.tmpl @@ -0,0 +1,104 @@ +{{/* Nullable type for OpenAPI nullable fields - implements three-state semantics: unspecified, null, or value */}} + +// Nullable is a generic type that can distinguish between: +// - Field not provided (unspecified) +// - Field explicitly set to null +// - Field has a value +// +// This is implemented as a map[bool]T where: +// - Empty map: unspecified +// - map[false]T: explicitly null +// - map[true]T: has a value +type Nullable[T any] map[bool]T + +// NewNullableWithValue creates a Nullable with the given value. +func NewNullableWithValue[T any](value T) Nullable[T] { + return Nullable[T]{true: value} +} + +// NewNullNullable creates a Nullable that is explicitly null. +func NewNullNullable[T any]() Nullable[T] { + return Nullable[T]{false: *new(T)} +} + +// Get returns the value if set, or an error if null or unspecified. +func (n Nullable[T]) Get() (T, error) { + if v, ok := n[true]; ok { + return v, nil + } + var zero T + if n.IsNull() { + return zero, ErrNullableIsNull + } + return zero, ErrNullableNotSpecified +} + +// MustGet returns the value or panics if null or unspecified. +func (n Nullable[T]) MustGet() T { + v, err := n.Get() + if err != nil { + panic(err) + } + return v +} + +// Set assigns a value. +func (n *Nullable[T]) Set(value T) { + *n = Nullable[T]{true: value} +} + +// SetNull marks the field as explicitly null. +func (n *Nullable[T]) SetNull() { + *n = Nullable[T]{false: *new(T)} +} + +// SetUnspecified clears the field (as if it was never set). +func (n *Nullable[T]) SetUnspecified() { + *n = nil +} + +// IsNull returns true if the field is explicitly null. +func (n Nullable[T]) IsNull() bool { + if n == nil { + return false + } + _, ok := n[false] + return ok +} + +// IsSpecified returns true if the field was provided (either null or a value). +func (n Nullable[T]) IsSpecified() bool { + return len(n) > 0 +} + +// MarshalJSON implements json.Marshaler. +func (n Nullable[T]) MarshalJSON() ([]byte, error) { + if n.IsNull() { + return []byte("null"), nil + } + if v, ok := n[true]; ok { + return json.Marshal(v) + } + // Unspecified - this shouldn't be called if omitempty is used correctly + return []byte("null"), nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (n *Nullable[T]) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + n.SetNull() + return nil + } + var v T + if err := json.Unmarshal(data, &v); err != nil { + return err + } + n.Set(v) + return nil +} + +// ErrNullableIsNull is returned when trying to get a value from a null Nullable. +var ErrNullableIsNull = errors.New("nullable value is null") + +// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. +var ErrNullableNotSpecified = errors.New("nullable value is not specified") diff --git a/experimental/internal/codegen/templates/files/types/uuid.tmpl b/experimental/internal/codegen/templates/files/types/uuid.tmpl new file mode 100644 index 0000000000..f136f6a150 --- /dev/null +++ b/experimental/internal/codegen/templates/files/types/uuid.tmpl @@ -0,0 +1,3 @@ +{{/* UUID type for OpenAPI format: uuid */}} + +type UUID = uuid.UUID diff --git a/experimental/internal/codegen/templates/funcs.go b/experimental/internal/codegen/templates/funcs.go new file mode 100644 index 0000000000..b790f1702e --- /dev/null +++ b/experimental/internal/codegen/templates/funcs.go @@ -0,0 +1,151 @@ +package templates + +import ( + "regexp" + "strings" + "text/template" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +var titleCaser = cases.Title(language.English) + +// pathParamRE matches OpenAPI path parameters including styled variants. +// Matches: {param}, {param*}, {.param}, {.param*}, {;param}, {;param*}, {?param}, {?param*} +var pathParamRE = regexp.MustCompile(`{[.;?]?([^{}*]+)\*?}`) + +// Funcs returns the template function map for server templates. +func Funcs() template.FuncMap { + return template.FuncMap{ + "pathToStdHTTPPattern": PathToStdHTTPPattern, + "pathToChiPattern": PathToChiPattern, + "pathToEchoPattern": PathToEchoPattern, + "pathToGinPattern": PathToGinPattern, + "pathToGorillaPattern": PathToGorillaPattern, + "pathToFiberPattern": PathToFiberPattern, + "pathToIrisPattern": PathToIrisPattern, + "toGoIdentifier": ToGoIdentifier, + "lower": strings.ToLower, + "title": titleCaser.String, + } +} + +// PathToStdHTTPPattern converts an OpenAPI path template to a Go 1.22+ std http pattern. +// OpenAPI: /users/{user_id}/posts/{post_id} +// StdHTTP: /users/{user_id}/posts/{post_id} +// Special case: "/" becomes "/{$}" to match only the root path. +func PathToStdHTTPPattern(path string) string { + // https://pkg.go.dev/net/http#hdr-Patterns-ServeMux + // The special wildcard {$} matches only the end of the URL. + if path == "/" { + return "/{$}" + } + return pathParamRE.ReplaceAllString(path, "{$1}") +} + +// PathToChiPattern converts an OpenAPI path template to a Chi-compatible pattern. +// OpenAPI: /users/{user_id}/posts/{post_id} +// Chi: /users/{user_id}/posts/{post_id} +func PathToChiPattern(path string) string { + return pathParamRE.ReplaceAllString(path, "{$1}") +} + +// PathToEchoPattern converts an OpenAPI path template to an Echo-compatible pattern. +// OpenAPI: /users/{user_id}/posts/{post_id} +// Echo: /users/:user_id/posts/:post_id +func PathToEchoPattern(path string) string { + return pathParamRE.ReplaceAllString(path, ":$1") +} + +// PathToGinPattern converts an OpenAPI path template to a Gin-compatible pattern. +// OpenAPI: /users/{user_id}/posts/{post_id} +// Gin: /users/:user_id/posts/:post_id +func PathToGinPattern(path string) string { + return pathParamRE.ReplaceAllString(path, ":$1") +} + +// PathToGorillaPattern converts an OpenAPI path template to a Gorilla Mux-compatible pattern. +// OpenAPI: /users/{user_id}/posts/{post_id} +// Gorilla: /users/{user_id}/posts/{post_id} +func PathToGorillaPattern(path string) string { + return pathParamRE.ReplaceAllString(path, "{$1}") +} + +// PathToFiberPattern converts an OpenAPI path template to a Fiber-compatible pattern. +// OpenAPI: /users/{user_id}/posts/{post_id} +// Fiber: /users/:user_id/posts/:post_id +func PathToFiberPattern(path string) string { + return pathParamRE.ReplaceAllString(path, ":$1") +} + +// PathToIrisPattern converts an OpenAPI path template to an Iris-compatible pattern. +// OpenAPI: /users/{user_id}/posts/{post_id} +// Iris: /users/:user_id/posts/:post_id +func PathToIrisPattern(path string) string { + return pathParamRE.ReplaceAllString(path, ":$1") +} + +// ToGoIdentifier converts a string to a valid Go identifier. +// This is a simple version for template usage. +func ToGoIdentifier(s string) string { + if s == "" { + return "Empty" + } + + // Replace non-alphanumeric characters with underscores + result := make([]byte, 0, len(s)) + capitalizeNext := true + + for i := 0; i < len(s); i++ { + c := s[i] + if c >= 'a' && c <= 'z' { + if capitalizeNext { + result = append(result, c-32) // uppercase + capitalizeNext = false + } else { + result = append(result, c) + } + } else if c >= 'A' && c <= 'Z' { + result = append(result, c) + capitalizeNext = false + } else if c >= '0' && c <= '9' { + result = append(result, c) + capitalizeNext = false + } else { + // Word separator + capitalizeNext = true + } + } + + if len(result) == 0 { + return "Empty" + } + + // Handle leading digit + if result[0] >= '0' && result[0] <= '9' { + result = append([]byte("N"), result...) + } + + str := string(result) + + // Handle Go keywords + lower := strings.ToLower(str) + if isGoKeyword(lower) { + str = str + "_" + } + + return str +} + +// isGoKeyword returns true if s is a Go keyword. +func isGoKeyword(s string) bool { + keywords := map[string]bool{ + "break": true, "case": true, "chan": true, "const": true, "continue": true, + "default": true, "defer": true, "else": true, "fallthrough": true, "for": true, + "func": true, "go": true, "goto": true, "if": true, "import": true, + "interface": true, "map": true, "package": true, "range": true, "return": true, + "select": true, "struct": true, "switch": true, "type": true, "var": true, + } + return keywords[s] +} diff --git a/experimental/internal/codegen/templates/registry.go b/experimental/internal/codegen/templates/registry.go new file mode 100644 index 0000000000..94ae240769 --- /dev/null +++ b/experimental/internal/codegen/templates/registry.go @@ -0,0 +1,909 @@ +package templates + +// Import represents a Go import with optional alias. +type Import struct { + Path string + Alias string // empty if no alias +} + +// TypeTemplate defines a template for a custom type along with its required imports. +type TypeTemplate struct { + Name string // Type name (e.g., "Email", "Date") + Imports []Import // Required imports for this type + Template string // Template name in embedded FS (e.g., "types/email.tmpl") +} + +// TypeTemplates maps type names to their template definitions. +var TypeTemplates = map[string]TypeTemplate{ + "Email": { + Name: "Email", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "errors"}, + {Path: "regexp"}, + }, + Template: "types/email.tmpl", + }, + "Date": { + Name: "Date", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "time"}, + }, + Template: "types/date.tmpl", + }, + "UUID": { + Name: "UUID", + Imports: []Import{ + {Path: "github.com/google/uuid"}, + }, + Template: "types/uuid.tmpl", + }, + "File": { + Name: "File", + Imports: []Import{ + {Path: "bytes"}, + {Path: "encoding/json"}, + {Path: "io"}, + {Path: "mime/multipart"}, + }, + Template: "types/file.tmpl", + }, + "Nullable": { + Name: "Nullable", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "errors"}, + }, + Template: "types/nullable.tmpl", + }, +} + +// ParamTemplate defines a template for a parameter styling/binding function. +type ParamTemplate struct { + Name string // Function name (e.g., "StyleSimpleParam") + Imports []Import // Required imports for this function + Template string // Template name in embedded FS (e.g., "params/style_simple.go.tmpl") +} + +// ParamHelpersTemplate is the template for shared helper functions. +// This is included whenever any param function is used. +var ParamHelpersTemplate = ParamTemplate{ + Name: "helpers", + Imports: []Import{ + {Path: "bytes"}, + {Path: "encoding"}, + {Path: "encoding/json"}, + {Path: "errors"}, + {Path: "fmt"}, + {Path: "net/url"}, + {Path: "reflect"}, + {Path: "sort"}, + {Path: "strconv"}, + {Path: "strings"}, + {Path: "time"}, + {Path: "github.com/google/uuid"}, + }, + Template: "params/helpers.go.tmpl", +} + +// ParamTemplates maps style/explode combinations to their template definitions. +// Keys follow the pattern: "style_{style}" or "style_{style}_explode" for styling, +// and "bind_{style}" or "bind_{style}_explode" for binding. +var ParamTemplates = map[string]ParamTemplate{ + // Style templates (serialization) + "style_simple": { + Name: "StyleSimpleParam", + Imports: []Import{ + {Path: "bytes"}, + {Path: "encoding"}, + {Path: "encoding/json"}, + {Path: "errors"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + {Path: "time"}, + }, + Template: "params/style_simple.go.tmpl", + }, + "style_simple_explode": { + Name: "StyleSimpleExplodeParam", + Imports: []Import{ + {Path: "bytes"}, + {Path: "encoding"}, + {Path: "encoding/json"}, + {Path: "errors"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + {Path: "time"}, + }, + Template: "params/style_simple_explode.go.tmpl", + }, + "style_label": { + Name: "StyleLabelParam", + Imports: []Import{ + {Path: "bytes"}, + {Path: "encoding"}, + {Path: "encoding/json"}, + {Path: "errors"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + {Path: "time"}, + }, + Template: "params/style_label.go.tmpl", + }, + "style_label_explode": { + Name: "StyleLabelExplodeParam", + Imports: []Import{ + {Path: "bytes"}, + {Path: "encoding"}, + {Path: "encoding/json"}, + {Path: "errors"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + {Path: "time"}, + }, + Template: "params/style_label_explode.go.tmpl", + }, + "style_matrix": { + Name: "StyleMatrixParam", + Imports: []Import{ + {Path: "bytes"}, + {Path: "encoding"}, + {Path: "encoding/json"}, + {Path: "errors"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + {Path: "time"}, + }, + Template: "params/style_matrix.go.tmpl", + }, + "style_matrix_explode": { + Name: "StyleMatrixExplodeParam", + Imports: []Import{ + {Path: "bytes"}, + {Path: "encoding"}, + {Path: "encoding/json"}, + {Path: "errors"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + {Path: "time"}, + }, + Template: "params/style_matrix_explode.go.tmpl", + }, + "style_form": { + Name: "StyleFormParam", + Imports: []Import{ + {Path: "bytes"}, + {Path: "encoding"}, + {Path: "encoding/json"}, + {Path: "errors"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + {Path: "time"}, + }, + Template: "params/style_form.go.tmpl", + }, + "style_form_explode": { + Name: "StyleFormExplodeParam", + Imports: []Import{ + {Path: "bytes"}, + {Path: "encoding"}, + {Path: "encoding/json"}, + {Path: "errors"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + {Path: "time"}, + }, + Template: "params/style_form_explode.go.tmpl", + }, + "style_spaceDelimited": { + Name: "StyleSpaceDelimitedParam", + Imports: []Import{ + {Path: "encoding"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + {Path: "time"}, + }, + Template: "params/style_space_delimited.go.tmpl", + }, + "style_spaceDelimited_explode": { + Name: "StyleSpaceDelimitedExplodeParam", + Imports: []Import{ + {Path: "encoding"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + {Path: "time"}, + }, + Template: "params/style_space_delimited_explode.go.tmpl", + }, + "style_pipeDelimited": { + Name: "StylePipeDelimitedParam", + Imports: []Import{ + {Path: "encoding"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + {Path: "time"}, + }, + Template: "params/style_pipe_delimited.go.tmpl", + }, + "style_pipeDelimited_explode": { + Name: "StylePipeDelimitedExplodeParam", + Imports: []Import{ + {Path: "encoding"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + {Path: "time"}, + }, + Template: "params/style_pipe_delimited_explode.go.tmpl", + }, + "style_deepObject": { + Name: "StyleDeepObjectParam", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "sort"}, + {Path: "strconv"}, + {Path: "strings"}, + }, + Template: "params/style_deep_object.go.tmpl", + }, + + // Bind templates (deserialization) + "bind_simple": { + Name: "BindSimpleParam", + Imports: []Import{ + {Path: "encoding"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + }, + Template: "params/bind_simple.go.tmpl", + }, + "bind_simple_explode": { + Name: "BindSimpleExplodeParam", + Imports: []Import{ + {Path: "encoding"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + }, + Template: "params/bind_simple_explode.go.tmpl", + }, + "bind_label": { + Name: "BindLabelParam", + Imports: []Import{ + {Path: "encoding"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + }, + Template: "params/bind_label.go.tmpl", + }, + "bind_label_explode": { + Name: "BindLabelExplodeParam", + Imports: []Import{ + {Path: "encoding"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + }, + Template: "params/bind_label_explode.go.tmpl", + }, + "bind_matrix": { + Name: "BindMatrixParam", + Imports: []Import{ + {Path: "encoding"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + }, + Template: "params/bind_matrix.go.tmpl", + }, + "bind_matrix_explode": { + Name: "BindMatrixExplodeParam", + Imports: []Import{ + {Path: "encoding"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + }, + Template: "params/bind_matrix_explode.go.tmpl", + }, + "bind_form": { + Name: "BindFormParam", + Imports: []Import{ + {Path: "encoding"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + }, + Template: "params/bind_form.go.tmpl", + }, + "bind_form_explode": { + Name: "BindFormExplodeParam", + Imports: []Import{ + {Path: "fmt"}, + {Path: "net/url"}, + {Path: "reflect"}, + {Path: "strings"}, + {Path: "time"}, + }, + Template: "params/bind_form_explode.go.tmpl", + }, + "bind_spaceDelimited": { + Name: "BindSpaceDelimitedParam", + Imports: []Import{ + {Path: "encoding"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + }, + Template: "params/bind_space_delimited.go.tmpl", + }, + "bind_spaceDelimited_explode": { + Name: "BindSpaceDelimitedExplodeParam", + Imports: []Import{ + {Path: "net/url"}, + }, + Template: "params/bind_space_delimited_explode.go.tmpl", + }, + "bind_pipeDelimited": { + Name: "BindPipeDelimitedParam", + Imports: []Import{ + {Path: "encoding"}, + {Path: "fmt"}, + {Path: "reflect"}, + {Path: "strings"}, + }, + Template: "params/bind_pipe_delimited.go.tmpl", + }, + "bind_pipeDelimited_explode": { + Name: "BindPipeDelimitedExplodeParam", + Imports: []Import{ + {Path: "net/url"}, + }, + Template: "params/bind_pipe_delimited_explode.go.tmpl", + }, + "bind_deepObject": { + Name: "BindDeepObjectParam", + Imports: []Import{ + {Path: "errors"}, + {Path: "fmt"}, + {Path: "net/url"}, + {Path: "reflect"}, + {Path: "sort"}, + {Path: "strconv"}, + {Path: "strings"}, + {Path: "time"}, + }, + Template: "params/bind_deep_object.go.tmpl", + }, +} + +// ParamStyleKey returns the registry key for a style/explode combination. +// The prefix should be "style_" for serialization or "bind_" for binding. +func ParamStyleKey(prefix, style string, explode bool) string { + key := prefix + style + if explode { + key += "_explode" + } + return key +} + +// ServerTemplate defines a template for server generation. +type ServerTemplate struct { + Name string // Template name (e.g., "interface", "handler") + Imports []Import // Required imports for this template + Template string // Template path in embedded FS +} + +// ReceiverTemplate defines a template for receiver (webhook/callback) generation. +type ReceiverTemplate struct { + Name string // Template name (e.g., "receiver") + Imports []Import // Required imports for this template + Template string // Template path in embedded FS +} + +// StdHTTPReceiverTemplates contains receiver templates for StdHTTP servers. +var StdHTTPReceiverTemplates = map[string]ReceiverTemplate{ + "receiver": { + Name: "receiver", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + }, + Template: "server/stdhttp/receiver.go.tmpl", + }, +} + +// ChiReceiverTemplates contains receiver templates for Chi servers. +var ChiReceiverTemplates = map[string]ReceiverTemplate{ + "receiver": { + Name: "receiver", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + }, + Template: "server/chi/receiver.go.tmpl", + }, +} + +// EchoReceiverTemplates contains receiver templates for Echo v5 servers. +var EchoReceiverTemplates = map[string]ReceiverTemplate{ + "receiver": { + Name: "receiver", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + {Path: "github.com/labstack/echo/v5"}, + }, + Template: "server/echo/receiver.go.tmpl", + }, +} + +// EchoV4ReceiverTemplates contains receiver templates for Echo v4 servers. +var EchoV4ReceiverTemplates = map[string]ReceiverTemplate{ + "receiver": { + Name: "receiver", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + {Path: "github.com/labstack/echo/v4"}, + }, + Template: "server/echo-v4/receiver.go.tmpl", + }, +} + +// GinReceiverTemplates contains receiver templates for Gin servers. +var GinReceiverTemplates = map[string]ReceiverTemplate{ + "receiver": { + Name: "receiver", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + {Path: "github.com/gin-gonic/gin"}, + }, + Template: "server/gin/receiver.go.tmpl", + }, +} + +// GorillaReceiverTemplates contains receiver templates for Gorilla servers. +var GorillaReceiverTemplates = map[string]ReceiverTemplate{ + "receiver": { + Name: "receiver", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + }, + Template: "server/gorilla/receiver.go.tmpl", + }, +} + +// FiberReceiverTemplates contains receiver templates for Fiber servers. +var FiberReceiverTemplates = map[string]ReceiverTemplate{ + "receiver": { + Name: "receiver", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + {Path: "github.com/gofiber/fiber/v3"}, + }, + Template: "server/fiber/receiver.go.tmpl", + }, +} + +// IrisReceiverTemplates contains receiver templates for Iris servers. +var IrisReceiverTemplates = map[string]ReceiverTemplate{ + "receiver": { + Name: "receiver", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + {Path: "github.com/kataras/iris/v12"}, + }, + Template: "server/iris/receiver.go.tmpl", + }, +} + +// StdHTTPServerTemplates contains templates for StdHTTP server generation. +var StdHTTPServerTemplates = map[string]ServerTemplate{ + "interface": { + Name: "interface", + Imports: []Import{ + {Path: "net/http"}, + }, + Template: "server/stdhttp/interface.go.tmpl", + }, + "handler": { + Name: "handler", + Imports: []Import{ + {Path: "net/http"}, + }, + Template: "server/stdhttp/handler.go.tmpl", + }, + "wrapper": { + Name: "wrapper", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + }, + Template: "server/stdhttp/wrapper.go.tmpl", + }, +} + +// ChiServerTemplates contains templates for Chi server generation. +var ChiServerTemplates = map[string]ServerTemplate{ + "interface": { + Name: "interface", + Imports: []Import{ + {Path: "net/http"}, + }, + Template: "server/chi/interface.go.tmpl", + }, + "handler": { + Name: "handler", + Imports: []Import{ + {Path: "net/http"}, + {Path: "github.com/go-chi/chi/v5"}, + }, + Template: "server/chi/handler.go.tmpl", + }, + "wrapper": { + Name: "wrapper", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + {Path: "github.com/go-chi/chi/v5"}, + }, + Template: "server/chi/wrapper.go.tmpl", + }, +} + +// EchoServerTemplates contains templates for Echo v5 server generation. +var EchoServerTemplates = map[string]ServerTemplate{ + "interface": { + Name: "interface", + Imports: []Import{ + {Path: "net/http"}, + {Path: "github.com/labstack/echo/v5"}, + }, + Template: "server/echo/interface.go.tmpl", + }, + "handler": { + Name: "handler", + Imports: []Import{ + {Path: "github.com/labstack/echo/v5"}, + }, + Template: "server/echo/handler.go.tmpl", + }, + "wrapper": { + Name: "wrapper", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + {Path: "github.com/labstack/echo/v5"}, + }, + Template: "server/echo/wrapper.go.tmpl", + }, +} + +// EchoV4ServerTemplates contains templates for Echo v4 server generation. +var EchoV4ServerTemplates = map[string]ServerTemplate{ + "interface": { + Name: "interface", + Imports: []Import{ + {Path: "net/http"}, + {Path: "github.com/labstack/echo/v4"}, + }, + Template: "server/echo-v4/interface.go.tmpl", + }, + "handler": { + Name: "handler", + Imports: []Import{ + {Path: "github.com/labstack/echo/v4"}, + }, + Template: "server/echo-v4/handler.go.tmpl", + }, + "wrapper": { + Name: "wrapper", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + {Path: "github.com/labstack/echo/v4"}, + }, + Template: "server/echo-v4/wrapper.go.tmpl", + }, +} + +// GinServerTemplates contains templates for Gin server generation. +var GinServerTemplates = map[string]ServerTemplate{ + "interface": { + Name: "interface", + Imports: []Import{ + {Path: "net/http"}, + {Path: "github.com/gin-gonic/gin"}, + }, + Template: "server/gin/interface.go.tmpl", + }, + "handler": { + Name: "handler", + Imports: []Import{ + {Path: "github.com/gin-gonic/gin"}, + }, + Template: "server/gin/handler.go.tmpl", + }, + "wrapper": { + Name: "wrapper", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + {Path: "github.com/gin-gonic/gin"}, + }, + Template: "server/gin/wrapper.go.tmpl", + }, +} + +// GorillaServerTemplates contains templates for Gorilla server generation. +var GorillaServerTemplates = map[string]ServerTemplate{ + "interface": { + Name: "interface", + Imports: []Import{ + {Path: "net/http"}, + }, + Template: "server/gorilla/interface.go.tmpl", + }, + "handler": { + Name: "handler", + Imports: []Import{ + {Path: "net/http"}, + {Path: "github.com/gorilla/mux"}, + }, + Template: "server/gorilla/handler.go.tmpl", + }, + "wrapper": { + Name: "wrapper", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + {Path: "github.com/gorilla/mux"}, + }, + Template: "server/gorilla/wrapper.go.tmpl", + }, +} + +// FiberServerTemplates contains templates for Fiber server generation. +var FiberServerTemplates = map[string]ServerTemplate{ + "interface": { + Name: "interface", + Imports: []Import{ + {Path: "github.com/gofiber/fiber/v3"}, + }, + Template: "server/fiber/interface.go.tmpl", + }, + "handler": { + Name: "handler", + Imports: []Import{ + {Path: "github.com/gofiber/fiber/v3"}, + }, + Template: "server/fiber/handler.go.tmpl", + }, + "wrapper": { + Name: "wrapper", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + {Path: "github.com/gofiber/fiber/v3"}, + }, + Template: "server/fiber/wrapper.go.tmpl", + }, +} + +// IrisServerTemplates contains templates for Iris server generation. +var IrisServerTemplates = map[string]ServerTemplate{ + "interface": { + Name: "interface", + Imports: []Import{ + {Path: "net/http"}, + {Path: "github.com/kataras/iris/v12"}, + }, + Template: "server/iris/interface.go.tmpl", + }, + "handler": { + Name: "handler", + Imports: []Import{ + {Path: "github.com/kataras/iris/v12"}, + }, + Template: "server/iris/handler.go.tmpl", + }, + "wrapper": { + Name: "wrapper", + Imports: []Import{ + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "net/http"}, + {Path: "net/url"}, + {Path: "github.com/kataras/iris/v12"}, + }, + Template: "server/iris/wrapper.go.tmpl", + }, +} + +// SharedServerTemplates contains templates shared across all server implementations. +var SharedServerTemplates = map[string]ServerTemplate{ + "errors": { + Name: "errors", + Imports: []Import{ + {Path: "fmt"}, + }, + Template: "server/errors.go.tmpl", + }, + "param_types": { + Name: "param_types", + Imports: []Import{}, + Template: "server/param_types.go.tmpl", + }, +} + +// InitiatorTemplate defines a template for initiator (webhook/callback sender) generation. +type InitiatorTemplate struct { + Name string // Template name (e.g., "initiator_base", "initiator_interface") + Imports []Import // Required imports for this template + Template string // Template path in embedded FS +} + +// InitiatorTemplates contains templates for initiator generation. +// These are shared between webhook and callback initiators (parameterized by prefix). +var InitiatorTemplates = map[string]InitiatorTemplate{ + "initiator_base": { + Name: "initiator_base", + Imports: []Import{ + {Path: "context"}, + {Path: "net/http"}, + }, + Template: "initiator/base.go.tmpl", + }, + "initiator_interface": { + Name: "initiator_interface", + Imports: []Import{ + {Path: "context"}, + {Path: "io"}, + {Path: "net/http"}, + }, + Template: "initiator/interface.go.tmpl", + }, + "initiator_methods": { + Name: "initiator_methods", + Imports: []Import{ + {Path: "context"}, + {Path: "io"}, + {Path: "net/http"}, + }, + Template: "initiator/methods.go.tmpl", + }, + "initiator_request_builders": { + Name: "initiator_request_builders", + Imports: []Import{ + {Path: "bytes"}, + {Path: "encoding/json"}, + {Path: "io"}, + {Path: "net/http"}, + {Path: "net/url"}, + }, + Template: "initiator/request_builders.go.tmpl", + }, + "initiator_simple": { + Name: "initiator_simple", + Imports: []Import{ + {Path: "context"}, + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "io"}, + {Path: "net/http"}, + }, + Template: "initiator/simple.go.tmpl", + }, +} + +// ClientTemplate defines a template for client generation. +type ClientTemplate struct { + Name string // Template name (e.g., "base", "interface") + Imports []Import // Required imports for this template + Template string // Template path in embedded FS +} + +// ClientTemplates contains templates for client generation. +var ClientTemplates = map[string]ClientTemplate{ + "base": { + Name: "base", + Imports: []Import{ + {Path: "context"}, + {Path: "net/http"}, + {Path: "net/url"}, + {Path: "strings"}, + }, + Template: "client/base.go.tmpl", + }, + "interface": { + Name: "interface", + Imports: []Import{ + {Path: "context"}, + {Path: "io"}, + {Path: "net/http"}, + }, + Template: "client/interface.go.tmpl", + }, + "methods": { + Name: "methods", + Imports: []Import{ + {Path: "context"}, + {Path: "io"}, + {Path: "net/http"}, + }, + Template: "client/methods.go.tmpl", + }, + "request_builders": { + Name: "request_builders", + Imports: []Import{ + {Path: "bytes"}, + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "io"}, + {Path: "net/http"}, + {Path: "net/url"}, + }, + Template: "client/request_builders.go.tmpl", + }, + "simple": { + Name: "simple", + Imports: []Import{ + {Path: "context"}, + {Path: "encoding/json"}, + {Path: "fmt"}, + {Path: "io"}, + {Path: "net/http"}, + }, + Template: "client/simple.go.tmpl", + }, +} diff --git a/experimental/internal/codegen/templates/test/types/date.gen.go b/experimental/internal/codegen/templates/test/types/date.gen.go new file mode 100644 index 0000000000..37c30d3b98 --- /dev/null +++ b/experimental/internal/codegen/templates/test/types/date.gen.go @@ -0,0 +1,46 @@ +// Code generated by gentypes. DO NOT EDIT. + +package types + +import ( + "encoding/json" + "time" +) + + +const DateFormat = "2006-01-02" + +type Date struct { + time.Time +} + +func (d Date) MarshalJSON() ([]byte, error) { + return json.Marshal(d.Format(DateFormat)) +} + +func (d *Date) UnmarshalJSON(data []byte) error { + var dateStr string + err := json.Unmarshal(data, &dateStr) + if err != nil { + return err + } + parsed, err := time.Parse(DateFormat, dateStr) + if err != nil { + return err + } + d.Time = parsed + return nil +} + +func (d Date) String() string { + return d.Format(DateFormat) +} + +func (d *Date) UnmarshalText(data []byte) error { + parsed, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = parsed + return nil +} diff --git a/experimental/internal/codegen/templates/test/types/date_test.go b/experimental/internal/codegen/templates/test/types/date_test.go new file mode 100644 index 0000000000..211776522f --- /dev/null +++ b/experimental/internal/codegen/templates/test/types/date_test.go @@ -0,0 +1,65 @@ +package types + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDate_MarshalJSON(t *testing.T) { + testDate := time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC) + b := struct { + DateField Date `json:"date"` + }{ + DateField: Date{testDate}, + } + jsonBytes, err := json.Marshal(b) + assert.NoError(t, err) + assert.JSONEq(t, `{"date":"2019-04-01"}`, string(jsonBytes)) +} + +func TestDate_UnmarshalJSON(t *testing.T) { + testDate := time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC) + jsonStr := `{"date":"2019-04-01"}` + b := struct { + DateField Date `json:"date"` + }{} + err := json.Unmarshal([]byte(jsonStr), &b) + assert.NoError(t, err) + assert.Equal(t, testDate, b.DateField.Time) +} + +func TestDate_Stringer(t *testing.T) { + t.Run("nil date", func(t *testing.T) { + var d *Date + assert.Equal(t, "", fmt.Sprintf("%v", d)) + }) + + t.Run("ptr date", func(t *testing.T) { + d := &Date{ + Time: time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC), + } + assert.Equal(t, "2019-04-01", fmt.Sprintf("%v", d)) + }) + + t.Run("value date", func(t *testing.T) { + d := Date{ + Time: time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC), + } + assert.Equal(t, "2019-04-01", fmt.Sprintf("%v", d)) + }) +} + +func TestDate_UnmarshalText(t *testing.T) { + testDate := time.Date(2022, 6, 14, 0, 0, 0, 0, time.UTC) + value := []byte("2022-06-14") + + date := Date{} + err := date.UnmarshalText(value) + + assert.NoError(t, err) + assert.Equal(t, testDate, date.Time) +} diff --git a/experimental/internal/codegen/templates/test/types/email.gen.go b/experimental/internal/codegen/templates/test/types/email.gen.go new file mode 100644 index 0000000000..6db0d58270 --- /dev/null +++ b/experimental/internal/codegen/templates/test/types/email.gen.go @@ -0,0 +1,52 @@ +// Code generated by gentypes. DO NOT EDIT. + +package types + +import ( + "encoding/json" + "errors" + "regexp" +) + + +const ( + emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" +) + +var ( + emailRegex = regexp.MustCompile(emailRegexString) +) + +// ErrValidationEmail is the sentinel error returned when an email fails validation +var ErrValidationEmail = errors.New("email: failed to pass regex validation") + +// Email represents an email address. +// It is a string type that must pass regex validation before being marshalled +// to JSON or unmarshalled from JSON. +type Email string + +func (e Email) MarshalJSON() ([]byte, error) { + if !emailRegex.MatchString(string(e)) { + return nil, ErrValidationEmail + } + + return json.Marshal(string(e)) +} + +func (e *Email) UnmarshalJSON(data []byte) error { + if e == nil { + return nil + } + + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + *e = Email(s) + if !emailRegex.MatchString(s) { + return ErrValidationEmail + } + + return nil +} diff --git a/experimental/internal/codegen/templates/test/types/email_test.go b/experimental/internal/codegen/templates/test/types/email_test.go new file mode 100644 index 0000000000..736056b172 --- /dev/null +++ b/experimental/internal/codegen/templates/test/types/email_test.go @@ -0,0 +1,176 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEmail_MarshalJSON_Validation(t *testing.T) { + type requiredEmail struct { + EmailField Email `json:"email"` + } + + testCases := map[string]struct { + email Email + expectedJSON []byte + expectedError error + }{ + "it should succeed marshalling a valid email and return valid JSON populated with the email": { + email: Email("validemail@openapicodegen.com"), + expectedJSON: []byte(`{"email":"validemail@openapicodegen.com"}`), + expectedError: nil, + }, + "it should fail marshalling an invalid email and return a validation error": { + email: Email("invalidemail"), + expectedJSON: nil, + expectedError: ErrValidationEmail, + }, + "it should fail marshalling an empty email and return a validation error": { + email: Email(""), + expectedJSON: nil, + expectedError: ErrValidationEmail, + }, + } + + for name, tc := range testCases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + jsonBytes, err := json.Marshal(requiredEmail{EmailField: tc.email}) + + if tc.expectedError != nil { + assert.ErrorIs(t, err, tc.expectedError) + } else { + assert.JSONEq(t, string(tc.expectedJSON), string(jsonBytes)) + } + }) + } +} + +func TestEmail_UnmarshalJSON_RequiredEmail_Validation(t *testing.T) { + type requiredEmail struct { + EmailField Email `json:"email"` + } + + requiredEmailTestCases := map[string]struct { + jsonStr string + expectedEmail Email + expectedError error + }{ + "it should succeed validating a valid email during the unmarshal process": { + jsonStr: `{"email":"gaben@valvesoftware.com"}`, + expectedError: nil, + expectedEmail: func() Email { + e := Email("gaben@valvesoftware.com") + return e + }(), + }, + "it should fail validating an invalid email": { + jsonStr: `{"email":"not-an-email"}`, + expectedError: ErrValidationEmail, + expectedEmail: func() Email { + e := Email("not-an-email") + return e + }(), + }, + "it should fail validating an empty email": { + jsonStr: `{"email":""}`, + expectedEmail: func() Email { + e := Email("") + return e + }(), + expectedError: ErrValidationEmail, + }, + "it should fail validating a null email": { + jsonStr: `{"email":null}`, + expectedEmail: func() Email { + e := Email("") + return e + }(), + expectedError: ErrValidationEmail, + }, + } + + for name, tc := range requiredEmailTestCases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + b := requiredEmail{} + err := json.Unmarshal([]byte(tc.jsonStr), &b) + assert.Equal(t, tc.expectedEmail, b.EmailField) + assert.ErrorIs(t, err, tc.expectedError) + }) + } + +} + +func TestEmail_UnmarshalJSON_NullableEmail_Validation(t *testing.T) { + + type nullableEmail struct { + EmailField *Email `json:"email,omitempty"` + } + + nullableEmailTestCases := map[string]struct { + body nullableEmail + jsonStr string + expectedEmail *Email + expectedError error + }{ + "it should succeed validating a valid email during the unmarshal process": { + body: nullableEmail{}, + jsonStr: `{"email":"gaben@valvesoftware.com"}`, + expectedError: nil, + expectedEmail: func() *Email { + e := Email("gaben@valvesoftware.com") + return &e + }(), + }, + "it should fail validating an invalid email": { + body: nullableEmail{}, + jsonStr: `{"email":"not-an-email"}`, + expectedError: ErrValidationEmail, + expectedEmail: func() *Email { + e := Email("not-an-email") + return &e + }(), + }, + "it should fail validating an empty email": { + body: nullableEmail{}, + jsonStr: `{"email":""}`, + expectedError: ErrValidationEmail, + expectedEmail: func() *Email { + e := Email("") + return &e + }(), + }, + "it should succeed validating a null email": { + body: nullableEmail{}, + jsonStr: `{"email":null}`, + expectedEmail: nil, + expectedError: nil, + }, + "it should succeed validating a missing email": { + body: nullableEmail{}, + jsonStr: `{}`, + expectedEmail: nil, + expectedError: nil, + }, + } + + for name, tc := range nullableEmailTestCases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + err := json.Unmarshal([]byte(tc.jsonStr), &tc.body) + assert.Equal(t, tc.expectedEmail, tc.body.EmailField) + if tc.expectedError != nil { + assert.ErrorIs(t, err, tc.expectedError) + } + }) + } +} diff --git a/experimental/internal/codegen/templates/test/types/file.gen.go b/experimental/internal/codegen/templates/test/types/file.gen.go new file mode 100644 index 0000000000..391c94a9da --- /dev/null +++ b/experimental/internal/codegen/templates/test/types/file.gen.go @@ -0,0 +1,74 @@ +// Code generated by gentypes. DO NOT EDIT. + +package types + +import ( + "bytes" + "encoding/json" + "io" + "mime/multipart" +) + + +type File struct { + multipart *multipart.FileHeader + data []byte + filename string +} + +func (file *File) InitFromMultipart(header *multipart.FileHeader) { + file.multipart = header + file.data = nil + file.filename = "" +} + +func (file *File) InitFromBytes(data []byte, filename string) { + file.data = data + file.filename = filename + file.multipart = nil +} + +func (file File) MarshalJSON() ([]byte, error) { + b, err := file.Bytes() + if err != nil { + return nil, err + } + return json.Marshal(b) +} + +func (file *File) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &file.data) +} + +func (file File) Bytes() ([]byte, error) { + if file.multipart != nil { + f, err := file.multipart.Open() + if err != nil { + return nil, err + } + defer func() { _ = f.Close() }() + return io.ReadAll(f) + } + return file.data, nil +} + +func (file File) Reader() (io.ReadCloser, error) { + if file.multipart != nil { + return file.multipart.Open() + } + return io.NopCloser(bytes.NewReader(file.data)), nil +} + +func (file File) Filename() string { + if file.multipart != nil { + return file.multipart.Filename + } + return file.filename +} + +func (file File) FileSize() int64 { + if file.multipart != nil { + return file.multipart.Size + } + return int64(len(file.data)) +} diff --git a/experimental/internal/codegen/templates/test/types/file_test.go b/experimental/internal/codegen/templates/test/types/file_test.go new file mode 100644 index 0000000000..fb4ce98ae1 --- /dev/null +++ b/experimental/internal/codegen/templates/test/types/file_test.go @@ -0,0 +1,54 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var _ json.Marshaler = (*File)(nil) +var _ json.Unmarshaler = (*File)(nil) + +func TestFileJSON(t *testing.T) { + type Object struct { + BinaryField File `json:"binary_field"` + } + + // Check whether we encode JSON properly. + var o Object + o.BinaryField.InitFromBytes([]byte("hello"), "") + buf, err := json.Marshal(o) + require.NoError(t, err) + t.Log(string(buf)) + + // Decode the same object back into File, ensure result is correct. + var o2 Object + err = json.Unmarshal(buf, &o2) + require.NoError(t, err) + o2Bytes, err := o2.BinaryField.Bytes() + require.NoError(t, err) + assert.Equal(t, []byte("hello"), o2Bytes) + + // Ensure it also works via pointer. + type Object2 struct { + BinaryFieldPtr *File `json:"binary_field"` + } + + var o3 Object2 + var f File + f.InitFromBytes([]byte("hello"), "") + o3.BinaryFieldPtr = &f + buf, err = json.Marshal(o) + require.NoError(t, err) + t.Log(string(buf)) + + var o4 Object2 + err = json.Unmarshal(buf, &o4) + require.NoError(t, err) + o4Bytes, err := o4.BinaryFieldPtr.Bytes() + require.NoError(t, err) + assert.Equal(t, []byte("hello"), o4Bytes) + +} diff --git a/experimental/internal/codegen/templates/test/types/generate.go b/experimental/internal/codegen/templates/test/types/generate.go new file mode 100644 index 0000000000..bab32084c7 --- /dev/null +++ b/experimental/internal/codegen/templates/test/types/generate.go @@ -0,0 +1,3 @@ +package types + +//go:generate go run ./gentypes -package types -output . Email Date UUID File diff --git a/experimental/internal/codegen/templates/test/types/gentypes/main.go b/experimental/internal/codegen/templates/test/types/gentypes/main.go new file mode 100644 index 0000000000..c22eee2751 --- /dev/null +++ b/experimental/internal/codegen/templates/test/types/gentypes/main.go @@ -0,0 +1,109 @@ +// gentypes generates Go type files from templates. +// Usage: gentypes -package -output [types...] +// +// Example: +// +// //go:generate gentypes -package types -output . Email Date UUID File +package main + +import ( + "bytes" + "flag" + "fmt" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/templates" +) + +func main() { + packageName := flag.String("package", "", "Go package name for generated files") + outputDir := flag.String("output", ".", "output directory for generated files") + flag.Parse() + + if *packageName == "" { + fmt.Fprintln(os.Stderr, "error: -package is required") + os.Exit(1) + } + + typeNames := flag.Args() + if len(typeNames) == 0 { + fmt.Fprintln(os.Stderr, "error: at least one type name required") + fmt.Fprintln(os.Stderr, "available types: Email, Date, UUID, File") + os.Exit(1) + } + + for _, typeName := range typeNames { + if err := generateType(*packageName, *outputDir, typeName); err != nil { + fmt.Fprintf(os.Stderr, "error generating %s: %v\n", typeName, err) + os.Exit(1) + } + } +} + +func generateType(packageName, outputDir, typeName string) error { + tt, ok := templates.TypeTemplates[typeName] + if !ok { + return fmt.Errorf("unknown type: %s", typeName) + } + + // Read template content + tmplPath := "files/" + tt.Template + tmplContent, err := templates.TemplateFS.ReadFile(tmplPath) + if err != nil { + return fmt.Errorf("reading template %s: %w", tmplPath, err) + } + + // Parse and execute template (in case it has any directives) + tmpl, err := template.New(typeName).Parse(string(tmplContent)) + if err != nil { + return fmt.Errorf("parsing template: %w", err) + } + + var body bytes.Buffer + if err := tmpl.Execute(&body, nil); err != nil { + return fmt.Errorf("executing template: %w", err) + } + + // Build the output file + var out bytes.Buffer + + // Package declaration + fmt.Fprintf(&out, "// Code generated by gentypes. DO NOT EDIT.\n\n") + fmt.Fprintf(&out, "package %s\n", packageName) + + // Imports + if len(tt.Imports) > 0 { + out.WriteString("\nimport (\n") + for _, imp := range tt.Imports { + if imp.Alias != "" { + fmt.Fprintf(&out, "\t%s %q\n", imp.Alias, imp.Path) + } else { + fmt.Fprintf(&out, "\t%q\n", imp.Path) + } + } + out.WriteString(")\n") + } + + // Template body (skip the leading template comment if present) + bodyStr := body.String() + if strings.HasPrefix(bodyStr, "{{/*") { + // Skip the comment line + if idx := strings.Index(bodyStr, "*/}}"); idx != -1 { + bodyStr = bodyStr[idx+4:] + } + } + out.WriteString(bodyStr) + + // Write output file + filename := strings.ToLower(typeName) + ".gen.go" + outputPath := filepath.Join(outputDir, filename) + if err := os.WriteFile(outputPath, out.Bytes(), 0644); err != nil { + return fmt.Errorf("writing %s: %w", outputPath, err) + } + + fmt.Printf("generated %s\n", outputPath) + return nil +} diff --git a/experimental/internal/codegen/templates/test/types/uuid.gen.go b/experimental/internal/codegen/templates/test/types/uuid.gen.go new file mode 100644 index 0000000000..f1c45b3c36 --- /dev/null +++ b/experimental/internal/codegen/templates/test/types/uuid.gen.go @@ -0,0 +1,10 @@ +// Code generated by gentypes. DO NOT EDIT. + +package types + +import ( + "github.com/google/uuid" +) + + +type UUID = uuid.UUID diff --git a/experimental/internal/codegen/templates/test/types/uuid_test.go b/experimental/internal/codegen/templates/test/types/uuid_test.go new file mode 100644 index 0000000000..bb62040b6c --- /dev/null +++ b/experimental/internal/codegen/templates/test/types/uuid_test.go @@ -0,0 +1,53 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestUUID_MarshalJSON_Zero(t *testing.T) { + var testUUID UUID + b := struct { + UUIDField UUID `json:"uuid"` + }{ + UUIDField: testUUID, + } + marshaled, err := json.Marshal(b) + assert.NoError(t, err) + assert.JSONEq(t, `{"uuid":"00000000-0000-0000-0000-000000000000"}`, string(marshaled)) +} + +func TestUUID_MarshalJSON_Pass(t *testing.T) { + testUUID := uuid.MustParse("9cb14230-b640-11ec-b909-0242ac120002") + b := struct { + UUIDField UUID `json:"uuid"` + }{ + UUIDField: testUUID, + } + jsonBytes, err := json.Marshal(b) + assert.NoError(t, err) + assert.JSONEq(t, `{"uuid":"9cb14230-b640-11ec-b909-0242ac120002"}`, string(jsonBytes)) +} + +func TestUUID_UnmarshalJSON_Fail(t *testing.T) { + jsonStr := `{"uuid":"this-is-not-a-uuid"}` + b := struct { + UUIDField UUID `json:"uuid"` + }{} + err := json.Unmarshal([]byte(jsonStr), &b) + assert.Error(t, err) +} + +func TestUUID_UnmarshalJSON_Pass(t *testing.T) { + testUUID := uuid.MustParse("9cb14230-b640-11ec-b909-0242ac120002") + jsonStr := `{"uuid":"9cb14230-b640-11ec-b909-0242ac120002"}` + b := struct { + UUIDField UUID `json:"uuid"` + }{} + err := json.Unmarshal([]byte(jsonStr), &b) + assert.NoError(t, err) + assert.Equal(t, testUUID, b.UUIDField) +} diff --git a/experimental/internal/codegen/test/comprehensive/doc.go b/experimental/internal/codegen/test/comprehensive/doc.go new file mode 100644 index 0000000000..658e592917 --- /dev/null +++ b/experimental/internal/codegen/test/comprehensive/doc.go @@ -0,0 +1,3 @@ +package comprehensive + +//go:generate go run ../../../../cmd/oapi-codegen -package output -output output/comprehensive.gen.go ../files/comprehensive.yaml diff --git a/experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go b/experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go new file mode 100644 index 0000000000..633aa2dbe7 --- /dev/null +++ b/experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go @@ -0,0 +1,2098 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "regexp" + "strings" + "sync" + "time" + + "github.com/google/uuid" +) + +// #/components/schemas/AllTypesRequired +type AllTypesRequired struct { + IntField int `json:"intField" form:"intField"` + Int32Field int32 `json:"int32Field" form:"int32Field"` + Int64Field int64 `json:"int64Field" form:"int64Field"` + FloatField float32 `json:"floatField" form:"floatField"` + DoubleField float64 `json:"doubleField" form:"doubleField"` + NumberField float32 `json:"numberField" form:"numberField"` + StringField string `json:"stringField" form:"stringField"` + BoolField bool `json:"boolField" form:"boolField"` + DateField Date `json:"dateField" form:"dateField"` + DateTimeField time.Time `json:"dateTimeField" form:"dateTimeField"` + UUIDField UUID `json:"uuidField" form:"uuidField"` + EmailField Email `json:"emailField" form:"emailField"` + URIField string `json:"uriField" form:"uriField"` + HostnameField string `json:"hostnameField" form:"hostnameField"` + Ipv4Field string `json:"ipv4Field" form:"ipv4Field"` + Ipv6Field string `json:"ipv6Field" form:"ipv6Field"` + ByteField []byte `json:"byteField" form:"byteField"` + BinaryField File `json:"binaryField" form:"binaryField"` + PasswordField string `json:"passwordField" form:"passwordField"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AllTypesRequired) ApplyDefaults() { +} + +// #/components/schemas/AllTypesOptional +type AllTypesOptional struct { + IntField *int `json:"intField,omitempty" form:"intField,omitempty"` + Int32Field *int32 `json:"int32Field,omitempty" form:"int32Field,omitempty"` + Int64Field *int64 `json:"int64Field,omitempty" form:"int64Field,omitempty"` + FloatField *float32 `json:"floatField,omitempty" form:"floatField,omitempty"` + DoubleField *float64 `json:"doubleField,omitempty" form:"doubleField,omitempty"` + NumberField *float32 `json:"numberField,omitempty" form:"numberField,omitempty"` + StringField *string `json:"stringField,omitempty" form:"stringField,omitempty"` + BoolField *bool `json:"boolField,omitempty" form:"boolField,omitempty"` + DateField *Date `json:"dateField,omitempty" form:"dateField,omitempty"` + DateTimeField *time.Time `json:"dateTimeField,omitempty" form:"dateTimeField,omitempty"` + UUIDField *UUID `json:"uuidField,omitempty" form:"uuidField,omitempty"` + EmailField *Email `json:"emailField,omitempty" form:"emailField,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AllTypesOptional) ApplyDefaults() { +} + +// #/components/schemas/NullableRequired +type NullableRequired struct { + NullableString Nullable[string] `json:"nullableString" form:"nullableString"` + NullableInt Nullable[int] `json:"nullableInt" form:"nullableInt"` + NullableObject Nullable[NullableRequiredNullableObject] `json:"nullableObject" form:"nullableObject"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *NullableRequired) ApplyDefaults() { +} + +// #/components/schemas/NullableRequired/properties/nullableObject +type NullableRequiredNullableObject struct { + Name *string `json:"name,omitempty" form:"name,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *NullableRequiredNullableObject) ApplyDefaults() { +} + +// #/components/schemas/NullableOptional +type NullableOptional struct { + NullableString Nullable[string] `json:"nullableString,omitempty" form:"nullableString,omitempty"` + NullableInt Nullable[int] `json:"nullableInt,omitempty" form:"nullableInt,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *NullableOptional) ApplyDefaults() { +} + +// #/components/schemas/ArrayTypes +type ArrayTypes struct { + StringArray []string `json:"stringArray,omitempty" form:"stringArray,omitempty"` + IntArray []int `json:"intArray,omitempty" form:"intArray,omitempty"` + ObjectArray []SimpleObject `json:"objectArray,omitempty" form:"objectArray,omitempty"` + InlineObjectArray []ArrayTypesInlineObjectArrayItem `json:"inlineObjectArray,omitempty" form:"inlineObjectArray,omitempty"` + NestedArray [][]string `json:"nestedArray,omitempty" form:"nestedArray,omitempty"` + NullableArray []string `json:"nullableArray,omitempty" form:"nullableArray,omitempty"` + ArrayWithConstraints []string `json:"arrayWithConstraints,omitempty" form:"arrayWithConstraints,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ArrayTypes) ApplyDefaults() { +} + +// #/components/schemas/ArrayTypes/properties/objectArray +type ArrayTypesObjectArray = []SimpleObject + +// #/components/schemas/ArrayTypes/properties/inlineObjectArray +type ArrayTypesInlineObjectArray = []ArrayTypesInlineObjectArrayItem + +// #/components/schemas/ArrayTypes/properties/inlineObjectArray/items +type ArrayTypesInlineObjectArrayItem struct { + ID *int `json:"id,omitempty" form:"id,omitempty"` + Name *string `json:"name,omitempty" form:"name,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ArrayTypesInlineObjectArrayItem) ApplyDefaults() { +} + +// #/components/schemas/SimpleObject +type SimpleObject struct { + ID int `json:"id" form:"id"` + Name *string `json:"name,omitempty" form:"name,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *SimpleObject) ApplyDefaults() { +} + +// #/components/schemas/NestedObject +type NestedObject struct { + Outer *NestedObjectOuter `json:"outer,omitempty" form:"outer,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *NestedObject) ApplyDefaults() { + if s.Outer != nil { + s.Outer.ApplyDefaults() + } +} + +// #/components/schemas/NestedObject/properties/outer +type NestedObjectOuter struct { + Inner *NestedObjectOuterInner `json:"inner,omitempty" form:"inner,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *NestedObjectOuter) ApplyDefaults() { + if s.Inner != nil { + s.Inner.ApplyDefaults() + } +} + +// #/components/schemas/NestedObject/properties/outer/properties/inner +type NestedObjectOuterInner struct { + Value *string `json:"value,omitempty" form:"value,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *NestedObjectOuterInner) ApplyDefaults() { +} + +// #/components/schemas/AdditionalPropsAny +type AdditionalPropsAny = map[string]any + +// #/components/schemas/AdditionalPropsNone +type AdditionalPropsNone struct { + Known *string `json:"known,omitempty" form:"known,omitempty"` + AdditionalProperties map[string]any `json:"-"` +} + +func (s AdditionalPropsNone) MarshalJSON() ([]byte, error) { + result := make(map[string]any) + + if s.Known != nil { + result["known"] = s.Known + } + + // Add additional properties + for k, v := range s.AdditionalProperties { + result[k] = v + } + + return json.Marshal(result) +} + +func (s *AdditionalPropsNone) UnmarshalJSON(data []byte) error { + // Known fields + knownFields := map[string]bool{ + "known": true, + } + + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + if v, ok := raw["known"]; ok { + var val string + if err := json.Unmarshal(v, &val); err != nil { + return err + } + s.Known = &val + } + + // Collect additional properties + s.AdditionalProperties = make(map[string]any) + for k, v := range raw { + if !knownFields[k] { + var val any + if err := json.Unmarshal(v, &val); err != nil { + return err + } + s.AdditionalProperties[k] = val + } + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AdditionalPropsNone) ApplyDefaults() { +} + +// #/components/schemas/AdditionalPropsTyped +type AdditionalPropsTyped = map[string]int + +// #/components/schemas/AdditionalPropsObject +type AdditionalPropsObject = map[string]any + +// #/components/schemas/AdditionalPropsWithProps +type AdditionalPropsWithProps struct { + ID *int `json:"id,omitempty" form:"id,omitempty"` + AdditionalProperties map[string]string `json:"-"` +} + +func (s AdditionalPropsWithProps) MarshalJSON() ([]byte, error) { + result := make(map[string]any) + + if s.ID != nil { + result["id"] = s.ID + } + + // Add additional properties + for k, v := range s.AdditionalProperties { + result[k] = v + } + + return json.Marshal(result) +} + +func (s *AdditionalPropsWithProps) UnmarshalJSON(data []byte) error { + // Known fields + knownFields := map[string]bool{ + "id": true, + } + + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + if v, ok := raw["id"]; ok { + var val int + if err := json.Unmarshal(v, &val); err != nil { + return err + } + s.ID = &val + } + + // Collect additional properties + s.AdditionalProperties = make(map[string]string) + for k, v := range raw { + if !knownFields[k] { + var val string + if err := json.Unmarshal(v, &val); err != nil { + return err + } + s.AdditionalProperties[k] = val + } + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AdditionalPropsWithProps) ApplyDefaults() { +} + +// #/components/schemas/StringEnum +type StringEnum string + +const ( + StringEnum_value1 StringEnum = "value1" + StringEnum_value2 StringEnum = "value2" + StringEnum_value3 StringEnum = "value3" +) + +// #/components/schemas/IntegerEnum +type IntegerEnum int + +const ( + IntegerEnum_N1 IntegerEnum = 1 + IntegerEnum_N2 IntegerEnum = 2 + IntegerEnum_N3 IntegerEnum = 3 +) + +// #/components/schemas/ObjectWithEnum +type ObjectWithEnum struct { + Status *string `json:"status,omitempty" form:"status,omitempty"` + Priority *int `json:"priority,omitempty" form:"priority,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ObjectWithEnum) ApplyDefaults() { +} + +// #/components/schemas/ObjectWithEnum/properties/status +type ObjectWithEnumStatus string + +const ( + ObjectWithEnumStatus_pending ObjectWithEnumStatus = "pending" + ObjectWithEnumStatus_active ObjectWithEnumStatus = "active" + ObjectWithEnumStatus_completed ObjectWithEnumStatus = "completed" +) + +// #/components/schemas/ObjectWithEnum/properties/priority +type ObjectWithEnumPriority int + +const ( + ObjectWithEnumPriority_N1 ObjectWithEnumPriority = 1 + ObjectWithEnumPriority_N2 ObjectWithEnumPriority = 2 + ObjectWithEnumPriority_N3 ObjectWithEnumPriority = 3 +) + +// #/components/schemas/InlineEnumInProperty +type InlineEnumInProperty struct { + InlineStatus *string `json:"inlineStatus,omitempty" form:"inlineStatus,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *InlineEnumInProperty) ApplyDefaults() { +} + +// #/components/schemas/InlineEnumInProperty/properties/inlineStatus +type InlineEnumInPropertyInlineStatus string + +const ( + InlineEnumInPropertyInlineStatus_on InlineEnumInPropertyInlineStatus = "on" + InlineEnumInPropertyInlineStatus_off InlineEnumInPropertyInlineStatus = "off" +) + +// #/components/schemas/BaseProperties +type BaseProperties struct { + ID *int `json:"id,omitempty" form:"id,omitempty"` + CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *BaseProperties) ApplyDefaults() { +} + +// #/components/schemas/ExtendedObject +type ExtendedObject struct { + ID *int `json:"id,omitempty" form:"id,omitempty"` + CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` + Name string `json:"name" form:"name"` + Description *string `json:"description,omitempty" form:"description,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ExtendedObject) ApplyDefaults() { +} + +// #/components/schemas/DeepInheritance +type DeepInheritance struct { + ID *int `json:"id,omitempty" form:"id,omitempty"` + CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` + Name string `json:"name" form:"name"` + Description *string `json:"description,omitempty" form:"description,omitempty"` + Extra *string `json:"extra,omitempty" form:"extra,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *DeepInheritance) ApplyDefaults() { +} + +// #/components/schemas/AllOfMultipleRefs +type AllOfMultipleRefs struct { + ID int `json:"id" form:"id"` + CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` + Name *string `json:"name,omitempty" form:"name,omitempty"` + Merged *bool `json:"merged,omitempty" form:"merged,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AllOfMultipleRefs) ApplyDefaults() { +} + +// #/components/schemas/AllOfInlineOnly +type AllOfInlineOnly struct { + First *string `json:"first,omitempty" form:"first,omitempty"` + Second *int `json:"second,omitempty" form:"second,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AllOfInlineOnly) ApplyDefaults() { +} + +// #/components/schemas/AnyOfPrimitives +type AnyOfPrimitives struct { + String0 *string + Int1 *int +} + +func (u AnyOfPrimitives) MarshalJSON() ([]byte, error) { + if u.String0 != nil { + return json.Marshal(u.String0) + } + if u.Int1 != nil { + return json.Marshal(u.Int1) + } + return []byte("null"), nil +} + +func (u *AnyOfPrimitives) UnmarshalJSON(data []byte) error { + var v0 string + if err := json.Unmarshal(data, &v0); err == nil { + u.String0 = &v0 + } + + var v1 int + if err := json.Unmarshal(data, &v1); err == nil { + u.Int1 = &v1 + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *AnyOfPrimitives) ApplyDefaults() { +} + +// #/components/schemas/AnyOfObjects +type AnyOfObjects struct { + SimpleObject *SimpleObject + BaseProperties *BaseProperties +} + +func (u AnyOfObjects) MarshalJSON() ([]byte, error) { + result := make(map[string]any) + + if u.SimpleObject != nil { + data, err := json.Marshal(u.SimpleObject) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err == nil { + for k, v := range m { + result[k] = v + } + } + } + if u.BaseProperties != nil { + data, err := json.Marshal(u.BaseProperties) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err == nil { + for k, v := range m { + result[k] = v + } + } + } + + return json.Marshal(result) +} + +func (u *AnyOfObjects) UnmarshalJSON(data []byte) error { + var v0 SimpleObject + if err := json.Unmarshal(data, &v0); err == nil { + u.SimpleObject = &v0 + } + + var v1 BaseProperties + if err := json.Unmarshal(data, &v1); err == nil { + u.BaseProperties = &v1 + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *AnyOfObjects) ApplyDefaults() { + if u.SimpleObject != nil { + u.SimpleObject.ApplyDefaults() + } + if u.BaseProperties != nil { + u.BaseProperties.ApplyDefaults() + } +} + +// #/components/schemas/AnyOfMixed +type AnyOfMixed struct { + String0 *string + SimpleObject *SimpleObject + AnyOfMixedAnyOf2 *AnyOfMixedAnyOf2 +} + +func (u AnyOfMixed) MarshalJSON() ([]byte, error) { + result := make(map[string]any) + + if u.String0 != nil { + return json.Marshal(u.String0) + } + if u.SimpleObject != nil { + data, err := json.Marshal(u.SimpleObject) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err == nil { + for k, v := range m { + result[k] = v + } + } + } + if u.AnyOfMixedAnyOf2 != nil { + data, err := json.Marshal(u.AnyOfMixedAnyOf2) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err == nil { + for k, v := range m { + result[k] = v + } + } + } + + return json.Marshal(result) +} + +func (u *AnyOfMixed) UnmarshalJSON(data []byte) error { + var v0 string + if err := json.Unmarshal(data, &v0); err == nil { + u.String0 = &v0 + } + + var v1 SimpleObject + if err := json.Unmarshal(data, &v1); err == nil { + u.SimpleObject = &v1 + } + + var v2 AnyOfMixedAnyOf2 + if err := json.Unmarshal(data, &v2); err == nil { + u.AnyOfMixedAnyOf2 = &v2 + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *AnyOfMixed) ApplyDefaults() { + if u.SimpleObject != nil { + u.SimpleObject.ApplyDefaults() + } + if u.AnyOfMixedAnyOf2 != nil { + u.AnyOfMixedAnyOf2.ApplyDefaults() + } +} + +// #/components/schemas/AnyOfMixed/anyOf/2 +type AnyOfMixedAnyOf2 struct { + Inline *bool `json:"inline,omitempty" form:"inline,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AnyOfMixedAnyOf2) ApplyDefaults() { +} + +// #/components/schemas/AnyOfNullable +type AnyOfNullable struct { + String0 *string + Any1 *any +} + +func (u AnyOfNullable) MarshalJSON() ([]byte, error) { + if u.String0 != nil { + return json.Marshal(u.String0) + } + if u.Any1 != nil { + return json.Marshal(u.Any1) + } + return []byte("null"), nil +} + +func (u *AnyOfNullable) UnmarshalJSON(data []byte) error { + var v0 string + if err := json.Unmarshal(data, &v0); err == nil { + u.String0 = &v0 + } + + var v1 any + if err := json.Unmarshal(data, &v1); err == nil { + u.Any1 = &v1 + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *AnyOfNullable) ApplyDefaults() { +} + +// #/components/schemas/ObjectWithAnyOfProperty +type ObjectWithAnyOfProperty struct { + Value *ObjectWithAnyOfPropertyValue `json:"value,omitempty" form:"value,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ObjectWithAnyOfProperty) ApplyDefaults() { +} + +// #/components/schemas/ObjectWithAnyOfProperty/properties/value +type ObjectWithAnyOfPropertyValue struct { + String0 *string + Int1 *int + Bool2 *bool +} + +func (u ObjectWithAnyOfPropertyValue) MarshalJSON() ([]byte, error) { + if u.String0 != nil { + return json.Marshal(u.String0) + } + if u.Int1 != nil { + return json.Marshal(u.Int1) + } + if u.Bool2 != nil { + return json.Marshal(u.Bool2) + } + return []byte("null"), nil +} + +func (u *ObjectWithAnyOfPropertyValue) UnmarshalJSON(data []byte) error { + var v0 string + if err := json.Unmarshal(data, &v0); err == nil { + u.String0 = &v0 + } + + var v1 int + if err := json.Unmarshal(data, &v1); err == nil { + u.Int1 = &v1 + } + + var v2 bool + if err := json.Unmarshal(data, &v2); err == nil { + u.Bool2 = &v2 + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *ObjectWithAnyOfPropertyValue) ApplyDefaults() { +} + +// #/components/schemas/ArrayOfAnyOf +type ArrayOfAnyOf = []ArrayOfAnyOfItem + +// #/components/schemas/ArrayOfAnyOf/items +type ArrayOfAnyOfItem struct { + SimpleObject *SimpleObject + BaseProperties *BaseProperties +} + +func (u ArrayOfAnyOfItem) MarshalJSON() ([]byte, error) { + result := make(map[string]any) + + if u.SimpleObject != nil { + data, err := json.Marshal(u.SimpleObject) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err == nil { + for k, v := range m { + result[k] = v + } + } + } + if u.BaseProperties != nil { + data, err := json.Marshal(u.BaseProperties) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err == nil { + for k, v := range m { + result[k] = v + } + } + } + + return json.Marshal(result) +} + +func (u *ArrayOfAnyOfItem) UnmarshalJSON(data []byte) error { + var v0 SimpleObject + if err := json.Unmarshal(data, &v0); err == nil { + u.SimpleObject = &v0 + } + + var v1 BaseProperties + if err := json.Unmarshal(data, &v1); err == nil { + u.BaseProperties = &v1 + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *ArrayOfAnyOfItem) ApplyDefaults() { + if u.SimpleObject != nil { + u.SimpleObject.ApplyDefaults() + } + if u.BaseProperties != nil { + u.BaseProperties.ApplyDefaults() + } +} + +// #/components/schemas/OneOfSimple +type OneOfSimple struct { + SimpleObject *SimpleObject + BaseProperties *BaseProperties +} + +func (u OneOfSimple) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.SimpleObject != nil { + count++ + data, err = json.Marshal(u.SimpleObject) + if err != nil { + return nil, err + } + } + if u.BaseProperties != nil { + count++ + data, err = json.Marshal(u.BaseProperties) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("OneOfSimple: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *OneOfSimple) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 SimpleObject + if err := json.Unmarshal(data, &v0); err == nil { + u.SimpleObject = &v0 + successCount++ + } + + var v1 BaseProperties + if err := json.Unmarshal(data, &v1); err == nil { + u.BaseProperties = &v1 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("OneOfSimple: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *OneOfSimple) ApplyDefaults() { + if u.SimpleObject != nil { + u.SimpleObject.ApplyDefaults() + } + if u.BaseProperties != nil { + u.BaseProperties.ApplyDefaults() + } +} + +// #/components/schemas/OneOfWithDiscriminator +type OneOfWithDiscriminator struct { + Cat *Cat + Dog *Dog +} + +func (u OneOfWithDiscriminator) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.Cat != nil { + count++ + data, err = json.Marshal(u.Cat) + if err != nil { + return nil, err + } + } + if u.Dog != nil { + count++ + data, err = json.Marshal(u.Dog) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("OneOfWithDiscriminator: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *OneOfWithDiscriminator) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 Cat + if err := json.Unmarshal(data, &v0); err == nil { + u.Cat = &v0 + successCount++ + } + + var v1 Dog + if err := json.Unmarshal(data, &v1); err == nil { + u.Dog = &v1 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("OneOfWithDiscriminator: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *OneOfWithDiscriminator) ApplyDefaults() { + if u.Cat != nil { + u.Cat.ApplyDefaults() + } + if u.Dog != nil { + u.Dog.ApplyDefaults() + } +} + +// #/components/schemas/OneOfWithDiscriminatorMapping +type OneOfWithDiscriminatorMapping struct { + Cat *Cat + Dog *Dog +} + +func (u OneOfWithDiscriminatorMapping) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.Cat != nil { + count++ + data, err = json.Marshal(u.Cat) + if err != nil { + return nil, err + } + } + if u.Dog != nil { + count++ + data, err = json.Marshal(u.Dog) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("OneOfWithDiscriminatorMapping: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *OneOfWithDiscriminatorMapping) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 Cat + if err := json.Unmarshal(data, &v0); err == nil { + u.Cat = &v0 + successCount++ + } + + var v1 Dog + if err := json.Unmarshal(data, &v1); err == nil { + u.Dog = &v1 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("OneOfWithDiscriminatorMapping: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *OneOfWithDiscriminatorMapping) ApplyDefaults() { + if u.Cat != nil { + u.Cat.ApplyDefaults() + } + if u.Dog != nil { + u.Dog.ApplyDefaults() + } +} + +// #/components/schemas/Cat +type Cat struct { + PetType string `json:"petType" form:"petType"` + Meow string `json:"meow" form:"meow"` + WhiskerLength *float32 `json:"whiskerLength,omitempty" form:"whiskerLength,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Cat) ApplyDefaults() { +} + +// #/components/schemas/Dog +type Dog struct { + PetType string `json:"petType" form:"petType"` + Bark string `json:"bark" form:"bark"` + TailLength *float32 `json:"tailLength,omitempty" form:"tailLength,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Dog) ApplyDefaults() { +} + +// #/components/schemas/OneOfInline +type OneOfInline struct { + OneOfInlineOneOf0 *OneOfInlineOneOf0 + OneOfInlineOneOf1 *OneOfInlineOneOf1 +} + +func (u OneOfInline) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.OneOfInlineOneOf0 != nil { + count++ + data, err = json.Marshal(u.OneOfInlineOneOf0) + if err != nil { + return nil, err + } + } + if u.OneOfInlineOneOf1 != nil { + count++ + data, err = json.Marshal(u.OneOfInlineOneOf1) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("OneOfInline: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *OneOfInline) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 OneOfInlineOneOf0 + if err := json.Unmarshal(data, &v0); err == nil { + u.OneOfInlineOneOf0 = &v0 + successCount++ + } + + var v1 OneOfInlineOneOf1 + if err := json.Unmarshal(data, &v1); err == nil { + u.OneOfInlineOneOf1 = &v1 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("OneOfInline: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *OneOfInline) ApplyDefaults() { + if u.OneOfInlineOneOf0 != nil { + u.OneOfInlineOneOf0.ApplyDefaults() + } + if u.OneOfInlineOneOf1 != nil { + u.OneOfInlineOneOf1.ApplyDefaults() + } +} + +// #/components/schemas/OneOfInline/oneOf/0 +type OneOfInlineOneOf0 struct { + OptionA *string `json:"optionA,omitempty" form:"optionA,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *OneOfInlineOneOf0) ApplyDefaults() { +} + +// #/components/schemas/OneOfInline/oneOf/1 +type OneOfInlineOneOf1 struct { + OptionB *int `json:"optionB,omitempty" form:"optionB,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *OneOfInlineOneOf1) ApplyDefaults() { +} + +// #/components/schemas/OneOfPrimitives +type OneOfPrimitives struct { + String0 *string + Float321 *float32 + Bool2 *bool +} + +func (u OneOfPrimitives) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.String0 != nil { + count++ + data, err = json.Marshal(u.String0) + if err != nil { + return nil, err + } + } + if u.Float321 != nil { + count++ + data, err = json.Marshal(u.Float321) + if err != nil { + return nil, err + } + } + if u.Bool2 != nil { + count++ + data, err = json.Marshal(u.Bool2) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("OneOfPrimitives: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *OneOfPrimitives) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 string + if err := json.Unmarshal(data, &v0); err == nil { + u.String0 = &v0 + successCount++ + } + + var v1 float32 + if err := json.Unmarshal(data, &v1); err == nil { + u.Float321 = &v1 + successCount++ + } + + var v2 bool + if err := json.Unmarshal(data, &v2); err == nil { + u.Bool2 = &v2 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("OneOfPrimitives: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *OneOfPrimitives) ApplyDefaults() { +} + +// #/components/schemas/ObjectWithOneOfProperty +type ObjectWithOneOfProperty struct { + ID *int `json:"id,omitempty" form:"id,omitempty"` + Variant *ObjectWithOneOfPropertyVariant `json:"variant,omitempty" form:"variant,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ObjectWithOneOfProperty) ApplyDefaults() { +} + +// #/components/schemas/ObjectWithOneOfProperty/properties/variant +type ObjectWithOneOfPropertyVariant struct { + Cat *Cat + Dog *Dog +} + +func (u ObjectWithOneOfPropertyVariant) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.Cat != nil { + count++ + data, err = json.Marshal(u.Cat) + if err != nil { + return nil, err + } + } + if u.Dog != nil { + count++ + data, err = json.Marshal(u.Dog) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("ObjectWithOneOfPropertyVariant: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *ObjectWithOneOfPropertyVariant) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 Cat + if err := json.Unmarshal(data, &v0); err == nil { + u.Cat = &v0 + successCount++ + } + + var v1 Dog + if err := json.Unmarshal(data, &v1); err == nil { + u.Dog = &v1 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("ObjectWithOneOfPropertyVariant: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *ObjectWithOneOfPropertyVariant) ApplyDefaults() { + if u.Cat != nil { + u.Cat.ApplyDefaults() + } + if u.Dog != nil { + u.Dog.ApplyDefaults() + } +} + +// #/components/schemas/AllOfWithOneOf +type AllOfWithOneOf struct { + ID *int `json:"id,omitempty" form:"id,omitempty"` + CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` + AllOfWithOneOfAllOf1 *AllOfWithOneOfAllOf1 `json:"-"` +} + +func (s AllOfWithOneOf) MarshalJSON() ([]byte, error) { + result := make(map[string]any) + + if s.ID != nil { + result["id"] = s.ID + } + if s.CreatedAt != nil { + result["createdAt"] = s.CreatedAt + } + + if s.AllOfWithOneOfAllOf1 != nil { + unionData, err := json.Marshal(s.AllOfWithOneOfAllOf1) + if err != nil { + return nil, err + } + var unionMap map[string]any + if err := json.Unmarshal(unionData, &unionMap); err == nil { + for k, v := range unionMap { + result[k] = v + } + } + } + + return json.Marshal(result) +} + +func (s *AllOfWithOneOf) UnmarshalJSON(data []byte) error { + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + if v, ok := raw["id"]; ok { + var val int + if err := json.Unmarshal(v, &val); err != nil { + return err + } + s.ID = &val + } + if v, ok := raw["createdAt"]; ok { + var val time.Time + if err := json.Unmarshal(v, &val); err != nil { + return err + } + s.CreatedAt = &val + } + + var AllOfWithOneOfAllOf1Val AllOfWithOneOfAllOf1 + if err := json.Unmarshal(data, &AllOfWithOneOfAllOf1Val); err != nil { + return err + } + s.AllOfWithOneOfAllOf1 = &AllOfWithOneOfAllOf1Val + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AllOfWithOneOf) ApplyDefaults() { +} + +// #/components/schemas/AllOfWithOneOf/allOf/1 +type AllOfWithOneOfAllOf1 struct { + Cat *Cat + Dog *Dog +} + +func (u AllOfWithOneOfAllOf1) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.Cat != nil { + count++ + data, err = json.Marshal(u.Cat) + if err != nil { + return nil, err + } + } + if u.Dog != nil { + count++ + data, err = json.Marshal(u.Dog) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("AllOfWithOneOfAllOf1: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *AllOfWithOneOfAllOf1) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 Cat + if err := json.Unmarshal(data, &v0); err == nil { + u.Cat = &v0 + successCount++ + } + + var v1 Dog + if err := json.Unmarshal(data, &v1); err == nil { + u.Dog = &v1 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("AllOfWithOneOfAllOf1: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *AllOfWithOneOfAllOf1) ApplyDefaults() { + if u.Cat != nil { + u.Cat.ApplyDefaults() + } + if u.Dog != nil { + u.Dog.ApplyDefaults() + } +} + +// #/components/schemas/OneOfWithAllOf +type OneOfWithAllOf struct { + OneOfWithAllOfOneOf0 *OneOfWithAllOfOneOf0 + OneOfWithAllOfOneOf1 *OneOfWithAllOfOneOf1 +} + +func (u OneOfWithAllOf) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.OneOfWithAllOfOneOf0 != nil { + count++ + data, err = json.Marshal(u.OneOfWithAllOfOneOf0) + if err != nil { + return nil, err + } + } + if u.OneOfWithAllOfOneOf1 != nil { + count++ + data, err = json.Marshal(u.OneOfWithAllOfOneOf1) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("OneOfWithAllOf: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *OneOfWithAllOf) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 OneOfWithAllOfOneOf0 + if err := json.Unmarshal(data, &v0); err == nil { + u.OneOfWithAllOfOneOf0 = &v0 + successCount++ + } + + var v1 OneOfWithAllOfOneOf1 + if err := json.Unmarshal(data, &v1); err == nil { + u.OneOfWithAllOfOneOf1 = &v1 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("OneOfWithAllOf: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *OneOfWithAllOf) ApplyDefaults() { + if u.OneOfWithAllOfOneOf0 != nil { + u.OneOfWithAllOfOneOf0.ApplyDefaults() + } + if u.OneOfWithAllOfOneOf1 != nil { + u.OneOfWithAllOfOneOf1.ApplyDefaults() + } +} + +// #/components/schemas/OneOfWithAllOf/oneOf/0 +type OneOfWithAllOfOneOf0 struct { + ID *int `json:"id,omitempty" form:"id,omitempty"` + CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` + Variant *string `json:"variant,omitempty" form:"variant,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *OneOfWithAllOfOneOf0) ApplyDefaults() { +} + +// #/components/schemas/OneOfWithAllOf/oneOf/1 +type OneOfWithAllOfOneOf1 struct { + ID *int `json:"id,omitempty" form:"id,omitempty"` + CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` + Variant *string `json:"variant,omitempty" form:"variant,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *OneOfWithAllOfOneOf1) ApplyDefaults() { +} + +// #/components/schemas/TreeNode +type TreeNode struct { + Value *string `json:"value,omitempty" form:"value,omitempty"` + Children []TreeNode `json:"children,omitempty" form:"children,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *TreeNode) ApplyDefaults() { +} + +// #/components/schemas/TreeNode/properties/children +type TreeNodeChildren = []TreeNode + +// #/components/schemas/LinkedListNode +type LinkedListNode struct { + Value *int `json:"value,omitempty" form:"value,omitempty"` + Next *LinkedListNode `json:"next,omitempty" form:"next,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *LinkedListNode) ApplyDefaults() { + if s.Next != nil { + s.Next.ApplyDefaults() + } +} + +// #/components/schemas/RecursiveOneOf +type RecursiveOneOf struct { + String0 *string + RecursiveOneOfOneOf1 *RecursiveOneOfOneOf1 +} + +func (u RecursiveOneOf) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.String0 != nil { + count++ + data, err = json.Marshal(u.String0) + if err != nil { + return nil, err + } + } + if u.RecursiveOneOfOneOf1 != nil { + count++ + data, err = json.Marshal(u.RecursiveOneOfOneOf1) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("RecursiveOneOf: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *RecursiveOneOf) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 string + if err := json.Unmarshal(data, &v0); err == nil { + u.String0 = &v0 + successCount++ + } + + var v1 RecursiveOneOfOneOf1 + if err := json.Unmarshal(data, &v1); err == nil { + u.RecursiveOneOfOneOf1 = &v1 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("RecursiveOneOf: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *RecursiveOneOf) ApplyDefaults() { + if u.RecursiveOneOfOneOf1 != nil { + u.RecursiveOneOfOneOf1.ApplyDefaults() + } +} + +// #/components/schemas/RecursiveOneOf/oneOf/1 +type RecursiveOneOfOneOf1 struct { + Nested *RecursiveOneOf `json:"nested,omitempty" form:"nested,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *RecursiveOneOfOneOf1) ApplyDefaults() { + if s.Nested != nil { + s.Nested.ApplyDefaults() + } +} + +// #/components/schemas/ReadWriteOnly +type ReadWriteOnly struct { + ID int `json:"id" form:"id"` + Password string `json:"password" form:"password"` + Name *string `json:"name,omitempty" form:"name,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ReadWriteOnly) ApplyDefaults() { +} + +// #/components/schemas/WithDefaults +type WithDefaults struct { + StringWithDefault *string `json:"stringWithDefault,omitempty" form:"stringWithDefault,omitempty"` + IntWithDefault *int `json:"intWithDefault,omitempty" form:"intWithDefault,omitempty"` + BoolWithDefault *bool `json:"boolWithDefault,omitempty" form:"boolWithDefault,omitempty"` + ArrayWithDefault []string `json:"arrayWithDefault,omitempty" form:"arrayWithDefault,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *WithDefaults) ApplyDefaults() { + if s.StringWithDefault == nil { + v := "default_value" + s.StringWithDefault = &v + } + if s.IntWithDefault == nil { + v := 42 + s.IntWithDefault = &v + } + if s.BoolWithDefault == nil { + v := true + s.BoolWithDefault = &v + } +} + +// #/components/schemas/WithConst +type WithConst struct { + Version *string `json:"version,omitempty" form:"version,omitempty"` + Type *string `json:"type,omitempty" form:"type,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *WithConst) ApplyDefaults() { +} + +// #/components/schemas/WithConstraints +type WithConstraints struct { + BoundedInt *int `json:"boundedInt,omitempty" form:"boundedInt,omitempty"` + ExclusiveBoundedInt *int `json:"exclusiveBoundedInt,omitempty" form:"exclusiveBoundedInt,omitempty"` + MultipleOf *int `json:"multipleOf,omitempty" form:"multipleOf,omitempty"` + BoundedString *string `json:"boundedString,omitempty" form:"boundedString,omitempty"` + PatternString *string `json:"patternString,omitempty" form:"patternString,omitempty"` + BoundedArray []string `json:"boundedArray,omitempty" form:"boundedArray,omitempty"` + UniqueArray []string `json:"uniqueArray,omitempty" form:"uniqueArray,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *WithConstraints) ApplyDefaults() { +} + +// #/components/schemas/TypeArray31 +type TypeArray31 struct { + Name *string `json:"name,omitempty" form:"name,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *TypeArray31) ApplyDefaults() { +} + +// #/components/schemas/ExplicitAny +type ExplicitAny = Nullable[string] + +// #/components/schemas/ComplexNested +type ComplexNested struct { + Metadata map[string]any `json:"metadata,omitempty" form:"metadata,omitempty"` + Items []ComplexNestedItemItem `json:"items,omitempty" form:"items,omitempty"` + Config *ComplexNestedConfig `json:"config,omitempty" form:"config,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ComplexNested) ApplyDefaults() { +} + +// #/components/schemas/ComplexNested/properties/metadata +type ComplexNestedMetadata = map[string]any + +// #/components/schemas/ComplexNested/properties/metadata/additionalProperties +type ComplexNestedMetadataValue struct { + String0 *string + Int1 *int + LBracketString2 *[]string +} + +func (u ComplexNestedMetadataValue) MarshalJSON() ([]byte, error) { + if u.String0 != nil { + return json.Marshal(u.String0) + } + if u.Int1 != nil { + return json.Marshal(u.Int1) + } + if u.LBracketString2 != nil { + return json.Marshal(u.LBracketString2) + } + return []byte("null"), nil +} + +func (u *ComplexNestedMetadataValue) UnmarshalJSON(data []byte) error { + var v0 string + if err := json.Unmarshal(data, &v0); err == nil { + u.String0 = &v0 + } + + var v1 int + if err := json.Unmarshal(data, &v1); err == nil { + u.Int1 = &v1 + } + + var v2 []string + if err := json.Unmarshal(data, &v2); err == nil { + u.LBracketString2 = &v2 + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *ComplexNestedMetadataValue) ApplyDefaults() { +} + +// #/components/schemas/ComplexNested/properties/items +type ComplexNestedItem = []ComplexNestedItemItem + +// #/components/schemas/ComplexNested/properties/items/items +type ComplexNestedItemItem struct { + ID *int `json:"id,omitempty" form:"id,omitempty"` + CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` + Tags []string `json:"tags,omitempty" form:"tags,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ComplexNestedItemItem) ApplyDefaults() { +} + +// #/components/schemas/ComplexNested/properties/config +type ComplexNestedConfig struct { + ComplexNestedConfigOneOf0 *ComplexNestedConfigOneOf0 + ComplexNestedConfigOneOf1 *ComplexNestedConfigOneOf1 +} + +func (u ComplexNestedConfig) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.ComplexNestedConfigOneOf0 != nil { + count++ + data, err = json.Marshal(u.ComplexNestedConfigOneOf0) + if err != nil { + return nil, err + } + } + if u.ComplexNestedConfigOneOf1 != nil { + count++ + data, err = json.Marshal(u.ComplexNestedConfigOneOf1) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("ComplexNestedConfig: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *ComplexNestedConfig) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 ComplexNestedConfigOneOf0 + if err := json.Unmarshal(data, &v0); err == nil { + u.ComplexNestedConfigOneOf0 = &v0 + successCount++ + } + + var v1 ComplexNestedConfigOneOf1 + if err := json.Unmarshal(data, &v1); err == nil { + u.ComplexNestedConfigOneOf1 = &v1 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("ComplexNestedConfig: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *ComplexNestedConfig) ApplyDefaults() { + if u.ComplexNestedConfigOneOf0 != nil { + u.ComplexNestedConfigOneOf0.ApplyDefaults() + } + if u.ComplexNestedConfigOneOf1 != nil { + u.ComplexNestedConfigOneOf1.ApplyDefaults() + } +} + +// #/components/schemas/ComplexNested/properties/config/oneOf/0 +type ComplexNestedConfigOneOf0 struct { + Mode *string `json:"mode,omitempty" form:"mode,omitempty"` + Value *string `json:"value,omitempty" form:"value,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ComplexNestedConfigOneOf0) ApplyDefaults() { +} + +// #/components/schemas/ComplexNested/properties/config/oneOf/1 +type ComplexNestedConfigOneOf1 struct { + Mode *string `json:"mode,omitempty" form:"mode,omitempty"` + Options map[string]string `json:"options,omitempty" form:"options,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ComplexNestedConfigOneOf1) ApplyDefaults() { +} + +// #/components/schemas/ComplexNested/properties/config/oneOf/1/properties/options +type ComplexNestedConfigOneOf1Options = map[string]string + +// #/components/schemas/StringMap +type StringMap = map[string]string + +// #/components/schemas/ObjectMap +type ObjectMap = map[string]any + +// #/components/schemas/NestedMap +type NestedMap = map[string]map[string]string + +// #/components/schemas/NestedMap/additionalProperties +type NestedMapValue = map[string]string + +// #/paths//inline-response/get/responses/200/content/application/json/schema +type GetInlineResponseJSONResponse struct { + ID int `json:"id" form:"id"` + Name string `json:"name" form:"name"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *GetInlineResponseJSONResponse) ApplyDefaults() { +} + +const DateFormat = "2006-01-02" + +type Date struct { + time.Time +} + +func (d Date) MarshalJSON() ([]byte, error) { + return json.Marshal(d.Format(DateFormat)) +} + +func (d *Date) UnmarshalJSON(data []byte) error { + var dateStr string + err := json.Unmarshal(data, &dateStr) + if err != nil { + return err + } + parsed, err := time.Parse(DateFormat, dateStr) + if err != nil { + return err + } + d.Time = parsed + return nil +} + +func (d Date) String() string { + return d.Format(DateFormat) +} + +func (d *Date) UnmarshalText(data []byte) error { + parsed, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = parsed + return nil +} + +const ( + emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" +) + +var ( + emailRegex = regexp.MustCompile(emailRegexString) +) + +// ErrValidationEmail is the sentinel error returned when an email fails validation +var ErrValidationEmail = errors.New("email: failed to pass regex validation") + +// Email represents an email address. +// It is a string type that must pass regex validation before being marshalled +// to JSON or unmarshalled from JSON. +type Email string + +func (e Email) MarshalJSON() ([]byte, error) { + if !emailRegex.MatchString(string(e)) { + return nil, ErrValidationEmail + } + + return json.Marshal(string(e)) +} + +func (e *Email) UnmarshalJSON(data []byte) error { + if e == nil { + return nil + } + + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + *e = Email(s) + if !emailRegex.MatchString(s) { + return ErrValidationEmail + } + + return nil +} + +type File struct { + multipart *multipart.FileHeader + data []byte + filename string +} + +func (file *File) InitFromMultipart(header *multipart.FileHeader) { + file.multipart = header + file.data = nil + file.filename = "" +} + +func (file *File) InitFromBytes(data []byte, filename string) { + file.data = data + file.filename = filename + file.multipart = nil +} + +func (file File) MarshalJSON() ([]byte, error) { + b, err := file.Bytes() + if err != nil { + return nil, err + } + return json.Marshal(b) +} + +func (file *File) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &file.data) +} + +func (file File) Bytes() ([]byte, error) { + if file.multipart != nil { + f, err := file.multipart.Open() + if err != nil { + return nil, err + } + defer func() { _ = f.Close() }() + return io.ReadAll(f) + } + return file.data, nil +} + +func (file File) Reader() (io.ReadCloser, error) { + if file.multipart != nil { + return file.multipart.Open() + } + return io.NopCloser(bytes.NewReader(file.data)), nil +} + +func (file File) Filename() string { + if file.multipart != nil { + return file.multipart.Filename + } + return file.filename +} + +func (file File) FileSize() int64 { + if file.multipart != nil { + return file.multipart.Size + } + return int64(len(file.data)) +} + +// Nullable is a generic type that can distinguish between: +// - Field not provided (unspecified) +// - Field explicitly set to null +// - Field has a value +// +// This is implemented as a map[bool]T where: +// - Empty map: unspecified +// - map[false]T: explicitly null +// - map[true]T: has a value +type Nullable[T any] map[bool]T + +// NewNullableWithValue creates a Nullable with the given value. +func NewNullableWithValue[T any](value T) Nullable[T] { + return Nullable[T]{true: value} +} + +// NewNullNullable creates a Nullable that is explicitly null. +func NewNullNullable[T any]() Nullable[T] { + return Nullable[T]{false: *new(T)} +} + +// Get returns the value if set, or an error if null or unspecified. +func (n Nullable[T]) Get() (T, error) { + if v, ok := n[true]; ok { + return v, nil + } + var zero T + if n.IsNull() { + return zero, ErrNullableIsNull + } + return zero, ErrNullableNotSpecified +} + +// MustGet returns the value or panics if null or unspecified. +func (n Nullable[T]) MustGet() T { + v, err := n.Get() + if err != nil { + panic(err) + } + return v +} + +// Set assigns a value. +func (n *Nullable[T]) Set(value T) { + *n = Nullable[T]{true: value} +} + +// SetNull marks the field as explicitly null. +func (n *Nullable[T]) SetNull() { + *n = Nullable[T]{false: *new(T)} +} + +// SetUnspecified clears the field (as if it was never set). +func (n *Nullable[T]) SetUnspecified() { + *n = nil +} + +// IsNull returns true if the field is explicitly null. +func (n Nullable[T]) IsNull() bool { + if n == nil { + return false + } + _, ok := n[false] + return ok +} + +// IsSpecified returns true if the field was provided (either null or a value). +func (n Nullable[T]) IsSpecified() bool { + return len(n) > 0 +} + +// MarshalJSON implements json.Marshaler. +func (n Nullable[T]) MarshalJSON() ([]byte, error) { + if n.IsNull() { + return []byte("null"), nil + } + if v, ok := n[true]; ok { + return json.Marshal(v) + } + // Unspecified - this shouldn't be called if omitempty is used correctly + return []byte("null"), nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (n *Nullable[T]) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + n.SetNull() + return nil + } + var v T + if err := json.Unmarshal(data, &v); err != nil { + return err + } + n.Set(v) + return nil +} + +// ErrNullableIsNull is returned when trying to get a value from a null Nullable. +var ErrNullableIsNull = errors.New("nullable value is null") + +// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. +var ErrNullableNotSpecified = errors.New("nullable value is not specified") + +type UUID = uuid.UUID + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/+wcXW8iOfKdX2Gx+3Zi8jGz84B0D4QwEnsEEJCdi1Z7J4cuEu90u3vcJgm32v9+st10", + "tz/6y2RGs6PNEynb9eVyVblcECdAcUKGqP/2zcWb836P0F087CHECQ9hiMZxlDB4BJqSJ0AbSDlabx8h", + "wj2EnoClJKZD1L94cy7WIhRAumUk4RIsZqcIhyFK5RKUYM6B0RTtYoZinJDBNg7gAWivl2D+mAq6ZymJ", + "khDER4QegKsPCMUJMCzwToOhgK/ltGyQQZrENIX0OBuh/uX5eb/412BNrc7XlaZtY8qB8vJKhHCShGQr", + "yZ/9nsZUH0WZfCYUoR8Z7Iao/8PZNo6SmALl6Zmam54pFhb3v8OW93tCckJDQmFwZKpRBVM5f6XL0FkV", + "CguKJSOI0C+uFH5IYJjRswYZfN4TBoG9DKEBIoETTHEE1kDChL44KWui+CNOCkfmCOXwAMwxQ5CqW5ly", + "RuiD3M4EMxwBB5ae/SHMeyn+/1MtLsaOyJQUQ5TPzIkQqqA9S0eIs30huK1vjaUCvItZhPkQ7feZPmts", + "7CPJ2JHMZpNs7gv+P++BHdYmSSGDHOnV24eTYw31lHJvvOa2aohHjOGDN2psrEaIcIgs06uV7xFwAEzf", + "fMWHGumuu87OYPGvb8MVChOHlA/u4+CQnZg4dRupGFip6VdxcOgVZySDDJsOjkPOeindMtZJOArDzSGB", + "dJVx0P+rb1C0D7kI3yUGanfoRswfq+l1W/QV9kIXxk3nZfD8/DwQjnKwZyFQkacEX4qw1GWCGT+TBAPM", + "cRtSNXG0LvbtSOiMYBXBwgwa94RidrAmRMCxzfj35Zg0meCFnyUhJh3TnmN+UBATMzN6atEP6J/t/7IV", + "y9X0ZrqZ/jJBm7vlZI0GaBSG2Z6lHljlEtNpHWVyGJ6dtg1EtP1AIAx00NtLB/D9OxO4C2NsLQ/i/X0I", + "JpTuo3tgJlQp2oTex3FoYcUcXLANiSy4SJlMGESYWEj3jJigxzjlItBb8idP7xyw9xbvB5tPdRpNaILT", + "9DlmGqcul3DcITtnNDOlYuOa5xa+Qq4q48j2uSOO9+9yeGEWNg5lBw4UclEOLxlRBxxqVT5QsrlGJCVL", + "bEzOc/u0Z4ohwLSQ42i2HTJ+sUZDkNt4RyQDTkpXrvxU+Fw+xF9xhDpgkIsKHrID14UFRnKodjg74Diu", + "K6z8eJo7IBFrygjeeyB4X9jQobthiDUFgsKpdEGhJwaaD+qA5rhOD0ELmQbgsCYE/e3g/nZw36OD884J", + "57ez2ehqlqWEvkngfB+G+D4EzySQZsvXVskjGyjXcwrooulk63gtpfb0AqHjVjNAfYGjb2Gc6hcLFzpX", + "ebASnxKlCaXjGmdhrL7XuaqSZj2y2EpPZ/rtqtz7iIxWq9Gd99GQFUMZnjpqUulGLrfdgVlJdNQRnX6D", + "UH4qRlPJShB/rN2v1uoJZHEi3crKSHVdxPUeUPcW4H4HcG4MhZRDcKo0pxSYj+fMzYNxpmxCDkfU1igl", + "so+EP45jmnKGSVbwqJctInQqCaCLMhS/HKHnHVjxdg6Lq58n4423dygbeNfKSe2lvUV2aVqnIxZIo2zk", + "zsVAvOfAbOzWcas6bIRSff0JxcwnHO6bq5n+8eH6erqZLuajGVquFsvJajP1z6RGQUBU4F2yOElH9FCj", + "eKxNzsRXzxYuZPOYQmdsOxzmr7wuJX+i8TOtNyODDREMg6589Ny27MLfaK/1BDo9cVjE5VOo+ND1Etp4", + "YNuo5VRTnsxvb/x9mSQ+oftIl13z9lAaFl5Mns0LE3BpAt4qElOlEZuGriqTSBl/GXWGVe2n2DkbcYs0", + "DfN92uLCpjOV1WCBBnYCjLecPIEBFNYYAofiupgwEjPCD23qCC7iF8b/l8b/uc5FtiU0M6WZ5R06V1gE", + "ivUpmoqpCdjtTnDZs9niAxqgKX0ERjimWzgbi+OeyiPmewKucAr24Xw1D4DQlgEWaSL3KlTIwckLBxqY", + "UR2H4WJXPjJ1XlAXs19aVRGg3f06VkuO98W1PFB+EGy87F4DJCUb8NKGrtAW2qgSE144w808jwRz8qU8", + "CWEFu/SV97D7fayzpBGwB7t7Sy/wFbIqB7Sg4aFS0o70d4SlvGXXTUfUKWxjWiGalrR4ua35nXRbt5TE", + "FMmigneeSQ+L3ZKRiIhYU9iQANuardCKnoWJpco0qtH5GFcXMy44uSEvhYm1Fesr2L6KhW1sX/B8rMb5", + "7U+58lUkOdnWe0Vy6yplMFTDlMtwXGO6EsSlf7EblanY5QDjdm/xNPDq3/CwPK8KwnwiD/U1EYErIlRE", + "eHXEvU/3gsJity71SCMUC9BXP4qSEWF0Jeli5sXTGLdm5Tp+OE4NXHRz2z7MVTstcOFM61i+wUlSKmh/", + "a5wX5S+NTZkziiSwBWMIBfFDLV9y6hh3LVSZPA5QBPFzjZPJFjTmuAJN46TnR5J+AjYD+sAfK18pVTYY", + "P5ws2j1mn15BNIGmcRLHJGwjl7ToqRZ3LPvtGMVimWCPvkwOpZBftUiipGR2FlMhXQVfxkO1Mw4V0TMj", + "6XcPbnHFe8KMYP2pyxCno8Pp5HRqHE+D2/QJfePFzdV0PrkWH5aLtayintCHqNz2oqytV7oT/SV2IA9c", + "o7LQ1lkwVOKvltqz3VyTt8y8ZY/xNqapiGi4/x0KdX9CIrmajG9X67y91/cgbRjAPA7g1JuBU+LtIwkD", + "BvTVX4qPXGf6mxH6CYIZSfnriWK9n8GLttV17On8ZEyuYLtnKXkCzWV1C16tg6p6YO7yTQSdvZMMc3R9", + "9nE13UzQYj678zXMFeDgIyNcKwV1fSwtdz6fGKkFJRxIZvSvCh3xtyjSPufy6Cia32Z9NuJ68mF0O9us", + "0WguYu58vfHdC3k/gh3eh9yvr6WEoIWagmwm6mef/ivPaLklhNditPcuR/nuUus7rEVjdh+W0Gjbl3c0", + "VGLybeApUfz1t2IvZOdEVzeXfTO7Wf3H6JR/g9vdGNKwekdeIOifki3O15vVaDr3b7eoaDJpqbD7eE8D", + "CFyNaA4Diwgl0T4aonO9PUUBL84LMLxsw71wtFedCOTLblyUilEXySh7P9CTpkpRitnoJ1MdFZ1+th1E", + "hGa3VbNj5wi+/OmnkhOVPwPQGns2f4j6//kVD/732z9+7JuctuyuetXGorw5mJLPe2jJgZqckdMcy5fs", + "ZFpO5qPlFL19c4HWy8l4+mE69s4hD4mS9e2FdsZ6lb2sRvuYs7W0MSYuGeyI2iaDsKbipJjW+amjqkrg", + "3JxXasq4WW7u0Bkaze9kZu+7KZMo4Qf1uxxD9Mef+SvDL+V0V/s+5Wi7hYSnCNMD+nm9mKvM+PiOnIRk", + "S7jZodSr7O4d2JUWW6tmhDVbDasN55QqxHI2+TeaT9abyTVab1a3483tyv8GNZatGi9zLeNuGWNcX5Gt", + "yPTrG4IqHmdqn2fqH2iKUbv7s7LVtLoT2ZzdOi9y3PVPue033PebfiOE44e07rc+3Lqq1Vf17TmmO/LQ", + "WBr0rF5EpZuyR+lC/SRP37P78luQAAdPmG6L310wK9I1xl1hOm0O6esGi5vR8sQGvhucnNicWZZBFc5P", + "wdmxH1N53dOFMHa0aR81yf8fAAD//ynhbdUwSwAA", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/default_values/default_values.yaml b/experimental/internal/codegen/test/default_values/default_values.yaml new file mode 100644 index 0000000000..8f773e63ee --- /dev/null +++ b/experimental/internal/codegen/test/default_values/default_values.yaml @@ -0,0 +1,158 @@ +openapi: "3.1.0" +info: + title: Default Values Test + version: "1.0" +paths: {} +components: + schemas: + # Basic primitives with defaults + SimpleDefaults: + type: object + properties: + stringField: + type: string + default: "hello" + intField: + type: integer + default: 42 + boolField: + type: boolean + default: true + floatField: + type: number + default: 3.14 + int64Field: + type: integer + format: int64 + default: 9223372036854775807 + + # Nested objects - should recurse ApplyDefaults + NestedDefaults: + type: object + properties: + name: + type: string + default: "parent" + child: + $ref: '#/components/schemas/SimpleDefaults' + inlineChild: + type: object + properties: + label: + type: string + default: "inline-default" + value: + type: integer + default: 100 + + # Object with additionalProperties and defaults + MapWithDefaults: + type: object + properties: + prefix: + type: string + default: "map-" + additionalProperties: + type: string + + # Array field with defaults + ArrayDefaults: + type: object + properties: + items: + type: array + items: + type: string + default: [] + count: + type: integer + default: 0 + + # anyOf with defaults in members + AnyOfWithDefaults: + type: object + properties: + value: + anyOf: + - type: object + properties: + stringVal: + type: string + default: "default-string" + - type: object + properties: + intVal: + type: integer + default: 999 + + # oneOf with defaults in members + OneOfWithDefaults: + type: object + properties: + variant: + oneOf: + - type: object + properties: + optionA: + type: string + default: "option-a-default" + - type: object + properties: + optionB: + type: integer + default: 123 + + # allOf with defaults + AllOfWithDefaults: + allOf: + - type: object + properties: + base: + type: string + default: "base-value" + - type: object + properties: + extended: + type: integer + default: 50 + + # Deep nesting - multiple levels + DeepNesting: + type: object + properties: + level1: + type: object + properties: + name: + type: string + default: "level1-name" + level2: + type: object + properties: + count: + type: integer + default: 2 + level3: + type: object + properties: + enabled: + type: boolean + default: false + + # Required vs optional with defaults + RequiredAndOptional: + type: object + required: + - requiredWithDefault + - requiredNoDefault + properties: + requiredWithDefault: + type: string + default: "required-default" + requiredNoDefault: + type: string + optionalWithDefault: + type: string + default: "optional-default" + optionalNoDefault: + type: string diff --git a/experimental/internal/codegen/test/default_values/output/default_values.gen.go b/experimental/internal/codegen/test/default_values/output/default_values.gen.go new file mode 100644 index 0000000000..a8b988994d --- /dev/null +++ b/experimental/internal/codegen/test/default_values/output/default_values.gen.go @@ -0,0 +1,514 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "encoding/json" + "fmt" +) + +// #/components/schemas/SimpleDefaults +type SimpleDefaultsSchemaComponent struct { + StringField *string `json:"stringField,omitempty" form:"stringField,omitempty"` + IntField *int `json:"intField,omitempty" form:"intField,omitempty"` + BoolField *bool `json:"boolField,omitempty" form:"boolField,omitempty"` + FloatField *float32 `json:"floatField,omitempty" form:"floatField,omitempty"` + Int64Field *int64 `json:"int64Field,omitempty" form:"int64Field,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *SimpleDefaultsSchemaComponent) ApplyDefaults() { + if s.StringField == nil { + v := "hello" + s.StringField = &v + } + if s.IntField == nil { + v := 42 + s.IntField = &v + } + if s.BoolField == nil { + v := true + s.BoolField = &v + } + if s.FloatField == nil { + v := float32(3.14) + s.FloatField = &v + } + if s.Int64Field == nil { + v := int64(9223372036854775807) + s.Int64Field = &v + } +} + +type SimpleDefaults = SimpleDefaultsSchemaComponent + +// #/components/schemas/NestedDefaults +type NestedDefaultsSchemaComponent struct { + Name *string `json:"name,omitempty" form:"name,omitempty"` + Child *SimpleDefaults `json:"child,omitempty" form:"child,omitempty"` + InlineChild *NestedDefaultsInlineChild `json:"inlineChild,omitempty" form:"inlineChild,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *NestedDefaultsSchemaComponent) ApplyDefaults() { + if s.Name == nil { + v := "parent" + s.Name = &v + } + if s.Child != nil { + s.Child.ApplyDefaults() + } + if s.InlineChild != nil { + s.InlineChild.ApplyDefaults() + } +} + +type NestedDefaults = NestedDefaultsSchemaComponent + +// #/components/schemas/NestedDefaults/properties/inlineChild +type NestedDefaultsInlineChildPropertySchemaComponent struct { + Label *string `json:"label,omitempty" form:"label,omitempty"` + Value *int `json:"value,omitempty" form:"value,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *NestedDefaultsInlineChildPropertySchemaComponent) ApplyDefaults() { + if s.Label == nil { + v := "inline-default" + s.Label = &v + } + if s.Value == nil { + v := 100 + s.Value = &v + } +} + +type NestedDefaultsInlineChild = NestedDefaultsInlineChildPropertySchemaComponent + +// #/components/schemas/MapWithDefaults +type MapWithDefaultsSchemaComponent struct { + Prefix *string `json:"prefix,omitempty" form:"prefix,omitempty"` + AdditionalProperties map[string]string `json:"-"` +} + +func (s MapWithDefaultsSchemaComponent) MarshalJSON() ([]byte, error) { + result := make(map[string]any) + + if s.Prefix != nil { + result["prefix"] = s.Prefix + } + + // Add additional properties + for k, v := range s.AdditionalProperties { + result[k] = v + } + + return json.Marshal(result) +} + +func (s *MapWithDefaultsSchemaComponent) UnmarshalJSON(data []byte) error { + // Known fields + knownFields := map[string]bool{ + "prefix": true, + } + + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + if v, ok := raw["prefix"]; ok { + var val string + if err := json.Unmarshal(v, &val); err != nil { + return err + } + s.Prefix = &val + } + + // Collect additional properties + s.AdditionalProperties = make(map[string]string) + for k, v := range raw { + if !knownFields[k] { + var val string + if err := json.Unmarshal(v, &val); err != nil { + return err + } + s.AdditionalProperties[k] = val + } + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *MapWithDefaultsSchemaComponent) ApplyDefaults() { + if s.Prefix == nil { + v := "map-" + s.Prefix = &v + } +} + +type MapWithDefaults = MapWithDefaultsSchemaComponent + +// #/components/schemas/ArrayDefaults +type ArrayDefaultsSchemaComponent struct { + Items []string `json:"items,omitempty" form:"items,omitempty"` + Count *int `json:"count,omitempty" form:"count,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ArrayDefaultsSchemaComponent) ApplyDefaults() { + if s.Count == nil { + v := 0 + s.Count = &v + } +} + +type ArrayDefaults = ArrayDefaultsSchemaComponent + +// #/components/schemas/AnyOfWithDefaults +type AnyOfWithDefaultsSchemaComponent struct { + Value *AnyOfWithDefaultsValue `json:"value,omitempty" form:"value,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AnyOfWithDefaultsSchemaComponent) ApplyDefaults() { +} + +type AnyOfWithDefaults = AnyOfWithDefaultsSchemaComponent + +// #/components/schemas/AnyOfWithDefaults/properties/value +type AnyOfWithDefaultsValuePropertySchemaComponent struct { + AnyOfWithDefaultsValueAnyOf0 *AnyOfWithDefaultsValueAnyOf0 + AnyOfWithDefaultsValueAnyOf1 *AnyOfWithDefaultsValueAnyOf1 +} + +func (u AnyOfWithDefaultsValuePropertySchemaComponent) MarshalJSON() ([]byte, error) { + result := make(map[string]any) + + if u.AnyOfWithDefaultsValueAnyOf0 != nil { + data, err := json.Marshal(u.AnyOfWithDefaultsValueAnyOf0) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err == nil { + for k, v := range m { + result[k] = v + } + } + } + if u.AnyOfWithDefaultsValueAnyOf1 != nil { + data, err := json.Marshal(u.AnyOfWithDefaultsValueAnyOf1) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err == nil { + for k, v := range m { + result[k] = v + } + } + } + + return json.Marshal(result) +} + +func (u *AnyOfWithDefaultsValuePropertySchemaComponent) UnmarshalJSON(data []byte) error { + var v0 AnyOfWithDefaultsValueAnyOf0 + if err := json.Unmarshal(data, &v0); err == nil { + u.AnyOfWithDefaultsValueAnyOf0 = &v0 + } + + var v1 AnyOfWithDefaultsValueAnyOf1 + if err := json.Unmarshal(data, &v1); err == nil { + u.AnyOfWithDefaultsValueAnyOf1 = &v1 + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *AnyOfWithDefaultsValuePropertySchemaComponent) ApplyDefaults() { + if u.AnyOfWithDefaultsValueAnyOf0 != nil { + u.AnyOfWithDefaultsValueAnyOf0.ApplyDefaults() + } + if u.AnyOfWithDefaultsValueAnyOf1 != nil { + u.AnyOfWithDefaultsValueAnyOf1.ApplyDefaults() + } +} + +type AnyOfWithDefaultsValue = AnyOfWithDefaultsValuePropertySchemaComponent + +// #/components/schemas/AnyOfWithDefaults/properties/value/anyOf/0 +type AnyOfWithDefaultsValueN0AnyOfPropertySchemaComponent struct { + StringVal *string `json:"stringVal,omitempty" form:"stringVal,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AnyOfWithDefaultsValueN0AnyOfPropertySchemaComponent) ApplyDefaults() { + if s.StringVal == nil { + v := "default-string" + s.StringVal = &v + } +} + +type AnyOfWithDefaultsValueAnyOf0 = AnyOfWithDefaultsValueN0AnyOfPropertySchemaComponent + +// #/components/schemas/AnyOfWithDefaults/properties/value/anyOf/1 +type AnyOfWithDefaultsValueN1AnyOfPropertySchemaComponent struct { + IntVal *int `json:"intVal,omitempty" form:"intVal,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AnyOfWithDefaultsValueN1AnyOfPropertySchemaComponent) ApplyDefaults() { + if s.IntVal == nil { + v := 999 + s.IntVal = &v + } +} + +type AnyOfWithDefaultsValueAnyOf1 = AnyOfWithDefaultsValueN1AnyOfPropertySchemaComponent + +// #/components/schemas/OneOfWithDefaults +type OneOfWithDefaultsSchemaComponent struct { + Variant *OneOfWithDefaultsVariant `json:"variant,omitempty" form:"variant,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *OneOfWithDefaultsSchemaComponent) ApplyDefaults() { +} + +type OneOfWithDefaults = OneOfWithDefaultsSchemaComponent + +// #/components/schemas/OneOfWithDefaults/properties/variant +type OneOfWithDefaultsVariantPropertySchemaComponent struct { + OneOfWithDefaultsVariantOneOf0 *OneOfWithDefaultsVariantOneOf0 + OneOfWithDefaultsVariantOneOf1 *OneOfWithDefaultsVariantOneOf1 +} + +func (u OneOfWithDefaultsVariantPropertySchemaComponent) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.OneOfWithDefaultsVariantOneOf0 != nil { + count++ + data, err = json.Marshal(u.OneOfWithDefaultsVariantOneOf0) + if err != nil { + return nil, err + } + } + if u.OneOfWithDefaultsVariantOneOf1 != nil { + count++ + data, err = json.Marshal(u.OneOfWithDefaultsVariantOneOf1) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("OneOfWithDefaultsVariantPropertySchemaComponent: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *OneOfWithDefaultsVariantPropertySchemaComponent) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 OneOfWithDefaultsVariantOneOf0 + if err := json.Unmarshal(data, &v0); err == nil { + u.OneOfWithDefaultsVariantOneOf0 = &v0 + successCount++ + } + + var v1 OneOfWithDefaultsVariantOneOf1 + if err := json.Unmarshal(data, &v1); err == nil { + u.OneOfWithDefaultsVariantOneOf1 = &v1 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("OneOfWithDefaultsVariantPropertySchemaComponent: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *OneOfWithDefaultsVariantPropertySchemaComponent) ApplyDefaults() { + if u.OneOfWithDefaultsVariantOneOf0 != nil { + u.OneOfWithDefaultsVariantOneOf0.ApplyDefaults() + } + if u.OneOfWithDefaultsVariantOneOf1 != nil { + u.OneOfWithDefaultsVariantOneOf1.ApplyDefaults() + } +} + +type OneOfWithDefaultsVariant = OneOfWithDefaultsVariantPropertySchemaComponent + +// #/components/schemas/OneOfWithDefaults/properties/variant/oneOf/0 +type OneOfWithDefaultsVariantN0OneOfPropertySchemaComponent struct { + OptionA *string `json:"optionA,omitempty" form:"optionA,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *OneOfWithDefaultsVariantN0OneOfPropertySchemaComponent) ApplyDefaults() { + if s.OptionA == nil { + v := "option-a-default" + s.OptionA = &v + } +} + +type OneOfWithDefaultsVariantOneOf0 = OneOfWithDefaultsVariantN0OneOfPropertySchemaComponent + +// #/components/schemas/OneOfWithDefaults/properties/variant/oneOf/1 +type OneOfWithDefaultsVariantN1OneOfPropertySchemaComponent struct { + OptionB *int `json:"optionB,omitempty" form:"optionB,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *OneOfWithDefaultsVariantN1OneOfPropertySchemaComponent) ApplyDefaults() { + if s.OptionB == nil { + v := 123 + s.OptionB = &v + } +} + +type OneOfWithDefaultsVariantOneOf1 = OneOfWithDefaultsVariantN1OneOfPropertySchemaComponent + +// #/components/schemas/AllOfWithDefaults +type AllOfWithDefaultsSchemaComponent struct { + Base *string `json:"base,omitempty" form:"base,omitempty"` + Extended *int `json:"extended,omitempty" form:"extended,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AllOfWithDefaultsSchemaComponent) ApplyDefaults() { + if s.Base == nil { + v := "base-value" + s.Base = &v + } + if s.Extended == nil { + v := 50 + s.Extended = &v + } +} + +type AllOfWithDefaults = AllOfWithDefaultsSchemaComponent + +// #/components/schemas/AllOfWithDefaults/allOf/0 +type AllOfWithDefaultsN0AllOfSchemaComponent struct { + Base *string `json:"base,omitempty" form:"base,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AllOfWithDefaultsN0AllOfSchemaComponent) ApplyDefaults() { + if s.Base == nil { + v := "base-value" + s.Base = &v + } +} + +type AllOfWithDefaultsAllOf0 = AllOfWithDefaultsN0AllOfSchemaComponent + +// #/components/schemas/AllOfWithDefaults/allOf/1 +type AllOfWithDefaultsN1AllOfSchemaComponent struct { + Extended *int `json:"extended,omitempty" form:"extended,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AllOfWithDefaultsN1AllOfSchemaComponent) ApplyDefaults() { + if s.Extended == nil { + v := 50 + s.Extended = &v + } +} + +type AllOfWithDefaultsAllOf1 = AllOfWithDefaultsN1AllOfSchemaComponent + +// #/components/schemas/DeepNesting +type DeepNestingSchemaComponent struct { + Level1 *DeepNestingLevel1 `json:"level1,omitempty" form:"level1,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *DeepNestingSchemaComponent) ApplyDefaults() { + if s.Level1 != nil { + s.Level1.ApplyDefaults() + } +} + +type DeepNesting = DeepNestingSchemaComponent + +// #/components/schemas/DeepNesting/properties/level1 +type DeepNestingLevel1PropertySchemaComponent struct { + Name *string `json:"name,omitempty" form:"name,omitempty"` + Level2 *DeepNestingLevel1Level2 `json:"level2,omitempty" form:"level2,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *DeepNestingLevel1PropertySchemaComponent) ApplyDefaults() { + if s.Name == nil { + v := "level1-name" + s.Name = &v + } + if s.Level2 != nil { + s.Level2.ApplyDefaults() + } +} + +type DeepNestingLevel1 = DeepNestingLevel1PropertySchemaComponent + +// #/components/schemas/DeepNesting/properties/level1/properties/level2 +type DeepNestingLevel1Level2PropertyPropertySchemaComponent struct { + Count *int `json:"count,omitempty" form:"count,omitempty"` + Level3 *DeepNestingLevel1Level2Level3 `json:"level3,omitempty" form:"level3,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *DeepNestingLevel1Level2PropertyPropertySchemaComponent) ApplyDefaults() { + if s.Count == nil { + v := 2 + s.Count = &v + } + if s.Level3 != nil { + s.Level3.ApplyDefaults() + } +} + +type DeepNestingLevel1Level2 = DeepNestingLevel1Level2PropertyPropertySchemaComponent + +// #/components/schemas/DeepNesting/properties/level1/properties/level2/properties/level3 +type DeepNestingLevel1Level2Level3PropertyPropertyPropertySchemaComponent struct { + Enabled *bool `json:"enabled,omitempty" form:"enabled,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *DeepNestingLevel1Level2Level3PropertyPropertyPropertySchemaComponent) ApplyDefaults() { + if s.Enabled == nil { + v := false + s.Enabled = &v + } +} + +type DeepNestingLevel1Level2Level3 = DeepNestingLevel1Level2Level3PropertyPropertyPropertySchemaComponent + +// #/components/schemas/RequiredAndOptional +type RequiredAndOptionalSchemaComponent struct { + RequiredWithDefault string `json:"requiredWithDefault" form:"requiredWithDefault"` + RequiredNoDefault string `json:"requiredNoDefault" form:"requiredNoDefault"` + OptionalWithDefault *string `json:"optionalWithDefault,omitempty" form:"optionalWithDefault,omitempty"` + OptionalNoDefault *string `json:"optionalNoDefault,omitempty" form:"optionalNoDefault,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *RequiredAndOptionalSchemaComponent) ApplyDefaults() { + if s.OptionalWithDefault == nil { + v := "optional-default" + s.OptionalWithDefault = &v + } +} + +type RequiredAndOptional = RequiredAndOptionalSchemaComponent diff --git a/experimental/internal/codegen/test/default_values/output/default_values_test.go b/experimental/internal/codegen/test/default_values/output/default_values_test.go new file mode 100644 index 0000000000..ccee0f18e3 --- /dev/null +++ b/experimental/internal/codegen/test/default_values/output/default_values_test.go @@ -0,0 +1,324 @@ +package output + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func ptr[T any](v T) *T { + return &v +} + +// TestSimpleDefaults tests ApplyDefaults on basic primitive types +func TestSimpleDefaults(t *testing.T) { + t.Run("applies all defaults to empty struct", func(t *testing.T) { + s := SimpleDefaults{} + s.ApplyDefaults() + + require.NotNil(t, s.StringField) + assert.Equal(t, "hello", *s.StringField) + + require.NotNil(t, s.IntField) + assert.Equal(t, 42, *s.IntField) + + require.NotNil(t, s.BoolField) + assert.Equal(t, true, *s.BoolField) + + require.NotNil(t, s.FloatField) + assert.Equal(t, float32(3.14), *s.FloatField) + + require.NotNil(t, s.Int64Field) + assert.Equal(t, int64(9223372036854775807), *s.Int64Field) + }) + + t.Run("does not overwrite existing values", func(t *testing.T) { + s := SimpleDefaults{ + StringField: ptr("custom"), + IntField: ptr(100), + BoolField: ptr(false), + FloatField: ptr(float32(1.5)), + Int64Field: ptr(int64(123)), + } + s.ApplyDefaults() + + assert.Equal(t, "custom", *s.StringField) + assert.Equal(t, 100, *s.IntField) + assert.Equal(t, false, *s.BoolField) + assert.Equal(t, float32(1.5), *s.FloatField) + assert.Equal(t, int64(123), *s.Int64Field) + }) + + t.Run("applies defaults after unmarshaling empty object", func(t *testing.T) { + input := `{}` + var s SimpleDefaults + err := json.Unmarshal([]byte(input), &s) + require.NoError(t, err) + + s.ApplyDefaults() + + assert.Equal(t, "hello", *s.StringField) + assert.Equal(t, 42, *s.IntField) + }) + + t.Run("applies defaults after unmarshaling partial object", func(t *testing.T) { + input := `{"stringField": "from-json"}` + var s SimpleDefaults + err := json.Unmarshal([]byte(input), &s) + require.NoError(t, err) + + s.ApplyDefaults() + + assert.Equal(t, "from-json", *s.StringField) // from JSON + assert.Equal(t, 42, *s.IntField) // from default + }) +} + +// TestNestedDefaults tests ApplyDefaults recursion into nested structs +func TestNestedDefaults(t *testing.T) { + t.Run("applies defaults to parent and recurses to children", func(t *testing.T) { + n := NestedDefaults{ + Child: &SimpleDefaults{}, + InlineChild: &NestedDefaultsInlineChild{}, + } + n.ApplyDefaults() + + // Parent defaults + require.NotNil(t, n.Name) + assert.Equal(t, "parent", *n.Name) + + // Child defaults (recursion) + require.NotNil(t, n.Child.StringField) + assert.Equal(t, "hello", *n.Child.StringField) + require.NotNil(t, n.Child.IntField) + assert.Equal(t, 42, *n.Child.IntField) + + // Inline child defaults (recursion) + require.NotNil(t, n.InlineChild.Label) + assert.Equal(t, "inline-default", *n.InlineChild.Label) + require.NotNil(t, n.InlineChild.Value) + assert.Equal(t, 100, *n.InlineChild.Value) + }) + + t.Run("does not recurse into nil children", func(t *testing.T) { + n := NestedDefaults{} + n.ApplyDefaults() + + // Parent defaults applied + require.NotNil(t, n.Name) + assert.Equal(t, "parent", *n.Name) + + // Children are still nil (not created) + assert.Nil(t, n.Child) + assert.Nil(t, n.InlineChild) + }) + + t.Run("applies defaults after unmarshaling nested JSON", func(t *testing.T) { + input := `{"child": {"stringField": "from-child"}, "inlineChild": {}}` + var n NestedDefaults + err := json.Unmarshal([]byte(input), &n) + require.NoError(t, err) + + n.ApplyDefaults() + + // Parent defaults + assert.Equal(t, "parent", *n.Name) + + // Child - one field from JSON, others from defaults + assert.Equal(t, "from-child", *n.Child.StringField) + assert.Equal(t, 42, *n.Child.IntField) + + // Inline child - all defaults + assert.Equal(t, "inline-default", *n.InlineChild.Label) + assert.Equal(t, 100, *n.InlineChild.Value) + }) +} + +// TestMapWithDefaults tests ApplyDefaults on structs with additionalProperties +func TestMapWithDefaults(t *testing.T) { + t.Run("applies defaults to known fields", func(t *testing.T) { + m := MapWithDefaults{} + m.ApplyDefaults() + + require.NotNil(t, m.Prefix) + assert.Equal(t, "map-", *m.Prefix) + }) + + t.Run("does not affect additional properties", func(t *testing.T) { + m := MapWithDefaults{ + AdditionalProperties: map[string]string{ + "extra": "value", + }, + } + m.ApplyDefaults() + + assert.Equal(t, "map-", *m.Prefix) + assert.Equal(t, "value", m.AdditionalProperties["extra"]) + }) +} + +// TestArrayDefaults tests ApplyDefaults on structs with array fields +func TestArrayDefaults(t *testing.T) { + t.Run("applies defaults to non-array fields", func(t *testing.T) { + a := ArrayDefaults{} + a.ApplyDefaults() + + require.NotNil(t, a.Count) + assert.Equal(t, 0, *a.Count) + // Array field is not touched (no default generation for arrays currently) + assert.Nil(t, a.Items) + }) +} + +// TestAnyOfWithDefaults tests ApplyDefaults on anyOf variant members +func TestAnyOfWithDefaults(t *testing.T) { + t.Run("applies defaults to anyOf variant 0", func(t *testing.T) { + v0 := AnyOfWithDefaultsValueAnyOf0{} + v0.ApplyDefaults() + + require.NotNil(t, v0.StringVal) + assert.Equal(t, "default-string", *v0.StringVal) + }) + + t.Run("applies defaults to anyOf variant 1", func(t *testing.T) { + v1 := AnyOfWithDefaultsValueAnyOf1{} + v1.ApplyDefaults() + + require.NotNil(t, v1.IntVal) + assert.Equal(t, 999, *v1.IntVal) + }) +} + +// TestOneOfWithDefaults tests ApplyDefaults on oneOf variant members +func TestOneOfWithDefaults(t *testing.T) { + t.Run("applies defaults to oneOf variant 0", func(t *testing.T) { + v0 := OneOfWithDefaultsVariantOneOf0{} + v0.ApplyDefaults() + + require.NotNil(t, v0.OptionA) + assert.Equal(t, "option-a-default", *v0.OptionA) + }) + + t.Run("applies defaults to oneOf variant 1", func(t *testing.T) { + v1 := OneOfWithDefaultsVariantOneOf1{} + v1.ApplyDefaults() + + require.NotNil(t, v1.OptionB) + assert.Equal(t, 123, *v1.OptionB) + }) +} + +// TestAllOfWithDefaults tests ApplyDefaults on allOf merged structs +func TestAllOfWithDefaults(t *testing.T) { + t.Run("applies defaults from all merged schemas", func(t *testing.T) { + a := AllOfWithDefaults{} + a.ApplyDefaults() + + // Default from allOf/0 + require.NotNil(t, a.Base) + assert.Equal(t, "base-value", *a.Base) + + // Default from allOf/1 + require.NotNil(t, a.Extended) + assert.Equal(t, 50, *a.Extended) + }) +} + +// TestDeepNesting tests ApplyDefaults recursion through multiple levels +func TestDeepNesting(t *testing.T) { + t.Run("recurses through all levels", func(t *testing.T) { + d := DeepNesting{ + Level1: &DeepNestingLevel1{ + Level2: &DeepNestingLevel1Level2{ + Level3: &DeepNestingLevel1Level2Level3{}, + }, + }, + } + d.ApplyDefaults() + + // Level 1 defaults + require.NotNil(t, d.Level1.Name) + assert.Equal(t, "level1-name", *d.Level1.Name) + + // Level 2 defaults + require.NotNil(t, d.Level1.Level2.Count) + assert.Equal(t, 2, *d.Level1.Level2.Count) + + // Level 3 defaults + require.NotNil(t, d.Level1.Level2.Level3.Enabled) + assert.Equal(t, false, *d.Level1.Level2.Level3.Enabled) + }) + + t.Run("stops at nil levels", func(t *testing.T) { + d := DeepNesting{ + Level1: &DeepNestingLevel1{ + // Level2 is nil + }, + } + d.ApplyDefaults() + + assert.Equal(t, "level1-name", *d.Level1.Name) + assert.Nil(t, d.Level1.Level2) + }) +} + +// TestRequiredAndOptional tests ApplyDefaults behavior with required fields +func TestRequiredAndOptional(t *testing.T) { + t.Run("applies defaults only to optional pointer fields", func(t *testing.T) { + r := RequiredAndOptional{ + RequiredWithDefault: "set-by-user", + RequiredNoDefault: "also-set", + } + r.ApplyDefaults() + + // Required fields are value types, not pointers, so they don't get defaults applied + assert.Equal(t, "set-by-user", r.RequiredWithDefault) + assert.Equal(t, "also-set", r.RequiredNoDefault) + + // Optional fields with defaults get defaults applied + require.NotNil(t, r.OptionalWithDefault) + assert.Equal(t, "optional-default", *r.OptionalWithDefault) + + // Optional fields without defaults stay nil + assert.Nil(t, r.OptionalNoDefault) + }) +} + +// TestApplyDefaultsIdempotent tests that ApplyDefaults can be called multiple times +func TestApplyDefaultsIdempotent(t *testing.T) { + t.Run("multiple calls have same effect", func(t *testing.T) { + s := SimpleDefaults{} + s.ApplyDefaults() + s.ApplyDefaults() + s.ApplyDefaults() + + assert.Equal(t, "hello", *s.StringField) + assert.Equal(t, 42, *s.IntField) + }) +} + +// TestApplyDefaultsChain tests typical usage pattern: unmarshal then apply defaults +func TestApplyDefaultsChain(t *testing.T) { + t.Run("unmarshal partial JSON then apply defaults", func(t *testing.T) { + input := `{ + "level1": { + "level2": { + "level3": {} + } + } + }` + + var d DeepNesting + err := json.Unmarshal([]byte(input), &d) + require.NoError(t, err) + + d.ApplyDefaults() + + // All defaults applied at all levels + assert.Equal(t, "level1-name", *d.Level1.Name) + assert.Equal(t, 2, *d.Level1.Level2.Count) + assert.Equal(t, false, *d.Level1.Level2.Level3.Enabled) + }) +} diff --git a/experimental/internal/codegen/test/external_ref/config.yaml b/experimental/internal/codegen/test/external_ref/config.yaml new file mode 100644 index 0000000000..919d5b9d47 --- /dev/null +++ b/experimental/internal/codegen/test/external_ref/config.yaml @@ -0,0 +1,5 @@ +package: externalref +output: internal/codegen/test/external_ref/spec.gen.go +import-mapping: + ./packagea/spec.yaml: github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packagea + ./packageb/spec.yaml: github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packageb diff --git a/experimental/internal/codegen/test/external_ref/external_ref_test.go b/experimental/internal/codegen/test/external_ref/external_ref_test.go new file mode 100644 index 0000000000..885ddb05bb --- /dev/null +++ b/experimental/internal/codegen/test/external_ref/external_ref_test.go @@ -0,0 +1,62 @@ +package externalref + +import ( + "testing" + + "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packagea" + "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packageb" +) + +// TestCrossPackageReferences verifies that types from external packages +// can be used correctly in the Container type. +func TestCrossPackageReferences(t *testing.T) { + // Create objects from each package + name := "test-name" + b := &packageb.ObjectB{ + Name: &name, + } + + a := &packagea.ObjectA{ + Name: &name, + ObjectB: b, + } + + // Create container that references both + container := Container{ + ObjectA: a, + ObjectB: b, + } + + // Verify the structure + if container.ObjectA == nil { + t.Error("ObjectA should not be nil") + } + if container.ObjectB == nil { + t.Error("ObjectB should not be nil") + } + if *container.ObjectA.Name != "test-name" { + t.Errorf("ObjectA.Name = %q, want %q", *container.ObjectA.Name, "test-name") + } + if *container.ObjectB.Name != "test-name" { + t.Errorf("ObjectB.Name = %q, want %q", *container.ObjectB.Name, "test-name") + } + + // Verify nested reference in ObjectA + if container.ObjectA.ObjectB == nil { + t.Error("ObjectA.ObjectB should not be nil") + } + if *container.ObjectA.ObjectB.Name != "test-name" { + t.Errorf("ObjectA.ObjectB.Name = %q, want %q", *container.ObjectA.ObjectB.Name, "test-name") + } +} + +// TestApplyDefaults verifies that ApplyDefaults works across package boundaries. +func TestApplyDefaults(t *testing.T) { + container := Container{ + ObjectA: &packagea.ObjectA{}, + ObjectB: &packageb.ObjectB{}, + } + + // Should not panic when calling ApplyDefaults across packages + container.ApplyDefaults() +} diff --git a/experimental/internal/codegen/test/external_ref/packagea/config.yaml b/experimental/internal/codegen/test/external_ref/packagea/config.yaml new file mode 100644 index 0000000000..7cacfcb822 --- /dev/null +++ b/experimental/internal/codegen/test/external_ref/packagea/config.yaml @@ -0,0 +1,4 @@ +package: packagea +output: internal/codegen/test/external_ref/packagea/spec.gen.go +import-mapping: + ../packageb/spec.yaml: github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packageb diff --git a/experimental/internal/codegen/test/external_ref/packagea/spec.gen.go b/experimental/internal/codegen/test/external_ref/packagea/spec.gen.go new file mode 100644 index 0000000000..b865bfb69d --- /dev/null +++ b/experimental/internal/codegen/test/external_ref/packagea/spec.gen.go @@ -0,0 +1,22 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package packagea + +import ( + ext_a5fddf6c "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packageb" +) + +// #/components/schemas/ObjectA +type ObjectASchemaComponent struct { + Name *string `json:"name,omitempty" form:"name,omitempty"` + ObjectB *ext_a5fddf6c.ObjectB `json:"object_b,omitempty" form:"object_b,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ObjectASchemaComponent) ApplyDefaults() { + if s.ObjectB != nil { + s.ObjectB.ApplyDefaults() + } +} + +type ObjectA = ObjectASchemaComponent diff --git a/experimental/internal/codegen/test/external_ref/packagea/spec.yaml b/experimental/internal/codegen/test/external_ref/packagea/spec.yaml new file mode 100644 index 0000000000..77f3ea45f5 --- /dev/null +++ b/experimental/internal/codegen/test/external_ref/packagea/spec.yaml @@ -0,0 +1,14 @@ +openapi: "3.1.0" +info: + title: Package A + version: "1.0" +paths: {} +components: + schemas: + ObjectA: + type: object + properties: + name: + type: string + object_b: + $ref: ../packageb/spec.yaml#/components/schemas/ObjectB diff --git a/experimental/internal/codegen/test/external_ref/packageb/config.yaml b/experimental/internal/codegen/test/external_ref/packageb/config.yaml new file mode 100644 index 0000000000..7b36e2c291 --- /dev/null +++ b/experimental/internal/codegen/test/external_ref/packageb/config.yaml @@ -0,0 +1,2 @@ +package: packageb +output: internal/codegen/test/external_ref/packageb/spec.gen.go diff --git a/experimental/internal/codegen/test/external_ref/packageb/spec.gen.go b/experimental/internal/codegen/test/external_ref/packageb/spec.gen.go new file mode 100644 index 0000000000..e72bf4ac99 --- /dev/null +++ b/experimental/internal/codegen/test/external_ref/packageb/spec.gen.go @@ -0,0 +1,14 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package packageb + +// #/components/schemas/ObjectB +type ObjectBSchemaComponent struct { + Name *string `json:"name,omitempty" form:"name,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ObjectBSchemaComponent) ApplyDefaults() { +} + +type ObjectB = ObjectBSchemaComponent diff --git a/experimental/internal/codegen/test/external_ref/packageb/spec.yaml b/experimental/internal/codegen/test/external_ref/packageb/spec.yaml new file mode 100644 index 0000000000..bf9af943f5 --- /dev/null +++ b/experimental/internal/codegen/test/external_ref/packageb/spec.yaml @@ -0,0 +1,12 @@ +openapi: "3.1.0" +info: + title: Package B + version: "1.0" +paths: {} +components: + schemas: + ObjectB: + type: object + properties: + name: + type: string diff --git a/experimental/internal/codegen/test/external_ref/spec.gen.go b/experimental/internal/codegen/test/external_ref/spec.gen.go new file mode 100644 index 0000000000..cf179d426b --- /dev/null +++ b/experimental/internal/codegen/test/external_ref/spec.gen.go @@ -0,0 +1,26 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package externalref + +import ( + ext_95d82e90 "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packagea" + ext_a5fddf6c "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packageb" +) + +// #/components/schemas/Container +type ContainerSchemaComponent struct { + ObjectA *ext_95d82e90.ObjectA `json:"object_a,omitempty" form:"object_a,omitempty"` + ObjectB *ext_a5fddf6c.ObjectB `json:"object_b,omitempty" form:"object_b,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ContainerSchemaComponent) ApplyDefaults() { + if s.ObjectA != nil { + s.ObjectA.ApplyDefaults() + } + if s.ObjectB != nil { + s.ObjectB.ApplyDefaults() + } +} + +type Container = ContainerSchemaComponent diff --git a/experimental/internal/codegen/test/external_ref/spec.yaml b/experimental/internal/codegen/test/external_ref/spec.yaml new file mode 100644 index 0000000000..5a8e0ada72 --- /dev/null +++ b/experimental/internal/codegen/test/external_ref/spec.yaml @@ -0,0 +1,14 @@ +openapi: "3.1.0" +info: + title: External Ref Test + version: "1.0" +paths: {} +components: + schemas: + Container: + type: object + properties: + object_a: + $ref: ./packagea/spec.yaml#/components/schemas/ObjectA + object_b: + $ref: ./packageb/spec.yaml#/components/schemas/ObjectB diff --git a/experimental/internal/codegen/test/files/comprehensive.yaml b/experimental/internal/codegen/test/files/comprehensive.yaml new file mode 100644 index 0000000000..7bdef74eea --- /dev/null +++ b/experimental/internal/codegen/test/files/comprehensive.yaml @@ -0,0 +1,861 @@ +openapi: "3.1.0" +info: + title: Comprehensive Test Schema + version: "1.0.0" + description: Tests all schema patterns for oapi-codegen + +paths: + /simple: + get: + operationId: getSimple + responses: + "200": + description: Simple response + content: + application/json: + schema: + $ref: "#/components/schemas/SimpleObject" + + /inline-response: + get: + operationId: getInlineResponse + responses: + "200": + description: Inline object in response + content: + application/json: + schema: + type: object + required: + - id + - name + properties: + id: + type: integer + name: + type: string + + /parameters/{pathParam}: + parameters: + - name: pathParam + in: path + required: true + schema: + type: string + format: uuid + get: + operationId: getWithParameters + parameters: + - name: queryString + in: query + schema: + type: string + - name: queryInt + in: query + schema: + type: integer + - name: queryArray + in: query + schema: + type: array + items: + type: string + - name: headerParam + in: header + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/SimpleObject" + + /request-body: + post: + operationId: postRequestBody + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AllTypesRequired" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/SimpleObject" + + /multi-content: + post: + operationId: postMultiContent + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SimpleObject" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/SimpleObject" + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + metadata: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/SimpleObject" + text/plain: + schema: + type: string + +components: + schemas: + # =========================================== + # PRIMITIVE TYPES - All formats + # =========================================== + + AllTypesRequired: + type: object + required: + - intField + - int32Field + - int64Field + - floatField + - doubleField + - numberField + - stringField + - boolField + - dateField + - dateTimeField + - uuidField + - emailField + - uriField + - hostnameField + - ipv4Field + - ipv6Field + - byteField + - binaryField + - passwordField + properties: + intField: + type: integer + int32Field: + type: integer + format: int32 + int64Field: + type: integer + format: int64 + floatField: + type: number + format: float + doubleField: + type: number + format: double + numberField: + type: number + stringField: + type: string + boolField: + type: boolean + dateField: + type: string + format: date + dateTimeField: + type: string + format: date-time + uuidField: + type: string + format: uuid + emailField: + type: string + format: email + uriField: + type: string + format: uri + hostnameField: + type: string + format: hostname + ipv4Field: + type: string + format: ipv4 + ipv6Field: + type: string + format: ipv6 + byteField: + type: string + format: byte + binaryField: + type: string + format: binary + passwordField: + type: string + format: password + + AllTypesOptional: + type: object + properties: + intField: + type: integer + int32Field: + type: integer + format: int32 + int64Field: + type: integer + format: int64 + floatField: + type: number + format: float + doubleField: + type: number + format: double + numberField: + type: number + stringField: + type: string + boolField: + type: boolean + dateField: + type: string + format: date + dateTimeField: + type: string + format: date-time + uuidField: + type: string + format: uuid + emailField: + type: string + format: email + + # =========================================== + # NULLABLE TYPES + # =========================================== + + NullableRequired: + type: object + required: + - nullableString + - nullableInt + - nullableObject + properties: + nullableString: + type: + - string + - "null" + nullableInt: + type: + - integer + - "null" + nullableObject: + type: + - object + - "null" + properties: + name: + type: string + + NullableOptional: + type: object + properties: + nullableString: + type: + - string + - "null" + nullableInt: + type: + - integer + - "null" + + # =========================================== + # ARRAYS + # =========================================== + + ArrayTypes: + type: object + properties: + stringArray: + type: array + items: + type: string + intArray: + type: array + items: + type: integer + objectArray: + type: array + items: + $ref: "#/components/schemas/SimpleObject" + inlineObjectArray: + type: array + items: + type: object + properties: + id: + type: integer + name: + type: string + nestedArray: + type: array + items: + type: array + items: + type: string + nullableArray: + type: + - array + - "null" + items: + type: string + arrayWithConstraints: + type: array + minItems: 1 + maxItems: 10 + items: + type: string + + # =========================================== + # OBJECTS + # =========================================== + + SimpleObject: + type: object + required: + - id + properties: + id: + type: integer + name: + type: string + + NestedObject: + type: object + properties: + outer: + type: object + properties: + inner: + type: object + properties: + value: + type: string + + # =========================================== + # ADDITIONAL PROPERTIES + # =========================================== + + AdditionalPropsAny: + type: object + additionalProperties: true + + AdditionalPropsNone: + type: object + additionalProperties: false + properties: + known: + type: string + + AdditionalPropsTyped: + type: object + additionalProperties: + type: integer + + AdditionalPropsObject: + type: object + additionalProperties: + $ref: "#/components/schemas/SimpleObject" + + AdditionalPropsWithProps: + type: object + properties: + id: + type: integer + additionalProperties: + type: string + + # =========================================== + # ENUMS + # =========================================== + + StringEnum: + type: string + enum: + - value1 + - value2 + - value3 + + IntegerEnum: + type: integer + enum: + - 1 + - 2 + - 3 + + ObjectWithEnum: + type: object + properties: + status: + type: string + enum: + - pending + - active + - completed + priority: + type: integer + enum: + - 1 + - 2 + - 3 + + InlineEnumInProperty: + type: object + properties: + inlineStatus: + type: string + enum: + - on + - off + + # =========================================== + # ALLOF - Inheritance/Composition + # =========================================== + + BaseProperties: + type: object + properties: + id: + type: integer + createdAt: + type: string + format: date-time + + ExtendedObject: + allOf: + - $ref: "#/components/schemas/BaseProperties" + - type: object + required: + - name + properties: + name: + type: string + description: + type: string + + DeepInheritance: + allOf: + - $ref: "#/components/schemas/ExtendedObject" + - type: object + properties: + extra: + type: string + + AllOfMultipleRefs: + allOf: + - $ref: "#/components/schemas/BaseProperties" + - $ref: "#/components/schemas/SimpleObject" + - type: object + properties: + merged: + type: boolean + + AllOfInlineOnly: + allOf: + - type: object + properties: + first: + type: string + - type: object + properties: + second: + type: integer + + # =========================================== + # ANYOF - Union Types + # =========================================== + + AnyOfPrimitives: + anyOf: + - type: string + - type: integer + + AnyOfObjects: + anyOf: + - $ref: "#/components/schemas/SimpleObject" + - $ref: "#/components/schemas/BaseProperties" + + AnyOfMixed: + anyOf: + - type: string + - $ref: "#/components/schemas/SimpleObject" + - type: object + properties: + inline: + type: boolean + + AnyOfNullable: + anyOf: + - type: string + - type: "null" + + ObjectWithAnyOfProperty: + type: object + properties: + value: + anyOf: + - type: string + - type: integer + - type: boolean + + ArrayOfAnyOf: + type: array + items: + anyOf: + - $ref: "#/components/schemas/SimpleObject" + - $ref: "#/components/schemas/BaseProperties" + + # =========================================== + # ONEOF - Discriminated Unions + # =========================================== + + OneOfSimple: + oneOf: + - $ref: "#/components/schemas/SimpleObject" + - $ref: "#/components/schemas/BaseProperties" + + OneOfWithDiscriminator: + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + discriminator: + propertyName: petType + + OneOfWithDiscriminatorMapping: + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + discriminator: + propertyName: petType + mapping: + cat: "#/components/schemas/Cat" + dog: "#/components/schemas/Dog" + + Cat: + type: object + required: + - petType + - meow + properties: + petType: + type: string + meow: + type: string + whiskerLength: + type: number + + Dog: + type: object + required: + - petType + - bark + properties: + petType: + type: string + bark: + type: string + tailLength: + type: number + + OneOfInline: + oneOf: + - type: object + properties: + optionA: + type: string + - type: object + properties: + optionB: + type: integer + + OneOfPrimitives: + oneOf: + - type: string + - type: number + - type: boolean + + ObjectWithOneOfProperty: + type: object + properties: + id: + type: integer + variant: + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + discriminator: + propertyName: petType + + # =========================================== + # COMBINED COMPOSITION + # =========================================== + + AllOfWithOneOf: + allOf: + - $ref: "#/components/schemas/BaseProperties" + - oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + discriminator: + propertyName: petType + + OneOfWithAllOf: + oneOf: + - allOf: + - $ref: "#/components/schemas/BaseProperties" + - type: object + properties: + variant: + type: string + const: "a" + - allOf: + - $ref: "#/components/schemas/BaseProperties" + - type: object + properties: + variant: + type: string + const: "b" + + # =========================================== + # RECURSIVE TYPES + # =========================================== + + TreeNode: + type: object + properties: + value: + type: string + children: + type: array + items: + $ref: "#/components/schemas/TreeNode" + + LinkedListNode: + type: object + properties: + value: + type: integer + next: + $ref: "#/components/schemas/LinkedListNode" + + RecursiveOneOf: + oneOf: + - type: string + - type: object + properties: + nested: + $ref: "#/components/schemas/RecursiveOneOf" + + # =========================================== + # READ/WRITE ONLY + # =========================================== + + ReadWriteOnly: + type: object + required: + - id + - password + properties: + id: + type: integer + readOnly: true + password: + type: string + writeOnly: true + name: + type: string + + # =========================================== + # DEFAULTS AND CONST + # =========================================== + + WithDefaults: + type: object + properties: + stringWithDefault: + type: string + default: "default_value" + intWithDefault: + type: integer + default: 42 + boolWithDefault: + type: boolean + default: true + arrayWithDefault: + type: array + items: + type: string + default: [] + + WithConst: + type: object + properties: + version: + type: string + const: "1.0.0" + type: + type: string + const: "fixed" + + # =========================================== + # CONSTRAINTS + # =========================================== + + WithConstraints: + type: object + properties: + boundedInt: + type: integer + minimum: 0 + maximum: 100 + exclusiveBoundedInt: + type: integer + exclusiveMinimum: 0 + exclusiveMaximum: 100 + multipleOf: + type: integer + multipleOf: 5 + boundedString: + type: string + minLength: 1 + maxLength: 255 + patternString: + type: string + pattern: "^[a-z]+$" + boundedArray: + type: array + minItems: 1 + maxItems: 10 + items: + type: string + uniqueArray: + type: array + uniqueItems: true + items: + type: string + + # =========================================== + # OPENAPI 3.1 SPECIFIC + # =========================================== + + TypeArray31: + type: + - object + - "null" + properties: + name: + type: string + + PrefixItems31: + type: array + prefixItems: + - type: string + - type: integer + - type: boolean + items: + type: string + + # =========================================== + # EMPTY / ANY TYPE + # =========================================== + + EmptySchema: {} + + AnyValue: + description: Accepts any JSON value + + ExplicitAny: + type: + - string + - number + - integer + - boolean + - array + - object + - "null" + + # =========================================== + # COMPLEX NESTED STRUCTURES + # =========================================== + + ComplexNested: + type: object + properties: + metadata: + type: object + additionalProperties: + anyOf: + - type: string + - type: integer + - type: array + items: + type: string + items: + type: array + items: + allOf: + - $ref: "#/components/schemas/BaseProperties" + - type: object + properties: + tags: + type: array + items: + type: string + config: + oneOf: + - type: object + properties: + mode: + type: string + const: "simple" + value: + type: string + - type: object + properties: + mode: + type: string + const: "advanced" + options: + type: object + additionalProperties: + type: string + + # =========================================== + # MAPS + # =========================================== + + StringMap: + type: object + additionalProperties: + type: string + + ObjectMap: + type: object + additionalProperties: + $ref: "#/components/schemas/SimpleObject" + + NestedMap: + type: object + additionalProperties: + type: object + additionalProperties: + type: string diff --git a/experimental/internal/codegen/test/issues/issue_1029/doc.go b/experimental/internal/codegen/test/issues/issue_1029/doc.go new file mode 100644 index 0000000000..dd7dc0eb30 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1029/doc.go @@ -0,0 +1,5 @@ +// Package issue_1029 tests that oneOf with multiple single-value string enums generates valid code. +// https://github.com/oapi-codegen/oapi-codegen/issues/1029 +package issue_1029 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go new file mode 100644 index 0000000000..7c5a5d2fa1 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go @@ -0,0 +1,186 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/Registration +type Registration struct { + State *RegistrationState `json:"state,omitempty" form:"state,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Registration) ApplyDefaults() { +} + +// #/components/schemas/Registration/properties/state +type RegistrationState struct { + RegistrationStateOneOf0 *RegistrationStateOneOf0 + RegistrationStateOneOf1 *RegistrationStateOneOf1 + RegistrationStateOneOf2 *RegistrationStateOneOf2 + RegistrationStateOneOf3 *RegistrationStateOneOf3 +} + +func (u RegistrationState) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.RegistrationStateOneOf0 != nil { + count++ + data, err = json.Marshal(u.RegistrationStateOneOf0) + if err != nil { + return nil, err + } + } + if u.RegistrationStateOneOf1 != nil { + count++ + data, err = json.Marshal(u.RegistrationStateOneOf1) + if err != nil { + return nil, err + } + } + if u.RegistrationStateOneOf2 != nil { + count++ + data, err = json.Marshal(u.RegistrationStateOneOf2) + if err != nil { + return nil, err + } + } + if u.RegistrationStateOneOf3 != nil { + count++ + data, err = json.Marshal(u.RegistrationStateOneOf3) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("RegistrationState: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *RegistrationState) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 RegistrationStateOneOf0 + if err := json.Unmarshal(data, &v0); err == nil { + u.RegistrationStateOneOf0 = &v0 + successCount++ + } + + var v1 RegistrationStateOneOf1 + if err := json.Unmarshal(data, &v1); err == nil { + u.RegistrationStateOneOf1 = &v1 + successCount++ + } + + var v2 RegistrationStateOneOf2 + if err := json.Unmarshal(data, &v2); err == nil { + u.RegistrationStateOneOf2 = &v2 + successCount++ + } + + var v3 RegistrationStateOneOf3 + if err := json.Unmarshal(data, &v3); err == nil { + u.RegistrationStateOneOf3 = &v3 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("RegistrationState: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *RegistrationState) ApplyDefaults() { +} + +// #/components/schemas/Registration/properties/state/oneOf/0 +type RegistrationStateOneOf0 string + +const ( + RegistrationStateOneOf0_undefined RegistrationStateOneOf0 = "undefined" +) + +// #/components/schemas/Registration/properties/state/oneOf/1 +type RegistrationStateOneOf1 string + +const ( + RegistrationStateOneOf1_registered RegistrationStateOneOf1 = "registered" +) + +// #/components/schemas/Registration/properties/state/oneOf/2 +type RegistrationStateOneOf2 string + +const ( + RegistrationStateOneOf2_pending RegistrationStateOneOf2 = "pending" +) + +// #/components/schemas/Registration/properties/state/oneOf/3 +type RegistrationStateOneOf3 string + +const ( + RegistrationStateOneOf3_active RegistrationStateOneOf3 = "active" +) + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/6yQzY7UMBCE736KknJlMrPsibwBJySExNmb1CQNjm2527MaId4dOeFnZw4Iob25q7vr", + "a1eH96qVeDi9fTcgRX44Q61InMFYV8XZS2iVJcyMLN6IXFJmCVfXYTHLOhyPs9hSn/oxrcfksxzGNHFm", + "vC2kofTYWK5zHT4vjKja7Hfys9iCtQaTHIjWCDxcfKi8OeqN62ALfx80oRGgS6qhvdcsgfBxwnMqXzGm", + "UjhauPYuZUafZcBjf+ofncRzGhxgYoHDiyjwiWoOuLCopDjgoT/1J5e9LTrg23fXICkymrZ9HReufnsC", + "HzmLWvHWFjcFsGvmgPT0haP9lPYQTai/hgA1b/xTYo/lpQActgxutV2vceJZIqe73g7fA/xHq7L9geU1", + "vDLjdD/9X0Z+NLnwbz4/AgAA//+mSD78zgIAAA==", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_1029/output/types_test.go b/experimental/internal/codegen/test/issues/issue_1029/output/types_test.go new file mode 100644 index 0000000000..4ea2eabfbe --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1029/output/types_test.go @@ -0,0 +1,89 @@ +package output + +import ( + "encoding/json" + "testing" +) + +// TestRegistrationStateOneOfEnums verifies that oneOf with string enums generates +// correctly with proper enum constants. +// https://github.com/oapi-codegen/oapi-codegen/issues/1029 +func TestRegistrationStateOneOfEnums(t *testing.T) { + // Verify enum constants exist and have correct values + tests := []struct { + name string + enum RegistrationStateOneOf0 + value string + }{ + {"undefined", RegistrationStateOneOf0_undefined, "undefined"}, + } + + for _, tt := range tests { + if string(tt.enum) != tt.value { + t.Errorf("%s enum = %q, want %q", tt.name, tt.enum, tt.value) + } + } +} + +func TestRegistrationStateMarshal(t *testing.T) { + // Test serialization of oneOf with string enum + state := RegistrationState{ + RegistrationStateOneOf0: ptrTo(RegistrationStateOneOf0(RegistrationStateOneOf0_undefined)), + } + + reg := Registration{ + State: &state, + } + + data, err := json.Marshal(reg) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + // Verify the JSON contains the expected value + expected := `{"state":"undefined"}` + if string(data) != expected { + t.Errorf("Marshal result = %s, want %s", string(data), expected) + } +} + +func TestRegistrationStateUnmarshalLimitation(t *testing.T) { + // Note: Unmarshaling oneOf with multiple string enum types is inherently + // ambiguous without a discriminator, since any string can match any of the + // enum types. This test documents that limitation. + input := `{"state":"undefined"}` + + var decoded Registration + err := json.Unmarshal([]byte(input), &decoded) + + // The error is expected because all 4 string enum types can unmarshal + // from the same string value + if err == nil { + t.Log("Unmarshal succeeded (all variants matched)") + } else { + t.Logf("Unmarshal failed as expected for ambiguous oneOf: %v", err) + } +} + +func TestAllEnumConstants(t *testing.T) { + // Verify all enum constants are defined + _ = RegistrationStateOneOf0_undefined + _ = RegistrationStateOneOf1_registered + _ = RegistrationStateOneOf2_pending + _ = RegistrationStateOneOf3_active + + // Test values + if string(RegistrationStateOneOf1_registered) != "registered" { + t.Error("registered enum has wrong value") + } + if string(RegistrationStateOneOf2_pending) != "pending" { + t.Error("pending enum has wrong value") + } + if string(RegistrationStateOneOf3_active) != "active" { + t.Error("active enum has wrong value") + } +} + +func ptrTo[T any](v T) *T { + return &v +} diff --git a/experimental/internal/codegen/test/issues/issue_1029/spec.yaml b/experimental/internal/codegen/test/issues/issue_1029/spec.yaml new file mode 100644 index 0000000000..698973cfbb --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1029/spec.yaml @@ -0,0 +1,29 @@ +# Issue 1029: oneOf string enums failing to generate properly +# https://github.com/oapi-codegen/oapi-codegen/issues/1029 +# +# When using oneOf with multiple single-value string enums, +# the generated code should compile and work correctly. +openapi: 3.0.3 +info: + title: Issue 1029 Test + version: 1.0.0 +paths: {} +components: + schemas: + Registration: + type: object + properties: + state: + oneOf: + - enum: + - undefined + type: string + - enum: + - registered + type: string + - enum: + - pending + type: string + - enum: + - active + type: string diff --git a/experimental/internal/codegen/test/issues/issue_1039/doc.go b/experimental/internal/codegen/test/issues/issue_1039/doc.go new file mode 100644 index 0000000000..64016470f8 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1039/doc.go @@ -0,0 +1,5 @@ +// Package issue_1039 tests nullable type generation. +// https://github.com/oapi-codegen/oapi-codegen/issues/1039 +package issue_1039 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go new file mode 100644 index 0000000000..d996cfb752 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go @@ -0,0 +1,291 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/PatchRequest +// A request to patch an existing user object. +type PatchRequest struct { + SimpleRequiredNullable SimpleRequiredNullable `json:"simple_required_nullable" form:"simple_required_nullable"` + SimpleOptionalNullable SimpleOptionalNullable `json:"simple_optional_nullable,omitempty" form:"simple_optional_nullable,omitempty"` + SimpleOptionalNonNullable *any `json:"simple_optional_non_nullable,omitempty" form:"simple_optional_non_nullable,omitempty"` + ComplexRequiredNullable Nullable[ComplexRequiredNullable] `json:"complex_required_nullable" form:"complex_required_nullable"` + ComplexOptionalNullable Nullable[ComplexOptionalNullable] `json:"complex_optional_nullable,omitempty" form:"complex_optional_nullable,omitempty"` + AdditionalProperties map[string]any `json:"-"` +} + +func (s PatchRequest) MarshalJSON() ([]byte, error) { + result := make(map[string]any) + + result["simple_required_nullable"] = s.SimpleRequiredNullable + result["simple_optional_nullable"] = s.SimpleOptionalNullable + if s.SimpleOptionalNonNullable != nil { + result["simple_optional_non_nullable"] = s.SimpleOptionalNonNullable + } + result["complex_required_nullable"] = s.ComplexRequiredNullable + result["complex_optional_nullable"] = s.ComplexOptionalNullable + + // Add additional properties + for k, v := range s.AdditionalProperties { + result[k] = v + } + + return json.Marshal(result) +} + +func (s *PatchRequest) UnmarshalJSON(data []byte) error { + // Known fields + knownFields := map[string]bool{ + "simple_required_nullable": true, + "simple_optional_nullable": true, + "simple_optional_non_nullable": true, + "complex_required_nullable": true, + "complex_optional_nullable": true, + } + + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + if v, ok := raw["simple_required_nullable"]; ok { + if err := json.Unmarshal(v, &s.SimpleRequiredNullable); err != nil { + return err + } + } + if v, ok := raw["simple_optional_nullable"]; ok { + if err := json.Unmarshal(v, &s.SimpleOptionalNullable); err != nil { + return err + } + } + if v, ok := raw["simple_optional_non_nullable"]; ok { + var val any + if err := json.Unmarshal(v, &val); err != nil { + return err + } + s.SimpleOptionalNonNullable = &val + } + if v, ok := raw["complex_required_nullable"]; ok { + if err := json.Unmarshal(v, &s.ComplexRequiredNullable); err != nil { + return err + } + } + if v, ok := raw["complex_optional_nullable"]; ok { + if err := json.Unmarshal(v, &s.ComplexOptionalNullable); err != nil { + return err + } + } + + // Collect additional properties + s.AdditionalProperties = make(map[string]any) + for k, v := range raw { + if !knownFields[k] { + var val any + if err := json.Unmarshal(v, &val); err != nil { + return err + } + s.AdditionalProperties[k] = val + } + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *PatchRequest) ApplyDefaults() { +} + +// #/components/schemas/simple_required_nullable +// Simple required and nullable +type SimpleRequiredNullable = Nullable[int] + +// #/components/schemas/simple_optional_nullable +// Simple optional and nullable +type SimpleOptionalNullable = Nullable[int] + +// #/components/schemas/complex_required_nullable +// Complex required and nullable +type ComplexRequiredNullable struct { + Name *string `json:"name,omitempty" form:"name,omitempty"` // Optional and non nullable +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ComplexRequiredNullable) ApplyDefaults() { +} + +// #/components/schemas/complex_optional_nullable +// Complex, optional and nullable +type ComplexOptionalNullable struct { + AliasName Nullable[string] `json:"alias_name,omitempty" form:"alias_name,omitempty"` // Optional and nullable + Name *string `json:"name,omitempty" form:"name,omitempty"` // Optional and non nullable +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ComplexOptionalNullable) ApplyDefaults() { +} + +// Nullable is a generic type that can distinguish between: +// - Field not provided (unspecified) +// - Field explicitly set to null +// - Field has a value +// +// This is implemented as a map[bool]T where: +// - Empty map: unspecified +// - map[false]T: explicitly null +// - map[true]T: has a value +type Nullable[T any] map[bool]T + +// NewNullableWithValue creates a Nullable with the given value. +func NewNullableWithValue[T any](value T) Nullable[T] { + return Nullable[T]{true: value} +} + +// NewNullNullable creates a Nullable that is explicitly null. +func NewNullNullable[T any]() Nullable[T] { + return Nullable[T]{false: *new(T)} +} + +// Get returns the value if set, or an error if null or unspecified. +func (n Nullable[T]) Get() (T, error) { + if v, ok := n[true]; ok { + return v, nil + } + var zero T + if n.IsNull() { + return zero, ErrNullableIsNull + } + return zero, ErrNullableNotSpecified +} + +// MustGet returns the value or panics if null or unspecified. +func (n Nullable[T]) MustGet() T { + v, err := n.Get() + if err != nil { + panic(err) + } + return v +} + +// Set assigns a value. +func (n *Nullable[T]) Set(value T) { + *n = Nullable[T]{true: value} +} + +// SetNull marks the field as explicitly null. +func (n *Nullable[T]) SetNull() { + *n = Nullable[T]{false: *new(T)} +} + +// SetUnspecified clears the field (as if it was never set). +func (n *Nullable[T]) SetUnspecified() { + *n = nil +} + +// IsNull returns true if the field is explicitly null. +func (n Nullable[T]) IsNull() bool { + if n == nil { + return false + } + _, ok := n[false] + return ok +} + +// IsSpecified returns true if the field was provided (either null or a value). +func (n Nullable[T]) IsSpecified() bool { + return len(n) > 0 +} + +// MarshalJSON implements json.Marshaler. +func (n Nullable[T]) MarshalJSON() ([]byte, error) { + if n.IsNull() { + return []byte("null"), nil + } + if v, ok := n[true]; ok { + return json.Marshal(v) + } + // Unspecified - this shouldn't be called if omitempty is used correctly + return []byte("null"), nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (n *Nullable[T]) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + n.SetNull() + return nil + } + var v T + if err := json.Unmarshal(data, &v); err != nil { + return err + } + n.Set(v) + return nil +} + +// ErrNullableIsNull is returned when trying to get a value from a null Nullable. +var ErrNullableIsNull = errors.New("nullable value is null") + +// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. +var ErrNullableNotSpecified = errors.New("nullable value is not specified") + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/7RUTY/TMBC9+1eMEqReaNNlT+QGnFYIdgXcKzeZJl5S23gmqJX48SjfSZu0ge7eEnvm", + "vTdvxuPDA1GOcLe+fx/C1zzL5DZD4KNFggQ1OsnKaOFDymwpDIJEcZpvV5HZB0ZatYxMjAnq4Y8qQCko", + "UIUvfPgifyJQ7hA4lQx6yCMdNlwYg3XGosuOwljU0qoQ7lfr1Z1QemdCAfAbHSmjQ1isi/OFAGDFGYaA", + "B7m3GQqAGClyynIZ90cAwP9JsJJTKkiDGjsssazkKK0+AYrQ0qSHuJXwVATU9w5/5Uj80cTHJuVE4I8U", + "K0jYmvjYxhSJymEcArsc2+PIaEbNHRaAtDZTUSkieCaj+3cAFKW4l8MzgDcOdyF4fhCZvTUaNVNQRVJQ", + "yv9W6fbaMsgaTUgd0OLder3o4w6q8h4/e0J06EVgTVDl9FkalKIbIZjtM0YsRkA/NHYCm9ozqQEPiljp", + "BHJCV2evxImLrc4lkCqatGmuNs0w9EIK3RkeJmOqEWHV92MKtm+R3woCqWM4Y77cmCkG71SDKf2S2YSG", + "5voGDWcMFzQYPUuH0ctbtfSYvN6LmejlyzVmkuJcxSu2ZpKiUSHjWFVXT938wk5mhELMGeHqfSrNmKCr", + "z9qY/p4avNrvJeqEwWLO2N7EO26pmDuqFTexUzqZzWL0CdPVIRxZfdcr/FShXpzdsVWl5R4nN/fjZCVd", + "wsAUMWu+r+32upa3F5/AWDEyU5I2/1DSlXK649EGvLR7o1R/AwAA///zkywGmQkAAA==", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_1039/output/types_test.go b/experimental/internal/codegen/test/issues/issue_1039/output/types_test.go new file mode 100644 index 0000000000..f165ca7ce8 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1039/output/types_test.go @@ -0,0 +1,266 @@ +package output + +import ( + "encoding/json" + "testing" +) + +// TestNullableTypes verifies that nullable types are generated properly. +// https://github.com/oapi-codegen/oapi-codegen/issues/1039 +// +// The implementation uses Nullable[T] for nullable types: +// - Nullable primitive schemas generate type aliases: type SimpleRequiredNullable = Nullable[int] +// - Nullable object fields are wrapped: Nullable[ComplexType] +// - Inline nullable primitives use Nullable[T] directly +func TestNullableTypes(t *testing.T) { + // Create a patch request with various nullable fields + name := "test-name" + + // SimpleRequiredNullable is Nullable[int] + simpleRequired := NewNullableWithValue(42) + + // ComplexRequiredNullable is wrapped in Nullable + complexRequired := NewNullableWithValue(ComplexRequiredNullable{Name: &name}) + + req := PatchRequest{ + SimpleRequiredNullable: simpleRequired, + ComplexRequiredNullable: complexRequired, + } + + // Verify simple nullable value + val, err := req.SimpleRequiredNullable.Get() + if err != nil { + t.Fatalf("SimpleRequiredNullable.Get() failed: %v", err) + } + if val != 42 { + t.Errorf("SimpleRequiredNullable = %v, want 42", val) + } + + // Verify complex nullable can retrieve value + complexVal, err := req.ComplexRequiredNullable.Get() + if err != nil { + t.Fatalf("ComplexRequiredNullable.Get() failed: %v", err) + } + if *complexVal.Name != "test-name" { + t.Errorf("ComplexRequiredNullable.Name = %q, want %q", *complexVal.Name, "test-name") + } +} + +func TestPatchRequestJSONRoundTrip(t *testing.T) { + name := "test" + original := PatchRequest{ + SimpleRequiredNullable: NewNullableWithValue(100), + ComplexRequiredNullable: NewNullableWithValue(ComplexRequiredNullable{Name: &name}), + } + + data, err := json.Marshal(original) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + t.Logf("Marshaled: %s", string(data)) + + var decoded PatchRequest + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + // Verify simple nullable round-trips correctly + decodedSimple, err := decoded.SimpleRequiredNullable.Get() + if err != nil { + t.Fatalf("SimpleRequiredNullable.Get() failed: %v", err) + } + if decodedSimple != 100 { + t.Errorf("SimpleRequiredNullable mismatch: got %v, want %v", decodedSimple, 100) + } + + // Verify complex nullable round-trips correctly + complexVal, err := decoded.ComplexRequiredNullable.Get() + if err != nil { + t.Fatalf("ComplexRequiredNullable.Get() failed: %v", err) + } + if *complexVal.Name != "test" { + t.Errorf("ComplexRequiredNullable.Name = %q, want %q", *complexVal.Name, "test") + } +} + +func TestComplexNullableTypes(t *testing.T) { + // Complex nullable types use Nullable[T] + name := "name" + opt := ComplexOptionalNullable{ + AliasName: NewNullableWithValue("alias"), + Name: &name, + } + + req := PatchRequest{ + SimpleRequiredNullable: NewNullNullable[int](), // explicitly null + ComplexRequiredNullable: NewNullNullable[ComplexRequiredNullable](), + ComplexOptionalNullable: NewNullableWithValue(opt), + } + + // Check the complex optional nullable + if !req.ComplexOptionalNullable.IsSpecified() { + t.Fatal("ComplexOptionalNullable should be specified") + } + optVal := req.ComplexOptionalNullable.MustGet() + aliasVal := optVal.AliasName.MustGet() + if aliasVal != "alias" { + t.Errorf("AliasName = %q, want %q", aliasVal, "alias") + } + + // Check that required nullable can be null + if !req.ComplexRequiredNullable.IsNull() { + t.Error("ComplexRequiredNullable should be null") + } + if !req.SimpleRequiredNullable.IsNull() { + t.Error("SimpleRequiredNullable should be null") + } +} + +func TestNullableThreeStates(t *testing.T) { + // Test unspecified (nil/empty map) + unspecified := Nullable[string](nil) + if unspecified.IsSpecified() { + t.Error("unspecified should not be specified") + } + if unspecified.IsNull() { + t.Error("unspecified should not be null") + } + _, err := unspecified.Get() + if err != ErrNullableNotSpecified { + t.Errorf("Get() on unspecified should return ErrNullableNotSpecified, got %v", err) + } + + // Test explicitly null + null := NewNullNullable[string]() + if !null.IsSpecified() { + t.Error("null should be specified") + } + if !null.IsNull() { + t.Error("null should be null") + } + _, err = null.Get() + if err != ErrNullableIsNull { + t.Errorf("Get() on null should return ErrNullableIsNull, got %v", err) + } + + // Test with value + withValue := NewNullableWithValue("hello") + if !withValue.IsSpecified() { + t.Error("withValue should be specified") + } + if withValue.IsNull() { + t.Error("withValue should not be null") + } + val, err := withValue.Get() + if err != nil { + t.Errorf("Get() on withValue should succeed, got %v", err) + } + if val != "hello" { + t.Errorf("Get() = %q, want %q", val, "hello") + } +} + +func TestNullableJSONMarshal(t *testing.T) { + // Test marshaling each state + tests := []struct { + name string + nullable Nullable[string] + want string + }{ + {"with value", NewNullableWithValue("test"), `"test"`}, + {"explicitly null", NewNullNullable[string](), "null"}, + {"unspecified", Nullable[string](nil), "null"}, // unspecified marshals as null + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.nullable) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + if string(data) != tt.want { + t.Errorf("Marshal() = %s, want %s", string(data), tt.want) + } + }) + } +} + +func TestNullableJSONUnmarshal(t *testing.T) { + tests := []struct { + name string + json string + wantNull bool + wantValue string + wantErr error + }{ + {"with value", `"test"`, false, "test", nil}, + {"explicitly null", "null", true, "", ErrNullableIsNull}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var n Nullable[string] + if err := json.Unmarshal([]byte(tt.json), &n); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + if n.IsNull() != tt.wantNull { + t.Errorf("IsNull() = %v, want %v", n.IsNull(), tt.wantNull) + } + val, err := n.Get() + if err != tt.wantErr { + t.Errorf("Get() error = %v, want %v", err, tt.wantErr) + } + if err == nil && val != tt.wantValue { + t.Errorf("Get() = %q, want %q", val, tt.wantValue) + } + }) + } +} + +// TestNullablePrimitiveTypeAlias verifies that nullable primitive schemas +// generate proper type aliases. +func TestNullablePrimitiveTypeAlias(t *testing.T) { + // SimpleRequiredNullable should be a type alias to Nullable[int] + var simple SimpleRequiredNullable + simple.Set(42) + + val, err := simple.Get() + if err != nil { + t.Fatalf("Get() failed: %v", err) + } + if val != 42 { + t.Errorf("Get() = %d, want 42", val) + } + + // Test null state + simple.SetNull() + if !simple.IsNull() { + t.Error("should be null after SetNull()") + } + + // Test unspecified state + simple.SetUnspecified() + if simple.IsSpecified() { + t.Error("should not be specified after SetUnspecified()") + } +} + +// TestAdditionalPropertiesFalse verifies that additionalProperties: false +// generates proper marshal/unmarshal that rejects extra fields. +func TestAdditionalPropertiesFalse(t *testing.T) { + // The struct has AdditionalProperties field but additionalProperties: false + // means unknown fields are still collected but not expected + req := PatchRequest{ + SimpleRequiredNullable: NewNullableWithValue(1), + ComplexRequiredNullable: NewNullNullable[ComplexRequiredNullable](), + AdditionalProperties: map[string]any{"extra": "value"}, + } + + // Should marshal with additional properties + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + t.Logf("Marshaled: %s", string(data)) +} diff --git a/experimental/internal/codegen/test/issues/issue_1039/spec.yaml b/experimental/internal/codegen/test/issues/issue_1039/spec.yaml new file mode 100644 index 0000000000..8d827acef5 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1039/spec.yaml @@ -0,0 +1,86 @@ +# Issue 1039: Nullable types generation +# https://github.com/oapi-codegen/oapi-codegen/issues/1039 +# +# Make sure that nullable types are generated properly +openapi: 3.0.1 +info: + version: '0.0.1' + title: example + description: | + Make sure that nullable types are generated properly +paths: + /example: + patch: + operationId: examplePatch + requestBody: + description: The patch body + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PatchRequest" + responses: + '200': + description: "OK" + +components: + schemas: + PatchRequest: + type: object + description: A request to patch an existing user object. + required: + - simple_required_nullable + - complex_required_nullable + properties: + simple_required_nullable: + # required and nullable + $ref: "#/components/schemas/simple_required_nullable" + simple_optional_nullable: + # optional and nullable + $ref: "#/components/schemas/simple_optional_nullable" + simple_optional_non_nullable: + # optional and non-nullable + $ref: "#/components/schemas/simple_optional_non_nullable" + complex_required_nullable: + # required and nullable + $ref: "#/components/schemas/complex_required_nullable" + complex_optional_nullable: + # optional and nullable + $ref: "#/components/schemas/complex_optional_nullable" + additionalProperties: false + + simple_required_nullable: + type: integer + nullable: true + description: Simple required and nullable + + simple_optional_nullable: + type: integer + nullable: true + description: Simple optional and nullable + + simple_optional_non_nullable: + type: string + description: Simple optional and non nullable + + complex_required_nullable: + type: object + nullable: true + description: Complex required and nullable + properties: + name: + description: Optional and non nullable + type: string + + complex_optional_nullable: + type: object + description: Complex, optional and nullable + properties: + alias_name: + description: Optional and nullable + type: string + nullable: true + name: + description: Optional and non nullable + type: string + nullable: true diff --git a/experimental/internal/codegen/test/issues/issue_1397/doc.go b/experimental/internal/codegen/test/issues/issue_1397/doc.go new file mode 100644 index 0000000000..94fef2bfb0 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1397/doc.go @@ -0,0 +1,5 @@ +// Package issue_1397 tests basic type generation with x-go-type-name. +// https://github.com/oapi-codegen/oapi-codegen/issues/1397 +package issue_1397 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go new file mode 100644 index 0000000000..7fd6bbf827 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go @@ -0,0 +1,114 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/Test +type MyTestRequest struct { + Field1 []TestField1Item `json:"field1,omitempty" form:"field1,omitempty"` // A array of enum values + Field2 *MyTestRequestNestedField `json:"field2,omitempty" form:"field2,omitempty"` // A nested object with allocated name + Field3 *TestField3 `json:"field3,omitempty" form:"field3,omitempty"` // A nested object without allocated name +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *MyTestRequest) ApplyDefaults() { + if s.Field2 != nil { + s.Field2.ApplyDefaults() + } + if s.Field3 != nil { + s.Field3.ApplyDefaults() + } +} + +// #/components/schemas/Test/properties/field1 +// A array of enum values +type TestField1 = []TestField1Item + +// #/components/schemas/Test/properties/field1/items +type TestField1Item string + +const ( + TestField1Item_option1 TestField1Item = "option1" + TestField1Item_option2 TestField1Item = "option2" +) + +// #/components/schemas/Test/properties/field2 +// A nested object with allocated name +type MyTestRequestNestedField struct { + Field1 bool `json:"field1" form:"field1"` + Field2 string `json:"field2" form:"field2"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *MyTestRequestNestedField) ApplyDefaults() { +} + +// #/components/schemas/Test/properties/field3 +// A nested object without allocated name +type TestField3 struct { + Field1 bool `json:"field1" form:"field1"` + Field2 string `json:"field2" form:"field2"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *TestField3) ApplyDefaults() { +} + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/9xUzZKbMAy+8xSapFdCIO106lv30Jkc2kOnL+AYBbwDltcS283bd2zYAJt0Z3otJ/xZ", + "P58+WdrCkXlAKA9fPit40GwNyMUjNOgwaLHk4LeVFl7yhvJ4kzvdY7aFVsSzKorGSjucdob6grS3uaEa", + "G3Trg41JuIhZMvLotLcKNofdflduMuvOpDIAsdKhWhCCX8iSATxjYEtOQbnb7/aZod6TQyccvdi02Ov0", + "C8lh/INUhgI6PaKRCfKBPAaxyK9GAGeLXV3OZ4Aa2QTrJaX8CjoEfQE6A7qhh2fdDcgLayvY89IdkuEa", + "yYFSvPIuWq3QkTdLsK5ZXPT65ZhSwaclat2E7rO3IRLxdZ3VO3U6ZMF6Emxsuu46Mjqiqemz6z0h74s5", + "0zkRdajdrX113/5GgYBPgw1YL83zKeUtVN3osXoK8Vu/aQXfL/H9/MSnAVl+JDm+xVBrDQ//pCEN8j/L", + "+K6EmdfSptIKuQ5mg9cJjcWnFXOsFcg4668MkeWB6stM0pATdLJkrb3vrEkRikcmtxZgXAxvRfkQ8Kxg", + "sy3mLVJMK6SI1DdXDuzJ8bIz1f7jX1vfENXZnwAAAP//PTfE300FAAA=", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_1397/output/types_test.go b/experimental/internal/codegen/test/issues/issue_1397/output/types_test.go new file mode 100644 index 0000000000..05ae316ffb --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1397/output/types_test.go @@ -0,0 +1,81 @@ +package output + +import ( + "encoding/json" + "testing" +) + +// TestNestedObjectTypes verifies that nested objects get proper types. +// https://github.com/oapi-codegen/oapi-codegen/issues/1397 +// +// Note: The x-go-type-name extension is not currently supported. Types are +// named based on their path in the schema rather than the specified names. +func TestNestedObjectTypes(t *testing.T) { + // Test schema should have array of enums, and two nested objects + test := MyTestRequest{ + Field1: []TestField1Item{ + TestField1Item_option1, + TestField1Item_option2, + }, + Field2: &MyTestRequestNestedField{ + Field1: true, + Field2: "value2", + }, + Field3: &TestField3{ + Field1: false, + Field2: "value3", + }, + } + + if len(test.Field1) != 2 { + t.Errorf("Field1 length = %d, want 2", len(test.Field1)) + } + if test.Field2.Field1 != true { + t.Errorf("Field2.Field1 = %v, want true", test.Field2.Field1) + } + if test.Field3.Field2 != "value3" { + t.Errorf("Field3.Field2 = %q, want %q", test.Field3.Field2, "value3") + } +} + +func TestEnumArrayField(t *testing.T) { + // Field1 is an array of enum values + _ = TestField1Item_option1 + _ = TestField1Item_option2 + + items := []TestField1Item{ + TestField1Item("option1"), + TestField1Item("option2"), + } + + if len(items) != 2 { + t.Errorf("items length = %d, want 2", len(items)) + } +} + +func TestTestJSONRoundTrip(t *testing.T) { + original := MyTestRequest{ + Field1: []TestField1Item{TestField1Item_option1}, + Field2: &MyTestRequestNestedField{Field1: true, Field2: "test"}, + } + + data, err := json.Marshal(original) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + var decoded MyTestRequest + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if len(decoded.Field1) != 1 { + t.Errorf("Field1 length = %d, want 1", len(decoded.Field1)) + } + if decoded.Field2 == nil { + t.Fatal("Field2 should not be nil") + } + if decoded.Field2.Field1 != true { + t.Errorf("Field2.Field1 = %v, want true", decoded.Field2.Field1) + } +} diff --git a/experimental/internal/codegen/test/issues/issue_1397/spec.yaml b/experimental/internal/codegen/test/issues/issue_1397/spec.yaml new file mode 100644 index 0000000000..3f1289ceac --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1397/spec.yaml @@ -0,0 +1,57 @@ +# Issue 1397: Basic type generation with x-go-type-name +# https://github.com/oapi-codegen/oapi-codegen/issues/1397 +openapi: "3.0.1" +info: + title: Issue 1397 Test + version: 1.0.0 +components: + schemas: + Test: + type: object + properties: + field1: + description: A array of enum values + items: + enum: + - option1 + - option2 + type: string + maxItems: 5 + minItems: 0 + type: array + field2: + description: A nested object with allocated name + properties: + field1: + type: boolean + field2: + type: string + required: + - field1 + - field2 + type: object + x-go-type-name: MyTestRequestNestedField + field3: + description: A nested object without allocated name + properties: + field1: + type: boolean + field2: + type: string + required: + - field1 + - field2 + type: object + x-go-type-name: MyTestRequest +paths: + /test: + get: + operationId: test + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Test" + responses: + 204: + description: good diff --git a/experimental/internal/codegen/test/issues/issue_1429/doc.go b/experimental/internal/codegen/test/issues/issue_1429/doc.go new file mode 100644 index 0000000000..ab6cbd6e21 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1429/doc.go @@ -0,0 +1,5 @@ +// Package issue_1429 tests that enums inside anyOf members are generated. +// https://github.com/oapi-codegen/oapi-codegen/issues/1429 +package issue_1429 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go new file mode 100644 index 0000000000..993992b0d7 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go @@ -0,0 +1,149 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/test +type Test struct { + TestAnyOf0 *TestAnyOf0 + TestAnyOf1 *TestAnyOf1 +} + +func (u Test) MarshalJSON() ([]byte, error) { + result := make(map[string]any) + + if u.TestAnyOf0 != nil { + data, err := json.Marshal(u.TestAnyOf0) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err == nil { + for k, v := range m { + result[k] = v + } + } + } + if u.TestAnyOf1 != nil { + data, err := json.Marshal(u.TestAnyOf1) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err == nil { + for k, v := range m { + result[k] = v + } + } + } + + return json.Marshal(result) +} + +func (u *Test) UnmarshalJSON(data []byte) error { + var v0 TestAnyOf0 + if err := json.Unmarshal(data, &v0); err == nil { + u.TestAnyOf0 = &v0 + } + + var v1 TestAnyOf1 + if err := json.Unmarshal(data, &v1); err == nil { + u.TestAnyOf1 = &v1 + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *Test) ApplyDefaults() { + if u.TestAnyOf0 != nil { + u.TestAnyOf0.ApplyDefaults() + } + if u.TestAnyOf1 != nil { + u.TestAnyOf1.ApplyDefaults() + } +} + +// #/components/schemas/test/anyOf/0 +type TestAnyOf0 struct { + FieldA *string `json:"fieldA,omitempty" form:"fieldA,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *TestAnyOf0) ApplyDefaults() { +} + +// #/components/schemas/test/anyOf/1 +type TestAnyOf1 struct { + FieldA *string `json:"fieldA,omitempty" form:"fieldA,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *TestAnyOf1) ApplyDefaults() { +} + +// #/components/schemas/test/anyOf/1/properties/fieldA +type TestAnyOf1FieldA string + +const ( + TestAnyOf1FieldA_foo TestAnyOf1FieldA = "foo" + TestAnyOf1FieldA_bar TestAnyOf1FieldA = "bar" +) + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/6xSsW7cMAzd9RUPcYEsvfMl7VJtHTN1KdBZtmlLwZkURLrF/X0hOwdfrh3DiXqkHt8T", + "1eBFdSE8fX3+5kG8zGAxTMRUgtGAP5EYi9YsWUTgy4/RNYhmWX3bTsni0h17mVsJOR16GWgifn9IdYS2", + "dYZrXINflTMgF8lU7ILEmgZC4I0eM80dFcSgFauiPsMibfLsksk10CjLeUBHu9ajk0wccvL4cjwdTy7x", + "KN4BluxM/sYpfpKaA35T0STs8bT252BR64XWSK0mwERvCVDVBkvCL4O/MtQopFlYSa+NwMPz6fSwH4Fe", + "2IjtFgJCzufUr5Ttqwq/rwLaR5rDPQp8KjR6PDZtL3MWJjZtt15dlT+6vVBvv9U2ot0a1rf0kO6V+quZ", + "dQP7yMN1S+nWXo0x0Xn4fi9uY1QriaePJdmifoF/H+SAUeQ/aBeK+xsAAP//dJpKJ+ICAAA=", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_1429/output/types_test.go b/experimental/internal/codegen/test/issues/issue_1429/output/types_test.go new file mode 100644 index 0000000000..9020a645b2 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1429/output/types_test.go @@ -0,0 +1,37 @@ +package output + +import ( + "encoding/json" + "testing" +) + +// TestEnumGenerated verifies that the enum type is generated for properties inside anyOf. +// Issue 1429: enum type was not being generated when used inside anyOf. +func TestEnumGenerated(t *testing.T) { + // The enum type should exist and have the expected constants + _ = TestAnyOf1FieldA_foo + _ = TestAnyOf1FieldA_bar + + // The alias should also exist + _ = TestAnyOf1FieldA(TestAnyOf1FieldA_foo) +} + +// TestAnyOfMarshal verifies that the anyOf type can be marshaled. +func TestAnyOfMarshal(t *testing.T) { + test := Test{ + TestAnyOf1: &TestAnyOf1{ + FieldA: ptr("foo"), + }, + } + + data, err := json.Marshal(test) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + + t.Logf("Marshaled: %s", string(data)) +} + +func ptr[T any](v T) *T { + return &v +} diff --git a/experimental/internal/codegen/test/issues/issue_1429/spec.yaml b/experimental/internal/codegen/test/issues/issue_1429/spec.yaml new file mode 100644 index 0000000000..a07aef195a --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1429/spec.yaml @@ -0,0 +1,33 @@ +# Issue 1429: enum not generated when used with anyOf +# https://github.com/oapi-codegen/oapi-codegen/issues/1429 +# +# When a property inside an anyOf member has an enum, the enum type +# should be generated. +openapi: 3.0.0 +info: + title: Issue 1429 Test + version: 1.0.0 +paths: + /test: + get: + operationId: Test + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/test' +components: + schemas: + test: + type: object + anyOf: + - properties: + fieldA: + type: string + - properties: + fieldA: + type: string + enum: + - foo + - bar diff --git a/experimental/internal/codegen/test/issues/issue_1496/doc.go b/experimental/internal/codegen/test/issues/issue_1496/doc.go new file mode 100644 index 0000000000..e09cb2f78e --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1496/doc.go @@ -0,0 +1,5 @@ +// Package issue_1496 tests that inline schemas generate valid Go identifiers. +// https://github.com/oapi-codegen/oapi-codegen/issues/1496 +package issue_1496 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go new file mode 100644 index 0000000000..2f95e2dbf8 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go @@ -0,0 +1,168 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + "sync" +) + +// #/paths//something/get/responses/200/content/application/json/schema +type GetSomethingJSONResponse struct { + Results []GetSomething200ResponseJSON2 `json:"results" form:"results"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *GetSomethingJSONResponse) ApplyDefaults() { +} + +// #/paths//something/get/responses/200/content/application/json/schema/properties/results +type GetSomething200ResponseJSON1 = []GetSomething200ResponseJSON2 + +// #/paths//something/get/responses/200/content/application/json/schema/properties/results/items +type GetSomething200ResponseJSON2 struct { + GetSomething200ResponseJSONAnyOf0 *GetSomething200ResponseJSONAnyOf0 + GetSomething200ResponseJSONAnyOf11 *GetSomething200ResponseJSONAnyOf11 +} + +func (u GetSomething200ResponseJSON2) MarshalJSON() ([]byte, error) { + result := make(map[string]any) + + if u.GetSomething200ResponseJSONAnyOf0 != nil { + data, err := json.Marshal(u.GetSomething200ResponseJSONAnyOf0) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err == nil { + for k, v := range m { + result[k] = v + } + } + } + if u.GetSomething200ResponseJSONAnyOf11 != nil { + data, err := json.Marshal(u.GetSomething200ResponseJSONAnyOf11) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err == nil { + for k, v := range m { + result[k] = v + } + } + } + + return json.Marshal(result) +} + +func (u *GetSomething200ResponseJSON2) UnmarshalJSON(data []byte) error { + var v0 GetSomething200ResponseJSONAnyOf0 + if err := json.Unmarshal(data, &v0); err == nil { + u.GetSomething200ResponseJSONAnyOf0 = &v0 + } + + var v1 GetSomething200ResponseJSONAnyOf11 + if err := json.Unmarshal(data, &v1); err == nil { + u.GetSomething200ResponseJSONAnyOf11 = &v1 + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *GetSomething200ResponseJSON2) ApplyDefaults() { + if u.GetSomething200ResponseJSONAnyOf0 != nil { + u.GetSomething200ResponseJSONAnyOf0.ApplyDefaults() + } + if u.GetSomething200ResponseJSONAnyOf11 != nil { + u.GetSomething200ResponseJSONAnyOf11.ApplyDefaults() + } +} + +// #/paths//something/get/responses/200/content/application/json/schema/properties/results/items/anyOf/0 +type GetSomething200ResponseJSONAnyOf0 struct { + Order *string `json:"order,omitempty" form:"order,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *GetSomething200ResponseJSONAnyOf0) ApplyDefaults() { +} + +// #/paths//something/get/responses/200/content/application/json/schema/properties/results/items/anyOf/1 +type GetSomething200ResponseJSONAnyOf11 struct { + Error *GetSomething200ResponseJSONAnyOf12 `json:"error,omitempty" form:"error,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *GetSomething200ResponseJSONAnyOf11) ApplyDefaults() { + if s.Error != nil { + s.Error.ApplyDefaults() + } +} + +// #/paths//something/get/responses/200/content/application/json/schema/properties/results/items/anyOf/1/properties/error +type GetSomething200ResponseJSONAnyOf12 struct { + Code *float32 `json:"code,omitempty" form:"code,omitempty"` + Message *string `json:"message,omitempty" form:"message,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *GetSomething200ResponseJSONAnyOf12) ApplyDefaults() { +} + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/7xTTW+bQBC98yue4ksrJeC0VaVy66nyKVJTqcdoDWOYyMxud4ZY/vfVgqlJncTJJZyY", + "fTP7PhgWWKn2hOsv376W+C5e9p3vFX59T5VBq5Y6hx1bCy90s0FDQtEZKVge3JZrcE1ivGGK2QKtWdCy", + "KBq2tl/nle8K7wJfVb6mhuRxwYlai8SdLbIFfrckcAKWLQtN5CxwiKTBixJ6JYWT/c2mGARdwlr6p6qG", + "7QNBXEfZAjunUHPRWJrRg4P03ZoiPlDe5Jf4tFze/STtt6Z3K6PuI3YtVy14Zk/ww+eZDyQucInP+TJf", + "ZiwbX2aAsW2pnIWIX6SWAQ8Ulb2UuB76g7NW00ChviNrWZpUAQ3Z+AL4kDywl1VdpvPbqfOATxnoNICk", + "/1gANWkVOdhAfHHbVxWpXswaKi9GYvMZwIWw5WpgLu7Vy2MUh+/w/ymGqMvDppyAkf70HKk+HQOukpWU", + "+QkWYgrBeO5xfucw9RQ0qXExuv2TOBt1z4xiXKjnwCT4Ra+v1X98fKwpvtwyOVKLxxV4B2kUo3+ltLOc", + "b+Ed17Om810T/fgrn23vSNU1b7j3kPjfAAAA///mDrwBGwUAAA==", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_1496/output/types_test.go b/experimental/internal/codegen/test/issues/issue_1496/output/types_test.go new file mode 100644 index 0000000000..5056b4583e --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1496/output/types_test.go @@ -0,0 +1,41 @@ +package output + +import ( + "encoding/json" + "testing" +) + +// TestValidIdentifiers verifies that all generated type names are valid Go identifiers. +// Issue 1496: Inline schemas in responses were generating identifiers starting with numbers. +func TestValidIdentifiers(t *testing.T) { + // If this compiles, the identifiers are valid + response := GetSomethingJSONResponse{ + Results: []GetSomething200ResponseJSON2{ + { + GetSomething200ResponseJSONAnyOf0: &GetSomething200ResponseJSONAnyOf0{ + Order: ptr("order-123"), + }, + }, + { + GetSomething200ResponseJSONAnyOf11: &GetSomething200ResponseJSONAnyOf11{ + Error: &GetSomething200ResponseJSONAnyOf12{ + Code: ptr(float32(400)), + Message: ptr("Bad request"), + }, + }, + }, + }, + } + + // Should be able to marshal + data, err := json.Marshal(response) + if err != nil { + t.Fatalf("Failed to marshal response: %v", err) + } + + t.Logf("Marshaled response: %s", string(data)) +} + +func ptr[T any](v T) *T { + return &v +} diff --git a/experimental/internal/codegen/test/issues/issue_1496/spec.yaml b/experimental/internal/codegen/test/issues/issue_1496/spec.yaml new file mode 100644 index 0000000000..f4156e97ad --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1496/spec.yaml @@ -0,0 +1,40 @@ +# Issue 1496: Anonymous object schema with oneOf generates invalid identifier +# https://github.com/oapi-codegen/oapi-codegen/issues/1496 +# +# When an inline schema in a response uses anyOf/oneOf, the generated type name +# was starting with a number (e.g., 200_Results_Item) which is invalid in Go. +openapi: 3.0.0 +info: + title: Issue 1496 Test + version: 1.0.0 +paths: + /something: + get: + operationId: getSomething + responses: + 200: + description: "Success" + content: + application/json: + schema: + type: object + required: + - results + properties: + results: + type: array + items: + anyOf: + - type: object + properties: + order: + type: string + - type: object + properties: + error: + type: object + properties: + code: + type: number + message: + type: string diff --git a/experimental/internal/codegen/test/issues/issue_1710/doc.go b/experimental/internal/codegen/test/issues/issue_1710/doc.go new file mode 100644 index 0000000000..3ce6436c35 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1710/doc.go @@ -0,0 +1,5 @@ +// Package issue_1710 tests that fields are not lost in nested allOf oneOf structures. +// https://github.com/oapi-codegen/oapi-codegen/issues/1710 +package issue_1710 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go new file mode 100644 index 0000000000..6efb31f3a5 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go @@ -0,0 +1,212 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/BasePrompt +type BasePrompt struct { + Name string `json:"name" form:"name"` + Version int `json:"version" form:"version"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *BasePrompt) ApplyDefaults() { +} + +// #/components/schemas/TextPrompt +type TextPrompt struct { + Prompt string `json:"prompt" form:"prompt"` + Name string `json:"name" form:"name"` + Version int `json:"version" form:"version"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *TextPrompt) ApplyDefaults() { +} + +// #/components/schemas/ChatMessage +type ChatMessage struct { + Role string `json:"role" form:"role"` + Content string `json:"content" form:"content"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ChatMessage) ApplyDefaults() { +} + +// #/components/schemas/ChatPrompt +type ChatPrompt struct { + Prompt []ChatMessage `json:"prompt" form:"prompt"` + Name string `json:"name" form:"name"` + Version int `json:"version" form:"version"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ChatPrompt) ApplyDefaults() { +} + +// #/components/schemas/ChatPrompt/properties/prompt +type ChatPromptPrompt = []ChatMessage + +// #/components/schemas/Prompt +type Prompt struct { + PromptOneOf0 *PromptOneOf0 + PromptOneOf1 *PromptOneOf1 +} + +func (u Prompt) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.PromptOneOf0 != nil { + count++ + data, err = json.Marshal(u.PromptOneOf0) + if err != nil { + return nil, err + } + } + if u.PromptOneOf1 != nil { + count++ + data, err = json.Marshal(u.PromptOneOf1) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("Prompt: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *Prompt) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 PromptOneOf0 + if err := json.Unmarshal(data, &v0); err == nil { + u.PromptOneOf0 = &v0 + successCount++ + } + + var v1 PromptOneOf1 + if err := json.Unmarshal(data, &v1); err == nil { + u.PromptOneOf1 = &v1 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("Prompt: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *Prompt) ApplyDefaults() { + if u.PromptOneOf0 != nil { + u.PromptOneOf0.ApplyDefaults() + } + if u.PromptOneOf1 != nil { + u.PromptOneOf1.ApplyDefaults() + } +} + +// #/components/schemas/Prompt/oneOf/0 +type PromptOneOf0 struct { + Type *string `json:"type,omitempty" form:"type,omitempty"` + Prompt []ChatMessage `json:"prompt" form:"prompt"` + Name string `json:"name" form:"name"` + Version int `json:"version" form:"version"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *PromptOneOf0) ApplyDefaults() { +} + +// #/components/schemas/Prompt/oneOf/0/allOf/0/properties/type +type PromptOneOf0AllOf0Type string + +const ( + PromptOneOf0AllOf0Type_chat PromptOneOf0AllOf0Type = "chat" +) + +// #/components/schemas/Prompt/oneOf/1 +type PromptOneOf1 struct { + Type *string `json:"type,omitempty" form:"type,omitempty"` + Prompt string `json:"prompt" form:"prompt"` + Name string `json:"name" form:"name"` + Version int `json:"version" form:"version"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *PromptOneOf1) ApplyDefaults() { +} + +// #/components/schemas/Prompt/oneOf/1/allOf/0/properties/type +type PromptOneOf1AllOf0Type string + +const ( + PromptOneOf1AllOf0Type_text PromptOneOf1AllOf0Type = "text" +) + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/9SUT4/TMBDF7/kUIwWpFzZpxQEpRzhxQMuhEmc3eYmNEtt4JvtHiO+O4qYbh2132RVC", + "oqfO68zzm5+b5PSJeQTt3u+2FbUGfUO9YyFjyYIFDam+v27JWVy3WU5axHNVlp0RPR6K2g2lU95c1a5B", + "B7suzOTN5WSe5VlOXzUsjWxstza/NaKPJ7ydlGMOJtZu7Bs6gHwAI9ygKbKc9hq08cENXjZz5FvFdMDk", + "O4UvMudhlTcVvSu2xTYztnVVRiRGelTJyrQHS0Z0g8DG2Yp2sd8r0VzRj59Z7QbvLKzwNM+1xqDiV6IP", + "ivElpjjWRHLvUZE7fEMts+SD8whiwKcmIqsGLNVpjCUY2z3Ip0SP+owVdAizHvB9NAHN0ncV/ZNydorK", + "Hnfyqsx+NXQh9bkwx8FZiLed/vomoK1ok5cL53KGXC58N3Hio1byGcyqwwvDB9c/D7x2VmBfs+Jkn5Sz", + "0UPmv8pbhaDuE9UIBk7b6EmmCcPNP7m09fLxEU+tztA4c+STvZcIrrg9Ui/8CZYP7DicG4s3rJX8Fu45", + "6CmTS9yXNf97QoK7lxBaXkt/TOhXAAAA//+WRkFKuQYAAA==", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_1710/output/types_test.go b/experimental/internal/codegen/test/issues/issue_1710/output/types_test.go new file mode 100644 index 0000000000..345ab353e9 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1710/output/types_test.go @@ -0,0 +1,102 @@ +package output + +import ( + "encoding/json" + "testing" +) + +// TestPromptOneOfHasPromptField verifies that the 'prompt' field is not lost +// in nested allOf/oneOf structures. +// https://github.com/oapi-codegen/oapi-codegen/issues/1710 +func TestPromptOneOfHasPromptField(t *testing.T) { + // Test ChatPrompt variant (PromptOneOf0) - prompt is []ChatMessage + chatType := "chat" + chatPrompt := PromptOneOf0{ + Type: &chatType, + Prompt: []ChatMessage{ + {Role: "user", Content: "hello"}, + }, + } + + if chatPrompt.Type == nil || *chatPrompt.Type != "chat" { + t.Error("ChatPrompt variant should have Type='chat'") + } + if len(chatPrompt.Prompt) != 1 { + t.Errorf("ChatPrompt.Prompt should have 1 message, got %d", len(chatPrompt.Prompt)) + } + if chatPrompt.Prompt[0].Role != "user" { + t.Errorf("ChatPrompt.Prompt[0].Role = %q, want %q", chatPrompt.Prompt[0].Role, "user") + } + + // Test TextPrompt variant (PromptOneOf1) - prompt is string + textType := "text" + textPrompt := PromptOneOf1{ + Type: &textType, + Prompt: "Hello, world!", + } + + if textPrompt.Type == nil || *textPrompt.Type != "text" { + t.Error("TextPrompt variant should have Type='text'") + } + if textPrompt.Prompt != "Hello, world!" { + t.Errorf("TextPrompt.Prompt = %q, want %q", textPrompt.Prompt, "Hello, world!") + } +} + +func TestPromptJSONRoundTrip(t *testing.T) { + // Test chat prompt variant + chatType := "chat" + chatVariant := Prompt{ + PromptOneOf0: &PromptOneOf0{ + Type: &chatType, + Prompt: []ChatMessage{ + {Role: "user", Content: "test message"}, + }, + }, + } + + data, err := json.Marshal(chatVariant) + if err != nil { + t.Fatalf("Marshal chat variant failed: %v", err) + } + + var decoded Prompt + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Unmarshal chat variant failed: %v", err) + } + + if decoded.PromptOneOf0 == nil { + t.Fatal("Expected PromptOneOf0 to be set after unmarshal") + } + if len(decoded.PromptOneOf0.Prompt) != 1 { + t.Errorf("Expected 1 message, got %d", len(decoded.PromptOneOf0.Prompt)) + } +} + +func TestTextPromptHasPromptField(t *testing.T) { + // Verify TextPrompt (from allOf) has the prompt field + tp := TextPrompt{ + Prompt: "my prompt", + Name: "test", + Version: 1, + } + + if tp.Prompt != "my prompt" { + t.Errorf("TextPrompt.Prompt = %q, want %q", tp.Prompt, "my prompt") + } +} + +func TestChatPromptHasPromptField(t *testing.T) { + // Verify ChatPrompt (from allOf) has the prompt field + cp := ChatPrompt{ + Prompt: []ChatMessage{ + {Role: "assistant", Content: "hello"}, + }, + Name: "test", + Version: 1, + } + + if len(cp.Prompt) != 1 { + t.Errorf("ChatPrompt.Prompt should have 1 message, got %d", len(cp.Prompt)) + } +} diff --git a/experimental/internal/codegen/test/issues/issue_1710/spec.yaml b/experimental/internal/codegen/test/issues/issue_1710/spec.yaml new file mode 100644 index 0000000000..b1219e00d7 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_1710/spec.yaml @@ -0,0 +1,76 @@ +# Issue 1710: field lost in nested allOf oneOf +# https://github.com/oapi-codegen/oapi-codegen/issues/1710 +# +# When using nested allOf with oneOf, all fields should be preserved. +# The 'prompt' field was being lost. +openapi: 3.0.0 +info: + title: Issue 1710 Test + version: 1.0.0 +paths: {} +components: + schemas: + BasePrompt: + type: object + properties: + name: + type: string + version: + type: integer + required: + - name + - version + TextPrompt: + type: object + properties: + prompt: + type: string + required: + - prompt + allOf: + - $ref: '#/components/schemas/BasePrompt' + ChatMessage: + type: object + properties: + role: + type: string + content: + type: string + required: + - role + - content + ChatPrompt: + type: object + properties: + prompt: + type: array + items: + $ref: '#/components/schemas/ChatMessage' + required: + - prompt + allOf: + - $ref: '#/components/schemas/BasePrompt' + Prompt: + oneOf: + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - chat + - $ref: '#/components/schemas/ChatPrompt' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - text + - $ref: '#/components/schemas/TextPrompt' + required: + - type diff --git a/experimental/internal/codegen/test/issues/issue_193/doc.go b/experimental/internal/codegen/test/issues/issue_193/doc.go new file mode 100644 index 0000000000..dcf3fbff8c --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_193/doc.go @@ -0,0 +1,5 @@ +// Package issue_193 tests allOf with additionalProperties merging. +// https://github.com/oapi-codegen/oapi-codegen/issues/193 +package issue_193 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_193/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_193/output/types.gen.go new file mode 100644 index 0000000000..33a950f497 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_193/output/types.gen.go @@ -0,0 +1,71 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/Person +type Person struct { + Metadata string `json:"metadata" form:"metadata"` + Name *string `json:"name,omitempty" form:"name,omitempty"` + Age *float32 `json:"age,omitempty" form:"age,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Person) ApplyDefaults() { +} + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/6yQvW7jQAyE+32KAVzbsuHK21151fkV1itK4kG73FtSFwRB3j2QHP8BMZAiHTn4SM5w", + "hd+qE2F32Hv8Gsc/HV7YBoS2ZWPJYTxWKVSNSZGo9px7t8JgVtQ3Tc82TKdNlNRIKLyO0lJP+bHh+YI2", + "u8PeSaEcCnvsN9vN1nHuxDvA2EbyMFKDxoFScMB/qsqSPXYLW4IN6vH27qKkIpmy6Tx75pcSOFJVyeca", + "CHOeSwOsECUlyeiYxlav+hr2WshDTn8p2lXGl0/wsDrRHVTp38SVWn+nzTsTWWiDhTu53LY8wBf0UcWn", + "LbU6P/2WoiwhoYUidxx/Ns4zjzkk+pa/5VL/hM1TOlF1HwEAAP//7z/Hg3YCAAA=", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_193/output/types_test.go b/experimental/internal/codegen/test/issues/issue_193/output/types_test.go new file mode 100644 index 0000000000..301c97d923 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_193/output/types_test.go @@ -0,0 +1,76 @@ +package output + +import ( + "encoding/json" + "testing" +) + +// TestAllOfWithAdditionalProperties verifies that allOf with additionalProperties: true +// merges fields correctly from multiple allOf members. +// https://github.com/oapi-codegen/oapi-codegen/issues/193 +func TestAllOfWithAdditionalProperties(t *testing.T) { + name := "John" + age := float32(30) + + person := Person{ + Metadata: "some-metadata", + Name: &name, + Age: &age, + } + + // All fields from both allOf members should be present + if person.Metadata != "some-metadata" { + t.Errorf("Metadata = %q, want %q", person.Metadata, "some-metadata") + } + if *person.Name != "John" { + t.Errorf("Name = %q, want %q", *person.Name, "John") + } + if *person.Age != 30 { + t.Errorf("Age = %v, want %v", *person.Age, 30) + } +} + +func TestPersonJSONRoundTrip(t *testing.T) { + name := "Jane" + age := float32(25) + original := Person{ + Metadata: "meta", + Name: &name, + Age: &age, + } + + data, err := json.Marshal(original) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + var decoded Person + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if decoded.Metadata != original.Metadata { + t.Errorf("Metadata mismatch: got %q, want %q", decoded.Metadata, original.Metadata) + } + if *decoded.Name != *original.Name { + t.Errorf("Name mismatch: got %q, want %q", *decoded.Name, *original.Name) + } + if *decoded.Age != *original.Age { + t.Errorf("Age mismatch: got %v, want %v", *decoded.Age, *original.Age) + } +} + +func TestMetadataIsRequired(t *testing.T) { + // Metadata is required (no omitempty), so empty struct should marshal with empty string + person := Person{} + data, err := json.Marshal(person) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + // Should contain "metadata" even if empty + expected := `{"metadata":""}` + if string(data) != expected { + t.Errorf("Marshal result = %s, want %s", string(data), expected) + } +} diff --git a/experimental/internal/codegen/test/issues/issue_193/spec.yaml b/experimental/internal/codegen/test/issues/issue_193/spec.yaml new file mode 100644 index 0000000000..375d227fb4 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_193/spec.yaml @@ -0,0 +1,27 @@ +# Issue 193: AllOf with additionalProperties merging +# https://github.com/oapi-codegen/oapi-codegen/issues/193 +openapi: 3.0.0 +info: + title: test schema + version: 1.0.0 +paths: {} +components: + schemas: + Person: + allOf: + # common fields + - type: object + additionalProperties: true + required: + - metadata + properties: + metadata: + type: string + # person specific fields + - type: object + additionalProperties: true + properties: + name: + type: string + age: + type: number diff --git a/experimental/internal/codegen/test/issues/issue_2102/doc.go b/experimental/internal/codegen/test/issues/issue_2102/doc.go new file mode 100644 index 0000000000..753b091ce1 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_2102/doc.go @@ -0,0 +1,5 @@ +// Package issue_2102 tests that properties defined at the same level as allOf are included. +// https://github.com/oapi-codegen/oapi-codegen/issues/2102 +package issue_2102 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go new file mode 100644 index 0000000000..43d8ca049e --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go @@ -0,0 +1,82 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/Foo +type Foo struct { + Foo string `json:"foo" form:"foo"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Foo) ApplyDefaults() { +} + +// #/components/schemas/Bar +type Bar struct { + Bar string `json:"bar" form:"bar"` + Foo string `json:"foo" form:"foo"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Bar) ApplyDefaults() { +} + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/3xSwY6bMBC98xVPS6VcmpBNbxz3sFJPvVTqNQYP2CvwuJ5hq/x9BSQ16UbLBfzmzXsz", + "z5T4LjIRTs/HUw0zDD86/PHqEBNHSupJYBRiRsJA7zRgf1dKBN8HTmSLEk41Sl1VvVc3NYeWx4pN9PuW", + "LfUU7g9+9pVqNi7KosQvRwEG0joaDZwRNKwO5+x2hgkW52XI8zyVOtpM9rUoF2QznqXOB7KwPlGrwwUc", + "1qbVRBxPg0VDGCn1ZJfNP6p0iccFW+NJ1FGi0JIcCo4UTPQ1vh2Oh2PhQ8d1AajXgepNtPhJogXwTkk8", + "hxrPCz8adTI3VI1J8xvoSdcPQKZxNOlSozHpCiWSyEFIbhzg6XQ8PuUjYEna5KMuPrl1floOSkG3bMDE", + "OPjWzPzqTTjcV3EN638U+JKoq7Erq5bHyIGCSrVypXoxaVdkfG6+lladV+aboF4i1eDmjVr9t+XvySey", + "2XOPjvl6yjeT613Wy5qiyYd+gV9u8T70e6TY5I6Hilj/hu2InwXyyrzbUD9uuOLzdf0NAAD//90rMTaT", + "AwAA", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_2102/output/types_test.go b/experimental/internal/codegen/test/issues/issue_2102/output/types_test.go new file mode 100644 index 0000000000..b4916b2c33 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_2102/output/types_test.go @@ -0,0 +1,52 @@ +package output + +import ( + "encoding/json" + "testing" +) + +// TestBarHasBothProperties verifies that Bar has both foo and bar properties. +// Issue 2102: When a schema has both properties and allOf at the same level, +// the properties were being ignored. +func TestBarHasBothProperties(t *testing.T) { + // Bar should have both foo (from allOf ref to Foo) and bar (from direct properties) + bar := Bar{ + Foo: "test-foo", + Bar: "test-bar", + } + + // Should be able to marshal/unmarshal + data, err := json.Marshal(bar) + if err != nil { + t.Fatalf("Failed to marshal Bar: %v", err) + } + + var unmarshaled Bar + if err := json.Unmarshal(data, &unmarshaled); err != nil { + t.Fatalf("Failed to unmarshal Bar: %v", err) + } + + if unmarshaled.Foo != "test-foo" { + t.Errorf("Expected Foo to be 'test-foo', got %q", unmarshaled.Foo) + } + if unmarshaled.Bar != "test-bar" { + t.Errorf("Expected Bar to be 'test-bar', got %q", unmarshaled.Bar) + } +} + +// TestBarRequiredFields verifies that bar is required (from allOf member's required array). +func TestBarRequiredFields(t *testing.T) { + // Both foo and bar should be required (no omitempty), so an empty struct + // should marshal with empty string values + bar := Bar{} + data, err := json.Marshal(bar) + if err != nil { + t.Fatalf("Failed to marshal empty Bar: %v", err) + } + + // Both fields should be present in JSON + expected := `{"bar":"","foo":""}` + if string(data) != expected { + t.Errorf("Expected %s, got %s", expected, string(data)) + } +} diff --git a/experimental/internal/codegen/test/issues/issue_2102/spec.yaml b/experimental/internal/codegen/test/issues/issue_2102/spec.yaml new file mode 100644 index 0000000000..cbafb5a6d3 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_2102/spec.yaml @@ -0,0 +1,39 @@ +# Issue 2102: allOf with properties at same level - properties are ignored +# https://github.com/oapi-codegen/oapi-codegen/issues/2102 +# +# When a schema has both `properties` and `allOf` at the same level, +# the properties defined directly on the schema should be merged with +# the properties from the allOf references. +openapi: 3.0.0 +info: + title: Issue 2102 Test + version: 1.0.0 +paths: + /bar: + get: + summary: bar + responses: + "200": + description: bar + content: + application/json: + schema: + $ref: '#/components/schemas/Bar' +components: + schemas: + Foo: + type: object + required: + - foo + properties: + foo: + type: string + Bar: + type: object + properties: + bar: + type: string + allOf: + - $ref: '#/components/schemas/Foo' + - required: + - bar diff --git a/experimental/internal/codegen/test/issues/issue_312/doc.go b/experimental/internal/codegen/test/issues/issue_312/doc.go new file mode 100644 index 0000000000..0f29af76ad --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_312/doc.go @@ -0,0 +1,6 @@ +// Package issue_312 tests proper escaping of paths with special characters. +// https://github.com/oapi-codegen/oapi-codegen/issues/312 +// This tests paths with colons like /pets:validate +package issue_312 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_312/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_312/output/types.gen.go new file mode 100644 index 0000000000..f5f81dbf6e --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_312/output/types.gen.go @@ -0,0 +1,97 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/Pet +type Pet struct { + Name string `json:"name" form:"name"` // The name of the pet. +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Pet) ApplyDefaults() { +} + +// #/components/schemas/PetNames +type PetNames struct { + Names []string `json:"names" form:"names"` // The names of the pets. +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *PetNames) ApplyDefaults() { +} + +// #/components/schemas/Error +type Error struct { + Code int32 `json:"code" form:"code"` // Error code + Message string `json:"message" form:"message"` // Error message +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Error) ApplyDefaults() { +} + +// #/paths//pets:validate/post/responses/200/content/application/json/schema +type ValidatePetsJSONResponse = []Pet + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/8SVTW/bPAzH7/oURPMAOdVOk5uOzzAMvQw5DLurMm2rtSVNpLsFw777IDmu7TpxsQ3D", + "bjEpkb8/X5QN3BN1CIe7vYSj4hqQtPLGVmIDNbMnmeeV4bp7yLRrc6e8udWuwArt/MPEOJQf7vZiA+9q", + "1E8EPjiP4SUkuBK84prgq+EayKM2qgFdq6A0YyBozBOCdo2zJJxHq7yRcHPIdtnuRhhbOikAnjGQcVbC", + "XbQLADbcoByVACOxACiQdDCe0+E1pKBajPlFoos5co9M8lk1plCM0QLgHXH/C4C6tlXhJOHz+QjEC2fn", + "LO+lAxFBRfd9IWFIchz9Ab90SPy/K05Dwt5oAhYSOHT4YtbOMloezwEo7xujU4L8kZyd+gBI19iquQ3g", + "v4ClhO0m1671zqJlyvuTlB+RP6oWafuCR95ZQhqDbPe73XYac1aDJHFagCvgb6Ffgwfgk0cJKgR1WvgM", + "Y0vLK29q3opRTKm6hq/q6yx+86gZC8AQXPhbKteA38fE22F08+8e+b740ceocDm4H5BjR6Ayz2jBFGjZ", + "lAZDdmlGK+Qj8tkz7stIeAtWtSghZZ1wGyvTyk9MV+b4suq+r8Qhvkh/On3/oi1pjkZ7vHx29XGOY2t6", + "re7hETWLV7V6VeihE+kxYzOtReqDWK3gokKfakz34mvIdXqqMjHgpdX/DUZ6A5KWlK/39yIkTSgpE6t7", + "PhOfPGlN5Ap6/D+bfLZIpKq1gscLSynGMlY4fQlKF1rFyXPYXxOZ8OYMZ4Jf7WkfacD/GQAA//+iio0s", + "6AcAAA==", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_312/output/types_test.go b/experimental/internal/codegen/test/issues/issue_312/output/types_test.go new file mode 100644 index 0000000000..3a2818f642 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_312/output/types_test.go @@ -0,0 +1,72 @@ +package output + +import ( + "encoding/json" + "testing" +) + +// TestPathWithColon verifies that paths with colons (like /pets:validate) generate properly. +// https://github.com/oapi-codegen/oapi-codegen/issues/312 +func TestPathWithColonGeneratesTypes(t *testing.T) { + // The path /pets:validate should generate a ValidatePetsJSONResponse type + response := ValidatePetsJSONResponse{ + {Name: "Fluffy"}, + {Name: "Spot"}, + } + + if len(response) != 2 { + t.Errorf("response length = %d, want 2", len(response)) + } + if response[0].Name != "Fluffy" { + t.Errorf("response[0].Name = %q, want %q", response[0].Name, "Fluffy") + } +} + +func TestPetSchema(t *testing.T) { + pet := Pet{ + Name: "Max", + } + + data, err := json.Marshal(pet) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + expected := `{"name":"Max"}` + if string(data) != expected { + t.Errorf("Marshal result = %s, want %s", string(data), expected) + } +} + +func TestPetNamesSchema(t *testing.T) { + petNames := PetNames{ + Names: []string{"Fluffy", "Spot", "Max"}, + } + + data, err := json.Marshal(petNames) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + var decoded PetNames + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if len(decoded.Names) != 3 { + t.Errorf("Names length = %d, want 3", len(decoded.Names)) + } +} + +func TestErrorSchema(t *testing.T) { + err := Error{ + Code: 404, + Message: "Not Found", + } + + data, _ := json.Marshal(err) + expected := `{"code":404,"message":"Not Found"}` + if string(data) != expected { + t.Errorf("Marshal result = %s, want %s", string(data), expected) + } +} diff --git a/experimental/internal/codegen/test/issues/issue_312/spec.yaml b/experimental/internal/codegen/test/issues/issue_312/spec.yaml new file mode 100644 index 0000000000..fa458ec41f --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_312/spec.yaml @@ -0,0 +1,86 @@ +# Issue 312: Path escaping +# https://github.com/oapi-codegen/oapi-codegen/issues/312 +# Checks proper escaping of paths with special characters like colons +openapi: "3.0.0" +info: + version: 1.0.0 + title: Issue 312 test + description: Checks proper escaping of parameters +paths: + /pets:validate: + post: + summary: Validate pets + description: Validate pets + operationId: validatePets + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PetNames' + responses: + '200': + description: valid pets + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /pets/{petId}: + get: + summary: Get pet given identifier. + operationId: getPet + parameters: + - name: petId + in: path + required: true + schema: + type: string + responses: + '200': + description: valid pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' +components: + schemas: + Pet: + type: object + required: + - name + properties: + name: + type: string + description: The name of the pet. + + PetNames: + type: object + required: + - names + properties: + names: + type: array + description: The names of the pets. + items: + type: string + + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + description: Error code + message: + type: string + description: Error message diff --git a/experimental/internal/codegen/test/issues/issue_502/doc.go b/experimental/internal/codegen/test/issues/issue_502/doc.go new file mode 100644 index 0000000000..992436f9e7 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_502/doc.go @@ -0,0 +1,5 @@ +// Package issue_502 tests that anyOf with only one ref generates the referenced type. +// https://github.com/oapi-codegen/oapi-codegen/issues/502 +package issue_502 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_502/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_502/output/types.gen.go new file mode 100644 index 0000000000..cda70dd5ea --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_502/output/types.gen.go @@ -0,0 +1,228 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/OptionalClaims +type OptionalClaims struct { + IDToken *string `json:"idToken,omitempty" form:"idToken,omitempty"` + AccessToken *string `json:"accessToken,omitempty" form:"accessToken,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *OptionalClaims) ApplyDefaults() { +} + +// #/components/schemas/Application +type Application struct { + Name *string `json:"name,omitempty" form:"name,omitempty"` + OptionalClaims Nullable[ApplicationOptionalClaims] `json:"optionalClaims,omitempty" form:"optionalClaims,omitempty"` // Optional claims configuration +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Application) ApplyDefaults() { +} + +// #/components/schemas/Application/properties/optionalClaims +// Optional claims configuration +type ApplicationOptionalClaims struct { + OptionalClaims *OptionalClaims +} + +func (u ApplicationOptionalClaims) MarshalJSON() ([]byte, error) { + result := make(map[string]any) + + if u.OptionalClaims != nil { + data, err := json.Marshal(u.OptionalClaims) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err == nil { + for k, v := range m { + result[k] = v + } + } + } + + return json.Marshal(result) +} + +func (u *ApplicationOptionalClaims) UnmarshalJSON(data []byte) error { + var v0 OptionalClaims + if err := json.Unmarshal(data, &v0); err == nil { + u.OptionalClaims = &v0 + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *ApplicationOptionalClaims) ApplyDefaults() { + if u.OptionalClaims != nil { + u.OptionalClaims.ApplyDefaults() + } +} + +// Nullable is a generic type that can distinguish between: +// - Field not provided (unspecified) +// - Field explicitly set to null +// - Field has a value +// +// This is implemented as a map[bool]T where: +// - Empty map: unspecified +// - map[false]T: explicitly null +// - map[true]T: has a value +type Nullable[T any] map[bool]T + +// NewNullableWithValue creates a Nullable with the given value. +func NewNullableWithValue[T any](value T) Nullable[T] { + return Nullable[T]{true: value} +} + +// NewNullNullable creates a Nullable that is explicitly null. +func NewNullNullable[T any]() Nullable[T] { + return Nullable[T]{false: *new(T)} +} + +// Get returns the value if set, or an error if null or unspecified. +func (n Nullable[T]) Get() (T, error) { + if v, ok := n[true]; ok { + return v, nil + } + var zero T + if n.IsNull() { + return zero, ErrNullableIsNull + } + return zero, ErrNullableNotSpecified +} + +// MustGet returns the value or panics if null or unspecified. +func (n Nullable[T]) MustGet() T { + v, err := n.Get() + if err != nil { + panic(err) + } + return v +} + +// Set assigns a value. +func (n *Nullable[T]) Set(value T) { + *n = Nullable[T]{true: value} +} + +// SetNull marks the field as explicitly null. +func (n *Nullable[T]) SetNull() { + *n = Nullable[T]{false: *new(T)} +} + +// SetUnspecified clears the field (as if it was never set). +func (n *Nullable[T]) SetUnspecified() { + *n = nil +} + +// IsNull returns true if the field is explicitly null. +func (n Nullable[T]) IsNull() bool { + if n == nil { + return false + } + _, ok := n[false] + return ok +} + +// IsSpecified returns true if the field was provided (either null or a value). +func (n Nullable[T]) IsSpecified() bool { + return len(n) > 0 +} + +// MarshalJSON implements json.Marshaler. +func (n Nullable[T]) MarshalJSON() ([]byte, error) { + if n.IsNull() { + return []byte("null"), nil + } + if v, ok := n[true]; ok { + return json.Marshal(v) + } + // Unspecified - this shouldn't be called if omitempty is used correctly + return []byte("null"), nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (n *Nullable[T]) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + n.SetNull() + return nil + } + var v T + if err := json.Unmarshal(data, &v); err != nil { + return err + } + n.Set(v) + return nil +} + +// ErrNullableIsNull is returned when trying to get a value from a null Nullable. +var ErrNullableIsNull = errors.New("nullable value is null") + +// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. +var ErrNullableNotSpecified = errors.New("nullable value is not specified") + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/5SRQWvcMBSE7/oVAy7kkqy3KbnoVnrqaS+BnrXys/1a+T0hPbcsIf+92NnueqEQcrNH", + "M8ynUYPvtc6Ep/2jR5DToW9DSocef9hGqKQTVAiFetgpEwYSKsGogsWo9CHSy6trMJrl6tt2YBvn4y7q", + "1GrI/BC1o4Hk9oeXyto+7R9d4xr8GEneuqEFb/VjqDftVEgi3YMNddQ5dRcS18DGjadbQe8halvGndNM", + "EjJ7fNntd3vH0qt3gLEl8tcZ8EzVHPCbSmUVj8+rPQcbq8fLq4s6ZRUSq0u8xpGmsH4Ch2ysEtK3FHg6", + "a1h5PPT4k6KdpVw0UzGmiwng7ll/kVyFf8lqhWW4yCFGqvV979ecE8ewEH2QRMJE72Lof++6Ai5PuRWA", + "B3wq1HvcNe11vva8XXs7290m2VGNhddjf1kXcfUhqvQ8zGW94iYkc0rhuDyqlZnc3wAAAP//6uhqZ+MC", + "AAA=", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_502/output/types_test.go b/experimental/internal/codegen/test/issues/issue_502/output/types_test.go new file mode 100644 index 0000000000..c0f7f6bb71 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_502/output/types_test.go @@ -0,0 +1,107 @@ +package output + +import ( + "encoding/json" + "testing" +) + +// TestAnyOfWithSingleRef verifies that anyOf with a single $ref generates +// correct types that can be used. +// https://github.com/oapi-codegen/oapi-codegen/issues/502 +func TestAnyOfWithSingleRef(t *testing.T) { + // OptionalClaims should be properly generated + claims := OptionalClaims{ + IDToken: ptrTo("id-token-value"), + AccessToken: ptrTo("access-token-value"), + } + + if *claims.IDToken != "id-token-value" { + t.Errorf("IDToken = %q, want %q", *claims.IDToken, "id-token-value") + } + if *claims.AccessToken != "access-token-value" { + t.Errorf("AccessToken = %q, want %q", *claims.AccessToken, "access-token-value") + } +} + +func TestApplicationWithAnyOfProperty(t *testing.T) { + // Application.OptionalClaims is an anyOf with a single ref + nullable: true + // It should be Nullable[ApplicationOptionalClaims] + app := Application{ + Name: ptrTo("my-app"), + OptionalClaims: NewNullableWithValue(ApplicationOptionalClaims{ + OptionalClaims: &OptionalClaims{ + IDToken: ptrTo("token"), + }, + }), + } + + if *app.Name != "my-app" { + t.Errorf("Name = %q, want %q", *app.Name, "my-app") + } + if !app.OptionalClaims.IsSpecified() { + t.Fatal("OptionalClaims should be specified") + } + optClaims := app.OptionalClaims.MustGet() + if optClaims.OptionalClaims == nil { + t.Fatal("OptionalClaims.OptionalClaims should not be nil") + } + if *optClaims.OptionalClaims.IDToken != "token" { + t.Errorf("IDToken = %q, want %q", *optClaims.OptionalClaims.IDToken, "token") + } +} + +func TestApplicationJSONRoundTrip(t *testing.T) { + original := Application{ + Name: ptrTo("test-app"), + OptionalClaims: NewNullableWithValue(ApplicationOptionalClaims{ + OptionalClaims: &OptionalClaims{ + IDToken: ptrTo("id"), + AccessToken: ptrTo("access"), + }, + }), + } + + data, err := json.Marshal(original) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + var decoded Application + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if *decoded.Name != *original.Name { + t.Errorf("Name mismatch: got %q, want %q", *decoded.Name, *original.Name) + } + if !decoded.OptionalClaims.IsSpecified() { + t.Fatal("OptionalClaims should be specified after round trip") + } + optClaims := decoded.OptionalClaims.MustGet() + if optClaims.OptionalClaims == nil { + t.Fatal("OptionalClaims.OptionalClaims should not be nil after round trip") + } +} + +func TestApplicationNullOptionalClaims(t *testing.T) { + // Test with explicitly null optional claims + app := Application{ + Name: ptrTo("null-test-app"), + OptionalClaims: NewNullNullable[ApplicationOptionalClaims](), + } + + if !app.OptionalClaims.IsNull() { + t.Error("OptionalClaims should be null") + } + + // Should marshal as null + data, err := json.Marshal(app) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + t.Logf("Marshaled with null optionalClaims: %s", string(data)) +} + +func ptrTo[T any](v T) *T { + return &v +} diff --git a/experimental/internal/codegen/test/issues/issue_502/spec.yaml b/experimental/internal/codegen/test/issues/issue_502/spec.yaml new file mode 100644 index 0000000000..04e5a324e8 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_502/spec.yaml @@ -0,0 +1,29 @@ +# Issue 502: anyOf/allOf with only one ref type generates interface{} +# https://github.com/oapi-codegen/oapi-codegen/issues/502 +# +# When anyOf or allOf has only one reference, it should generate +# the referenced type, not interface{}. +openapi: 3.0.0 +info: + title: Issue 502 Test + version: 1.0.0 +paths: {} +components: + schemas: + OptionalClaims: + type: object + properties: + idToken: + type: string + accessToken: + type: string + Application: + type: object + properties: + name: + type: string + optionalClaims: + anyOf: + - $ref: '#/components/schemas/OptionalClaims' + description: Optional claims configuration + nullable: true diff --git a/experimental/internal/codegen/test/issues/issue_52/doc.go b/experimental/internal/codegen/test/issues/issue_52/doc.go new file mode 100644 index 0000000000..f4646cbdb6 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_52/doc.go @@ -0,0 +1,5 @@ +// Package issue_52 tests that recursive types are handled properly. +// https://github.com/oapi-codegen/oapi-codegen/issues/52 +package issue_52 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_52/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_52/output/types.gen.go new file mode 100644 index 0000000000..72354dfa93 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_52/output/types.gen.go @@ -0,0 +1,87 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/Document +type Document struct { + Fields map[string]any `json:"fields,omitempty" form:"fields,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Document) ApplyDefaults() { +} + +// #/components/schemas/Document/properties/fields +type DocumentFields = map[string]any + +// #/components/schemas/Value +type Value struct { + StringValue *string `json:"stringValue,omitempty" form:"stringValue,omitempty"` + ArrayValue *ArrayValue `json:"arrayValue,omitempty" form:"arrayValue,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Value) ApplyDefaults() { +} + +// #/components/schemas/ArrayValue +type ArrayValue = []Value + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/5xQTWvcMBC961c8sgWdarlbctEtUCihlJYeelfkWVupLQnNOHShP77Y3g/vJvQjOknz", + "3rwPbXDPPBJutxbfyI+FwxNB9pkYnYtNH2KrNuhEMltj2iDd+FD5NJjkcnjrU0MtxctHmBTZ3G7VRm3w", + "2f0g8FgI0jlBuTJxhRYjapBLylT6vUqZosvB4n1VV1sV4i5ZBTxR4ZCiha6runqnFSBBerKgn27IPSmg", + "IfYlZJl5vxSAVyXITjqePM1B2s5SLclyASaim2zum5P/R5IDWohzikx8pAN6W9f6/LyKevPl080K8ykK", + "RVnTAe1y7oOfXc0jp6gvcYB9R4O7ngJvCu0s9Mb4NOQUKQqbhcvmQ/LjQFG0OoOTwgFfxI6ko/T0dRbp", + "4ZH8sfLydxLWnXeB+obXeV5YnI5rmjDVcv3XF2T+VuG760fSM32+/mdKlhJie7F53l7A09iV4vbPqH8K", + "d3faWBLePVNYjGblwyQIDauA/9D9dwAAAP//+4PlsMkDAAA=", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_52/output/types_test.go b/experimental/internal/codegen/test/issues/issue_52/output/types_test.go new file mode 100644 index 0000000000..7bb4f39267 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_52/output/types_test.go @@ -0,0 +1,64 @@ +package output + +import ( + "encoding/json" + "testing" +) + +// TestRecursiveTypes verifies that recursive type definitions work correctly. +// https://github.com/oapi-codegen/oapi-codegen/issues/52 +func TestRecursiveTypes(t *testing.T) { + // Value references ArrayValue which is []Value - recursive + str := "test" + val := Value{ + StringValue: &str, + ArrayValue: &ArrayValue{ + {StringValue: &str}, + }, + } + + if *val.StringValue != "test" { + t.Errorf("StringValue = %q, want %q", *val.StringValue, "test") + } + if len(*val.ArrayValue) != 1 { + t.Errorf("ArrayValue length = %d, want 1", len(*val.ArrayValue)) + } +} + +func TestRecursiveJSONRoundTrip(t *testing.T) { + str := "test" + nested := "nested" + original := Value{ + StringValue: &str, + ArrayValue: &ArrayValue{ + {StringValue: &nested}, + }, + } + + data, err := json.Marshal(original) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + var decoded Value + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if *decoded.StringValue != *original.StringValue { + t.Errorf("StringValue mismatch: got %q, want %q", *decoded.StringValue, *original.StringValue) + } +} + +func TestDocumentWithRecursiveFields(t *testing.T) { + // Document.Fields is map[string]any (due to additionalProperties: $ref Value) + doc := Document{ + Fields: map[string]any{ + "key1": "value1", + }, + } + + if doc.Fields["key1"] != "value1" { + t.Errorf("Fields[key1] = %v, want %q", doc.Fields["key1"], "value1") + } +} diff --git a/experimental/internal/codegen/test/issues/issue_52/spec.yaml b/experimental/internal/codegen/test/issues/issue_52/spec.yaml new file mode 100644 index 0000000000..a29ee7a7be --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_52/spec.yaml @@ -0,0 +1,41 @@ +# Issue 52: Recursive types handling +# https://github.com/oapi-codegen/oapi-codegen/issues/52 +# +# Make sure that recursive types are handled properly +openapi: 3.0.2 +info: + version: '0.0.1' + title: example + description: | + Make sure that recursive types are handled properly +paths: + /example: + get: + operationId: exampleGet + responses: + '200': + description: "OK" + content: + 'application/json': + schema: + $ref: '#/components/schemas/Document' +components: + schemas: + Document: + type: object + properties: + fields: + type: object + additionalProperties: + $ref: '#/components/schemas/Value' + Value: + type: object + properties: + stringValue: + type: string + arrayValue: + $ref: '#/components/schemas/ArrayValue' + ArrayValue: + type: array + items: + $ref: '#/components/schemas/Value' diff --git a/experimental/internal/codegen/test/issues/issue_579/doc.go b/experimental/internal/codegen/test/issues/issue_579/doc.go new file mode 100644 index 0000000000..10d90fa7f5 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_579/doc.go @@ -0,0 +1,5 @@ +// Package issue_579 tests aliased types with date format. +// https://github.com/oapi-codegen/oapi-codegen/issues/579 +package issue_579 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_579/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_579/output/types.gen.go new file mode 100644 index 0000000000..071c05c0f4 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_579/output/types.gen.go @@ -0,0 +1,110 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + "sync" + "time" +) + +// #/components/schemas/Pet +type Pet struct { + Born *any `json:"born,omitempty" form:"born,omitempty"` + BornAt *Date `json:"born_at,omitempty" form:"born_at,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Pet) ApplyDefaults() { +} + +const DateFormat = "2006-01-02" + +type Date struct { + time.Time +} + +func (d Date) MarshalJSON() ([]byte, error) { + return json.Marshal(d.Format(DateFormat)) +} + +func (d *Date) UnmarshalJSON(data []byte) error { + var dateStr string + err := json.Unmarshal(data, &dateStr) + if err != nil { + return err + } + parsed, err := time.Parse(DateFormat, dateStr) + if err != nil { + return err + } + d.Time = parsed + return nil +} + +func (d Date) String() string { + return d.Format(DateFormat) +} + +func (d *Date) UnmarshalText(data []byte) error { + parsed, err := time.Parse(DateFormat, string(data)) + if err != nil { + return err + } + d.Time = parsed + return nil +} + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/2SSz86bMBDE7zzFiPTamLaKovhWqZfc8gaVYzbgCryWd9M/b18Z+ET8fZzY0c74N+AD", + "riJPwul8sfg+BSfUQ/8lEvwJOqJ3Snhwnp02B4yqSawxQ9DxeT96ng27FD577mmgWA+hBIs5nS8NJ4ou", + "BYv227E7dm0T4oNtA/ymLIGjxZeiN4AGncjuUFASbZLTUcq+SZPzNPLUUy4zMJCuLwAnyk4Dx2tvi37b", + "d7eNTJI4CsmbBfjadfsA9CQ+h6QLVPoQUB7PUSnqqwtQ+qsFLsRaB8SPNLv3KpaPbCGaQxwaz3PiSFEX", + "stWyQd72gquF77/I6yalXFpreK1051xRfMr0sGgPZj/FbEeY7Zf/cEptFfDTVQ0r2l1eb4Zdrskiv+TV", + "1JW1sv0PAAD//3OxuKeDAgAA", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_579/output/types_test.go b/experimental/internal/codegen/test/issues/issue_579/output/types_test.go new file mode 100644 index 0000000000..895928b075 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_579/output/types_test.go @@ -0,0 +1,64 @@ +package output + +import ( + "encoding/json" + "testing" + "time" +) + +// TestAliasedDateType verifies that date format types work correctly. +// https://github.com/oapi-codegen/oapi-codegen/issues/579 +func TestDateType(t *testing.T) { + // Direct date type should use Date + date := Date{Time: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)} + + data, err := json.Marshal(date) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + if string(data) != `"2024-01-15"` { + t.Errorf("Marshal result = %s, want %q", string(data), "2024-01-15") + } + + var decoded Date + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if !decoded.Equal(date.Time) { + t.Errorf("Unmarshal result = %v, want %v", decoded.Time, date.Time) + } +} + +func TestPetWithDateFields(t *testing.T) { + // Pet has born_at as *Date (direct format: date) + date := Date{Time: time.Date(2020, 6, 15, 0, 0, 0, 0, time.UTC)} + pet := Pet{ + BornAt: &date, + } + + if pet.BornAt == nil { + t.Fatal("BornAt should not be nil") + } + if pet.BornAt.String() != "2020-06-15" { + t.Errorf("BornAt = %q, want %q", pet.BornAt.String(), "2020-06-15") + } +} + +// Note: The current implementation generates Born as *any instead of the ideal +// AliasedDate type. This is a known limitation with $ref to type aliases. +func TestPetBornFieldExists(t *testing.T) { + // Just verify the field exists and can hold a value + pet := Pet{ + Born: ptrTo[any]("2020-06-15"), + } + + if pet.Born == nil { + t.Fatal("Born should not be nil") + } +} + +func ptrTo[T any](v T) *T { + return &v +} diff --git a/experimental/internal/codegen/test/issues/issue_579/spec.yaml b/experimental/internal/codegen/test/issues/issue_579/spec.yaml new file mode 100644 index 0000000000..8baf490045 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_579/spec.yaml @@ -0,0 +1,30 @@ +# Issue 579: Aliased types with date format +# https://github.com/oapi-codegen/oapi-codegen/issues/579 +openapi: "3.0.0" +info: + version: 1.0.0 + title: Issue 579 test +paths: + /placeholder: + get: + operationId: getPlaceholder + responses: + 200: + description: placeholder + content: + text/plain: + schema: + type: string +components: + schemas: + Pet: + type: object + properties: + born: + $ref: "#/components/schemas/AliasedDate" + born_at: + type: string + format: date + AliasedDate: + type: string + format: date diff --git a/experimental/internal/codegen/test/issues/issue_697/doc.go b/experimental/internal/codegen/test/issues/issue_697/doc.go new file mode 100644 index 0000000000..3b3d314ae0 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_697/doc.go @@ -0,0 +1,5 @@ +// Package issue_697 tests that properties alongside allOf are included. +// https://github.com/oapi-codegen/oapi-codegen/issues/697 +package issue_697 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_697/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_697/output/types.gen.go new file mode 100644 index 0000000000..1a342a9e92 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_697/output/types.gen.go @@ -0,0 +1,81 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/YBase +type YBase struct { + BaseField *string `json:"baseField,omitempty" form:"baseField,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *YBase) ApplyDefaults() { +} + +// #/components/schemas/X +type X struct { + A *string `json:"a,omitempty" form:"a,omitempty"` + B *int `json:"b,omitempty" form:"b,omitempty"` + BaseField *string `json:"baseField,omitempty" form:"baseField,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *X) ApplyDefaults() { +} + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/3yQwWrcMBCG73qKH1zIpbE3KSRExx4KPbWHQNvj2Jq1psiS0MwGSum7lyhZ3NKyt/Hv", + "bzTfzICPqifG3cO9x+dWKjcTVmSmBkrp0xHUGLLm0ji4AdGsqp+mVSye5nEp21SoyvVSAq+c//6Q57d1", + "unu4d4Mb8CVyBkGXyBshkmIuFs9jckDdBchgkaG0MRI/cXrrhp78wWgspxQwMyQv6RQ4QHKHVs7cyDjA", + "flQe3YDHKApRqGySqMEKuh1ubw63oyuVM1XxeDcexoOTfCzeASaW2O83wiOrOeCJm0rJHjcdr2RRPX7+", + "ckvZasmcTZ/bXzbtJfDtPSm/lOhaHmX+zou9RvtiZwiYSfmDcAp7dO5Va5LXHn89/+2X3NFrvGl89Lga", + "pt1repWaus/VheF0cWjX+5eQbLxy+9+avwMAAP//FjslWWwCAAA=", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_697/output/types_test.go b/experimental/internal/codegen/test/issues/issue_697/output/types_test.go new file mode 100644 index 0000000000..f3d5263a93 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_697/output/types_test.go @@ -0,0 +1,58 @@ +package output + +import ( + "encoding/json" + "testing" +) + +// TestXHasAllFields verifies that schema X has properties from both its own +// definition AND from the allOf reference to YBase. +// https://github.com/oapi-codegen/oapi-codegen/issues/697 +func TestXHasAllFields(t *testing.T) { + a := "a-value" + b := 42 + baseField := "base-value" + + x := X{ + A: &a, + B: &b, + BaseField: &baseField, + } + + // Verify all fields are accessible + if *x.A != "a-value" { + t.Errorf("X.A = %q, want %q", *x.A, "a-value") + } + if *x.B != 42 { + t.Errorf("X.B = %d, want %d", *x.B, 42) + } + if *x.BaseField != "base-value" { + t.Errorf("X.BaseField = %q, want %q", *x.BaseField, "base-value") + } +} + +func TestXJSONRoundTrip(t *testing.T) { + a := "a-value" + b := 42 + baseField := "base-value" + + original := X{ + A: &a, + B: &b, + BaseField: &baseField, + } + + data, err := json.Marshal(original) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + var decoded X + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if *decoded.A != *original.A || *decoded.B != *original.B || *decoded.BaseField != *original.BaseField { + t.Errorf("Round trip failed: got %+v, want %+v", decoded, original) + } +} diff --git a/experimental/internal/codegen/test/issues/issue_697/spec.yaml b/experimental/internal/codegen/test/issues/issue_697/spec.yaml new file mode 100644 index 0000000000..314044fc9b --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_697/spec.yaml @@ -0,0 +1,27 @@ +# Issue 697: Properties near allOf are ignored +# https://github.com/oapi-codegen/oapi-codegen/issues/697 +# +# When a schema has both allOf and properties at the same level, +# the properties should be included in the generated type. +# This is similar to issue 2102. +openapi: 3.0.0 +info: + title: Issue 697 Test + version: 1.0.0 +paths: {} +components: + schemas: + YBase: + type: object + properties: + baseField: + type: string + X: + allOf: + - $ref: '#/components/schemas/YBase' + properties: + a: + type: string + b: + type: integer + type: object diff --git a/experimental/internal/codegen/test/issues/issue_775/doc.go b/experimental/internal/codegen/test/issues/issue_775/doc.go new file mode 100644 index 0000000000..f97f08fec2 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_775/doc.go @@ -0,0 +1,5 @@ +// Package issue_775 tests that allOf with format specification works correctly. +// https://github.com/oapi-codegen/oapi-codegen/issues/775 +package issue_775 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_775/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_775/output/types.gen.go new file mode 100644 index 0000000000..1cd65cb493 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_775/output/types.gen.go @@ -0,0 +1,86 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/TestObject +type TestObject struct { + UUIDProperty *TestObjectUUIDProperty `json:"uuidProperty,omitempty" form:"uuidProperty,omitempty"` + DateProperty *TestObjectDateProperty `json:"dateProperty,omitempty" form:"dateProperty,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *TestObject) ApplyDefaults() { +} + +// #/components/schemas/TestObject/properties/uuidProperty +type TestObjectUUIDProperty struct { +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *TestObjectUUIDProperty) ApplyDefaults() { +} + +// #/components/schemas/TestObject/properties/dateProperty +type TestObjectDateProperty struct { +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *TestObjectDateProperty) ApplyDefaults() { +} + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/6xQQW7bMBC88xUD61hYclEYAviDntxD+wBKWkvbSFyCu4phBPl7IFmOnXtunOHM7OwW", + "+K06E+r66BHG8XTGD5wlT8GQAyspdm2IiGKYKPcEjq1MKRg3I21K3YFyluwKDGZJfVX1bMPclK1MlYTE", + "+1Y66il+BbyM1qquj65wBf4px34rYYLQdQj3LgtGE5Rg10TQQeaxw0XyCy5sg8x2q6Clk0QxJPb4VR7K", + "g+N4Fu8AYxvJP9bFX1JzwCtlZYkeP1d5Cjaox9u7W9aUSNF0sWs70BTWJ1brqflPrd0w1lYesnIblbIk", + "ysakdxEwz9z9ufHXB3u3q2WO/RO93uJZB+y3i/g16/OrC0bfFbxkuY8AAAD//xKKQGYZAgAA", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_775/output/types_test.go b/experimental/internal/codegen/test/issues/issue_775/output/types_test.go new file mode 100644 index 0000000000..c2efe59909 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_775/output/types_test.go @@ -0,0 +1,37 @@ +package output + +import ( + "testing" +) + +// TestAllOfWithFormatCompiles verifies that using allOf to add format +// specifications doesn't cause generation errors. +// https://github.com/oapi-codegen/oapi-codegen/issues/775 +// +// Note: The current implementation generates empty struct types for these +// properties instead of the ideal Go types (uuid.UUID for format:uuid, +// time.Time for format:date). This is a known limitation. +func TestAllOfWithFormatCompiles(t *testing.T) { + // The fact that this compiles proves the original issue is fixed + // (generation no longer errors on allOf + format) + obj := TestObject{ + UUIDProperty: &TestObjectUUIDProperty{}, + DateProperty: &TestObjectDateProperty{}, + } + + // Access the fields to ensure they exist + _ = obj.UUIDProperty + _ = obj.DateProperty +} + +// TestIdealBehavior documents the expected ideal behavior. +// Currently this would require changes to handle format-only allOf schemas. +func TestIdealBehavior(t *testing.T) { + t.Skip("TODO: allOf with format-only schemas should produce proper Go types (uuid.UUID, time.Time)") + + // Ideal behavior would be: + // type TestObject struct { + // UUIDProperty *uuid.UUID `json:"uuidProperty,omitempty"` + // DateProperty *time.Time `json:"dateProperty,omitempty"` + // } +} diff --git a/experimental/internal/codegen/test/issues/issue_775/spec.yaml b/experimental/internal/codegen/test/issues/issue_775/spec.yaml new file mode 100644 index 0000000000..353485dd5b --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_775/spec.yaml @@ -0,0 +1,22 @@ +# Issue 775: allOf + format raises "can not merge incompatible formats" error +# https://github.com/oapi-codegen/oapi-codegen/issues/775 +# +# Using allOf to add a format to a base type should work without errors. +openapi: 3.0.0 +info: + title: Issue 775 Test + version: 1.0.0 +paths: {} +components: + schemas: + TestObject: + type: object + properties: + uuidProperty: + type: string + allOf: + - format: uuid + dateProperty: + type: string + allOf: + - format: date diff --git a/experimental/internal/codegen/test/issues/issue_832/doc.go b/experimental/internal/codegen/test/issues/issue_832/doc.go new file mode 100644 index 0000000000..a1e8c49610 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_832/doc.go @@ -0,0 +1,5 @@ +// Package issue_832 tests x-go-type-name override for enum types. +// https://github.com/oapi-codegen/oapi-codegen/issues/832 +package issue_832 + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_832/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_832/output/types.gen.go new file mode 100644 index 0000000000..d9dd314469 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_832/output/types.gen.go @@ -0,0 +1,91 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/Document +type Document struct { + Name *string `json:"name,omitempty" form:"name,omitempty"` + Status *string `json:"status,omitempty" form:"status,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *Document) ApplyDefaults() { +} + +// #/components/schemas/Document/properties/status +type Document_Status string + +const ( + Document_Status_one Document_Status = "one" + Document_Status_two Document_Status = "two" + Document_Status_three Document_Status = "three" + Document_Status_four Document_Status = "four" +) + +// #/components/schemas/DocumentStatus +type DocumentStatus struct { + Value *string `json:"value,omitempty" form:"value,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *DocumentStatus) ApplyDefaults() { +} + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/9RSPYvbQBDt9Sse54AaW1Lk5tg6EI4UKS5dCGFvPZb2sHaWnZFzgfz4IMkfkiHE7XWa", + "maf3wb4VnkR6wuO2NnjbNLzR35E2wXaEPSdQ6DsMK8lWaFWjmLJsvLb9S+G4K9lGv3G8o4bCcvADr5SP", + "2zrjSMFGb7AtqqLOfNizyYAjJfEcDPKqqIqPeQao1wMZ0Jvt4oEyYEfiko864v5kAPCNRG+t8pFS8ruZ", + "55g4UlJPkkWrrQyC5YnXjDwN6fQBDFA7aDztLuKfSU/XRBI5CMkZDuR1VeXX8cbnw9cvD7Ob46AUdA4H", + "chvjwbtRtXwVDvnyDohrqbO3W+BDor1Bviodd5EDBZVywkr5iV3fUdB8lra+N279fvM+q9Ve8uwKGXhO", + "qInyDD0LDOUx4JdXcueHvnbm6mKo19zT9Jto8qG5rGWUn8OW/TQX9Z+T0/8RYiyxwXcOtIb+4jW0TURr", + "7LlPPxaBnhfid8Y62kP/71x/AwAA//+qyWhEFgQAAA==", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_832/output/types_test.go b/experimental/internal/codegen/test/issues/issue_832/output/types_test.go new file mode 100644 index 0000000000..8537eb4648 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_832/output/types_test.go @@ -0,0 +1,62 @@ +package output + +import ( + "encoding/json" + "testing" +) + +// TestEnumTypeGeneration verifies that enum types in properties are generated. +// https://github.com/oapi-codegen/oapi-codegen/issues/832 +// +// Note: The x-go-type-name extension is not currently supported. The enum type +// is generated with a name derived from the property path rather than the +// specified x-go-type-name. +func TestEnumTypeGeneration(t *testing.T) { + // Enum constants should exist + _ = Document_Status_one + _ = Document_Status_two + _ = Document_Status_three + _ = Document_Status_four + + if string(Document_Status_one) != "one" { + t.Errorf("one = %q, want %q", Document_Status_one, "one") + } +} + +func TestDocumentWithStatus(t *testing.T) { + name := "test" + status := "one" + doc := Document{ + Name: &name, + Status: &status, + } + + data, err := json.Marshal(doc) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + var decoded Document + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if *decoded.Name != *doc.Name { + t.Errorf("Name = %q, want %q", *decoded.Name, *doc.Name) + } + if *decoded.Status != *doc.Status { + t.Errorf("Status = %q, want %q", *decoded.Status, *doc.Status) + } +} + +func TestDocumentStatusSchema(t *testing.T) { + // There's also a DocumentStatus schema (separate from the enum property) + value := "test-value" + ds := DocumentStatus{ + Value: &value, + } + + if *ds.Value != "test-value" { + t.Errorf("Value = %q, want %q", *ds.Value, "test-value") + } +} diff --git a/experimental/internal/codegen/test/issues/issue_832/spec.yaml b/experimental/internal/codegen/test/issues/issue_832/spec.yaml new file mode 100644 index 0000000000..fe2e387fdb --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_832/spec.yaml @@ -0,0 +1,45 @@ +# Issue 832: x-go-type-name for enum types +# https://github.com/oapi-codegen/oapi-codegen/issues/832 +openapi: 3.0.2 +info: + version: '0.0.1' + title: example + description: | + Test x-go-type-name override for enum properties +paths: + /example: + get: + operationId: exampleGet + responses: + '200': + description: "OK" + content: + 'application/json': + schema: + $ref: '#/components/schemas/Document' + /example2: + get: + operationId: exampleGet2 + responses: + '200': + description: "OK" + content: + 'application/json': + schema: + $ref: '#/components/schemas/DocumentStatus' +components: + schemas: + Document: + type: object + properties: + name: + type: string + status: + x-go-type-name: Document_Status + type: string + enum: [one, two, three, four] + DocumentStatus: + type: object + properties: + value: + type: string diff --git a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/doc.go b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/doc.go new file mode 100644 index 0000000000..db5bcd0016 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/doc.go @@ -0,0 +1,5 @@ +// Package issue_head_digit_operation_id tests operation IDs starting with digits. +// https://github.com/oapi-codegen/oapi-codegen/issues/head-digit-of-operation-id +package issue_head_digit_operation_id + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go new file mode 100644 index 0000000000..3bbe71257a --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go @@ -0,0 +1,69 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "strings" + "sync" +) + +// #/paths//3gpp/foo/get/responses/200/content/application/json/schema +type N3GPPFooJSONResponse struct { + Value *string `json:"value,omitempty" form:"value,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *N3GPPFooJSONResponse) ApplyDefaults() { +} + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/2yPsXLqMBBFe33FHdMbP+hUM48wKaDIDyj2Ii9xtBrtQiZ/nzGBsUmiStK5ujq7wE71", + "TB77TCUYS8JuA7VQjFPEB1uPjiObW+CF1BTWB4PM0oqB3wjVens4/BepECmNlHAJA3fYCrijZHxkKuok", + "UwqZPdZ1U68cp6N4B1yoKEvyqJq6qf9VDjC2gTyeKHTYjA6PkqOOy8F6Hd8v1zHn5VGuZUAk+95gct11", + "HjfJGyqkWZKS3rPAqmmmA9CRtoWzXdX2zzPSSjJKNg8DIeeB2+tvy5NKeqSAtj29h5+3gH1m8pDXE7X2", + "C+YyjmA815zWJQxn+gvcW9UKp+i+AgAA//9y+0ZQ6gEAAA==", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types_test.go b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types_test.go new file mode 100644 index 0000000000..df0bc5f952 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types_test.go @@ -0,0 +1,50 @@ +package output + +import ( + "encoding/json" + "testing" +) + +// TestOperationIdStartingWithDigit verifies that operation IDs starting with +// digits generate valid Go identifiers with an N prefix. +func TestOperationIdStartingWithDigit(t *testing.T) { + // The operationId is "3GPPFoo" which should generate N3GPPFooJSONResponse + // (N prefix added to make it a valid Go identifier) + value := "test" + response := N3GPPFooJSONResponse{ + Value: &value, + } + + if *response.Value != "test" { + t.Errorf("Value = %q, want %q", *response.Value, "test") + } +} + +func TestN3GPPFooJSONRoundTrip(t *testing.T) { + value := "test-value" + original := N3GPPFooJSONResponse{ + Value: &value, + } + + data, err := json.Marshal(original) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + var decoded N3GPPFooJSONResponse + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if *decoded.Value != *original.Value { + t.Errorf("Value = %q, want %q", *decoded.Value, *original.Value) + } +} + +// TestTypeNameIsValid ensures the type name is a valid Go identifier +func TestTypeNameIsValid(t *testing.T) { + // This test passes if it compiles - the type N3GPPFooJSONResponse + // must be a valid Go identifier + var _ N3GPPFooJSONResponse + var _ N3GPPFooJSONResponse +} diff --git a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/spec.yaml b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/spec.yaml new file mode 100644 index 0000000000..5bcd0f7d70 --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/spec.yaml @@ -0,0 +1,20 @@ +# Issue: Operation ID starting with digit +# Tests that operation IDs like "3GPPFoo" generate valid Go identifiers +openapi: 3.0.2 +info: + version: "0.0.1" + title: Head Digit Operation ID Test +paths: + /3gpp/foo: + get: + operationId: 3GPPFoo + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + value: + type: string diff --git a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/doc.go b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/doc.go new file mode 100644 index 0000000000..e40f41cd2e --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/doc.go @@ -0,0 +1,5 @@ +// Package issue_illegal_enum_names tests enum constant generation with edge cases. +// This tests various edge cases like empty strings, spaces, hyphens, leading digits, etc. +package issue_illegal_enum_names + +//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go new file mode 100644 index 0000000000..ac2d373dde --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go @@ -0,0 +1,80 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/Bar +type Bar string + +const ( + Bar_Value Bar = "" + Bar_Foo Bar = "Foo" + Bar_Bar Bar = "Bar" + Bar_Foo_Bar Bar = "Foo Bar" + Bar_Foo_Bar_1 Bar = "Foo-Bar" + Bar_Foo_1 Bar = "1Foo" + Bar__Foo Bar = " Foo" + Bar__Foo_ Bar = " Foo " + Bar__Foo__1 Bar = "_Foo_" + Bar_Value_1 Bar = "1" +) + +// #/paths//foo/get/responses/200/content/application/json/schema +type GetFooJSONResponse = []Bar + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/2SRQYujQBCF7/0rHpMFT6PO7M1jYAfCwu5l76ExFdOLVjVdlSz590urQU1u1veq3ie6", + "w0H1Sg0OfU+d70F8HcB+IHU7/CE1nVArrObZ0BFT8haE8S/YBTefglwVdOoIrVdSJ5HYx9Dge1mXn84F", + "PkvjAAvWr1Q/cu+vrBpFDrhR0iDcoC7rsnYuertovqzOMjYAHdn0AEicX+RwajL/EpmTRBqFlfSxCnzW", + "9TIAJ9I2hWij7ffPVdIKG7GtlwEfYx/aUVb9VeFtCmh7ocE/U8DukRr4lPz9JQtGg76eAN8SnRsUu6qV", + "IQoTm1aTQKu9T4VzS5Dv52yq2vv06JzkailwN6P8KxflO4piNSzfL097n7bZK3nfko9tQZFXimeANTl+", + "iRxX89vHm/sfAAD//5W/OQySAgAA", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types_test.go b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types_test.go new file mode 100644 index 0000000000..65a49f009f --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types_test.go @@ -0,0 +1,51 @@ +package output + +import ( + "testing" +) + +// TestIllegalEnumNames verifies that enum constants with various edge case values +// are generated with valid Go identifiers. +func TestIllegalEnumNames(t *testing.T) { + // All these enum constants should exist and have valid Go names + tests := []struct { + name string + constant Bar + value string + }{ + {"empty string", Bar_Value, ""}, + {"Foo", Bar_Foo, "Foo"}, + {"Bar", Bar_Bar, "Bar"}, + {"Foo Bar (with space)", Bar_Foo_Bar, "Foo Bar"}, + {"Foo-Bar (with hyphen)", Bar_Foo_Bar_1, "Foo-Bar"}, + {"1Foo (leading digit)", Bar_Foo_1, "1Foo"}, + {" Foo (leading space)", Bar__Foo, " Foo"}, + {" Foo (leading and trailing space)", Bar__Foo_, " Foo "}, + {"_Foo_ (underscores)", Bar__Foo__1, "_Foo_"}, + {"1 (just digit)", Bar_Value_1, "1"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if string(tt.constant) != tt.value { + t.Errorf("constant %q = %q, want %q", tt.name, tt.constant, tt.value) + } + }) + } +} + +func TestBarCanBeUsedInSlice(t *testing.T) { + // The response type is []Bar + response := GetFooJSONResponse{ + Bar_Foo, + Bar_Bar, + Bar_Value, // empty string + } + + if len(response) != 3 { + t.Errorf("response length = %d, want 3", len(response)) + } + if response[0] != "Foo" { + t.Errorf("response[0] = %q, want %q", response[0], "Foo") + } +} diff --git a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/spec.yaml b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/spec.yaml new file mode 100644 index 0000000000..5d80f246cf --- /dev/null +++ b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/spec.yaml @@ -0,0 +1,37 @@ +# Issue: Illegal enum names +# Tests enum constant generation with various edge cases +openapi: 3.0.2 + +info: + title: Illegal Enum Names Test + version: 0.0.0 + +paths: + /foo: + get: + operationId: getFoo + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Bar' + +components: + schemas: + Bar: + type: string + enum: + - '' + - Foo + - Bar + - Foo Bar + - Foo-Bar + - 1Foo + - ' Foo' + - ' Foo ' + - _Foo_ + - "1" diff --git a/experimental/internal/codegen/test/nested_aggregate/doc.go b/experimental/internal/codegen/test/nested_aggregate/doc.go new file mode 100644 index 0000000000..6d934ad0d9 --- /dev/null +++ b/experimental/internal/codegen/test/nested_aggregate/doc.go @@ -0,0 +1,3 @@ +package nested_aggregate + +//go:generate go run ../../../../cmd/oapi-codegen -package output -output output/nested_aggregate.gen.go nested_aggregate.yaml diff --git a/experimental/internal/codegen/test/nested_aggregate/nested_aggregate.yaml b/experimental/internal/codegen/test/nested_aggregate/nested_aggregate.yaml new file mode 100644 index 0000000000..7a9c081ec2 --- /dev/null +++ b/experimental/internal/codegen/test/nested_aggregate/nested_aggregate.yaml @@ -0,0 +1,62 @@ +openapi: "3.1.0" +info: + title: Nested Aggregate Test + version: "1.0" +paths: {} +components: + schemas: + # Case 1: Array with anyOf items + ArrayOfAnyOf: + type: array + items: + anyOf: + - type: string + - type: object + properties: + id: + type: integer + + # Case 2: Object with anyOf property + ObjectWithAnyOfProperty: + type: object + properties: + value: + anyOf: + - type: string + - type: integer + + # Case 3: Object with oneOf property containing inline objects + ObjectWithOneOfProperty: + type: object + properties: + variant: + oneOf: + - type: object + properties: + kind: + type: string + name: + type: string + - type: object + properties: + kind: + type: string + count: + type: integer + + # Case 4: allOf containing oneOf + AllOfWithOneOf: + allOf: + - type: object + properties: + base: + type: string + - oneOf: + - type: object + properties: + optionA: + type: boolean + - type: object + properties: + optionB: + type: integer diff --git a/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go b/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go new file mode 100644 index 0000000000..8e9cb4c44b --- /dev/null +++ b/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go @@ -0,0 +1,400 @@ +// Code generated by oapi-codegen; DO NOT EDIT. + +package output + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + "sync" +) + +// #/components/schemas/ArrayOfAnyOf +type ArrayOfAnyOf = []ArrayOfAnyOfItem + +// #/components/schemas/ArrayOfAnyOf/items +type ArrayOfAnyOfItem struct { + String0 *string + ArrayOfAnyOfAnyOf1 *ArrayOfAnyOfAnyOf1 +} + +func (u ArrayOfAnyOfItem) MarshalJSON() ([]byte, error) { + result := make(map[string]any) + + if u.String0 != nil { + return json.Marshal(u.String0) + } + if u.ArrayOfAnyOfAnyOf1 != nil { + data, err := json.Marshal(u.ArrayOfAnyOfAnyOf1) + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(data, &m); err == nil { + for k, v := range m { + result[k] = v + } + } + } + + return json.Marshal(result) +} + +func (u *ArrayOfAnyOfItem) UnmarshalJSON(data []byte) error { + var v0 string + if err := json.Unmarshal(data, &v0); err == nil { + u.String0 = &v0 + } + + var v1 ArrayOfAnyOfAnyOf1 + if err := json.Unmarshal(data, &v1); err == nil { + u.ArrayOfAnyOfAnyOf1 = &v1 + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *ArrayOfAnyOfItem) ApplyDefaults() { + if u.ArrayOfAnyOfAnyOf1 != nil { + u.ArrayOfAnyOfAnyOf1.ApplyDefaults() + } +} + +// #/components/schemas/ArrayOfAnyOf/items/anyOf/1 +type ArrayOfAnyOfAnyOf1 struct { + ID *int `json:"id,omitempty" form:"id,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ArrayOfAnyOfAnyOf1) ApplyDefaults() { +} + +// #/components/schemas/ObjectWithAnyOfProperty +type ObjectWithAnyOfProperty struct { + Value *ObjectWithAnyOfPropertyValue `json:"value,omitempty" form:"value,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ObjectWithAnyOfProperty) ApplyDefaults() { +} + +// #/components/schemas/ObjectWithAnyOfProperty/properties/value +type ObjectWithAnyOfPropertyValue struct { + String0 *string + Int1 *int +} + +func (u ObjectWithAnyOfPropertyValue) MarshalJSON() ([]byte, error) { + if u.String0 != nil { + return json.Marshal(u.String0) + } + if u.Int1 != nil { + return json.Marshal(u.Int1) + } + return []byte("null"), nil +} + +func (u *ObjectWithAnyOfPropertyValue) UnmarshalJSON(data []byte) error { + var v0 string + if err := json.Unmarshal(data, &v0); err == nil { + u.String0 = &v0 + } + + var v1 int + if err := json.Unmarshal(data, &v1); err == nil { + u.Int1 = &v1 + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *ObjectWithAnyOfPropertyValue) ApplyDefaults() { +} + +// #/components/schemas/ObjectWithOneOfProperty +type ObjectWithOneOfProperty struct { + Variant *ObjectWithOneOfPropertyVariant `json:"variant,omitempty" form:"variant,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ObjectWithOneOfProperty) ApplyDefaults() { +} + +// #/components/schemas/ObjectWithOneOfProperty/properties/variant +type ObjectWithOneOfPropertyVariant struct { + ObjectWithOneOfPropertyVariantOneOf0 *ObjectWithOneOfPropertyVariantOneOf0 + ObjectWithOneOfPropertyVariantOneOf1 *ObjectWithOneOfPropertyVariantOneOf1 +} + +func (u ObjectWithOneOfPropertyVariant) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.ObjectWithOneOfPropertyVariantOneOf0 != nil { + count++ + data, err = json.Marshal(u.ObjectWithOneOfPropertyVariantOneOf0) + if err != nil { + return nil, err + } + } + if u.ObjectWithOneOfPropertyVariantOneOf1 != nil { + count++ + data, err = json.Marshal(u.ObjectWithOneOfPropertyVariantOneOf1) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("ObjectWithOneOfPropertyVariant: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *ObjectWithOneOfPropertyVariant) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 ObjectWithOneOfPropertyVariantOneOf0 + if err := json.Unmarshal(data, &v0); err == nil { + u.ObjectWithOneOfPropertyVariantOneOf0 = &v0 + successCount++ + } + + var v1 ObjectWithOneOfPropertyVariantOneOf1 + if err := json.Unmarshal(data, &v1); err == nil { + u.ObjectWithOneOfPropertyVariantOneOf1 = &v1 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("ObjectWithOneOfPropertyVariant: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *ObjectWithOneOfPropertyVariant) ApplyDefaults() { + if u.ObjectWithOneOfPropertyVariantOneOf0 != nil { + u.ObjectWithOneOfPropertyVariantOneOf0.ApplyDefaults() + } + if u.ObjectWithOneOfPropertyVariantOneOf1 != nil { + u.ObjectWithOneOfPropertyVariantOneOf1.ApplyDefaults() + } +} + +// #/components/schemas/ObjectWithOneOfProperty/properties/variant/oneOf/0 +type ObjectWithOneOfPropertyVariantOneOf0 struct { + Kind *string `json:"kind,omitempty" form:"kind,omitempty"` + Name *string `json:"name,omitempty" form:"name,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ObjectWithOneOfPropertyVariantOneOf0) ApplyDefaults() { +} + +// #/components/schemas/ObjectWithOneOfProperty/properties/variant/oneOf/1 +type ObjectWithOneOfPropertyVariantOneOf1 struct { + Kind *string `json:"kind,omitempty" form:"kind,omitempty"` + Count *int `json:"count,omitempty" form:"count,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *ObjectWithOneOfPropertyVariantOneOf1) ApplyDefaults() { +} + +// #/components/schemas/AllOfWithOneOf +type AllOfWithOneOf struct { + Base *string `json:"base,omitempty" form:"base,omitempty"` + AllOfWithOneOfAllOf1 *AllOfWithOneOfAllOf1 `json:"-"` +} + +func (s AllOfWithOneOf) MarshalJSON() ([]byte, error) { + result := make(map[string]any) + + if s.Base != nil { + result["base"] = s.Base + } + + if s.AllOfWithOneOfAllOf1 != nil { + unionData, err := json.Marshal(s.AllOfWithOneOfAllOf1) + if err != nil { + return nil, err + } + var unionMap map[string]any + if err := json.Unmarshal(unionData, &unionMap); err == nil { + for k, v := range unionMap { + result[k] = v + } + } + } + + return json.Marshal(result) +} + +func (s *AllOfWithOneOf) UnmarshalJSON(data []byte) error { + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + if v, ok := raw["base"]; ok { + var val string + if err := json.Unmarshal(v, &val); err != nil { + return err + } + s.Base = &val + } + + var AllOfWithOneOfAllOf1Val AllOfWithOneOfAllOf1 + if err := json.Unmarshal(data, &AllOfWithOneOfAllOf1Val); err != nil { + return err + } + s.AllOfWithOneOfAllOf1 = &AllOfWithOneOfAllOf1Val + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AllOfWithOneOf) ApplyDefaults() { +} + +// #/components/schemas/AllOfWithOneOf/allOf/1 +type AllOfWithOneOfAllOf1 struct { + AllOfWithOneOfAllOf1OneOf0 *AllOfWithOneOfAllOf1OneOf0 + AllOfWithOneOfAllOf1OneOf1 *AllOfWithOneOfAllOf1OneOf1 +} + +func (u AllOfWithOneOfAllOf1) MarshalJSON() ([]byte, error) { + var count int + var data []byte + var err error + + if u.AllOfWithOneOfAllOf1OneOf0 != nil { + count++ + data, err = json.Marshal(u.AllOfWithOneOfAllOf1OneOf0) + if err != nil { + return nil, err + } + } + if u.AllOfWithOneOfAllOf1OneOf1 != nil { + count++ + data, err = json.Marshal(u.AllOfWithOneOfAllOf1OneOf1) + if err != nil { + return nil, err + } + } + + if count != 1 { + return nil, fmt.Errorf("AllOfWithOneOfAllOf1: exactly one member must be set, got %d", count) + } + + return data, nil +} + +func (u *AllOfWithOneOfAllOf1) UnmarshalJSON(data []byte) error { + var successCount int + + var v0 AllOfWithOneOfAllOf1OneOf0 + if err := json.Unmarshal(data, &v0); err == nil { + u.AllOfWithOneOfAllOf1OneOf0 = &v0 + successCount++ + } + + var v1 AllOfWithOneOfAllOf1OneOf1 + if err := json.Unmarshal(data, &v1); err == nil { + u.AllOfWithOneOfAllOf1OneOf1 = &v1 + successCount++ + } + + if successCount != 1 { + return fmt.Errorf("AllOfWithOneOfAllOf1: expected exactly one type to match, got %d", successCount) + } + + return nil +} + +// ApplyDefaults sets default values for fields that are nil. +func (u *AllOfWithOneOfAllOf1) ApplyDefaults() { + if u.AllOfWithOneOfAllOf1OneOf0 != nil { + u.AllOfWithOneOfAllOf1OneOf0.ApplyDefaults() + } + if u.AllOfWithOneOfAllOf1OneOf1 != nil { + u.AllOfWithOneOfAllOf1OneOf1.ApplyDefaults() + } +} + +// #/components/schemas/AllOfWithOneOf/allOf/1/oneOf/0 +type AllOfWithOneOfAllOf1OneOf0 struct { + OptionA *bool `json:"optionA,omitempty" form:"optionA,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AllOfWithOneOfAllOf1OneOf0) ApplyDefaults() { +} + +// #/components/schemas/AllOfWithOneOf/allOf/1/oneOf/1 +type AllOfWithOneOfAllOf1OneOf1 struct { + OptionB *int `json:"optionB,omitempty" form:"optionB,omitempty"` +} + +// ApplyDefaults sets default values for fields that are nil. +func (s *AllOfWithOneOfAllOf1OneOf1) ApplyDefaults() { +} + +// Base64-encoded, gzip-compressed OpenAPI spec. +var swaggerSpecJSON = []string{ + "H4sIAAAAAAAC/7yTQY/TMBCF7/kVT+W8K5bl5FvgTjggcXbTSTKQjC17WlQh/juKk9KkTVqBKnpq5nnG", + "75uXOE9iPRtsXp9fnt9uMpbKmQxQ1pYMPlFU2iGv60C1VcIXipoBBwqRnRhsUpe32kSDn7+y0nXeCYnG", + "fkosG+ps+gu8wUcbCS8GeQj2iB+sDawciwqs1MV0KElFlffloQ3QoycD2ytjJZ0/yRiGnB+Bp7EnamCp", + "FwS3/UalTgTAB+cpKFM0szrAu8vKyROLUk0hm/K9MyjS9CngOHzwP8hfWZuE+XnU5rgzh0veDrbd09TY", + "1RZu7OEsLSG8zhGc0AQBpRO1LCw1WFoWGr3GC7iib/tnuMBWdEqTXCzjLaR5K0/gO8tCpljfVv8T29Ff", + "Nf1fe6Xbzxd2/1V9b2DbtqimkaY1D59iL/0J8jQ5NZyvWWVc49vaeLXFRa6nhybuvLKTfH0/W+dasvLI", + "2z7cT+N3AAAA//8nzKKtgAUAAA==", +} + +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") + raw, err := base64.StdEncoding.DecodeString(joined) + if err != nil { + return nil, fmt.Errorf("decoding base64: %w", err) + } + r, err := gzip.NewReader(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("creating gzip reader: %w", err) + } + defer r.Close() + var out bytes.Buffer + if _, err := out.ReadFrom(r); err != nil { + return nil, fmt.Errorf("decompressing: %w", err) + } + return out.Bytes(), nil +} + +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { + var cached []byte + var cachedErr error + var once sync.Once + return func() ([]byte, error) { + once.Do(func() { + cached, cachedErr = decodeSwaggerSpec() + }) + return cached, cachedErr + } +} + +var swaggerSpec = decodeSwaggerSpecCached() + +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() +} diff --git a/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate_test.go b/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate_test.go new file mode 100644 index 0000000000..e70e8cb49e --- /dev/null +++ b/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate_test.go @@ -0,0 +1,332 @@ +package output + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func ptr[T any](v T) *T { + return &v +} + +// TestArrayOfAnyOf tests marshaling/unmarshaling of arrays with anyOf items +func TestArrayOfAnyOf(t *testing.T) { + t.Run("unmarshal string item", func(t *testing.T) { + input := `["hello", "world"]` + var arr ArrayOfAnyOf + err := json.Unmarshal([]byte(input), &arr) + require.NoError(t, err) + require.Len(t, arr, 2) + + // String items should populate the string field + assert.NotNil(t, arr[0].String0) + assert.Equal(t, "hello", *arr[0].String0) + assert.NotNil(t, arr[1].String0) + assert.Equal(t, "world", *arr[1].String0) + }) + + t.Run("unmarshal object item", func(t *testing.T) { + input := `[{"id": 42}]` + var arr ArrayOfAnyOf + err := json.Unmarshal([]byte(input), &arr) + require.NoError(t, err) + require.Len(t, arr, 1) + + // Object item should populate the object field + assert.NotNil(t, arr[0].ArrayOfAnyOfAnyOf1) + assert.NotNil(t, arr[0].ArrayOfAnyOfAnyOf1.ID) + assert.Equal(t, 42, *arr[0].ArrayOfAnyOfAnyOf1.ID) + }) + + t.Run("unmarshal mixed items", func(t *testing.T) { + input := `["hello", {"id": 1}, "world", {"id": 2}]` + var arr ArrayOfAnyOf + err := json.Unmarshal([]byte(input), &arr) + require.NoError(t, err) + require.Len(t, arr, 4) + + assert.NotNil(t, arr[0].String0) + assert.Equal(t, "hello", *arr[0].String0) + + assert.NotNil(t, arr[1].ArrayOfAnyOfAnyOf1) + assert.Equal(t, 1, *arr[1].ArrayOfAnyOfAnyOf1.ID) + + assert.NotNil(t, arr[2].String0) + assert.Equal(t, "world", *arr[2].String0) + + assert.NotNil(t, arr[3].ArrayOfAnyOfAnyOf1) + assert.Equal(t, 2, *arr[3].ArrayOfAnyOfAnyOf1.ID) + }) + + t.Run("marshal string item", func(t *testing.T) { + arr := ArrayOfAnyOf{ + {String0: ptr("hello")}, + } + data, err := json.Marshal(arr) + require.NoError(t, err) + assert.JSONEq(t, `["hello"]`, string(data)) + }) + + t.Run("marshal object item", func(t *testing.T) { + arr := ArrayOfAnyOf{ + {ArrayOfAnyOfAnyOf1: &ArrayOfAnyOfAnyOf1{ID: ptr(42)}}, + } + data, err := json.Marshal(arr) + require.NoError(t, err) + assert.JSONEq(t, `[{"id": 42}]`, string(data)) + }) + + t.Run("round trip mixed", func(t *testing.T) { + original := ArrayOfAnyOf{ + {String0: ptr("test")}, + {ArrayOfAnyOfAnyOf1: &ArrayOfAnyOfAnyOf1{ID: ptr(99)}}, + } + + data, err := json.Marshal(original) + require.NoError(t, err) + + var decoded ArrayOfAnyOf + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + require.Len(t, decoded, 2) + assert.Equal(t, "test", *decoded[0].String0) + assert.Equal(t, 99, *decoded[1].ArrayOfAnyOfAnyOf1.ID) + }) +} + +// TestObjectWithAnyOfProperty tests marshaling/unmarshaling of objects with anyOf properties +func TestObjectWithAnyOfProperty(t *testing.T) { + t.Run("unmarshal string value", func(t *testing.T) { + input := `{"value": "hello"}` + var obj ObjectWithAnyOfProperty + err := json.Unmarshal([]byte(input), &obj) + require.NoError(t, err) + + require.NotNil(t, obj.Value) + assert.NotNil(t, obj.Value.String0) + assert.Equal(t, "hello", *obj.Value.String0) + }) + + t.Run("unmarshal integer value", func(t *testing.T) { + input := `{"value": 42}` + var obj ObjectWithAnyOfProperty + err := json.Unmarshal([]byte(input), &obj) + require.NoError(t, err) + + require.NotNil(t, obj.Value) + assert.NotNil(t, obj.Value.Int1) + assert.Equal(t, 42, *obj.Value.Int1) + }) + + t.Run("marshal string value", func(t *testing.T) { + obj := ObjectWithAnyOfProperty{ + Value: &ObjectWithAnyOfPropertyValue{ + String0: ptr("hello"), + }, + } + data, err := json.Marshal(obj) + require.NoError(t, err) + assert.JSONEq(t, `{"value": "hello"}`, string(data)) + }) + + t.Run("marshal integer value", func(t *testing.T) { + obj := ObjectWithAnyOfProperty{ + Value: &ObjectWithAnyOfPropertyValue{ + Int1: ptr(42), + }, + } + data, err := json.Marshal(obj) + require.NoError(t, err) + assert.JSONEq(t, `{"value": 42}`, string(data)) + }) + + t.Run("round trip string", func(t *testing.T) { + original := ObjectWithAnyOfProperty{ + Value: &ObjectWithAnyOfPropertyValue{String0: ptr("test")}, + } + + data, err := json.Marshal(original) + require.NoError(t, err) + + var decoded ObjectWithAnyOfProperty + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + require.NotNil(t, decoded.Value) + assert.Equal(t, "test", *decoded.Value.String0) + }) +} + +// TestObjectWithOneOfProperty tests marshaling/unmarshaling of objects with oneOf properties +func TestObjectWithOneOfProperty(t *testing.T) { + t.Run("unmarshal ambiguous input errors", func(t *testing.T) { + // Both variants have optional "kind" field, so this JSON matches both + // oneOf requires exactly one match, so this should error + input := `{"variant": {"kind": "person", "name": "Alice"}}` + var obj ObjectWithOneOfProperty + err := json.Unmarshal([]byte(input), &obj) + require.Error(t, err) + assert.Contains(t, err.Error(), "expected exactly one type to match, got 2") + }) + + t.Run("unmarshal unambiguous variant 0", func(t *testing.T) { + // Only variant 0 has "name" as a field that can be set + // But since all fields are optional, both variants still match + // This demonstrates why discriminators are important for oneOf + input := `{"variant": {"name": "Alice"}}` + var obj ObjectWithOneOfProperty + err := json.Unmarshal([]byte(input), &obj) + // Still ambiguous because both variants can unmarshal (missing fields are just nil) + require.Error(t, err) + }) + + t.Run("unmarshal unambiguous variant 1", func(t *testing.T) { + // Only variant 1 has "count" field + // But since all fields are optional, both variants still match + input := `{"variant": {"count": 10}}` + var obj ObjectWithOneOfProperty + err := json.Unmarshal([]byte(input), &obj) + // Still ambiguous because both variants can unmarshal (missing fields are just nil) + require.Error(t, err) + }) + + t.Run("marshal variant 0", func(t *testing.T) { + obj := ObjectWithOneOfProperty{ + Variant: &ObjectWithOneOfPropertyVariant{ + ObjectWithOneOfPropertyVariantOneOf0: &ObjectWithOneOfPropertyVariantOneOf0{ + Kind: ptr("person"), + Name: ptr("Alice"), + }, + }, + } + data, err := json.Marshal(obj) + require.NoError(t, err) + assert.JSONEq(t, `{"variant": {"kind": "person", "name": "Alice"}}`, string(data)) + }) + + t.Run("marshal variant 1", func(t *testing.T) { + obj := ObjectWithOneOfProperty{ + Variant: &ObjectWithOneOfPropertyVariant{ + ObjectWithOneOfPropertyVariantOneOf1: &ObjectWithOneOfPropertyVariantOneOf1{ + Kind: ptr("counter"), + Count: ptr(10), + }, + }, + } + data, err := json.Marshal(obj) + require.NoError(t, err) + assert.JSONEq(t, `{"variant": {"kind": "counter", "count": 10}}`, string(data)) + }) + + t.Run("marshal fails with zero variants set", func(t *testing.T) { + obj := ObjectWithOneOfProperty{ + Variant: &ObjectWithOneOfPropertyVariant{}, + } + _, err := json.Marshal(obj) + assert.Error(t, err) + assert.Contains(t, err.Error(), "exactly one member must be set") + }) + + t.Run("marshal fails with two variants set", func(t *testing.T) { + obj := ObjectWithOneOfProperty{ + Variant: &ObjectWithOneOfPropertyVariant{ + ObjectWithOneOfPropertyVariantOneOf0: &ObjectWithOneOfPropertyVariantOneOf0{ + Kind: ptr("person"), + Name: ptr("Alice"), + }, + ObjectWithOneOfPropertyVariantOneOf1: &ObjectWithOneOfPropertyVariantOneOf1{ + Kind: ptr("counter"), + Count: ptr(10), + }, + }, + } + _, err := json.Marshal(obj) + assert.Error(t, err) + assert.Contains(t, err.Error(), "exactly one member must be set") + }) +} + +// TestAllOfWithOneOf tests marshaling/unmarshaling of allOf containing oneOf +func TestAllOfWithOneOf(t *testing.T) { + t.Run("unmarshal with optionA - ambiguous oneOf errors", func(t *testing.T) { + // The nested oneOf has same ambiguity issue - both variants match + input := `{"base": "test", "optionA": true}` + var obj AllOfWithOneOf + err := json.Unmarshal([]byte(input), &obj) + // The nested AllOfWithOneOfAllOf1 (oneOf) will error due to ambiguity + require.Error(t, err) + assert.Contains(t, err.Error(), "expected exactly one type to match") + }) + + t.Run("unmarshal with optionB - ambiguous oneOf errors", func(t *testing.T) { + input := `{"base": "test", "optionB": 42}` + var obj AllOfWithOneOf + err := json.Unmarshal([]byte(input), &obj) + require.Error(t, err) + assert.Contains(t, err.Error(), "expected exactly one type to match") + }) + + t.Run("marshal with optionA", func(t *testing.T) { + obj := AllOfWithOneOf{ + Base: ptr("test"), + AllOfWithOneOfAllOf1: &AllOfWithOneOfAllOf1{ + AllOfWithOneOfAllOf1OneOf0: &AllOfWithOneOfAllOf1OneOf0{ + OptionA: ptr(true), + }, + }, + } + + data, err := json.Marshal(obj) + require.NoError(t, err) + + // Should contain both base and optionA merged + var m map[string]any + err = json.Unmarshal(data, &m) + require.NoError(t, err) + + assert.Equal(t, "test", m["base"]) + assert.Equal(t, true, m["optionA"]) + }) + + t.Run("marshal with optionB", func(t *testing.T) { + obj := AllOfWithOneOf{ + Base: ptr("test"), + AllOfWithOneOfAllOf1: &AllOfWithOneOfAllOf1{ + AllOfWithOneOfAllOf1OneOf1: &AllOfWithOneOfAllOf1OneOf1{ + OptionB: ptr(42), + }, + }, + } + + data, err := json.Marshal(obj) + require.NoError(t, err) + + var m map[string]any + err = json.Unmarshal(data, &m) + require.NoError(t, err) + + assert.Equal(t, "test", m["base"]) + assert.Equal(t, float64(42), m["optionB"]) // JSON numbers are float64 + }) + + t.Run("marshal with nil union", func(t *testing.T) { + obj := AllOfWithOneOf{ + Base: ptr("only-base"), + } + + data, err := json.Marshal(obj) + require.NoError(t, err) + + var m map[string]any + err = json.Unmarshal(data, &m) + require.NoError(t, err) + + assert.Equal(t, "only-base", m["base"]) + assert.NotContains(t, m, "optionA") + assert.NotContains(t, m, "optionB") + }) +} diff --git a/experimental/internal/codegen/typegen.go b/experimental/internal/codegen/typegen.go new file mode 100644 index 0000000000..e27424bff0 --- /dev/null +++ b/experimental/internal/codegen/typegen.go @@ -0,0 +1,918 @@ +package codegen + +import ( + "fmt" + "sort" + "strings" + + "github.com/pb33f/libopenapi/datamodel/high/base" +) + +// TypeGenerator converts OpenAPI schemas to Go type expressions. +// It tracks required imports and handles recursive type references. +type TypeGenerator struct { + typeMapping TypeMapping + converter *NameConverter + importResolver *ImportResolver + tagGenerator *StructTagGenerator + imports map[string]string // path -> alias (empty string = no alias) + + // schemaIndex maps JSON pointer refs to their descriptors + schemaIndex map[string]*SchemaDescriptor + + // requiredTemplates tracks which custom type templates are needed + requiredTemplates map[string]bool +} + +// NewTypeGenerator creates a TypeGenerator with the given configuration. +func NewTypeGenerator(typeMapping TypeMapping, converter *NameConverter, importResolver *ImportResolver, tagGenerator *StructTagGenerator) *TypeGenerator { + return &TypeGenerator{ + typeMapping: typeMapping, + converter: converter, + importResolver: importResolver, + tagGenerator: tagGenerator, + imports: make(map[string]string), + schemaIndex: make(map[string]*SchemaDescriptor), + requiredTemplates: make(map[string]bool), + } +} + +// IndexSchemas builds a lookup table from JSON pointer to schema descriptor. +// This is called before generation to enable $ref resolution. +func (g *TypeGenerator) IndexSchemas(schemas []*SchemaDescriptor) { + for _, s := range schemas { + ref := s.Path.String() + g.schemaIndex[ref] = s + } +} + +// AddImport records an import path needed by the generated code. +func (g *TypeGenerator) AddImport(path string) { + if path != "" { + g.imports[path] = "" + } +} + +// AddImportAlias records an import path with an alias. +func (g *TypeGenerator) AddImportAlias(path, alias string) { + if path != "" { + g.imports[path] = alias + } +} + +// AddJSONImport adds encoding/json import (used by marshal/unmarshal code). +func (g *TypeGenerator) AddJSONImport() { + g.AddImport("encoding/json") +} + +// AddJSONImports adds encoding/json and fmt imports (used by oneOf marshal/unmarshal code). +func (g *TypeGenerator) AddJSONImports() { + g.AddImport("encoding/json") + g.AddImport("fmt") +} + +// AddNullableTemplate adds the nullable type template to the output. +func (g *TypeGenerator) AddNullableTemplate() { + g.addTemplate("nullable") +} + +// Imports returns the collected imports as a map[path]alias. +func (g *TypeGenerator) Imports() map[string]string { + return g.imports +} + +// RequiredTemplates returns the set of template names needed for custom types. +func (g *TypeGenerator) RequiredTemplates() map[string]bool { + return g.requiredTemplates +} + +// addTemplate records that a custom type template is needed. +func (g *TypeGenerator) addTemplate(templateName string) { + if templateName != "" { + g.requiredTemplates[templateName] = true + } +} + +// GoTypeExpr returns the Go type expression for a schema descriptor. +// This handles references by looking up the target schema's name, +// and inline schemas by generating the appropriate Go type. +func (g *TypeGenerator) GoTypeExpr(desc *SchemaDescriptor) string { + // Handle $ref - return the referenced type's name + if desc.IsReference() { + // Check for external reference first + if desc.IsExternalReference() { + return g.externalRefType(desc) + } + + // Internal reference - look up in schema index + if target, ok := g.schemaIndex[desc.Ref]; ok { + return target.ShortName + } + // Fallback for unresolved references + return "any" + } + + return g.goTypeForSchema(desc.Schema, desc) +} + +// externalRefType resolves an external reference to a qualified Go type. +// Returns "any" if the external ref cannot be resolved. +func (g *TypeGenerator) externalRefType(desc *SchemaDescriptor) string { + filePath, internalPath := desc.ParseExternalRef() + if filePath == "" { + return "any" + } + + // Look up import mapping + if g.importResolver == nil { + // No import resolver configured - can't resolve external refs + return "any" + } + + imp := g.importResolver.Resolve(filePath) + if imp == nil { + // External file not in import mapping + return "any" + } + + // Extract type name from internal path (e.g., #/components/schemas/Pet -> Pet) + typeName := extractTypeNameFromRef(internalPath, g.converter) + if typeName == "" { + return "any" + } + + // If alias is empty, it's the current package (marked with "-") + if imp.Alias == "" { + return typeName + } + + // Add the import + g.AddImportAlias(imp.Path, imp.Alias) + + // Return qualified type + return imp.Alias + "." + typeName +} + +// extractTypeNameFromRef extracts a Go type name from an internal ref path. +// e.g., "#/components/schemas/Pet" -> "Pet" +func extractTypeNameFromRef(ref string, converter *NameConverter) string { + // Remove leading #/ + ref = strings.TrimPrefix(ref, "#/") + parts := strings.Split(ref, "/") + + if len(parts) < 3 { + return "" + } + + // For #/components/schemas/TypeName, the type name is the last part + // We assume external refs point to component schemas + typeName := parts[len(parts)-1] + return converter.ToTypeName(typeName) +} + +// goTypeForSchema generates a Go type expression from an OpenAPI schema. +// The desc parameter provides context (path, parent) for complex types. +func (g *TypeGenerator) goTypeForSchema(schema *base.Schema, desc *SchemaDescriptor) string { + if schema == nil { + return "any" + } + + // Handle composition types + if len(schema.AllOf) > 0 { + return g.allOfType(desc) + } + if len(schema.AnyOf) > 0 { + return g.anyOfType(desc) + } + if len(schema.OneOf) > 0 { + return g.oneOfType(desc) + } + + // Get the primary type from the type array + // OpenAPI 3.1 allows type to be an array like ["string", "null"] + primaryType := getPrimaryType(schema) + + // Check if this is a nullable primitive - wrap in Nullable[T] + nullable := isNullable(schema) + isPrimitive := primaryType == "string" || primaryType == "integer" || primaryType == "number" || primaryType == "boolean" + + var baseType string + switch primaryType { + case "object": + return g.objectType(schema, desc) + case "array": + return g.arrayType(schema, desc) + case "string": + baseType = g.stringType(schema) + case "integer": + baseType = g.integerType(schema) + case "number": + baseType = g.numberType(schema) + case "boolean": + baseType = g.booleanType(schema) + default: + // Unknown or empty type - could be a free-form object + if schema.Properties != nil && schema.Properties.Len() > 0 { + return g.objectType(schema, desc) + } + return "any" + } + + // Wrap nullable primitives in Nullable[T] + if nullable && isPrimitive { + g.AddNullableTemplate() + return "Nullable[" + baseType + "]" + } + + return baseType +} + +// getPrimaryType extracts the primary (non-null) type from a schema. +// OpenAPI 3.1 supports type arrays like ["string", "null"] for nullable. +func getPrimaryType(schema *base.Schema) string { + if len(schema.Type) == 0 { + return "" + } + for _, t := range schema.Type { + if t != "null" { + return t + } + } + return schema.Type[0] +} + +// objectType generates the Go type for an object schema. +// Simple objects with only additionalProperties become maps. +// Objects with properties become named struct types. +func (g *TypeGenerator) objectType(schema *base.Schema, desc *SchemaDescriptor) string { + hasProperties := schema.Properties != nil && schema.Properties.Len() > 0 + hasAdditionalProps := schema.AdditionalProperties != nil + + // Pure map case: no properties, only additionalProperties + if !hasProperties && hasAdditionalProps { + return g.mapType(schema, desc) + } + + // Empty object (no properties, no additionalProperties) + if !hasProperties && !hasAdditionalProps { + return "map[string]any" + } + + // Struct case: has properties (with or without additionalProperties) + // Return the type name - actual struct definition is generated separately + if desc != nil && desc.ShortName != "" { + return desc.ShortName + } + return "any" +} + +// mapType generates a map[string]T type for additionalProperties schemas. +func (g *TypeGenerator) mapType(schema *base.Schema, desc *SchemaDescriptor) string { + if schema.AdditionalProperties == nil { + return "map[string]any" + } + + // additionalProperties can be a boolean or a schema + // If it's a schema proxy (A), get the value type + if schema.AdditionalProperties.A != nil { + valueSchema := schema.AdditionalProperties.A.Schema() + if valueSchema != nil { + valueType := g.goTypeForSchema(valueSchema, nil) + return "map[string]" + valueType + } + } + + // additionalProperties: true or just present + return "map[string]any" +} + +// arrayType generates a []T type for array schemas. +func (g *TypeGenerator) arrayType(schema *base.Schema, desc *SchemaDescriptor) string { + if schema.Items == nil || schema.Items.A == nil { + return "[]any" + } + + // Check if items is a reference + itemProxy := schema.Items.A + if itemProxy.IsReference() { + ref := itemProxy.GetReference() + // Check for external reference first + if !strings.HasPrefix(ref, "#") && strings.Contains(ref, "#") { + // External reference - use import mapping + tempDesc := &SchemaDescriptor{Ref: ref} + itemType := g.externalRefType(tempDesc) + return "[]" + itemType + } + // Internal reference - look up in schema index + if target, ok := g.schemaIndex[ref]; ok { + return "[]" + target.ShortName + } + } + + // Check if we have a descriptor for the items schema + if desc != nil && desc.Items != nil && desc.Items.ShortName != "" { + return "[]" + desc.Items.ShortName + } + + // Inline items schema + itemSchema := itemProxy.Schema() + itemType := g.goTypeForSchema(itemSchema, nil) + return "[]" + itemType +} + +// stringType returns the Go type for a string schema. +func (g *TypeGenerator) stringType(schema *base.Schema) string { + spec := g.typeMapping.String.Default + if schema.Format != "" { + if formatSpec, ok := g.typeMapping.String.Formats[schema.Format]; ok { + spec = formatSpec + } + } + + g.AddImport(spec.Import) + g.addTemplate(spec.Template) + return spec.Type +} + +// integerType returns the Go type for an integer schema. +func (g *TypeGenerator) integerType(schema *base.Schema) string { + spec := g.typeMapping.Integer.Default + if schema.Format != "" { + if formatSpec, ok := g.typeMapping.Integer.Formats[schema.Format]; ok { + spec = formatSpec + } + } + + g.AddImport(spec.Import) + return spec.Type +} + +// numberType returns the Go type for a number schema. +func (g *TypeGenerator) numberType(schema *base.Schema) string { + spec := g.typeMapping.Number.Default + if schema.Format != "" { + if formatSpec, ok := g.typeMapping.Number.Formats[schema.Format]; ok { + spec = formatSpec + } + } + + g.AddImport(spec.Import) + return spec.Type +} + +// booleanType returns the Go type for a boolean schema. +func (g *TypeGenerator) booleanType(schema *base.Schema) string { + spec := g.typeMapping.Boolean.Default + g.AddImport(spec.Import) + return spec.Type +} + +// allOfType returns the type name for an allOf composition. +// allOf is typically used for struct embedding/inheritance. +func (g *TypeGenerator) allOfType(desc *SchemaDescriptor) string { + if desc != nil && desc.ShortName != "" { + return desc.ShortName + } + return "any" +} + +// anyOfType returns the type name for an anyOf composition. +func (g *TypeGenerator) anyOfType(desc *SchemaDescriptor) string { + if desc != nil && desc.ShortName != "" { + return desc.ShortName + } + return "any" +} + +// oneOfType returns the type name for a oneOf composition. +func (g *TypeGenerator) oneOfType(desc *SchemaDescriptor) string { + if desc != nil && desc.ShortName != "" { + return desc.ShortName + } + return "any" +} + +// StructField represents a field in a generated Go struct. +type StructField struct { + Name string // Go field name + Type string // Go type expression + JSONName string // Original JSON property name + Required bool // Is this field required in the schema + Nullable bool // Is this field nullable (type includes "null") + Pointer bool // Should this be a pointer type + OmitEmpty bool // Include omitempty in json tag + OmitZero bool // Include omitzero in json tag (Go 1.24+) + JSONIgnore bool // Use json:"-" tag to exclude from marshaling + Doc string // Field documentation + Default string // Go literal for default value (empty if no default) + IsStruct bool // True if this field is a struct type (for recursive ApplyDefaults) + IsNullableAlias bool // True if type is a type alias to Nullable[T] (don't wrap or pointer) + Order *int // Optional field ordering (lower values come first) +} + +// GenerateStructFields creates the list of struct fields for an object schema. +func (g *TypeGenerator) GenerateStructFields(desc *SchemaDescriptor) []StructField { + schema := desc.Schema + if schema == nil || schema.Properties == nil { + return nil + } + + // Build required set + required := make(map[string]bool) + for _, r := range schema.Required { + required[r] = true + } + + var fields []StructField + needsNullableImport := false + + for pair := schema.Properties.First(); pair != nil; pair = pair.Next() { + propName := pair.Key() + propProxy := pair.Value() + + field := StructField{ + Name: g.converter.ToPropertyName(propName), + JSONName: propName, + Required: required[propName], + } + + // Parse extensions from the property schema + var propExtensions *Extensions + var propSchema *base.Schema + + // Resolve the property schema + var propType string + if propProxy.IsReference() { + ref := propProxy.GetReference() + // Check if this is an external reference + if !strings.HasPrefix(ref, "#") && strings.Contains(ref, "#") { + // External reference - use import mapping + tempDesc := &SchemaDescriptor{Ref: ref} + propType = g.externalRefType(tempDesc) + field.IsStruct = true // external references are typically to struct types + } else if target, ok := g.schemaIndex[ref]; ok { + propType = target.ShortName + // Only set IsStruct if the referenced schema has ApplyDefaults + // This filters out array/map type aliases which don't have ApplyDefaults + field.IsStruct = schemaHasApplyDefaults(target.Schema) + // Check if the referenced schema is nullable + // BUT: if it's a nullable primitive, the type alias already wraps Nullable[T], + // so we shouldn't double-wrap it or add a pointer (Nullable handles "unspecified") + if isNullablePrimitive(target.Schema) { + // Already Nullable[T] - use as value type directly + field.IsNullableAlias = true + } else if isNullable(target.Schema) { + field.Nullable = true + } + // Extensions from referenced schema apply to the field + propExtensions = target.Extensions + } else { + propType = "any" + } + } else { + propSchema = propProxy.Schema() + field.Nullable = isNullable(propSchema) + field.Doc = extractDescription(propSchema) + + // Parse extensions from the property schema + if propSchema != nil && propSchema.Extensions != nil { + ext, err := ParseExtensions(propSchema.Extensions, desc.Path.Append("properties", propName).String()) + if err == nil { + propExtensions = ext + } + } + + // Generate the Go type for this property + // Always use goTypeForSchema to get the correct type expression + // This handles arrays, maps, and primitive types correctly + propType = g.goTypeForSchema(propSchema, desc.Properties[propName]) + + // Check if this is a struct type (object with properties, or a named type) + if propSchema != nil { + if propSchema.Properties != nil && propSchema.Properties.Len() > 0 { + field.IsStruct = true + } + // Extract default value + if propSchema.Default != nil { + field.Default = formatDefaultValue(propSchema.Default.Value, propType) + } + } + } + + // Apply extensions to the field + if propExtensions != nil { + // Name override + if propExtensions.NameOverride != "" { + field.Name = propExtensions.NameOverride + } + + // Type override replaces the generated type entirely + if propExtensions.TypeOverride != nil { + propType = propExtensions.TypeOverride.TypeName + if propExtensions.TypeOverride.ImportPath != "" { + if propExtensions.TypeOverride.ImportAlias != "" { + g.AddImportAlias(propExtensions.TypeOverride.ImportPath, propExtensions.TypeOverride.ImportAlias) + } else { + g.AddImport(propExtensions.TypeOverride.ImportPath) + } + } + // Type override bypasses nullable wrapping - the user specifies the exact type + field.IsNullableAlias = true // Don't wrap or add pointer + } + + // JSON ignore + if propExtensions.JSONIgnore != nil && *propExtensions.JSONIgnore { + field.JSONIgnore = true + } + + // Deprecated reason appended to documentation + if propExtensions.DeprecatedReason != "" { + if field.Doc != "" { + field.Doc = field.Doc + "\nDeprecated: " + propExtensions.DeprecatedReason + } else { + field.Doc = "Deprecated: " + propExtensions.DeprecatedReason + } + } + + // Order for field sorting + if propExtensions.Order != nil { + field.Order = propExtensions.Order + } + } + + // Determine type semantics: + // - Nullable fields: use Nullable[T] + // - Optional (not nullable) fields: use *T (pointer) + // - Required (not nullable) fields: use T (value type) + // - Collections (slices/maps) are never wrapped + // - Types already wrapped in Nullable[] are not double-wrapped + // - Type aliases to Nullable[T] are used as-is (IsNullableAlias) + isCollection := strings.HasPrefix(propType, "[]") || strings.HasPrefix(propType, "map[") + alreadyNullable := strings.HasPrefix(propType, "Nullable[") || field.IsNullableAlias + + if field.Nullable && !isCollection && !alreadyNullable { + // Use Nullable[T] for nullable fields (generated inline from template) + field.Type = "Nullable[" + propType + "]" + field.Pointer = false + needsNullableImport = true + } else if !field.Required && !isCollection && !alreadyNullable { + // Check for skip optional pointer extension + skipPointer := false + if propExtensions != nil && propExtensions.SkipOptionalPointer != nil && *propExtensions.SkipOptionalPointer { + skipPointer = true + } + + if skipPointer { + // Use value type even though optional + field.Type = propType + field.Pointer = false + } else { + // Use pointer for optional non-nullable fields + field.Type = "*" + propType + field.Pointer = true + } + } else { + // Value type for required non-nullable fields, collections, and Nullable aliases + field.Type = propType + field.Pointer = false + } + + // Determine omitempty/omitzero behavior + field.OmitEmpty = !field.Required + if propExtensions != nil { + // Explicit omitempty override + if propExtensions.OmitEmpty != nil { + field.OmitEmpty = *propExtensions.OmitEmpty + } + // Explicit omitzero + if propExtensions.OmitZero != nil && *propExtensions.OmitZero { + field.OmitZero = true + } + } + + fields = append(fields, field) + } + + if needsNullableImport { + g.AddNullableTemplate() + } + + // Sort fields by order if any have explicit ordering + sortFieldsByOrder(fields) + + return fields +} + +// collectFieldsRecursive returns the struct fields for a schema, recursively +// following allOf chains. For schemas with direct properties (no allOf), this +// falls through to GenerateStructFields. For allOf-composed schemas, it +// collects fields from all allOf members recursively, so that nested allOf +// references (e.g., A: allOf[$ref:B, ...] where B: allOf[$ref:C, ...]) are +// properly flattened. +func (g *TypeGenerator) collectFieldsRecursive(desc *SchemaDescriptor) []StructField { + schema := desc.Schema + if schema == nil { + return nil + } + + // If this schema has no allOf, use the standard field generation + if len(schema.AllOf) == 0 { + return g.GenerateStructFields(desc) + } + + // Collect fields from direct properties first + var fields []StructField + if schema.Properties != nil && schema.Properties.Len() > 0 { + fields = append(fields, g.GenerateStructFields(desc)...) + } + + // Recursively collect fields from each allOf member + for i, proxy := range schema.AllOf { + memberSchema := proxy.Schema() + if memberSchema == nil { + continue + } + + var memberFields []StructField + if proxy.IsReference() { + ref := proxy.GetReference() + if target, ok := g.schemaIndex[ref]; ok { + // Recurse: the target may itself be an allOf composition + memberFields = g.collectFieldsRecursive(target) + } + } else if memberSchema.Properties != nil && memberSchema.Properties.Len() > 0 { + if desc.AllOf != nil && i < len(desc.AllOf) { + memberFields = g.GenerateStructFields(desc.AllOf[i]) + } + } + + // Apply required array from this allOf member to collected fields + if len(memberSchema.Required) > 0 { + reqSet := make(map[string]bool) + for _, r := range memberSchema.Required { + reqSet[r] = true + } + for j := range memberFields { + if reqSet[memberFields[j].JSONName] && !memberFields[j].Required { + memberFields[j].Required = true + memberFields[j].OmitEmpty = false + if !memberFields[j].Nullable && !strings.HasPrefix(memberFields[j].Type, "[]") && !strings.HasPrefix(memberFields[j].Type, "map[") { + memberFields[j].Type = strings.TrimPrefix(memberFields[j].Type, "*") + memberFields[j].Pointer = false + } + } + } + } + + fields = append(fields, memberFields...) + } + + return fields +} + +// isNullable checks if a schema allows null values. +// In OpenAPI 3.1, this is expressed as type: ["string", "null"] +// In OpenAPI 3.0, this is expressed as nullable: true +func isNullable(schema *base.Schema) bool { + if schema == nil { + return false + } + + // OpenAPI 3.1 style: type array includes "null" + for _, t := range schema.Type { + if t == "null" { + return true + } + } + + // OpenAPI 3.0 style: nullable: true + if schema.Nullable != nil && *schema.Nullable { + return true + } + + return false +} + +// extractDescription gets the description from a schema. +func extractDescription(schema *base.Schema) string { + if schema == nil { + return "" + } + return schema.Description +} + +// formatDefaultValue converts an OpenAPI default value to a Go literal. +// goType is used to determine the correct format for the literal. +func formatDefaultValue(value any, goType string) string { + if value == nil { + return "" + } + + // Strip pointer prefix for type matching + baseType := strings.TrimPrefix(goType, "*") + + switch v := value.(type) { + case string: + // Check if the target type is not a string + // YAML/JSON might parse "10" or "true" as strings + switch baseType { + case "int", "int8", "int16", "int32", "int64", + "uint", "uint8", "uint16", "uint32", "uint64": + // Return the string as-is if it looks like a number + return v + case "bool": + // Return the string as-is if it looks like a bool + if v == "true" || v == "false" { + return v + } + case "float32", "float64": + return v + } + // It's actually a string type - quote it + return fmt.Sprintf("%q", v) + case bool: + return fmt.Sprintf("%t", v) + case float64: + // JSON numbers are always float64 + // Check if it's actually an integer + if v == float64(int64(v)) { + // It's a whole number + if strings.HasPrefix(baseType, "int") || strings.HasPrefix(baseType, "uint") { + return fmt.Sprintf("%d", int64(v)) + } + } + return fmt.Sprintf("%v", v) + case int, int64: + return fmt.Sprintf("%d", v) + case []any: + // Arrays - generate a slice literal + // For now, return empty slice if complex + if len(v) == 0 { + return fmt.Sprintf("%s{}", goType) + } + // Complex array defaults would need recursive handling + return "" + case map[string]any: + // Objects - for now, skip complex defaults + if len(v) == 0 { + return fmt.Sprintf("%s{}", goType) + } + return "" + default: + // Try a simple string conversion + return fmt.Sprintf("%v", v) + } +} + +// HasAdditionalProperties returns true if the schema has explicit additionalProperties. +func (g *TypeGenerator) HasAdditionalProperties(desc *SchemaDescriptor) bool { + if desc == nil || desc.Schema == nil { + return false + } + return desc.Schema.AdditionalProperties != nil +} + +// AdditionalPropertiesType returns the Go type for the additionalProperties. +func (g *TypeGenerator) AdditionalPropertiesType(desc *SchemaDescriptor) string { + if desc == nil || desc.Schema == nil || desc.Schema.AdditionalProperties == nil { + return "any" + } + + if desc.Schema.AdditionalProperties.A != nil { + valueSchema := desc.Schema.AdditionalProperties.A.Schema() + if valueSchema != nil { + return g.goTypeForSchema(valueSchema, nil) + } + } + + return "any" +} + +// SchemaKind represents the kind of schema for code generation. +type SchemaKind int + +const ( + KindStruct SchemaKind = iota + KindMap + KindAlias + KindEnum + KindAllOf + KindAnyOf + KindOneOf + KindReference +) + +// GetSchemaKind determines what kind of Go type to generate for a schema. +func GetSchemaKind(desc *SchemaDescriptor) SchemaKind { + if desc.IsReference() { + return KindReference + } + + schema := desc.Schema + if schema == nil { + return KindAlias + } + + // Enum check first + if len(schema.Enum) > 0 { + return KindEnum + } + + // Composition types + if len(schema.AllOf) > 0 { + return KindAllOf + } + if len(schema.AnyOf) > 0 { + return KindAnyOf + } + if len(schema.OneOf) > 0 { + return KindOneOf + } + + // Object with properties -> struct + if schema.Properties != nil && schema.Properties.Len() > 0 { + return KindStruct + } + + // Object with only additionalProperties -> map + primaryType := getPrimaryType(schema) + if primaryType == "object" { + if schema.AdditionalProperties != nil { + return KindMap + } + return KindStruct // empty struct + } + + // Everything else is an alias to a primitive type + return KindAlias +} + +// FormatJSONTag generates a JSON struct tag for a field. +// Deprecated: Use StructTagGenerator instead. +func FormatJSONTag(jsonName string, omitEmpty bool) string { + if omitEmpty { + return fmt.Sprintf("`json:\"%s,omitempty\"`", jsonName) + } + return fmt.Sprintf("`json:\"%s\"`", jsonName) +} + +// GenerateFieldTag generates struct tags for a field using the configured templates. +func (g *TypeGenerator) GenerateFieldTag(field StructField) string { + if g.tagGenerator == nil { + // Fallback to legacy behavior + if field.JSONIgnore { + return "`json:\"-\"`" + } + return FormatJSONTag(field.JSONName, field.OmitEmpty) + } + + info := StructTagInfo{ + FieldName: field.JSONName, + GoFieldName: field.Name, + IsOptional: !field.Required, + IsNullable: field.Nullable, + IsPointer: field.Pointer, + OmitEmpty: field.OmitEmpty, + OmitZero: field.OmitZero, + JSONIgnore: field.JSONIgnore, + } + return g.tagGenerator.GenerateTags(info) +} + +// TagGenerator returns the struct tag generator. +func (g *TypeGenerator) TagGenerator() *StructTagGenerator { + return g.tagGenerator +} + +// sortFieldsByOrder sorts fields by their Order value. +// Fields without an Order value are placed after fields with explicit ordering, +// maintaining their original relative order (stable sort). +func sortFieldsByOrder(fields []StructField) { + // Check if any fields have explicit ordering + hasOrder := false + for _, f := range fields { + if f.Order != nil { + hasOrder = true + break + } + } + if !hasOrder { + return + } + + // Stable sort to preserve relative order of fields without explicit ordering + sort.SliceStable(fields, func(i, j int) bool { + // Fields with Order come before fields without + if fields[i].Order == nil && fields[j].Order == nil { + return false // preserve original order + } + if fields[i].Order == nil { + return false // i (no order) comes after j + } + if fields[j].Order == nil { + return true // i (has order) comes before j + } + // Both have order - sort by value + return *fields[i].Order < *fields[j].Order + }) +} diff --git a/experimental/internal/codegen/typemapping.go b/experimental/internal/codegen/typemapping.go new file mode 100644 index 0000000000..235b06260f --- /dev/null +++ b/experimental/internal/codegen/typemapping.go @@ -0,0 +1,98 @@ +package codegen + +// SimpleTypeSpec is used to define the Go typename of a simple type like +// an int or a string, along with the import required to use it. +type SimpleTypeSpec struct { + Type string `yaml:"type"` + Import string `yaml:"import,omitempty"` + Template string `yaml:"template,omitempty"` +} + +// FormatMapping defines the default Go type and format-specific overrides. +type FormatMapping struct { + Default SimpleTypeSpec `yaml:"default"` + Formats map[string]SimpleTypeSpec `yaml:"formats,omitempty"` +} + +// TypeMapping defines the mapping from OpenAPI types to Go types. +type TypeMapping struct { + Integer FormatMapping `yaml:"integer,omitempty"` + Number FormatMapping `yaml:"number,omitempty"` + Boolean FormatMapping `yaml:"boolean,omitempty"` + String FormatMapping `yaml:"string,omitempty"` +} + +// Merge returns a new TypeMapping with user overrides applied on top of base. +func (base TypeMapping) Merge(user TypeMapping) TypeMapping { + return TypeMapping{ + Integer: base.Integer.merge(user.Integer), + Number: base.Number.merge(user.Number), + Boolean: base.Boolean.merge(user.Boolean), + String: base.String.merge(user.String), + } +} + +func (base FormatMapping) merge(user FormatMapping) FormatMapping { + result := FormatMapping{ + Default: base.Default, + Formats: make(map[string]SimpleTypeSpec), + } + + // Copy base formats + for k, v := range base.Formats { + result.Formats[k] = v + } + + // Override with user default if specified + if user.Default.Type != "" { + result.Default = user.Default + } + + // Override/add user formats + for k, v := range user.Formats { + result.Formats[k] = v + } + + return result +} + +// DefaultTypeMapping provides the default OpenAPI type/format to Go type mappings. +var DefaultTypeMapping = TypeMapping{ + Integer: FormatMapping{ + Default: SimpleTypeSpec{Type: "int"}, + Formats: map[string]SimpleTypeSpec{ + "int": {Type: "int"}, + "int8": {Type: "int8"}, + "int16": {Type: "int16"}, + "int32": {Type: "int32"}, + "int64": {Type: "int64"}, + "uint": {Type: "uint"}, + "uint8": {Type: "uint8"}, + "uint16": {Type: "uint16"}, + "uint32": {Type: "uint32"}, + "uint64": {Type: "uint64"}, + }, + }, + Number: FormatMapping{ + Default: SimpleTypeSpec{Type: "float32"}, + Formats: map[string]SimpleTypeSpec{ + "float": {Type: "float32"}, + "double": {Type: "float64"}, + }, + }, + Boolean: FormatMapping{ + Default: SimpleTypeSpec{Type: "bool"}, + }, + String: FormatMapping{ + Default: SimpleTypeSpec{Type: "string"}, + Formats: map[string]SimpleTypeSpec{ + "byte": {Type: "[]byte"}, + "email": {Type: "Email", Template: "email.tmpl"}, + "date": {Type: "Date", Template: "date.tmpl"}, + "date-time": {Type: "time.Time", Import: "time"}, + "json": {Type: "json.RawMessage", Import: "encoding/json"}, + "uuid": {Type: "UUID", Template: "uuid.tmpl"}, + "binary": {Type: "File", Template: "file.tmpl"}, + }, + }, +} diff --git a/experimental/internal/codegen/typenames.go b/experimental/internal/codegen/typenames.go new file mode 100644 index 0000000000..c4d47b51af --- /dev/null +++ b/experimental/internal/codegen/typenames.go @@ -0,0 +1 @@ +package codegen diff --git a/scripts/foreach-module.sh b/scripts/foreach-module.sh new file mode 100755 index 0000000000..7addae7825 --- /dev/null +++ b/scripts/foreach-module.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Run a make target in each child go module whose go directive is compatible +# with the current Go toolchain. Modules requiring a newer Go are skipped. +# +# Usage: foreach-module.sh +set -euo pipefail + +target="${1:?usage: foreach-module.sh }" +cur_go="$(go env GOVERSION | sed 's/^go//')" + +git ls-files '**/*go.mod' -z | while IFS= read -r -d '' modfile; do + mod_go="$(sed -n 's/^go *//p' "$modfile")" + moddir="$(dirname "$modfile")" + + if [ "$(printf '%s\n%s' "$mod_go" "$cur_go" | sort -V | head -1)" = "$mod_go" ]; then + (set -x; cd "$moddir" && env GOBIN="${GOBIN:-}" make "$target") + else + echo "Skipping $moddir: requires go $mod_go, have go $cur_go" + fi +done From 98e5d1a848441a135ed4addd4934329b2309e81f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:20:16 -0800 Subject: [PATCH 02/44] fix(deps): update module github.com/go-chi/chi/v5 to v5.2.2 [security] (#2198) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- experimental/examples/petstore-expanded/chi/go.mod | 2 +- experimental/examples/petstore-expanded/chi/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/experimental/examples/petstore-expanded/chi/go.mod b/experimental/examples/petstore-expanded/chi/go.mod index 3c9d10741e..58fa2448db 100644 --- a/experimental/examples/petstore-expanded/chi/go.mod +++ b/experimental/examples/petstore-expanded/chi/go.mod @@ -3,7 +3,7 @@ module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expan go 1.24.0 require ( - github.com/go-chi/chi/v5 v5.2.0 + github.com/go-chi/chi/v5 v5.2.2 github.com/google/uuid v1.6.0 github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 ) diff --git a/experimental/examples/petstore-expanded/chi/go.sum b/experimental/examples/petstore-expanded/chi/go.sum index ad495c8907..54ce010d7c 100644 --- a/experimental/examples/petstore-expanded/chi/go.sum +++ b/experimental/examples/petstore-expanded/chi/go.sum @@ -1,4 +1,4 @@ -github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= -github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= From db8abb866c5b99d169290020eb23416c3c5c79fc Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Fri, 6 Feb 2026 21:19:06 -0800 Subject: [PATCH 03/44] Add output filtering (#2201) * Add output filtering Fixes #2200 Add output filtering like in V2, and support fetching specs via HTTP. * Fix lint issue --------- Co-authored-by: Marcin Romaszewicz --- experimental/Configuration.md | 20 ++ experimental/cmd/oapi-codegen/main.go | 46 ++- experimental/internal/codegen/codegen.go | 9 + .../internal/codegen/configuration.go | 16 + experimental/internal/codegen/filter.go | 97 ++++++ experimental/internal/codegen/filter_test.go | 323 ++++++++++++++++++ 6 files changed, 506 insertions(+), 5 deletions(-) create mode 100644 experimental/internal/codegen/filter.go create mode 100644 experimental/internal/codegen/filter_test.go diff --git a/experimental/Configuration.md b/experimental/Configuration.md index 55f550e2f3..db2d3ed508 100644 --- a/experimental/Configuration.md +++ b/experimental/Configuration.md @@ -37,6 +37,26 @@ generation: path: github.com/org/project/models alias: models # optional, defaults to last segment of path +# Output options: control which operations and schemas are included. +output-options: + # Only include operations tagged with one of these tags. Ignored when empty. + include-tags: + - public + - beta + # Exclude operations tagged with one of these tags. Ignored when empty. + exclude-tags: + - internal + # Only include operations with one of these operation IDs. Ignored when empty. + include-operation-ids: + - listPets + - createPet + # Exclude operations with one of these operation IDs. Ignored when empty. + exclude-operation-ids: + - deprecatedEndpoint + # Exclude schemas with the given names from generation. Ignored when empty. + exclude-schemas: + - InternalConfig + # Type mappings: OpenAPI type/format to Go type. # User values are merged on top of defaults — you only need to specify overrides. type-mapping: diff --git a/experimental/cmd/oapi-codegen/main.go b/experimental/cmd/oapi-codegen/main.go index 24c5320c90..50205e846c 100644 --- a/experimental/cmd/oapi-codegen/main.go +++ b/experimental/cmd/oapi-codegen/main.go @@ -3,6 +3,9 @@ package main import ( "flag" "fmt" + "io" + "net/http" + "net/url" "os" "path/filepath" "strings" @@ -20,9 +23,9 @@ func main() { flagPackage := flag.String("package", "", "Go package name for generated code") flagOutput := flag.String("output", "", "output file path (default: .gen.go)") flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage: %s [options] \n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage: %s [options] \n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Arguments:\n") - fmt.Fprintf(os.Stderr, " spec-path path to OpenAPI spec file\n\n") + fmt.Fprintf(os.Stderr, " spec-path-or-url path or URL to OpenAPI spec file\n\n") fmt.Fprintf(os.Stderr, "Options:\n") flag.PrintDefaults() } @@ -35,8 +38,8 @@ func main() { specPath := flag.Arg(0) - // Parse the OpenAPI spec - specData, err := os.ReadFile(specPath) + // Load the OpenAPI spec from file or URL + specData, err := loadSpec(specPath) if err != nil { fmt.Fprintf(os.Stderr, "error reading spec: %v\n", err) os.Exit(1) @@ -78,7 +81,12 @@ func main() { // Default output to .gen.go if cfg.Output == "" { - base := filepath.Base(specPath) + // For URLs, extract the filename from the URL path + baseName := specPath + if u, err := url.Parse(specPath); err == nil && u.Scheme != "" && u.Host != "" { + baseName = u.Path + } + base := filepath.Base(baseName) ext := filepath.Ext(base) cfg.Output = strings.TrimSuffix(base, ext) + ".gen.go" } @@ -103,3 +111,31 @@ func main() { fmt.Printf("Generated %s\n", cfg.Output) } + +// loadSpec loads an OpenAPI spec from a file path or URL. +func loadSpec(specPath string) ([]byte, error) { + u, err := url.Parse(specPath) + if err == nil && u.Scheme != "" && u.Host != "" { + return loadSpecFromURL(u.String()) + } + return os.ReadFile(specPath) +} + +// loadSpecFromURL fetches an OpenAPI spec from an HTTP(S) URL. +func loadSpecFromURL(specURL string) ([]byte, error) { + resp, err := http.Get(specURL) //nolint:gosec // URL comes from user-provided spec path + if err != nil { + return nil, fmt.Errorf("fetching spec from URL: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("fetching spec from URL: HTTP %d %s", resp.StatusCode, resp.Status) + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("reading spec from URL: %w", err) + } + return data, nil +} diff --git a/experimental/internal/codegen/codegen.go b/experimental/internal/codegen/codegen.go index 5b6fdff894..7ff2175067 100644 --- a/experimental/internal/codegen/codegen.go +++ b/experimental/internal/codegen/codegen.go @@ -31,6 +31,9 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri return "", fmt.Errorf("gathering schemas: %w", err) } + // Filter excluded schemas + schemas = FilterSchemasByName(schemas, cfg.OutputOptions.ExcludeSchemas) + // Pass 2: Compute names for all schemas converter := NewNameConverter(cfg.NameMangling, cfg.NameSubstitutions) ComputeSchemaNames(schemas, converter, contentTypeNamer) @@ -102,6 +105,9 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri return "", fmt.Errorf("gathering operations: %w", err) } + // Apply operation filters + ops = FilterOperations(ops, cfg.OutputOptions) + // Generate client clientGen, err := NewClientGenerator(schemaIndex, cfg.Generation.SimpleClient, cfg.Generation.ModelsPackage) if err != nil { @@ -158,6 +164,9 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri return "", fmt.Errorf("gathering operations: %w", err) } + // Apply operation filters + ops = FilterOperations(ops, cfg.OutputOptions) + if len(ops) > 0 { // Generate server serverGen, err := NewServerGenerator(cfg.Generation.Server) diff --git a/experimental/internal/codegen/configuration.go b/experimental/internal/codegen/configuration.go index c8d1b87388..0dc128f9ea 100644 --- a/experimental/internal/codegen/configuration.go +++ b/experimental/internal/codegen/configuration.go @@ -15,6 +15,8 @@ type Configuration struct { Output string `yaml:"output"` // Generation controls which parts of the code are generated Generation GenerationOptions `yaml:"generation,omitempty"` + // OutputOptions controls filtering of operations and schemas + OutputOptions OutputOptions `yaml:"output-options,omitempty"` // TypeMapping allows customizing OpenAPI type/format to Go type mappings TypeMapping TypeMapping `yaml:"type-mapping,omitempty"` // NameMangling configures how OpenAPI names are converted to Go identifiers @@ -38,6 +40,20 @@ type Configuration struct { StructTags StructTagsConfig `yaml:"struct-tags,omitempty"` } +// OutputOptions controls filtering of which operations and schemas are included in generation. +type OutputOptions struct { + // IncludeTags only includes operations tagged with one of these tags. Ignored when empty. + IncludeTags []string `yaml:"include-tags,omitempty"` + // ExcludeTags excludes operations tagged with one of these tags. Ignored when empty. + ExcludeTags []string `yaml:"exclude-tags,omitempty"` + // IncludeOperationIDs only includes operations with one of these operation IDs. Ignored when empty. + IncludeOperationIDs []string `yaml:"include-operation-ids,omitempty"` + // ExcludeOperationIDs excludes operations with one of these operation IDs. Ignored when empty. + ExcludeOperationIDs []string `yaml:"exclude-operation-ids,omitempty"` + // ExcludeSchemas excludes schemas with the given names from generation. Ignored when empty. + ExcludeSchemas []string `yaml:"exclude-schemas,omitempty"` +} + // ModelsPackage specifies an external package containing the model types. type ModelsPackage struct { // Path is the import path for the models package (e.g., "github.com/org/project/models") diff --git a/experimental/internal/codegen/filter.go b/experimental/internal/codegen/filter.go new file mode 100644 index 0000000000..8e20bed5af --- /dev/null +++ b/experimental/internal/codegen/filter.go @@ -0,0 +1,97 @@ +package codegen + +// FilterOperationsByTag filters operations based on include/exclude tag lists. +// Exclude is applied first, then include. +func FilterOperationsByTag(ops []*OperationDescriptor, opts OutputOptions) []*OperationDescriptor { + if len(opts.ExcludeTags) > 0 { + tags := sliceToSet(opts.ExcludeTags) + ops = filterOps(ops, func(op *OperationDescriptor) bool { + return !operationHasTag(op, tags) + }) + } + if len(opts.IncludeTags) > 0 { + tags := sliceToSet(opts.IncludeTags) + ops = filterOps(ops, func(op *OperationDescriptor) bool { + return operationHasTag(op, tags) + }) + } + return ops +} + +// FilterOperationsByOperationID filters operations based on include/exclude operation ID lists. +// Exclude is applied first, then include. +func FilterOperationsByOperationID(ops []*OperationDescriptor, opts OutputOptions) []*OperationDescriptor { + if len(opts.ExcludeOperationIDs) > 0 { + ids := sliceToSet(opts.ExcludeOperationIDs) + ops = filterOps(ops, func(op *OperationDescriptor) bool { + return !ids[op.OperationID] + }) + } + if len(opts.IncludeOperationIDs) > 0 { + ids := sliceToSet(opts.IncludeOperationIDs) + ops = filterOps(ops, func(op *OperationDescriptor) bool { + return ids[op.OperationID] + }) + } + return ops +} + +// FilterOperations applies all operation filters (tags, operation IDs) from OutputOptions. +func FilterOperations(ops []*OperationDescriptor, opts OutputOptions) []*OperationDescriptor { + ops = FilterOperationsByTag(ops, opts) + ops = FilterOperationsByOperationID(ops, opts) + return ops +} + +// FilterSchemasByName removes schemas whose component name is in the exclude list. +// Only filters top-level component schemas (path: components/schemas/). +func FilterSchemasByName(schemas []*SchemaDescriptor, excludeNames []string) []*SchemaDescriptor { + if len(excludeNames) == 0 { + return schemas + } + excluded := sliceToSet(excludeNames) + result := make([]*SchemaDescriptor, 0, len(schemas)) + for _, s := range schemas { + // Check if this is a top-level component schema + if len(s.Path) == 3 && s.Path[0] == "components" && s.Path[1] == "schemas" { + if excluded[s.Path[2]] { + continue + } + } + result = append(result, s) + } + return result +} + +// operationHasTag returns true if the operation has any of the given tags. +func operationHasTag(op *OperationDescriptor, tags map[string]bool) bool { + if op == nil || op.Spec == nil { + return false + } + for _, tag := range op.Spec.Tags { + if tags[tag] { + return true + } + } + return false +} + +// filterOps returns operations that satisfy the predicate. +func filterOps(ops []*OperationDescriptor, keep func(*OperationDescriptor) bool) []*OperationDescriptor { + result := make([]*OperationDescriptor, 0, len(ops)) + for _, op := range ops { + if keep(op) { + result = append(result, op) + } + } + return result +} + +// sliceToSet converts a string slice to a set (map[string]bool). +func sliceToSet(items []string) map[string]bool { + m := make(map[string]bool, len(items)) + for _, item := range items { + m[item] = true + } + return m +} diff --git a/experimental/internal/codegen/filter_test.go b/experimental/internal/codegen/filter_test.go new file mode 100644 index 0000000000..9e0f960a86 --- /dev/null +++ b/experimental/internal/codegen/filter_test.go @@ -0,0 +1,323 @@ +package codegen + +import ( + "testing" + + "github.com/pb33f/libopenapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const filterTestSpec = `openapi: "3.1.0" +info: + title: Filter Test API + version: "1.0" +paths: + /users: + get: + operationId: listUsers + tags: + - users + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + /pets: + get: + operationId: listPets + tags: + - pets + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /admin/settings: + get: + operationId: getSettings + tags: + - admin + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Settings' + put: + operationId: updateSettings + tags: + - admin + responses: + "200": + description: OK +components: + schemas: + User: + type: object + properties: + name: + type: string + Pet: + type: object + properties: + name: + type: string + Settings: + type: object + properties: + theme: + type: string +` + +func gatherTestOps(t *testing.T) []*OperationDescriptor { + t.Helper() + doc, err := libopenapi.NewDocument([]byte(filterTestSpec)) + require.NoError(t, err) + + tracker := NewParamUsageTracker() + ops, err := GatherOperations(doc, tracker) + require.NoError(t, err) + return ops +} + +func opIDs(ops []*OperationDescriptor) []string { + ids := make([]string, len(ops)) + for i, op := range ops { + ids[i] = op.OperationID + } + return ids +} + +func TestFilterOperationsByTag_Include(t *testing.T) { + ops := gatherTestOps(t) + require.Len(t, ops, 4) + + filtered := FilterOperationsByTag(ops, OutputOptions{ + IncludeTags: []string{"users"}, + }) + + ids := opIDs(filtered) + assert.Contains(t, ids, "listUsers") + assert.NotContains(t, ids, "listPets") + assert.NotContains(t, ids, "getSettings") + assert.NotContains(t, ids, "updateSettings") +} + +func TestFilterOperationsByTag_Exclude(t *testing.T) { + ops := gatherTestOps(t) + + filtered := FilterOperationsByTag(ops, OutputOptions{ + ExcludeTags: []string{"admin"}, + }) + + ids := opIDs(filtered) + assert.Contains(t, ids, "listUsers") + assert.Contains(t, ids, "listPets") + assert.NotContains(t, ids, "getSettings") + assert.NotContains(t, ids, "updateSettings") +} + +func TestFilterOperationsByTag_IncludeMultiple(t *testing.T) { + ops := gatherTestOps(t) + + filtered := FilterOperationsByTag(ops, OutputOptions{ + IncludeTags: []string{"users", "pets"}, + }) + + ids := opIDs(filtered) + assert.Contains(t, ids, "listUsers") + assert.Contains(t, ids, "listPets") + assert.NotContains(t, ids, "getSettings") + assert.Len(t, filtered, 2) +} + +func TestFilterOperationsByTag_Empty(t *testing.T) { + ops := gatherTestOps(t) + + filtered := FilterOperationsByTag(ops, OutputOptions{}) + assert.Len(t, filtered, 4) +} + +func TestFilterOperationsByOperationID_Include(t *testing.T) { + ops := gatherTestOps(t) + + filtered := FilterOperationsByOperationID(ops, OutputOptions{ + IncludeOperationIDs: []string{"listPets"}, + }) + + ids := opIDs(filtered) + assert.Contains(t, ids, "listPets") + assert.Len(t, filtered, 1) +} + +func TestFilterOperationsByOperationID_Exclude(t *testing.T) { + ops := gatherTestOps(t) + + filtered := FilterOperationsByOperationID(ops, OutputOptions{ + ExcludeOperationIDs: []string{"getSettings", "updateSettings"}, + }) + + ids := opIDs(filtered) + assert.Contains(t, ids, "listUsers") + assert.Contains(t, ids, "listPets") + assert.NotContains(t, ids, "getSettings") + assert.NotContains(t, ids, "updateSettings") + assert.Len(t, filtered, 2) +} + +func TestFilterOperations_Combined(t *testing.T) { + ops := gatherTestOps(t) + + // Include only admin, then exclude updateSettings + filtered := FilterOperations(ops, OutputOptions{ + IncludeTags: []string{"admin"}, + ExcludeOperationIDs: []string{"updateSettings"}, + }) + + ids := opIDs(filtered) + assert.Contains(t, ids, "getSettings") + assert.NotContains(t, ids, "updateSettings") + assert.Len(t, filtered, 1) +} + +func TestFilterSchemasByName(t *testing.T) { + doc, err := libopenapi.NewDocument([]byte(filterTestSpec)) + require.NoError(t, err) + + matcher := NewContentTypeMatcher(DefaultContentTypes()) + schemas, err := GatherSchemas(doc, matcher) + require.NoError(t, err) + + // Count component schemas + var componentNames []string + for _, s := range schemas { + if len(s.Path) == 3 && s.Path[0] == "components" && s.Path[1] == "schemas" { + componentNames = append(componentNames, s.Path[2]) + } + } + assert.Contains(t, componentNames, "User") + assert.Contains(t, componentNames, "Pet") + assert.Contains(t, componentNames, "Settings") + + // Exclude Pet + filtered := FilterSchemasByName(schemas, []string{"Pet"}) + + var filteredNames []string + for _, s := range filtered { + if len(s.Path) == 3 && s.Path[0] == "components" && s.Path[1] == "schemas" { + filteredNames = append(filteredNames, s.Path[2]) + } + } + assert.Contains(t, filteredNames, "User") + assert.NotContains(t, filteredNames, "Pet") + assert.Contains(t, filteredNames, "Settings") +} + +func TestFilterSchemasByName_Empty(t *testing.T) { + doc, err := libopenapi.NewDocument([]byte(filterTestSpec)) + require.NoError(t, err) + + matcher := NewContentTypeMatcher(DefaultContentTypes()) + schemas, err := GatherSchemas(doc, matcher) + require.NoError(t, err) + + filtered := FilterSchemasByName(schemas, nil) + assert.Equal(t, len(schemas), len(filtered)) +} + +func TestFilterIntegration_GenerateWithIncludeTags(t *testing.T) { + doc, err := libopenapi.NewDocument([]byte(filterTestSpec)) + require.NoError(t, err) + + cfg := Configuration{ + PackageName: "testpkg", + Generation: GenerationOptions{ + Client: true, + }, + OutputOptions: OutputOptions{ + IncludeTags: []string{"users"}, + }, + } + + code, err := Generate(doc, []byte(filterTestSpec), cfg) + require.NoError(t, err) + // Client interface should only contain the included operation + assert.Contains(t, code, "ListUsers(ctx context.Context") + assert.NotContains(t, code, "ListPets(ctx context.Context") + assert.NotContains(t, code, "GetSettings(ctx context.Context") +} + +func TestFilterIntegration_GenerateWithExcludeTags(t *testing.T) { + doc, err := libopenapi.NewDocument([]byte(filterTestSpec)) + require.NoError(t, err) + + cfg := Configuration{ + PackageName: "testpkg", + Generation: GenerationOptions{ + Client: true, + }, + OutputOptions: OutputOptions{ + ExcludeTags: []string{"admin"}, + }, + } + + code, err := Generate(doc, []byte(filterTestSpec), cfg) + require.NoError(t, err) + // Client interface should include users and pets but not admin operations + assert.Contains(t, code, "ListUsers(ctx context.Context") + assert.Contains(t, code, "ListPets(ctx context.Context") + assert.NotContains(t, code, "GetSettings(ctx context.Context") + assert.NotContains(t, code, "UpdateSettings(ctx context.Context") +} + +func TestFilterIntegration_GenerateWithExcludeSchemas(t *testing.T) { + doc, err := libopenapi.NewDocument([]byte(filterTestSpec)) + require.NoError(t, err) + + cfg := Configuration{ + PackageName: "testpkg", + OutputOptions: OutputOptions{ + ExcludeSchemas: []string{"Pet"}, + }, + } + + code, err := Generate(doc, []byte(filterTestSpec), cfg) + require.NoError(t, err) + // User and Settings types should still exist + assert.Contains(t, code, "type User struct") + assert.Contains(t, code, "type Settings struct") + // Pet type should be excluded + assert.NotContains(t, code, "type Pet struct") +} + +func TestFilterIntegration_ServerWithIncludeTags(t *testing.T) { + doc, err := libopenapi.NewDocument([]byte(filterTestSpec)) + require.NoError(t, err) + + cfg := Configuration{ + PackageName: "testpkg", + Generation: GenerationOptions{ + Server: ServerTypeStdHTTP, + }, + OutputOptions: OutputOptions{ + IncludeTags: []string{"pets"}, + }, + } + + code, err := Generate(doc, []byte(filterTestSpec), cfg) + require.NoError(t, err) + // Server interface should only contain the included operation + assert.Contains(t, code, "ListPets(w http.ResponseWriter") + assert.NotContains(t, code, "ListUsers(w http.ResponseWriter") + assert.NotContains(t, code, "GetSettings(w http.ResponseWriter") +} From 17ba42e132420ffe0b67b15fafa4750ab144b1c5 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Sat, 7 Feb 2026 00:41:32 -0800 Subject: [PATCH 04/44] Rename Swagger references to OpenAPI (#2202) Co-authored-by: Marcin Romaszewicz --- .../examples/callback/treefarm.gen.go | 22 ++++++++--------- .../petstore-expanded/petstore.gen.go | 22 ++++++++--------- .../examples/webhook/doorbadge.gen.go | 22 ++++++++--------- experimental/internal/codegen/inline.go | 24 +++++++++---------- experimental/internal/codegen/inline_test.go | 18 +++++++------- .../templates/files/client/base.go.tmpl | 2 +- .../comprehensive/output/comprehensive.gen.go | 22 ++++++++--------- .../issues/issue_1029/output/types.gen.go | 22 ++++++++--------- .../issues/issue_1039/output/types.gen.go | 22 ++++++++--------- .../issues/issue_1397/output/types.gen.go | 22 ++++++++--------- .../issues/issue_1429/output/types.gen.go | 22 ++++++++--------- .../issues/issue_1496/output/types.gen.go | 22 ++++++++--------- .../issues/issue_1710/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_193/output/types.gen.go | 22 ++++++++--------- .../issues/issue_2102/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_312/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_502/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_52/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_579/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_697/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_775/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_832/output/types.gen.go | 22 ++++++++--------- .../output/types.gen.go | 22 ++++++++--------- .../output/types.gen.go | 22 ++++++++--------- .../output/nested_aggregate.gen.go | 22 ++++++++--------- 25 files changed, 264 insertions(+), 264 deletions(-) diff --git a/experimental/examples/callback/treefarm.gen.go b/experimental/examples/callback/treefarm.gen.go index a1018c08e9..6d74b06f80 100644 --- a/experimental/examples/callback/treefarm.gen.go +++ b/experimental/examples/callback/treefarm.gen.go @@ -80,7 +80,7 @@ func (s *Error) ApplyDefaults() { type UUID = uuid.UUID // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/8RXwW7bOBC96ysGaQG3QCO76U23bJEFAhSbIHGQ44IWxxYbilTJUbzG7gL7EfuF+yUL", "khJFy4rr9NKcopDzZvhm5s1EN6hYIwo4+5R/zBdnmVBrXWQAz2is0KqAj/kiX2QAJEhiAUuDCL8yU2cA", "HG1pREP+3l8ZAMAlkLvQSKZIqA1YNM+iRKCKEXCstbJkGKGFmwbV5e01lEzKFSufbO4BlhVCKQUqAtuu", @@ -103,9 +103,9 @@ var swaggerSpecJSON = []string{ "36d8LLPs/wAAAP//y9dX5mEQAAA=", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -122,24 +122,24 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } // ServerInterface represents all server handlers. diff --git a/experimental/examples/petstore-expanded/petstore.gen.go b/experimental/examples/petstore-expanded/petstore.gen.go index 4149df2b00..c958265c74 100644 --- a/experimental/examples/petstore-expanded/petstore.gen.go +++ b/experimental/examples/petstore-expanded/petstore.gen.go @@ -46,7 +46,7 @@ func (s *Error) ApplyDefaults() { type FindPetsJSONResponse = []Pet // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/+xXXW9byQ19168gtgXyoly5yaIPemo2TgED3cStd/tOz6UkLubLQ44coe1/Lzj3Srqy", "ZG+CYvuF9YMNz3A4hzyHHN6UKWLmJXzztrvqrr6ZcVyl5QxgS0U4xSX8ztZnAMrqaQl3j7heU4FbUtFU", "aAbQk7jCWZv5OxAM2RO8u70B3aBCFRJAyOMBQAGMQJ8HM03QU0hRtKASrAi1FhLgCLoh+JQpmqe33RVI", @@ -77,9 +77,9 @@ var swaggerSpecJSON = []string{ "5gbvFMOI4GujHzzt4f8zAAD//6ZcwVZEFgAA", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -96,22 +96,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/examples/webhook/doorbadge.gen.go b/experimental/examples/webhook/doorbadge.gen.go index 702d74db62..b51b7d405f 100644 --- a/experimental/examples/webhook/doorbadge.gen.go +++ b/experimental/examples/webhook/doorbadge.gen.go @@ -72,7 +72,7 @@ const ( type UUID = uuid.UUID // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/9RWTY/bNhC961cM0gI+RfLu9qTbJuvDAkGycFL0TItjaxKJZIaUvEbb/16Q1GdX3l0D", "RZv6ZEnDmfceh4+jDSphKIc3N+lVun6TkNrrPAFokS1plcNVuk7XCYAjV2EOd1ozvBPygLBFIZETAIm2", "YDIuxP+RAADcgvSBuxDIIRAscksFgiuFA4m1VtaxcGjhk0F1+3APN+kVHHFXav3NpiHP+4pQOQuMB7IO", @@ -91,9 +91,9 @@ var swaggerSpecJSON = []string{ "xUw9/L8CAAD//+XREy3yDQAA", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -110,24 +110,24 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } // ServerInterface represents all server handlers. diff --git a/experimental/internal/codegen/inline.go b/experimental/internal/codegen/inline.go index 25767cffa8..1a9554f73e 100644 --- a/experimental/internal/codegen/inline.go +++ b/experimental/internal/codegen/inline.go @@ -9,7 +9,7 @@ import ( ) // generateEmbeddedSpec produces Go code that embeds the raw OpenAPI spec as -// gzip+base64 encoded data, with a public GetSwaggerSpecJSON() function to +// gzip+base64 encoded data, with a public GetOpenAPISpecJSON() function to // retrieve the decompressed JSON bytes. func generateEmbeddedSpec(specData []byte) (string, error) { // Gzip compress @@ -43,15 +43,15 @@ func generateEmbeddedSpec(specData []byte) (string, error) { var b strings.Builder b.WriteString("// Base64-encoded, gzip-compressed OpenAPI spec.\n") - b.WriteString("var swaggerSpecJSON = []string{\n") + b.WriteString("var openAPISpecJSON = []string{\n") for _, chunk := range chunks { fmt.Fprintf(&b, "\t%q,\n", chunk) } b.WriteString("}\n\n") - b.WriteString("// decodeSwaggerSpec decodes and decompresses the embedded spec.\n") - b.WriteString("func decodeSwaggerSpec() ([]byte, error) {\n") - b.WriteString("\tjoined := strings.Join(swaggerSpecJSON, \"\")\n") + b.WriteString("// decodeOpenAPISpec decodes and decompresses the embedded spec.\n") + b.WriteString("func decodeOpenAPISpec() ([]byte, error) {\n") + b.WriteString("\tjoined := strings.Join(openAPISpecJSON, \"\")\n") b.WriteString("\traw, err := base64.StdEncoding.DecodeString(joined)\n") b.WriteString("\tif err != nil {\n") b.WriteString("\t\treturn nil, fmt.Errorf(\"decoding base64: %w\", err)\n") @@ -68,24 +68,24 @@ func generateEmbeddedSpec(specData []byte) (string, error) { b.WriteString("\treturn out.Bytes(), nil\n") b.WriteString("}\n\n") - b.WriteString("// decodeSwaggerSpecCached returns a closure that caches the decoded spec.\n") - b.WriteString("func decodeSwaggerSpecCached() func() ([]byte, error) {\n") + b.WriteString("// decodeOpenAPISpecCached returns a closure that caches the decoded spec.\n") + b.WriteString("func decodeOpenAPISpecCached() func() ([]byte, error) {\n") b.WriteString("\tvar cached []byte\n") b.WriteString("\tvar cachedErr error\n") b.WriteString("\tvar once sync.Once\n") b.WriteString("\treturn func() ([]byte, error) {\n") b.WriteString("\t\tonce.Do(func() {\n") - b.WriteString("\t\t\tcached, cachedErr = decodeSwaggerSpec()\n") + b.WriteString("\t\t\tcached, cachedErr = decodeOpenAPISpec()\n") b.WriteString("\t\t})\n") b.WriteString("\t\treturn cached, cachedErr\n") b.WriteString("\t}\n") b.WriteString("}\n\n") - b.WriteString("var swaggerSpec = decodeSwaggerSpecCached()\n\n") + b.WriteString("var openAPISpec = decodeOpenAPISpecCached()\n\n") - b.WriteString("// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes.\n") - b.WriteString("func GetSwaggerSpecJSON() ([]byte, error) {\n") - b.WriteString("\treturn swaggerSpec()\n") + b.WriteString("// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes.\n") + b.WriteString("func GetOpenAPISpecJSON() ([]byte, error) {\n") + b.WriteString("\treturn openAPISpec()\n") b.WriteString("}\n") return b.String(), nil diff --git a/experimental/internal/codegen/inline_test.go b/experimental/internal/codegen/inline_test.go index ae35938493..233aab2e79 100644 --- a/experimental/internal/codegen/inline_test.go +++ b/experimental/internal/codegen/inline_test.go @@ -19,19 +19,19 @@ func TestGenerateEmbeddedSpec(t *testing.T) { require.NoError(t, err) // Should contain the chunked base64 variable - assert.Contains(t, code, "var swaggerSpecJSON = []string{") + assert.Contains(t, code, "var openAPISpecJSON = []string{") // Should contain the decode function - assert.Contains(t, code, "func decodeSwaggerSpec() ([]byte, error)") + assert.Contains(t, code, "func decodeOpenAPISpec() ([]byte, error)") // Should contain the cached decode function - assert.Contains(t, code, "func decodeSwaggerSpecCached() func() ([]byte, error)") + assert.Contains(t, code, "func decodeOpenAPISpecCached() func() ([]byte, error)") // Should contain the public API - assert.Contains(t, code, "func GetSwaggerSpecJSON() ([]byte, error)") + assert.Contains(t, code, "func GetOpenAPISpecJSON() ([]byte, error)") // Should contain the cached var - assert.Contains(t, code, "var swaggerSpec = decodeSwaggerSpecCached()") + assert.Contains(t, code, "var openAPISpec = decodeOpenAPISpecCached()") } func TestGenerateEmbeddedSpecRoundTrip(t *testing.T) { @@ -101,8 +101,8 @@ components: assert.Contains(t, code, "type Pet struct") // Should contain the embedded spec - assert.Contains(t, code, "GetSwaggerSpecJSON") - assert.Contains(t, code, "swaggerSpecJSON") + assert.Contains(t, code, "GetOpenAPISpecJSON") + assert.Contains(t, code, "openAPISpecJSON") } func TestGenerateWithNilSpecData(t *testing.T) { @@ -134,6 +134,6 @@ components: assert.Contains(t, code, "type Pet struct") // Should NOT contain the embedded spec - assert.NotContains(t, code, "GetSwaggerSpecJSON") - assert.NotContains(t, code, "swaggerSpecJSON") + assert.NotContains(t, code, "GetOpenAPISpecJSON") + assert.NotContains(t, code, "openAPISpecJSON") } diff --git a/experimental/internal/codegen/templates/files/client/base.go.tmpl b/experimental/internal/codegen/templates/files/client/base.go.tmpl index 96d32997f4..04aebaf713 100644 --- a/experimental/internal/codegen/templates/files/client/base.go.tmpl +++ b/experimental/internal/codegen/templates/files/client/base.go.tmpl @@ -14,7 +14,7 @@ type Client struct { // The endpoint of the server conforming to this interface, with scheme, // https://api.deepmap.com for example. This can contain a path relative // to the server, such as https://api.deepmap.com/dev-test, and all the - // paths in the swagger spec will be appended to the server. + // paths in the OpenAPI spec will be appended to the server. Server string // Doer for performing requests, typically a *http.Client with any diff --git a/experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go b/experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go index 633aa2dbe7..04a06f9db5 100644 --- a/experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go +++ b/experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go @@ -2009,7 +2009,7 @@ var ErrNullableNotSpecified = errors.New("nullable value is not specified") type UUID = uuid.UUID // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/+wcXW8iOfKdX2Gx+3Zi8jGz84B0D4QwEnsEEJCdi1Z7J4cuEu90u3vcJgm32v9+st10", "tz/6y2RGs6PNEynb9eVyVblcECdAcUKGqP/2zcWb836P0F087CHECQ9hiMZxlDB4BJqSJ0AbSDlabx8h", "wj2EnoClJKZD1L94cy7WIhRAumUk4RIsZqcIhyFK5RKUYM6B0RTtYoZinJDBNg7gAWivl2D+mAq6ZymJ", @@ -2058,9 +2058,9 @@ var swaggerSpecJSON = []string{ "wdmxH1N53dOFMHa0aR81yf8fAAD//ynhbdUwSwAA", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -2077,22 +2077,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go index 7c5a5d2fa1..a2e22ce566 100644 --- a/experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go @@ -137,7 +137,7 @@ const ( ) // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/6yQzY7UMBCE736KknJlMrPsibwBJySExNmb1CQNjm2527MaId4dOeFnZw4Iob25q7vr", "a1eH96qVeDi9fTcgRX44Q61InMFYV8XZS2iVJcyMLN6IXFJmCVfXYTHLOhyPs9hSn/oxrcfksxzGNHFm", "vC2kofTYWK5zHT4vjKja7Hfys9iCtQaTHIjWCDxcfKi8OeqN62ALfx80oRGgS6qhvdcsgfBxwnMqXzGm", @@ -146,9 +146,9 @@ var swaggerSpecJSON = []string{ "vDLjdD/9X0Z+NLnwbz4/AgAA//+mSD78zgIAAA==", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -165,22 +165,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go index d996cfb752..8077efc300 100644 --- a/experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go @@ -238,7 +238,7 @@ var ErrNullableIsNull = errors.New("nullable value is null") var ErrNullableNotSpecified = errors.New("nullable value is not specified") // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/7RUTY/TMBC9+1eMEqReaNNlT+QGnFYIdgXcKzeZJl5S23gmqJX48SjfSZu0ge7eEnvm", "vTdvxuPDA1GOcLe+fx/C1zzL5DZD4KNFggQ1OsnKaOFDymwpDIJEcZpvV5HZB0ZatYxMjAnq4Y8qQCko", "UIUvfPgifyJQ7hA4lQx6yCMdNlwYg3XGosuOwljU0qoQ7lfr1Z1QemdCAfAbHSmjQ1isi/OFAGDFGYaA", @@ -251,9 +251,9 @@ var swaggerSpecJSON = []string{ "wsAUMWu+r+32upa3F5/AWDEyU5I2/1DSlXK649EGvLR7o1R/AwAA///zkywGmQkAAA==", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -270,22 +270,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go index 7fd6bbf827..8b54637dde 100644 --- a/experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go @@ -63,7 +63,7 @@ func (s *TestField3) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/9xUzZKbMAy+8xSapFdCIO106lv30Jkc2kOnL+AYBbwDltcS283bd2zYAJt0Z3otJ/xZ", "P58+WdrCkXlAKA9fPit40GwNyMUjNOgwaLHk4LeVFl7yhvJ4kzvdY7aFVsSzKorGSjucdob6grS3uaEa", "G3Trg41JuIhZMvLotLcKNofdflduMuvOpDIAsdKhWhCCX8iSATxjYEtOQbnb7/aZod6TQyccvdi02Ov0", @@ -74,9 +74,9 @@ var swaggerSpecJSON = []string{ "sy3mLVJMK6SI1DdXDuzJ8bIz1f7jX1vfENXZnwAAAP//PTfE300FAAA=", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -93,22 +93,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go index 993992b0d7..5cbd404ef6 100644 --- a/experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go @@ -100,7 +100,7 @@ const ( ) // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/6xSsW7cMAzd9RUPcYEsvfMl7VJtHTN1KdBZtmlLwZkURLrF/X0hOwdfrh3DiXqkHt8T", "1eBFdSE8fX3+5kG8zGAxTMRUgtGAP5EYi9YsWUTgy4/RNYhmWX3bTsni0h17mVsJOR16GWgifn9IdYS2", "dYZrXINflTMgF8lU7ILEmgZC4I0eM80dFcSgFauiPsMibfLsksk10CjLeUBHu9ajk0wccvL4cjwdTy7x", @@ -109,9 +109,9 @@ var swaggerSpecJSON = []string{ "dQP7yMN1S+nWXo0x0Xn4fi9uY1QriaePJdmifoF/H+SAUeQ/aBeK+xsAAP//dJpKJ+ICAAA=", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -128,22 +128,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go index 2f95e2dbf8..6e3f8a4fa6 100644 --- a/experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go @@ -117,7 +117,7 @@ func (s *GetSomething200ResponseJSONAnyOf12) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/7xTTW+bQBC98yue4ksrJeC0VaVy66nyKVJTqcdoDWOYyMxud4ZY/vfVgqlJncTJJZyY", "fTP7PhgWWKn2hOsv376W+C5e9p3vFX59T5VBq5Y6hx1bCy90s0FDQtEZKVge3JZrcE1ivGGK2QKtWdCy", "KBq2tl/nle8K7wJfVb6mhuRxwYlai8SdLbIFfrckcAKWLQtN5CxwiKTBixJ6JYWT/c2mGARdwlr6p6qG", @@ -128,9 +128,9 @@ var swaggerSpecJSON = []string{ "b+Ed17Om810T/fgrn23vSNU1b7j3kPjfAAAA///mDrwBGwUAAA==", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -147,22 +147,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go index 6efb31f3a5..92bee3e2c8 100644 --- a/experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go @@ -161,7 +161,7 @@ const ( ) // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/9SUT4/TMBDF7/kUIwWpFzZpxQEpRzhxQMuhEmc3eYmNEtt4JvtHiO+O4qYbh2132RVC", "oqfO68zzm5+b5PSJeQTt3u+2FbUGfUO9YyFjyYIFDam+v27JWVy3WU5axHNVlp0RPR6K2g2lU95c1a5B", "B7suzOTN5WSe5VlOXzUsjWxstza/NaKPJ7ydlGMOJtZu7Bs6gHwAI9ygKbKc9hq08cENXjZz5FvFdMDk", @@ -172,9 +172,9 @@ var swaggerSpecJSON = []string{ "6CmTS9yXNf97QoK7lxBaXkt/TOhXAAAA//+WRkFKuQYAAA==", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -191,22 +191,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_193/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_193/output/types.gen.go index 33a950f497..39adf0b5c2 100644 --- a/experimental/internal/codegen/test/issues/issue_193/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_193/output/types.gen.go @@ -23,7 +23,7 @@ func (s *Person) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/6yQvW7jQAyE+32KAVzbsuHK21151fkV1itK4kG73FtSFwRB3j2QHP8BMZAiHTn4SM5w", "hd+qE2F32Hv8Gsc/HV7YBoS2ZWPJYTxWKVSNSZGo9px7t8JgVtQ3Tc82TKdNlNRIKLyO0lJP+bHh+YI2", "u8PeSaEcCnvsN9vN1nHuxDvA2EbyMFKDxoFScMB/qsqSPXYLW4IN6vH27qKkIpmy6Tx75pcSOFJVyeca", @@ -31,9 +31,9 @@ var swaggerSpecJSON = []string{ "LbU6P/2WoiwhoYUidxx/Ns4zjzkk+pa/5VL/hM1TOlF1HwEAAP//7z/Hg3YCAAA=", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -50,22 +50,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go index 43d8ca049e..644b5df861 100644 --- a/experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go @@ -31,7 +31,7 @@ func (s *Bar) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/3xSwY6bMBC98xVPS6VcmpBNbxz3sFJPvVTqNQYP2CvwuJ5hq/x9BSQ16UbLBfzmzXsz", "z5T4LjIRTs/HUw0zDD86/PHqEBNHSupJYBRiRsJA7zRgf1dKBN8HTmSLEk41Sl1VvVc3NYeWx4pN9PuW", "LfUU7g9+9pVqNi7KosQvRwEG0joaDZwRNKwO5+x2hgkW52XI8zyVOtpM9rUoF2QznqXOB7KwPlGrwwUc", @@ -42,9 +42,9 @@ var swaggerSpecJSON = []string{ "AwAA", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -61,22 +61,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_312/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_312/output/types.gen.go index f5f81dbf6e..b844e76c4f 100644 --- a/experimental/internal/codegen/test/issues/issue_312/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_312/output/types.gen.go @@ -43,7 +43,7 @@ func (s *Error) ApplyDefaults() { type ValidatePetsJSONResponse = []Pet // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/8SVTW/bPAzH7/oURPMAOdVOk5uOzzAMvQw5DLurMm2rtSVNpLsFw777IDmu7TpxsQ3D", "bjEpkb8/X5QN3BN1CIe7vYSj4hqQtPLGVmIDNbMnmeeV4bp7yLRrc6e8udWuwArt/MPEOJQf7vZiA+9q", "1E8EPjiP4SUkuBK84prgq+EayKM2qgFdq6A0YyBozBOCdo2zJJxHq7yRcHPIdtnuRhhbOikAnjGQcVbC", @@ -57,9 +57,9 @@ var swaggerSpecJSON = []string{ "6AcAAA==", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -76,22 +76,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_502/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_502/output/types.gen.go index cda70dd5ea..a459a0a12a 100644 --- a/experimental/internal/codegen/test/issues/issue_502/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_502/output/types.gen.go @@ -178,7 +178,7 @@ var ErrNullableIsNull = errors.New("nullable value is null") var ErrNullableNotSpecified = errors.New("nullable value is not specified") // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/5SRQWvcMBSE7/oVAy7kkqy3KbnoVnrqaS+BnrXys/1a+T0hPbcsIf+92NnueqEQcrNH", "M8ynUYPvtc6Ep/2jR5DToW9DSocef9hGqKQTVAiFetgpEwYSKsGogsWo9CHSy6trMJrl6tt2YBvn4y7q", "1GrI/BC1o4Hk9oeXyto+7R9d4xr8GEneuqEFb/VjqDftVEgi3YMNddQ5dRcS18DGjadbQe8halvGndNM", @@ -188,9 +188,9 @@ var swaggerSpecJSON = []string{ "AAA=", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -207,22 +207,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_52/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_52/output/types.gen.go index 72354dfa93..06e8ba7b45 100644 --- a/experimental/internal/codegen/test/issues/issue_52/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_52/output/types.gen.go @@ -37,7 +37,7 @@ func (s *Value) ApplyDefaults() { type ArrayValue = []Value // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/5xQTWvcMBC961c8sgWdarlbctEtUCihlJYeelfkWVupLQnNOHShP77Y3g/vJvQjOknz", "3rwPbXDPPBJutxbfyI+FwxNB9pkYnYtNH2KrNuhEMltj2iDd+FD5NJjkcnjrU0MtxctHmBTZ3G7VRm3w", "2f0g8FgI0jlBuTJxhRYjapBLylT6vUqZosvB4n1VV1sV4i5ZBTxR4ZCiha6runqnFSBBerKgn27IPSmg", @@ -47,9 +47,9 @@ var swaggerSpecJSON = []string{ "d3faWBLePVNYjGblwyQIDauA/9D9dwAAAP//+4PlsMkDAAA=", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -66,22 +66,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_579/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_579/output/types.gen.go index 071c05c0f4..3197d9cfaf 100644 --- a/experimental/internal/codegen/test/issues/issue_579/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_579/output/types.gen.go @@ -61,7 +61,7 @@ func (d *Date) UnmarshalText(data []byte) error { } // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/2SSz86bMBDE7zzFiPTamLaKovhWqZfc8gaVYzbgCryWd9M/b18Z+ET8fZzY0c74N+AD", "riJPwul8sfg+BSfUQ/8lEvwJOqJ3Snhwnp02B4yqSawxQ9DxeT96ng27FD577mmgWA+hBIs5nS8NJ4ou", "BYv227E7dm0T4oNtA/ymLIGjxZeiN4AGncjuUFASbZLTUcq+SZPzNPLUUy4zMJCuLwAnyk4Dx2tvi37b", @@ -70,9 +70,9 @@ var swaggerSpecJSON = []string{ "1JW1sv0PAAD//3OxuKeDAgAA", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -89,22 +89,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_697/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_697/output/types.gen.go index 1a342a9e92..be13202ea5 100644 --- a/experimental/internal/codegen/test/issues/issue_697/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_697/output/types.gen.go @@ -32,7 +32,7 @@ func (s *X) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/3yQwWrcMBCG73qKH1zIpbE3KSRExx4KPbWHQNvj2Jq1psiS0MwGSum7lyhZ3NKyt/Hv", "bzTfzICPqifG3cO9x+dWKjcTVmSmBkrp0xHUGLLm0ji4AdGsqp+mVSye5nEp21SoyvVSAq+c//6Q57d1", "unu4d4Mb8CVyBkGXyBshkmIuFs9jckDdBchgkaG0MRI/cXrrhp78wWgspxQwMyQv6RQ4QHKHVs7cyDjA", @@ -41,9 +41,9 @@ var swaggerSpecJSON = []string{ "pt1repWaus/VheF0cWjX+5eQbLxy+9+avwMAAP//FjslWWwCAAA=", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -60,22 +60,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_775/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_775/output/types.gen.go index 1cd65cb493..f709f7be1c 100644 --- a/experimental/internal/codegen/test/issues/issue_775/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_775/output/types.gen.go @@ -38,7 +38,7 @@ func (s *TestObjectDateProperty) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/6xQQW7bMBC88xUD61hYclEYAviDntxD+wBKWkvbSFyCu4phBPl7IFmOnXtunOHM7OwW", "+K06E+r66BHG8XTGD5wlT8GQAyspdm2IiGKYKPcEjq1MKRg3I21K3YFyluwKDGZJfVX1bMPclK1MlYTE", "+1Y66il+BbyM1qquj65wBf4px34rYYLQdQj3LgtGE5Rg10TQQeaxw0XyCy5sg8x2q6Clk0QxJPb4VR7K", @@ -46,9 +46,9 @@ var swaggerSpecJSON = []string{ "ysakdxEwz9z9ufHXB3u3q2WO/RO93uJZB+y3i/g16/OrC0bfFbxkuY8AAAD//xKKQGYZAgAA", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -65,22 +65,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_832/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_832/output/types.gen.go index d9dd314469..94d3f095c7 100644 --- a/experimental/internal/codegen/test/issues/issue_832/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_832/output/types.gen.go @@ -41,7 +41,7 @@ func (s *DocumentStatus) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/9RSPYvbQBDt9Sse54AaW1Lk5tg6EI4UKS5dCGFvPZb2sHaWnZFzgfz4IMkfkiHE7XWa", "maf3wb4VnkR6wuO2NnjbNLzR35E2wXaEPSdQ6DsMK8lWaFWjmLJsvLb9S+G4K9lGv3G8o4bCcvADr5SP", "2zrjSMFGb7AtqqLOfNizyYAjJfEcDPKqqIqPeQao1wMZ0Jvt4oEyYEfiko864v5kAPCNRG+t8pFS8ruZ", @@ -51,9 +51,9 @@ var swaggerSpecJSON = []string{ "7LlPPxaBnhfid8Y62kP/71x/AwAA//+qyWhEFgQAAA==", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -70,22 +70,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go index 3bbe71257a..8e35db9031 100644 --- a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go @@ -21,7 +21,7 @@ func (s *N3GPPFooJSONResponse) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/2yPsXLqMBBFe33FHdMbP+hUM48wKaDIDyj2Ii9xtBrtQiZ/nzGBsUmiStK5ujq7wE71", "TB77TCUYS8JuA7VQjFPEB1uPjiObW+CF1BTWB4PM0oqB3wjVens4/BepECmNlHAJA3fYCrijZHxkKuok", "UwqZPdZ1U68cp6N4B1yoKEvyqJq6qf9VDjC2gTyeKHTYjA6PkqOOy8F6Hd8v1zHn5VGuZUAk+95gct11", @@ -29,9 +29,9 @@ var swaggerSpecJSON = []string{ "C+YyjmA815zWJQxn+gvcW9UKp+i+AgAA//9y+0ZQ6gEAAA==", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -48,22 +48,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go index ac2d373dde..2b95537fbc 100644 --- a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go @@ -31,7 +31,7 @@ const ( type GetFooJSONResponse = []Bar // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/2SRQYujQBCF7/0rHpMFT6PO7M1jYAfCwu5l76ExFdOLVjVdlSz590urQU1u1veq3ie6", "w0H1Sg0OfU+d70F8HcB+IHU7/CE1nVArrObZ0BFT8haE8S/YBTefglwVdOoIrVdSJ5HYx9Dge1mXn84F", "PkvjAAvWr1Q/cu+vrBpFDrhR0iDcoC7rsnYuertovqzOMjYAHdn0AEicX+RwajL/EpmTRBqFlfSxCnzW", @@ -40,9 +40,9 @@ var swaggerSpecJSON = []string{ "iRxX89vHm/sfAAD//5W/OQySAgAA", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -59,22 +59,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } diff --git a/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go b/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go index 8e9cb4c44b..6da0efc8ad 100644 --- a/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go +++ b/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go @@ -350,7 +350,7 @@ func (s *AllOfWithOneOfAllOf1OneOf1) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ +var openAPISpecJSON = []string{ "H4sIAAAAAAAC/7yTQY/TMBCF7/kVT+W8K5bl5FvgTjggcXbTSTKQjC17WlQh/juKk9KkTVqBKnpq5nnG", "75uXOE9iPRtsXp9fnt9uMpbKmQxQ1pYMPlFU2iGv60C1VcIXipoBBwqRnRhsUpe32kSDn7+y0nXeCYnG", "fkosG+ps+gu8wUcbCS8GeQj2iB+sDawciwqs1MV0KElFlffloQ3QoycD2ytjJZ0/yRiGnB+Bp7EnamCp", @@ -360,9 +360,9 @@ var swaggerSpecJSON = []string{ "2z7cT+N3AAAA//8nzKKtgAUAAA==", } -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") +// decodeOpenAPISpec decodes and decompresses the embedded spec. +func decodeOpenAPISpec() ([]byte, error) { + joined := strings.Join(openAPISpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -379,22 +379,22 @@ func decodeSwaggerSpec() ([]byte, error) { return out.Bytes(), nil } -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { +// decodeOpenAPISpecCached returns a closure that caches the decoded spec. +func decodeOpenAPISpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() + cached, cachedErr = decodeOpenAPISpec() }) return cached, cachedErr } } -var swaggerSpec = decodeSwaggerSpecCached() +var openAPISpec = decodeOpenAPISpecCached() -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() +// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetOpenAPISpecJSON() ([]byte, error) { + return openAPISpec() } From af027b0c66fa15931366a346c462cd959aca73ea Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Thu, 5 Feb 2026 09:07:59 +0000 Subject: [PATCH 05/44] docs(sponsors): update section Thanks to Speakeasy and LivePeer for their support over the years! --- README.md | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/README.md b/README.md index b5ed0c8b98..ecf9a55e66 100644 --- a/README.md +++ b/README.md @@ -4478,7 +4478,7 @@ Please consider sponsoring us through GitHub Sponsors either [on the organisatio See [this blog post from Tidelift](https://blog.tidelift.com/paying-maintainers-the-howto) for more details on how to talk to your company about sponsoring maintainers of (Open Source) projects you depend on. -In addition, we are also generously sponsored by the following folks, each of whom provide sponsorship for 1 hour of work a month: +We are also generously sponsored by the following folks, each of whom provide sponsorship for 1 hour of work a month:

@@ -4490,30 +4490,10 @@ In addition, we are also generously sponsored by the following folks, each of wh

-

- - - - - Speakeasy logo - - -

-

Cybozu logo

-

- - - - - Livepeer logo - - -

- (Note that the order of appearance the order in which sponsorship was received) From 58bb3e67c25c1cf1d3207b7547504c5e465e6f11 Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Sat, 7 Feb 2026 09:02:51 +0000 Subject: [PATCH 06/44] Revert "Rename Swagger references to OpenAPI (#2202)" This reverts commit 17ba42e132420ffe0b67b15fafa4750ab144b1c5. --- .../examples/callback/treefarm.gen.go | 22 ++++++++--------- .../petstore-expanded/petstore.gen.go | 22 ++++++++--------- .../examples/webhook/doorbadge.gen.go | 22 ++++++++--------- experimental/internal/codegen/inline.go | 24 +++++++++---------- experimental/internal/codegen/inline_test.go | 18 +++++++------- .../templates/files/client/base.go.tmpl | 2 +- .../comprehensive/output/comprehensive.gen.go | 22 ++++++++--------- .../issues/issue_1029/output/types.gen.go | 22 ++++++++--------- .../issues/issue_1039/output/types.gen.go | 22 ++++++++--------- .../issues/issue_1397/output/types.gen.go | 22 ++++++++--------- .../issues/issue_1429/output/types.gen.go | 22 ++++++++--------- .../issues/issue_1496/output/types.gen.go | 22 ++++++++--------- .../issues/issue_1710/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_193/output/types.gen.go | 22 ++++++++--------- .../issues/issue_2102/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_312/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_502/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_52/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_579/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_697/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_775/output/types.gen.go | 22 ++++++++--------- .../test/issues/issue_832/output/types.gen.go | 22 ++++++++--------- .../output/types.gen.go | 22 ++++++++--------- .../output/types.gen.go | 22 ++++++++--------- .../output/nested_aggregate.gen.go | 22 ++++++++--------- 25 files changed, 264 insertions(+), 264 deletions(-) diff --git a/experimental/examples/callback/treefarm.gen.go b/experimental/examples/callback/treefarm.gen.go index 6d74b06f80..a1018c08e9 100644 --- a/experimental/examples/callback/treefarm.gen.go +++ b/experimental/examples/callback/treefarm.gen.go @@ -80,7 +80,7 @@ func (s *Error) ApplyDefaults() { type UUID = uuid.UUID // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/8RXwW7bOBC96ysGaQG3QCO76U23bJEFAhSbIHGQ44IWxxYbilTJUbzG7gL7EfuF+yUL", "khJFy4rr9NKcopDzZvhm5s1EN6hYIwo4+5R/zBdnmVBrXWQAz2is0KqAj/kiX2QAJEhiAUuDCL8yU2cA", "HG1pREP+3l8ZAMAlkLvQSKZIqA1YNM+iRKCKEXCstbJkGKGFmwbV5e01lEzKFSufbO4BlhVCKQUqAtuu", @@ -103,9 +103,9 @@ var openAPISpecJSON = []string{ "36d8LLPs/wAAAP//y9dX5mEQAAA=", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -122,24 +122,24 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } // ServerInterface represents all server handlers. diff --git a/experimental/examples/petstore-expanded/petstore.gen.go b/experimental/examples/petstore-expanded/petstore.gen.go index c958265c74..4149df2b00 100644 --- a/experimental/examples/petstore-expanded/petstore.gen.go +++ b/experimental/examples/petstore-expanded/petstore.gen.go @@ -46,7 +46,7 @@ func (s *Error) ApplyDefaults() { type FindPetsJSONResponse = []Pet // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/+xXXW9byQ19168gtgXyoly5yaIPemo2TgED3cStd/tOz6UkLubLQ44coe1/Lzj3Srqy", "ZG+CYvuF9YMNz3A4hzyHHN6UKWLmJXzztrvqrr6ZcVyl5QxgS0U4xSX8ztZnAMrqaQl3j7heU4FbUtFU", "aAbQk7jCWZv5OxAM2RO8u70B3aBCFRJAyOMBQAGMQJ8HM03QU0hRtKASrAi1FhLgCLoh+JQpmqe33RVI", @@ -77,9 +77,9 @@ var openAPISpecJSON = []string{ "5gbvFMOI4GujHzzt4f8zAAD//6ZcwVZEFgAA", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -96,22 +96,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/examples/webhook/doorbadge.gen.go b/experimental/examples/webhook/doorbadge.gen.go index b51b7d405f..702d74db62 100644 --- a/experimental/examples/webhook/doorbadge.gen.go +++ b/experimental/examples/webhook/doorbadge.gen.go @@ -72,7 +72,7 @@ const ( type UUID = uuid.UUID // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/9RWTY/bNhC961cM0gI+RfLu9qTbJuvDAkGycFL0TItjaxKJZIaUvEbb/16Q1GdX3l0D", "RZv6ZEnDmfceh4+jDSphKIc3N+lVun6TkNrrPAFokS1plcNVuk7XCYAjV2EOd1ozvBPygLBFIZETAIm2", "YDIuxP+RAADcgvSBuxDIIRAscksFgiuFA4m1VtaxcGjhk0F1+3APN+kVHHFXav3NpiHP+4pQOQuMB7IO", @@ -91,9 +91,9 @@ var openAPISpecJSON = []string{ "xUw9/L8CAAD//+XREy3yDQAA", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -110,24 +110,24 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } // ServerInterface represents all server handlers. diff --git a/experimental/internal/codegen/inline.go b/experimental/internal/codegen/inline.go index 1a9554f73e..25767cffa8 100644 --- a/experimental/internal/codegen/inline.go +++ b/experimental/internal/codegen/inline.go @@ -9,7 +9,7 @@ import ( ) // generateEmbeddedSpec produces Go code that embeds the raw OpenAPI spec as -// gzip+base64 encoded data, with a public GetOpenAPISpecJSON() function to +// gzip+base64 encoded data, with a public GetSwaggerSpecJSON() function to // retrieve the decompressed JSON bytes. func generateEmbeddedSpec(specData []byte) (string, error) { // Gzip compress @@ -43,15 +43,15 @@ func generateEmbeddedSpec(specData []byte) (string, error) { var b strings.Builder b.WriteString("// Base64-encoded, gzip-compressed OpenAPI spec.\n") - b.WriteString("var openAPISpecJSON = []string{\n") + b.WriteString("var swaggerSpecJSON = []string{\n") for _, chunk := range chunks { fmt.Fprintf(&b, "\t%q,\n", chunk) } b.WriteString("}\n\n") - b.WriteString("// decodeOpenAPISpec decodes and decompresses the embedded spec.\n") - b.WriteString("func decodeOpenAPISpec() ([]byte, error) {\n") - b.WriteString("\tjoined := strings.Join(openAPISpecJSON, \"\")\n") + b.WriteString("// decodeSwaggerSpec decodes and decompresses the embedded spec.\n") + b.WriteString("func decodeSwaggerSpec() ([]byte, error) {\n") + b.WriteString("\tjoined := strings.Join(swaggerSpecJSON, \"\")\n") b.WriteString("\traw, err := base64.StdEncoding.DecodeString(joined)\n") b.WriteString("\tif err != nil {\n") b.WriteString("\t\treturn nil, fmt.Errorf(\"decoding base64: %w\", err)\n") @@ -68,24 +68,24 @@ func generateEmbeddedSpec(specData []byte) (string, error) { b.WriteString("\treturn out.Bytes(), nil\n") b.WriteString("}\n\n") - b.WriteString("// decodeOpenAPISpecCached returns a closure that caches the decoded spec.\n") - b.WriteString("func decodeOpenAPISpecCached() func() ([]byte, error) {\n") + b.WriteString("// decodeSwaggerSpecCached returns a closure that caches the decoded spec.\n") + b.WriteString("func decodeSwaggerSpecCached() func() ([]byte, error) {\n") b.WriteString("\tvar cached []byte\n") b.WriteString("\tvar cachedErr error\n") b.WriteString("\tvar once sync.Once\n") b.WriteString("\treturn func() ([]byte, error) {\n") b.WriteString("\t\tonce.Do(func() {\n") - b.WriteString("\t\t\tcached, cachedErr = decodeOpenAPISpec()\n") + b.WriteString("\t\t\tcached, cachedErr = decodeSwaggerSpec()\n") b.WriteString("\t\t})\n") b.WriteString("\t\treturn cached, cachedErr\n") b.WriteString("\t}\n") b.WriteString("}\n\n") - b.WriteString("var openAPISpec = decodeOpenAPISpecCached()\n\n") + b.WriteString("var swaggerSpec = decodeSwaggerSpecCached()\n\n") - b.WriteString("// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes.\n") - b.WriteString("func GetOpenAPISpecJSON() ([]byte, error) {\n") - b.WriteString("\treturn openAPISpec()\n") + b.WriteString("// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes.\n") + b.WriteString("func GetSwaggerSpecJSON() ([]byte, error) {\n") + b.WriteString("\treturn swaggerSpec()\n") b.WriteString("}\n") return b.String(), nil diff --git a/experimental/internal/codegen/inline_test.go b/experimental/internal/codegen/inline_test.go index 233aab2e79..ae35938493 100644 --- a/experimental/internal/codegen/inline_test.go +++ b/experimental/internal/codegen/inline_test.go @@ -19,19 +19,19 @@ func TestGenerateEmbeddedSpec(t *testing.T) { require.NoError(t, err) // Should contain the chunked base64 variable - assert.Contains(t, code, "var openAPISpecJSON = []string{") + assert.Contains(t, code, "var swaggerSpecJSON = []string{") // Should contain the decode function - assert.Contains(t, code, "func decodeOpenAPISpec() ([]byte, error)") + assert.Contains(t, code, "func decodeSwaggerSpec() ([]byte, error)") // Should contain the cached decode function - assert.Contains(t, code, "func decodeOpenAPISpecCached() func() ([]byte, error)") + assert.Contains(t, code, "func decodeSwaggerSpecCached() func() ([]byte, error)") // Should contain the public API - assert.Contains(t, code, "func GetOpenAPISpecJSON() ([]byte, error)") + assert.Contains(t, code, "func GetSwaggerSpecJSON() ([]byte, error)") // Should contain the cached var - assert.Contains(t, code, "var openAPISpec = decodeOpenAPISpecCached()") + assert.Contains(t, code, "var swaggerSpec = decodeSwaggerSpecCached()") } func TestGenerateEmbeddedSpecRoundTrip(t *testing.T) { @@ -101,8 +101,8 @@ components: assert.Contains(t, code, "type Pet struct") // Should contain the embedded spec - assert.Contains(t, code, "GetOpenAPISpecJSON") - assert.Contains(t, code, "openAPISpecJSON") + assert.Contains(t, code, "GetSwaggerSpecJSON") + assert.Contains(t, code, "swaggerSpecJSON") } func TestGenerateWithNilSpecData(t *testing.T) { @@ -134,6 +134,6 @@ components: assert.Contains(t, code, "type Pet struct") // Should NOT contain the embedded spec - assert.NotContains(t, code, "GetOpenAPISpecJSON") - assert.NotContains(t, code, "openAPISpecJSON") + assert.NotContains(t, code, "GetSwaggerSpecJSON") + assert.NotContains(t, code, "swaggerSpecJSON") } diff --git a/experimental/internal/codegen/templates/files/client/base.go.tmpl b/experimental/internal/codegen/templates/files/client/base.go.tmpl index 04aebaf713..96d32997f4 100644 --- a/experimental/internal/codegen/templates/files/client/base.go.tmpl +++ b/experimental/internal/codegen/templates/files/client/base.go.tmpl @@ -14,7 +14,7 @@ type Client struct { // The endpoint of the server conforming to this interface, with scheme, // https://api.deepmap.com for example. This can contain a path relative // to the server, such as https://api.deepmap.com/dev-test, and all the - // paths in the OpenAPI spec will be appended to the server. + // paths in the swagger spec will be appended to the server. Server string // Doer for performing requests, typically a *http.Client with any diff --git a/experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go b/experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go index 04a06f9db5..633aa2dbe7 100644 --- a/experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go +++ b/experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go @@ -2009,7 +2009,7 @@ var ErrNullableNotSpecified = errors.New("nullable value is not specified") type UUID = uuid.UUID // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/+wcXW8iOfKdX2Gx+3Zi8jGz84B0D4QwEnsEEJCdi1Z7J4cuEu90u3vcJgm32v9+st10", "tz/6y2RGs6PNEynb9eVyVblcECdAcUKGqP/2zcWb836P0F087CHECQ9hiMZxlDB4BJqSJ0AbSDlabx8h", "wj2EnoClJKZD1L94cy7WIhRAumUk4RIsZqcIhyFK5RKUYM6B0RTtYoZinJDBNg7gAWivl2D+mAq6ZymJ", @@ -2058,9 +2058,9 @@ var openAPISpecJSON = []string{ "wdmxH1N53dOFMHa0aR81yf8fAAD//ynhbdUwSwAA", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -2077,22 +2077,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go index a2e22ce566..7c5a5d2fa1 100644 --- a/experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go @@ -137,7 +137,7 @@ const ( ) // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/6yQzY7UMBCE736KknJlMrPsibwBJySExNmb1CQNjm2527MaId4dOeFnZw4Iob25q7vr", "a1eH96qVeDi9fTcgRX44Q61InMFYV8XZS2iVJcyMLN6IXFJmCVfXYTHLOhyPs9hSn/oxrcfksxzGNHFm", "vC2kofTYWK5zHT4vjKja7Hfys9iCtQaTHIjWCDxcfKi8OeqN62ALfx80oRGgS6qhvdcsgfBxwnMqXzGm", @@ -146,9 +146,9 @@ var openAPISpecJSON = []string{ "vDLjdD/9X0Z+NLnwbz4/AgAA//+mSD78zgIAAA==", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -165,22 +165,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go index 8077efc300..d996cfb752 100644 --- a/experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go @@ -238,7 +238,7 @@ var ErrNullableIsNull = errors.New("nullable value is null") var ErrNullableNotSpecified = errors.New("nullable value is not specified") // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/7RUTY/TMBC9+1eMEqReaNNlT+QGnFYIdgXcKzeZJl5S23gmqJX48SjfSZu0ge7eEnvm", "vTdvxuPDA1GOcLe+fx/C1zzL5DZD4KNFggQ1OsnKaOFDymwpDIJEcZpvV5HZB0ZatYxMjAnq4Y8qQCko", "UIUvfPgifyJQ7hA4lQx6yCMdNlwYg3XGosuOwljU0qoQ7lfr1Z1QemdCAfAbHSmjQ1isi/OFAGDFGYaA", @@ -251,9 +251,9 @@ var openAPISpecJSON = []string{ "wsAUMWu+r+32upa3F5/AWDEyU5I2/1DSlXK649EGvLR7o1R/AwAA///zkywGmQkAAA==", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -270,22 +270,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go index 8b54637dde..7fd6bbf827 100644 --- a/experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go @@ -63,7 +63,7 @@ func (s *TestField3) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/9xUzZKbMAy+8xSapFdCIO106lv30Jkc2kOnL+AYBbwDltcS283bd2zYAJt0Z3otJ/xZ", "P58+WdrCkXlAKA9fPit40GwNyMUjNOgwaLHk4LeVFl7yhvJ4kzvdY7aFVsSzKorGSjucdob6grS3uaEa", "G3Trg41JuIhZMvLotLcKNofdflduMuvOpDIAsdKhWhCCX8iSATxjYEtOQbnb7/aZod6TQyccvdi02Ov0", @@ -74,9 +74,9 @@ var openAPISpecJSON = []string{ "sy3mLVJMK6SI1DdXDuzJ8bIz1f7jX1vfENXZnwAAAP//PTfE300FAAA=", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -93,22 +93,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go index 5cbd404ef6..993992b0d7 100644 --- a/experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go @@ -100,7 +100,7 @@ const ( ) // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/6xSsW7cMAzd9RUPcYEsvfMl7VJtHTN1KdBZtmlLwZkURLrF/X0hOwdfrh3DiXqkHt8T", "1eBFdSE8fX3+5kG8zGAxTMRUgtGAP5EYi9YsWUTgy4/RNYhmWX3bTsni0h17mVsJOR16GWgifn9IdYS2", "dYZrXINflTMgF8lU7ILEmgZC4I0eM80dFcSgFauiPsMibfLsksk10CjLeUBHu9ajk0wccvL4cjwdTy7x", @@ -109,9 +109,9 @@ var openAPISpecJSON = []string{ "dQP7yMN1S+nWXo0x0Xn4fi9uY1QriaePJdmifoF/H+SAUeQ/aBeK+xsAAP//dJpKJ+ICAAA=", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -128,22 +128,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go index 6e3f8a4fa6..2f95e2dbf8 100644 --- a/experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go @@ -117,7 +117,7 @@ func (s *GetSomething200ResponseJSONAnyOf12) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/7xTTW+bQBC98yue4ksrJeC0VaVy66nyKVJTqcdoDWOYyMxud4ZY/vfVgqlJncTJJZyY", "fTP7PhgWWKn2hOsv376W+C5e9p3vFX59T5VBq5Y6hx1bCy90s0FDQtEZKVge3JZrcE1ivGGK2QKtWdCy", "KBq2tl/nle8K7wJfVb6mhuRxwYlai8SdLbIFfrckcAKWLQtN5CxwiKTBixJ6JYWT/c2mGARdwlr6p6qG", @@ -128,9 +128,9 @@ var openAPISpecJSON = []string{ "b+Ed17Om810T/fgrn23vSNU1b7j3kPjfAAAA///mDrwBGwUAAA==", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -147,22 +147,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go index 92bee3e2c8..6efb31f3a5 100644 --- a/experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go @@ -161,7 +161,7 @@ const ( ) // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/9SUT4/TMBDF7/kUIwWpFzZpxQEpRzhxQMuhEmc3eYmNEtt4JvtHiO+O4qYbh2132RVC", "oqfO68zzm5+b5PSJeQTt3u+2FbUGfUO9YyFjyYIFDam+v27JWVy3WU5axHNVlp0RPR6K2g2lU95c1a5B", "B7suzOTN5WSe5VlOXzUsjWxstza/NaKPJ7ydlGMOJtZu7Bs6gHwAI9ygKbKc9hq08cENXjZz5FvFdMDk", @@ -172,9 +172,9 @@ var openAPISpecJSON = []string{ "6CmTS9yXNf97QoK7lxBaXkt/TOhXAAAA//+WRkFKuQYAAA==", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -191,22 +191,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_193/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_193/output/types.gen.go index 39adf0b5c2..33a950f497 100644 --- a/experimental/internal/codegen/test/issues/issue_193/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_193/output/types.gen.go @@ -23,7 +23,7 @@ func (s *Person) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/6yQvW7jQAyE+32KAVzbsuHK21151fkV1itK4kG73FtSFwRB3j2QHP8BMZAiHTn4SM5w", "hd+qE2F32Hv8Gsc/HV7YBoS2ZWPJYTxWKVSNSZGo9px7t8JgVtQ3Tc82TKdNlNRIKLyO0lJP+bHh+YI2", "u8PeSaEcCnvsN9vN1nHuxDvA2EbyMFKDxoFScMB/qsqSPXYLW4IN6vH27qKkIpmy6Tx75pcSOFJVyeca", @@ -31,9 +31,9 @@ var openAPISpecJSON = []string{ "LbU6P/2WoiwhoYUidxx/Ns4zjzkk+pa/5VL/hM1TOlF1HwEAAP//7z/Hg3YCAAA=", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -50,22 +50,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go index 644b5df861..43d8ca049e 100644 --- a/experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go @@ -31,7 +31,7 @@ func (s *Bar) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/3xSwY6bMBC98xVPS6VcmpBNbxz3sFJPvVTqNQYP2CvwuJ5hq/x9BSQ16UbLBfzmzXsz", "z5T4LjIRTs/HUw0zDD86/PHqEBNHSupJYBRiRsJA7zRgf1dKBN8HTmSLEk41Sl1VvVc3NYeWx4pN9PuW", "LfUU7g9+9pVqNi7KosQvRwEG0joaDZwRNKwO5+x2hgkW52XI8zyVOtpM9rUoF2QznqXOB7KwPlGrwwUc", @@ -42,9 +42,9 @@ var openAPISpecJSON = []string{ "AwAA", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -61,22 +61,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_312/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_312/output/types.gen.go index b844e76c4f..f5f81dbf6e 100644 --- a/experimental/internal/codegen/test/issues/issue_312/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_312/output/types.gen.go @@ -43,7 +43,7 @@ func (s *Error) ApplyDefaults() { type ValidatePetsJSONResponse = []Pet // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/8SVTW/bPAzH7/oURPMAOdVOk5uOzzAMvQw5DLurMm2rtSVNpLsFw777IDmu7TpxsQ3D", "bjEpkb8/X5QN3BN1CIe7vYSj4hqQtPLGVmIDNbMnmeeV4bp7yLRrc6e8udWuwArt/MPEOJQf7vZiA+9q", "1E8EPjiP4SUkuBK84prgq+EayKM2qgFdq6A0YyBozBOCdo2zJJxHq7yRcHPIdtnuRhhbOikAnjGQcVbC", @@ -57,9 +57,9 @@ var openAPISpecJSON = []string{ "6AcAAA==", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -76,22 +76,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_502/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_502/output/types.gen.go index a459a0a12a..cda70dd5ea 100644 --- a/experimental/internal/codegen/test/issues/issue_502/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_502/output/types.gen.go @@ -178,7 +178,7 @@ var ErrNullableIsNull = errors.New("nullable value is null") var ErrNullableNotSpecified = errors.New("nullable value is not specified") // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/5SRQWvcMBSE7/oVAy7kkqy3KbnoVnrqaS+BnrXys/1a+T0hPbcsIf+92NnueqEQcrNH", "M8ynUYPvtc6Ep/2jR5DToW9DSocef9hGqKQTVAiFetgpEwYSKsGogsWo9CHSy6trMJrl6tt2YBvn4y7q", "1GrI/BC1o4Hk9oeXyto+7R9d4xr8GEneuqEFb/VjqDftVEgi3YMNddQ5dRcS18DGjadbQe8halvGndNM", @@ -188,9 +188,9 @@ var openAPISpecJSON = []string{ "AAA=", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -207,22 +207,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_52/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_52/output/types.gen.go index 06e8ba7b45..72354dfa93 100644 --- a/experimental/internal/codegen/test/issues/issue_52/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_52/output/types.gen.go @@ -37,7 +37,7 @@ func (s *Value) ApplyDefaults() { type ArrayValue = []Value // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/5xQTWvcMBC961c8sgWdarlbctEtUCihlJYeelfkWVupLQnNOHShP77Y3g/vJvQjOknz", "3rwPbXDPPBJutxbfyI+FwxNB9pkYnYtNH2KrNuhEMltj2iDd+FD5NJjkcnjrU0MtxctHmBTZ3G7VRm3w", "2f0g8FgI0jlBuTJxhRYjapBLylT6vUqZosvB4n1VV1sV4i5ZBTxR4ZCiha6runqnFSBBerKgn27IPSmg", @@ -47,9 +47,9 @@ var openAPISpecJSON = []string{ "d3faWBLePVNYjGblwyQIDauA/9D9dwAAAP//+4PlsMkDAAA=", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -66,22 +66,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_579/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_579/output/types.gen.go index 3197d9cfaf..071c05c0f4 100644 --- a/experimental/internal/codegen/test/issues/issue_579/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_579/output/types.gen.go @@ -61,7 +61,7 @@ func (d *Date) UnmarshalText(data []byte) error { } // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/2SSz86bMBDE7zzFiPTamLaKovhWqZfc8gaVYzbgCryWd9M/b18Z+ET8fZzY0c74N+AD", "riJPwul8sfg+BSfUQ/8lEvwJOqJ3Snhwnp02B4yqSawxQ9DxeT96ng27FD577mmgWA+hBIs5nS8NJ4ou", "BYv227E7dm0T4oNtA/ymLIGjxZeiN4AGncjuUFASbZLTUcq+SZPzNPLUUy4zMJCuLwAnyk4Dx2tvi37b", @@ -70,9 +70,9 @@ var openAPISpecJSON = []string{ "1JW1sv0PAAD//3OxuKeDAgAA", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -89,22 +89,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_697/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_697/output/types.gen.go index be13202ea5..1a342a9e92 100644 --- a/experimental/internal/codegen/test/issues/issue_697/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_697/output/types.gen.go @@ -32,7 +32,7 @@ func (s *X) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/3yQwWrcMBCG73qKH1zIpbE3KSRExx4KPbWHQNvj2Jq1psiS0MwGSum7lyhZ3NKyt/Hv", "bzTfzICPqifG3cO9x+dWKjcTVmSmBkrp0xHUGLLm0ji4AdGsqp+mVSye5nEp21SoyvVSAq+c//6Q57d1", "unu4d4Mb8CVyBkGXyBshkmIuFs9jckDdBchgkaG0MRI/cXrrhp78wWgspxQwMyQv6RQ4QHKHVs7cyDjA", @@ -41,9 +41,9 @@ var openAPISpecJSON = []string{ "pt1repWaus/VheF0cWjX+5eQbLxy+9+avwMAAP//FjslWWwCAAA=", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -60,22 +60,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_775/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_775/output/types.gen.go index f709f7be1c..1cd65cb493 100644 --- a/experimental/internal/codegen/test/issues/issue_775/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_775/output/types.gen.go @@ -38,7 +38,7 @@ func (s *TestObjectDateProperty) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/6xQQW7bMBC88xUD61hYclEYAviDntxD+wBKWkvbSFyCu4phBPl7IFmOnXtunOHM7OwW", "+K06E+r66BHG8XTGD5wlT8GQAyspdm2IiGKYKPcEjq1MKRg3I21K3YFyluwKDGZJfVX1bMPclK1MlYTE", "+1Y66il+BbyM1qquj65wBf4px34rYYLQdQj3LgtGE5Rg10TQQeaxw0XyCy5sg8x2q6Clk0QxJPb4VR7K", @@ -46,9 +46,9 @@ var openAPISpecJSON = []string{ "ysakdxEwz9z9ufHXB3u3q2WO/RO93uJZB+y3i/g16/OrC0bfFbxkuY8AAAD//xKKQGYZAgAA", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -65,22 +65,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_832/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_832/output/types.gen.go index 94d3f095c7..d9dd314469 100644 --- a/experimental/internal/codegen/test/issues/issue_832/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_832/output/types.gen.go @@ -41,7 +41,7 @@ func (s *DocumentStatus) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/9RSPYvbQBDt9Sse54AaW1Lk5tg6EI4UKS5dCGFvPZb2sHaWnZFzgfz4IMkfkiHE7XWa", "maf3wb4VnkR6wuO2NnjbNLzR35E2wXaEPSdQ6DsMK8lWaFWjmLJsvLb9S+G4K9lGv3G8o4bCcvADr5SP", "2zrjSMFGb7AtqqLOfNizyYAjJfEcDPKqqIqPeQao1wMZ0Jvt4oEyYEfiko864v5kAPCNRG+t8pFS8ruZ", @@ -51,9 +51,9 @@ var openAPISpecJSON = []string{ "7LlPPxaBnhfid8Y62kP/71x/AwAA//+qyWhEFgQAAA==", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -70,22 +70,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go index 8e35db9031..3bbe71257a 100644 --- a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go @@ -21,7 +21,7 @@ func (s *N3GPPFooJSONResponse) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/2yPsXLqMBBFe33FHdMbP+hUM48wKaDIDyj2Ii9xtBrtQiZ/nzGBsUmiStK5ujq7wE71", "TB77TCUYS8JuA7VQjFPEB1uPjiObW+CF1BTWB4PM0oqB3wjVens4/BepECmNlHAJA3fYCrijZHxkKuok", "UwqZPdZ1U68cp6N4B1yoKEvyqJq6qf9VDjC2gTyeKHTYjA6PkqOOy8F6Hd8v1zHn5VGuZUAk+95gct11", @@ -29,9 +29,9 @@ var openAPISpecJSON = []string{ "C+YyjmA815zWJQxn+gvcW9UKp+i+AgAA//9y+0ZQ6gEAAA==", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -48,22 +48,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go index 2b95537fbc..ac2d373dde 100644 --- a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go +++ b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go @@ -31,7 +31,7 @@ const ( type GetFooJSONResponse = []Bar // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/2SRQYujQBCF7/0rHpMFT6PO7M1jYAfCwu5l76ExFdOLVjVdlSz590urQU1u1veq3ie6", "w0H1Sg0OfU+d70F8HcB+IHU7/CE1nVArrObZ0BFT8haE8S/YBTefglwVdOoIrVdSJ5HYx9Dge1mXn84F", "PkvjAAvWr1Q/cu+vrBpFDrhR0iDcoC7rsnYuertovqzOMjYAHdn0AEicX+RwajL/EpmTRBqFlfSxCnzW", @@ -40,9 +40,9 @@ var openAPISpecJSON = []string{ "iRxX89vHm/sfAAD//5W/OQySAgAA", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -59,22 +59,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } diff --git a/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go b/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go index 6da0efc8ad..8e9cb4c44b 100644 --- a/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go +++ b/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go @@ -350,7 +350,7 @@ func (s *AllOfWithOneOfAllOf1OneOf1) ApplyDefaults() { } // Base64-encoded, gzip-compressed OpenAPI spec. -var openAPISpecJSON = []string{ +var swaggerSpecJSON = []string{ "H4sIAAAAAAAC/7yTQY/TMBCF7/kVT+W8K5bl5FvgTjggcXbTSTKQjC17WlQh/juKk9KkTVqBKnpq5nnG", "75uXOE9iPRtsXp9fnt9uMpbKmQxQ1pYMPlFU2iGv60C1VcIXipoBBwqRnRhsUpe32kSDn7+y0nXeCYnG", "fkosG+ps+gu8wUcbCS8GeQj2iB+sDawciwqs1MV0KElFlffloQ3QoycD2ytjJZ0/yRiGnB+Bp7EnamCp", @@ -360,9 +360,9 @@ var openAPISpecJSON = []string{ "2z7cT+N3AAAA//8nzKKtgAUAAA==", } -// decodeOpenAPISpec decodes and decompresses the embedded spec. -func decodeOpenAPISpec() ([]byte, error) { - joined := strings.Join(openAPISpecJSON, "") +// decodeSwaggerSpec decodes and decompresses the embedded spec. +func decodeSwaggerSpec() ([]byte, error) { + joined := strings.Join(swaggerSpecJSON, "") raw, err := base64.StdEncoding.DecodeString(joined) if err != nil { return nil, fmt.Errorf("decoding base64: %w", err) @@ -379,22 +379,22 @@ func decodeOpenAPISpec() ([]byte, error) { return out.Bytes(), nil } -// decodeOpenAPISpecCached returns a closure that caches the decoded spec. -func decodeOpenAPISpecCached() func() ([]byte, error) { +// decodeSwaggerSpecCached returns a closure that caches the decoded spec. +func decodeSwaggerSpecCached() func() ([]byte, error) { var cached []byte var cachedErr error var once sync.Once return func() ([]byte, error) { once.Do(func() { - cached, cachedErr = decodeOpenAPISpec() + cached, cachedErr = decodeSwaggerSpec() }) return cached, cachedErr } } -var openAPISpec = decodeOpenAPISpecCached() +var swaggerSpec = decodeSwaggerSpecCached() -// GetOpenAPISpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetOpenAPISpecJSON() ([]byte, error) { - return openAPISpec() +// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. +func GetSwaggerSpecJSON() ([]byte, error) { + return swaggerSpec() } From a5d982065da7a87d781fa84dd97d00462a56ca9a Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Sat, 7 Feb 2026 09:02:52 +0000 Subject: [PATCH 07/44] Revert "Add output filtering (#2201)" This reverts commit db8abb866c5b99d169290020eb23416c3c5c79fc. --- experimental/Configuration.md | 20 -- experimental/cmd/oapi-codegen/main.go | 46 +-- experimental/internal/codegen/codegen.go | 9 - .../internal/codegen/configuration.go | 16 - experimental/internal/codegen/filter.go | 97 ------ experimental/internal/codegen/filter_test.go | 323 ------------------ 6 files changed, 5 insertions(+), 506 deletions(-) delete mode 100644 experimental/internal/codegen/filter.go delete mode 100644 experimental/internal/codegen/filter_test.go diff --git a/experimental/Configuration.md b/experimental/Configuration.md index db2d3ed508..55f550e2f3 100644 --- a/experimental/Configuration.md +++ b/experimental/Configuration.md @@ -37,26 +37,6 @@ generation: path: github.com/org/project/models alias: models # optional, defaults to last segment of path -# Output options: control which operations and schemas are included. -output-options: - # Only include operations tagged with one of these tags. Ignored when empty. - include-tags: - - public - - beta - # Exclude operations tagged with one of these tags. Ignored when empty. - exclude-tags: - - internal - # Only include operations with one of these operation IDs. Ignored when empty. - include-operation-ids: - - listPets - - createPet - # Exclude operations with one of these operation IDs. Ignored when empty. - exclude-operation-ids: - - deprecatedEndpoint - # Exclude schemas with the given names from generation. Ignored when empty. - exclude-schemas: - - InternalConfig - # Type mappings: OpenAPI type/format to Go type. # User values are merged on top of defaults — you only need to specify overrides. type-mapping: diff --git a/experimental/cmd/oapi-codegen/main.go b/experimental/cmd/oapi-codegen/main.go index 50205e846c..24c5320c90 100644 --- a/experimental/cmd/oapi-codegen/main.go +++ b/experimental/cmd/oapi-codegen/main.go @@ -3,9 +3,6 @@ package main import ( "flag" "fmt" - "io" - "net/http" - "net/url" "os" "path/filepath" "strings" @@ -23,9 +20,9 @@ func main() { flagPackage := flag.String("package", "", "Go package name for generated code") flagOutput := flag.String("output", "", "output file path (default: .gen.go)") flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage: %s [options] \n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage: %s [options] \n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Arguments:\n") - fmt.Fprintf(os.Stderr, " spec-path-or-url path or URL to OpenAPI spec file\n\n") + fmt.Fprintf(os.Stderr, " spec-path path to OpenAPI spec file\n\n") fmt.Fprintf(os.Stderr, "Options:\n") flag.PrintDefaults() } @@ -38,8 +35,8 @@ func main() { specPath := flag.Arg(0) - // Load the OpenAPI spec from file or URL - specData, err := loadSpec(specPath) + // Parse the OpenAPI spec + specData, err := os.ReadFile(specPath) if err != nil { fmt.Fprintf(os.Stderr, "error reading spec: %v\n", err) os.Exit(1) @@ -81,12 +78,7 @@ func main() { // Default output to .gen.go if cfg.Output == "" { - // For URLs, extract the filename from the URL path - baseName := specPath - if u, err := url.Parse(specPath); err == nil && u.Scheme != "" && u.Host != "" { - baseName = u.Path - } - base := filepath.Base(baseName) + base := filepath.Base(specPath) ext := filepath.Ext(base) cfg.Output = strings.TrimSuffix(base, ext) + ".gen.go" } @@ -111,31 +103,3 @@ func main() { fmt.Printf("Generated %s\n", cfg.Output) } - -// loadSpec loads an OpenAPI spec from a file path or URL. -func loadSpec(specPath string) ([]byte, error) { - u, err := url.Parse(specPath) - if err == nil && u.Scheme != "" && u.Host != "" { - return loadSpecFromURL(u.String()) - } - return os.ReadFile(specPath) -} - -// loadSpecFromURL fetches an OpenAPI spec from an HTTP(S) URL. -func loadSpecFromURL(specURL string) ([]byte, error) { - resp, err := http.Get(specURL) //nolint:gosec // URL comes from user-provided spec path - if err != nil { - return nil, fmt.Errorf("fetching spec from URL: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("fetching spec from URL: HTTP %d %s", resp.StatusCode, resp.Status) - } - - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("reading spec from URL: %w", err) - } - return data, nil -} diff --git a/experimental/internal/codegen/codegen.go b/experimental/internal/codegen/codegen.go index 7ff2175067..5b6fdff894 100644 --- a/experimental/internal/codegen/codegen.go +++ b/experimental/internal/codegen/codegen.go @@ -31,9 +31,6 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri return "", fmt.Errorf("gathering schemas: %w", err) } - // Filter excluded schemas - schemas = FilterSchemasByName(schemas, cfg.OutputOptions.ExcludeSchemas) - // Pass 2: Compute names for all schemas converter := NewNameConverter(cfg.NameMangling, cfg.NameSubstitutions) ComputeSchemaNames(schemas, converter, contentTypeNamer) @@ -105,9 +102,6 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri return "", fmt.Errorf("gathering operations: %w", err) } - // Apply operation filters - ops = FilterOperations(ops, cfg.OutputOptions) - // Generate client clientGen, err := NewClientGenerator(schemaIndex, cfg.Generation.SimpleClient, cfg.Generation.ModelsPackage) if err != nil { @@ -164,9 +158,6 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri return "", fmt.Errorf("gathering operations: %w", err) } - // Apply operation filters - ops = FilterOperations(ops, cfg.OutputOptions) - if len(ops) > 0 { // Generate server serverGen, err := NewServerGenerator(cfg.Generation.Server) diff --git a/experimental/internal/codegen/configuration.go b/experimental/internal/codegen/configuration.go index 0dc128f9ea..c8d1b87388 100644 --- a/experimental/internal/codegen/configuration.go +++ b/experimental/internal/codegen/configuration.go @@ -15,8 +15,6 @@ type Configuration struct { Output string `yaml:"output"` // Generation controls which parts of the code are generated Generation GenerationOptions `yaml:"generation,omitempty"` - // OutputOptions controls filtering of operations and schemas - OutputOptions OutputOptions `yaml:"output-options,omitempty"` // TypeMapping allows customizing OpenAPI type/format to Go type mappings TypeMapping TypeMapping `yaml:"type-mapping,omitempty"` // NameMangling configures how OpenAPI names are converted to Go identifiers @@ -40,20 +38,6 @@ type Configuration struct { StructTags StructTagsConfig `yaml:"struct-tags,omitempty"` } -// OutputOptions controls filtering of which operations and schemas are included in generation. -type OutputOptions struct { - // IncludeTags only includes operations tagged with one of these tags. Ignored when empty. - IncludeTags []string `yaml:"include-tags,omitempty"` - // ExcludeTags excludes operations tagged with one of these tags. Ignored when empty. - ExcludeTags []string `yaml:"exclude-tags,omitempty"` - // IncludeOperationIDs only includes operations with one of these operation IDs. Ignored when empty. - IncludeOperationIDs []string `yaml:"include-operation-ids,omitempty"` - // ExcludeOperationIDs excludes operations with one of these operation IDs. Ignored when empty. - ExcludeOperationIDs []string `yaml:"exclude-operation-ids,omitempty"` - // ExcludeSchemas excludes schemas with the given names from generation. Ignored when empty. - ExcludeSchemas []string `yaml:"exclude-schemas,omitempty"` -} - // ModelsPackage specifies an external package containing the model types. type ModelsPackage struct { // Path is the import path for the models package (e.g., "github.com/org/project/models") diff --git a/experimental/internal/codegen/filter.go b/experimental/internal/codegen/filter.go deleted file mode 100644 index 8e20bed5af..0000000000 --- a/experimental/internal/codegen/filter.go +++ /dev/null @@ -1,97 +0,0 @@ -package codegen - -// FilterOperationsByTag filters operations based on include/exclude tag lists. -// Exclude is applied first, then include. -func FilterOperationsByTag(ops []*OperationDescriptor, opts OutputOptions) []*OperationDescriptor { - if len(opts.ExcludeTags) > 0 { - tags := sliceToSet(opts.ExcludeTags) - ops = filterOps(ops, func(op *OperationDescriptor) bool { - return !operationHasTag(op, tags) - }) - } - if len(opts.IncludeTags) > 0 { - tags := sliceToSet(opts.IncludeTags) - ops = filterOps(ops, func(op *OperationDescriptor) bool { - return operationHasTag(op, tags) - }) - } - return ops -} - -// FilterOperationsByOperationID filters operations based on include/exclude operation ID lists. -// Exclude is applied first, then include. -func FilterOperationsByOperationID(ops []*OperationDescriptor, opts OutputOptions) []*OperationDescriptor { - if len(opts.ExcludeOperationIDs) > 0 { - ids := sliceToSet(opts.ExcludeOperationIDs) - ops = filterOps(ops, func(op *OperationDescriptor) bool { - return !ids[op.OperationID] - }) - } - if len(opts.IncludeOperationIDs) > 0 { - ids := sliceToSet(opts.IncludeOperationIDs) - ops = filterOps(ops, func(op *OperationDescriptor) bool { - return ids[op.OperationID] - }) - } - return ops -} - -// FilterOperations applies all operation filters (tags, operation IDs) from OutputOptions. -func FilterOperations(ops []*OperationDescriptor, opts OutputOptions) []*OperationDescriptor { - ops = FilterOperationsByTag(ops, opts) - ops = FilterOperationsByOperationID(ops, opts) - return ops -} - -// FilterSchemasByName removes schemas whose component name is in the exclude list. -// Only filters top-level component schemas (path: components/schemas/). -func FilterSchemasByName(schemas []*SchemaDescriptor, excludeNames []string) []*SchemaDescriptor { - if len(excludeNames) == 0 { - return schemas - } - excluded := sliceToSet(excludeNames) - result := make([]*SchemaDescriptor, 0, len(schemas)) - for _, s := range schemas { - // Check if this is a top-level component schema - if len(s.Path) == 3 && s.Path[0] == "components" && s.Path[1] == "schemas" { - if excluded[s.Path[2]] { - continue - } - } - result = append(result, s) - } - return result -} - -// operationHasTag returns true if the operation has any of the given tags. -func operationHasTag(op *OperationDescriptor, tags map[string]bool) bool { - if op == nil || op.Spec == nil { - return false - } - for _, tag := range op.Spec.Tags { - if tags[tag] { - return true - } - } - return false -} - -// filterOps returns operations that satisfy the predicate. -func filterOps(ops []*OperationDescriptor, keep func(*OperationDescriptor) bool) []*OperationDescriptor { - result := make([]*OperationDescriptor, 0, len(ops)) - for _, op := range ops { - if keep(op) { - result = append(result, op) - } - } - return result -} - -// sliceToSet converts a string slice to a set (map[string]bool). -func sliceToSet(items []string) map[string]bool { - m := make(map[string]bool, len(items)) - for _, item := range items { - m[item] = true - } - return m -} diff --git a/experimental/internal/codegen/filter_test.go b/experimental/internal/codegen/filter_test.go deleted file mode 100644 index 9e0f960a86..0000000000 --- a/experimental/internal/codegen/filter_test.go +++ /dev/null @@ -1,323 +0,0 @@ -package codegen - -import ( - "testing" - - "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const filterTestSpec = `openapi: "3.1.0" -info: - title: Filter Test API - version: "1.0" -paths: - /users: - get: - operationId: listUsers - tags: - - users - responses: - "200": - description: OK - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/User' - /pets: - get: - operationId: listPets - tags: - - pets - responses: - "200": - description: OK - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Pet' - /admin/settings: - get: - operationId: getSettings - tags: - - admin - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/Settings' - put: - operationId: updateSettings - tags: - - admin - responses: - "200": - description: OK -components: - schemas: - User: - type: object - properties: - name: - type: string - Pet: - type: object - properties: - name: - type: string - Settings: - type: object - properties: - theme: - type: string -` - -func gatherTestOps(t *testing.T) []*OperationDescriptor { - t.Helper() - doc, err := libopenapi.NewDocument([]byte(filterTestSpec)) - require.NoError(t, err) - - tracker := NewParamUsageTracker() - ops, err := GatherOperations(doc, tracker) - require.NoError(t, err) - return ops -} - -func opIDs(ops []*OperationDescriptor) []string { - ids := make([]string, len(ops)) - for i, op := range ops { - ids[i] = op.OperationID - } - return ids -} - -func TestFilterOperationsByTag_Include(t *testing.T) { - ops := gatherTestOps(t) - require.Len(t, ops, 4) - - filtered := FilterOperationsByTag(ops, OutputOptions{ - IncludeTags: []string{"users"}, - }) - - ids := opIDs(filtered) - assert.Contains(t, ids, "listUsers") - assert.NotContains(t, ids, "listPets") - assert.NotContains(t, ids, "getSettings") - assert.NotContains(t, ids, "updateSettings") -} - -func TestFilterOperationsByTag_Exclude(t *testing.T) { - ops := gatherTestOps(t) - - filtered := FilterOperationsByTag(ops, OutputOptions{ - ExcludeTags: []string{"admin"}, - }) - - ids := opIDs(filtered) - assert.Contains(t, ids, "listUsers") - assert.Contains(t, ids, "listPets") - assert.NotContains(t, ids, "getSettings") - assert.NotContains(t, ids, "updateSettings") -} - -func TestFilterOperationsByTag_IncludeMultiple(t *testing.T) { - ops := gatherTestOps(t) - - filtered := FilterOperationsByTag(ops, OutputOptions{ - IncludeTags: []string{"users", "pets"}, - }) - - ids := opIDs(filtered) - assert.Contains(t, ids, "listUsers") - assert.Contains(t, ids, "listPets") - assert.NotContains(t, ids, "getSettings") - assert.Len(t, filtered, 2) -} - -func TestFilterOperationsByTag_Empty(t *testing.T) { - ops := gatherTestOps(t) - - filtered := FilterOperationsByTag(ops, OutputOptions{}) - assert.Len(t, filtered, 4) -} - -func TestFilterOperationsByOperationID_Include(t *testing.T) { - ops := gatherTestOps(t) - - filtered := FilterOperationsByOperationID(ops, OutputOptions{ - IncludeOperationIDs: []string{"listPets"}, - }) - - ids := opIDs(filtered) - assert.Contains(t, ids, "listPets") - assert.Len(t, filtered, 1) -} - -func TestFilterOperationsByOperationID_Exclude(t *testing.T) { - ops := gatherTestOps(t) - - filtered := FilterOperationsByOperationID(ops, OutputOptions{ - ExcludeOperationIDs: []string{"getSettings", "updateSettings"}, - }) - - ids := opIDs(filtered) - assert.Contains(t, ids, "listUsers") - assert.Contains(t, ids, "listPets") - assert.NotContains(t, ids, "getSettings") - assert.NotContains(t, ids, "updateSettings") - assert.Len(t, filtered, 2) -} - -func TestFilterOperations_Combined(t *testing.T) { - ops := gatherTestOps(t) - - // Include only admin, then exclude updateSettings - filtered := FilterOperations(ops, OutputOptions{ - IncludeTags: []string{"admin"}, - ExcludeOperationIDs: []string{"updateSettings"}, - }) - - ids := opIDs(filtered) - assert.Contains(t, ids, "getSettings") - assert.NotContains(t, ids, "updateSettings") - assert.Len(t, filtered, 1) -} - -func TestFilterSchemasByName(t *testing.T) { - doc, err := libopenapi.NewDocument([]byte(filterTestSpec)) - require.NoError(t, err) - - matcher := NewContentTypeMatcher(DefaultContentTypes()) - schemas, err := GatherSchemas(doc, matcher) - require.NoError(t, err) - - // Count component schemas - var componentNames []string - for _, s := range schemas { - if len(s.Path) == 3 && s.Path[0] == "components" && s.Path[1] == "schemas" { - componentNames = append(componentNames, s.Path[2]) - } - } - assert.Contains(t, componentNames, "User") - assert.Contains(t, componentNames, "Pet") - assert.Contains(t, componentNames, "Settings") - - // Exclude Pet - filtered := FilterSchemasByName(schemas, []string{"Pet"}) - - var filteredNames []string - for _, s := range filtered { - if len(s.Path) == 3 && s.Path[0] == "components" && s.Path[1] == "schemas" { - filteredNames = append(filteredNames, s.Path[2]) - } - } - assert.Contains(t, filteredNames, "User") - assert.NotContains(t, filteredNames, "Pet") - assert.Contains(t, filteredNames, "Settings") -} - -func TestFilterSchemasByName_Empty(t *testing.T) { - doc, err := libopenapi.NewDocument([]byte(filterTestSpec)) - require.NoError(t, err) - - matcher := NewContentTypeMatcher(DefaultContentTypes()) - schemas, err := GatherSchemas(doc, matcher) - require.NoError(t, err) - - filtered := FilterSchemasByName(schemas, nil) - assert.Equal(t, len(schemas), len(filtered)) -} - -func TestFilterIntegration_GenerateWithIncludeTags(t *testing.T) { - doc, err := libopenapi.NewDocument([]byte(filterTestSpec)) - require.NoError(t, err) - - cfg := Configuration{ - PackageName: "testpkg", - Generation: GenerationOptions{ - Client: true, - }, - OutputOptions: OutputOptions{ - IncludeTags: []string{"users"}, - }, - } - - code, err := Generate(doc, []byte(filterTestSpec), cfg) - require.NoError(t, err) - // Client interface should only contain the included operation - assert.Contains(t, code, "ListUsers(ctx context.Context") - assert.NotContains(t, code, "ListPets(ctx context.Context") - assert.NotContains(t, code, "GetSettings(ctx context.Context") -} - -func TestFilterIntegration_GenerateWithExcludeTags(t *testing.T) { - doc, err := libopenapi.NewDocument([]byte(filterTestSpec)) - require.NoError(t, err) - - cfg := Configuration{ - PackageName: "testpkg", - Generation: GenerationOptions{ - Client: true, - }, - OutputOptions: OutputOptions{ - ExcludeTags: []string{"admin"}, - }, - } - - code, err := Generate(doc, []byte(filterTestSpec), cfg) - require.NoError(t, err) - // Client interface should include users and pets but not admin operations - assert.Contains(t, code, "ListUsers(ctx context.Context") - assert.Contains(t, code, "ListPets(ctx context.Context") - assert.NotContains(t, code, "GetSettings(ctx context.Context") - assert.NotContains(t, code, "UpdateSettings(ctx context.Context") -} - -func TestFilterIntegration_GenerateWithExcludeSchemas(t *testing.T) { - doc, err := libopenapi.NewDocument([]byte(filterTestSpec)) - require.NoError(t, err) - - cfg := Configuration{ - PackageName: "testpkg", - OutputOptions: OutputOptions{ - ExcludeSchemas: []string{"Pet"}, - }, - } - - code, err := Generate(doc, []byte(filterTestSpec), cfg) - require.NoError(t, err) - // User and Settings types should still exist - assert.Contains(t, code, "type User struct") - assert.Contains(t, code, "type Settings struct") - // Pet type should be excluded - assert.NotContains(t, code, "type Pet struct") -} - -func TestFilterIntegration_ServerWithIncludeTags(t *testing.T) { - doc, err := libopenapi.NewDocument([]byte(filterTestSpec)) - require.NoError(t, err) - - cfg := Configuration{ - PackageName: "testpkg", - Generation: GenerationOptions{ - Server: ServerTypeStdHTTP, - }, - OutputOptions: OutputOptions{ - IncludeTags: []string{"pets"}, - }, - } - - code, err := Generate(doc, []byte(filterTestSpec), cfg) - require.NoError(t, err) - // Server interface should only contain the included operation - assert.Contains(t, code, "ListPets(w http.ResponseWriter") - assert.NotContains(t, code, "ListUsers(w http.ResponseWriter") - assert.NotContains(t, code, "GetSettings(w http.ResponseWriter") -} From b097c1da068826d5e52be94c3b78f920e2758af6 Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Sat, 7 Feb 2026 09:02:53 +0000 Subject: [PATCH 08/44] Revert "fix(deps): update module github.com/go-chi/chi/v5 to v5.2.2 [security] (#2198)" This reverts commit 98e5d1a848441a135ed4addd4934329b2309e81f. --- experimental/examples/petstore-expanded/chi/go.mod | 2 +- experimental/examples/petstore-expanded/chi/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/experimental/examples/petstore-expanded/chi/go.mod b/experimental/examples/petstore-expanded/chi/go.mod index 58fa2448db..3c9d10741e 100644 --- a/experimental/examples/petstore-expanded/chi/go.mod +++ b/experimental/examples/petstore-expanded/chi/go.mod @@ -3,7 +3,7 @@ module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expan go 1.24.0 require ( - github.com/go-chi/chi/v5 v5.2.2 + github.com/go-chi/chi/v5 v5.2.0 github.com/google/uuid v1.6.0 github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 ) diff --git a/experimental/examples/petstore-expanded/chi/go.sum b/experimental/examples/petstore-expanded/chi/go.sum index 54ce010d7c..ad495c8907 100644 --- a/experimental/examples/petstore-expanded/chi/go.sum +++ b/experimental/examples/petstore-expanded/chi/go.sum @@ -1,4 +1,4 @@ -github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= -github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= +github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= From b0f22a0006d36ded82373df14b8ec27bd2f3e57f Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Sat, 7 Feb 2026 09:02:54 +0000 Subject: [PATCH 09/44] Revert "OpenAPI v3.1 support, experimental re-implementation using libopenapi (#2197)" This reverts commit 5206145c6c6fec742fcdb10d3caa2042a95baf2d. --- Makefile | 12 +- README.md | 4 +- experimental/Configuration.md | 246 -- experimental/Makefile | 35 - experimental/README.md | 164 -- experimental/cmd/oapi-codegen/main.go | 105 - experimental/examples/callback/client/main.go | 144 -- experimental/examples/callback/config.yaml | 6 - experimental/examples/callback/doc.go | 13 - experimental/examples/callback/server/main.go | 116 - experimental/examples/callback/tree-farm.yaml | 138 -- .../examples/callback/treefarm.gen.go | 503 ---- .../examples/petstore-expanded/chi/Makefile | 35 - .../examples/petstore-expanded/chi/go.mod | 11 - .../examples/petstore-expanded/chi/go.sum | 4 - .../examples/petstore-expanded/chi/main.go | 40 - .../petstore-expanded/chi/server/petstore.go | 135 -- .../chi/server/server.config.yaml | 6 - .../chi/server/server.gen.go | 1039 -------- .../client/client.config.yaml | 8 - .../petstore-expanded/client/client.gen.go | 1225 ---------- .../petstore-expanded/client/client_test.go | 161 -- .../client/validator/main.go | 143 -- .../examples/petstore-expanded/doc.go | 2 - .../petstore-expanded/echo-v4/Makefile | 35 - .../examples/petstore-expanded/echo-v4/go.mod | 23 - .../examples/petstore-expanded/echo-v4/go.sum | 33 - .../petstore-expanded/echo-v4/main.go | 34 - .../echo-v4/server/petstore.go | 123 - .../echo-v4/server/server.config.yaml | 6 - .../echo-v4/server/server.gen.go | 976 -------- .../examples/petstore-expanded/echo/Makefile | 35 - .../examples/petstore-expanded/echo/go.mod | 11 - .../examples/petstore-expanded/echo/go.sum | 16 - .../examples/petstore-expanded/echo/main.go | 34 - .../petstore-expanded/echo/server/petstore.go | 123 - .../echo/server/server.config.yaml | 6 - .../echo/server/server.gen.go | 977 -------- .../examples/petstore-expanded/fiber/Makefile | 35 - .../examples/petstore-expanded/fiber/go.mod | 31 - .../examples/petstore-expanded/fiber/go.sum | 51 - .../examples/petstore-expanded/fiber/main.go | 35 - .../fiber/server/petstore.go | 122 - .../fiber/server/server.config.yaml | 6 - .../fiber/server/server.gen.go | 969 -------- .../examples/petstore-expanded/generate.go | 11 - .../examples/petstore-expanded/gin/Makefile | 35 - .../examples/petstore-expanded/gin/go.mod | 40 - .../examples/petstore-expanded/gin/go.sum | 92 - .../examples/petstore-expanded/gin/main.go | 40 - .../petstore-expanded/gin/server/petstore.go | 126 - .../gin/server/server.config.yaml | 6 - .../gin/server/server.gen.go | 1007 -------- .../petstore-expanded/gorilla/Makefile | 35 - .../examples/petstore-expanded/gorilla/go.mod | 11 - .../examples/petstore-expanded/gorilla/go.sum | 4 - .../petstore-expanded/gorilla/main.go | 40 - .../gorilla/server/petstore.go | 135 -- .../gorilla/server/server.config.yaml | 6 - .../gorilla/server/server.gen.go | 1035 -------- .../examples/petstore-expanded/iris/Makefile | 35 - .../examples/petstore-expanded/iris/go.mod | 55 - .../examples/petstore-expanded/iris/go.sum | 178 -- .../examples/petstore-expanded/iris/main.go | 35 - .../petstore-expanded/iris/server/petstore.go | 130 - .../iris/server/server.config.yaml | 6 - .../iris/server/server.gen.go | 973 -------- .../petstore-expanded/models.config.yaml | 2 - .../petstore-expanded/petstore-expanded.yaml | 164 -- .../petstore-expanded/petstore.gen.go | 117 - .../petstore-expanded/stdhttp/Makefile | 35 - .../examples/petstore-expanded/stdhttp/go.mod | 7 - .../examples/petstore-expanded/stdhttp/go.sum | 2 - .../petstore-expanded/stdhttp/main.go | 39 - .../stdhttp/server/petstore.go | 163 -- .../stdhttp/server/server.config.yaml | 7 - .../stdhttp/server/server.gen.go | 1009 -------- experimental/examples/webhook/client/main.go | 150 -- experimental/examples/webhook/config.yaml | 6 - experimental/examples/webhook/doc.go | 13 - .../examples/webhook/door-badge-reader.yaml | 139 -- .../examples/webhook/doorbadge.gen.go | 1069 --------- experimental/examples/webhook/server/main.go | 186 -- experimental/go.mod | 27 - experimental/go.sum | 41 - experimental/internal/codegen/clientgen.go | 349 --- .../internal/codegen/clientgen_test.go | 188 -- experimental/internal/codegen/codegen.go | 1101 --------- .../internal/codegen/configuration.go | 317 --- .../internal/codegen/configuration_test.go | 152 -- experimental/internal/codegen/extension.go | 337 --- .../codegen/extension_integration_test.go | 201 -- .../internal/codegen/extension_test.go | 180 -- experimental/internal/codegen/gather.go | 621 ----- .../internal/codegen/gather_operations.go | 864 ------- experimental/internal/codegen/identifiers.go | 125 - .../internal/codegen/identifiers_test.go | 129 - experimental/internal/codegen/initiatorgen.go | 216 -- experimental/internal/codegen/inline.go | 92 - experimental/internal/codegen/inline_test.go | 139 -- experimental/internal/codegen/namemangling.go | 420 ---- .../internal/codegen/namemangling_test.go | 195 -- experimental/internal/codegen/operation.go | 287 --- experimental/internal/codegen/output.go | 764 ------ experimental/internal/codegen/paramgen.go | 178 -- .../internal/codegen/paramgen_test.go | 161 -- experimental/internal/codegen/receivergen.go | 131 - experimental/internal/codegen/schema.go | 117 - experimental/internal/codegen/schemanames.go | 812 ------- experimental/internal/codegen/servergen.go | 180 -- .../codegen/skip_external_ref_test.go | 68 - experimental/internal/codegen/structtags.go | 172 -- .../internal/codegen/templates/embed.go | 9 - .../templates/files/client/base.go.tmpl | 95 - .../templates/files/client/interface.go.tmpl | 17 - .../templates/files/client/methods.go.tmpl | 40 - .../files/client/request_builders.go.tmpl | 177 -- .../templates/files/client/simple.go.tmpl | 121 - .../templates/files/initiator/base.go.tmpl | 73 - .../files/initiator/interface.go.tmpl | 17 - .../templates/files/initiator/methods.go.tmpl | 41 - .../files/initiator/request_builders.go.tmpl | 149 -- .../templates/files/initiator/simple.go.tmpl | 121 - .../files/params/bind_deep_object.go.tmpl | 271 --- .../templates/files/params/bind_form.go.tmpl | 38 - .../files/params/bind_form_explode.go.tmpl | 145 -- .../templates/files/params/bind_label.go.tmpl | 46 - .../files/params/bind_label_explode.go.tmpl | 51 - .../files/params/bind_matrix.go.tmpl | 47 - .../files/params/bind_matrix_explode.go.tmpl | 65 - .../files/params/bind_pipe_delimited.go.tmpl | 33 - .../bind_pipe_delimited_explode.go.tmpl | 9 - .../files/params/bind_simple.go.tmpl | 38 - .../files/params/bind_simple_explode.go.tmpl | 38 - .../files/params/bind_space_delimited.go.tmpl | 33 - .../bind_space_delimited_explode.go.tmpl | 9 - .../templates/files/params/helpers.go.tmpl | 260 -- .../files/params/style_deep_object.go.tmpl | 74 - .../templates/files/params/style_form.go.tmpl | 132 -- .../files/params/style_form_explode.go.tmpl | 130 - .../files/params/style_label.go.tmpl | 129 - .../files/params/style_label_explode.go.tmpl | 129 - .../files/params/style_matrix.go.tmpl | 132 -- .../files/params/style_matrix_explode.go.tmpl | 130 - .../files/params/style_pipe_delimited.go.tmpl | 66 - .../style_pipe_delimited_explode.go.tmpl | 66 - .../files/params/style_simple.go.tmpl | 158 -- .../files/params/style_simple_explode.go.tmpl | 128 - .../params/style_space_delimited.go.tmpl | 66 - .../style_space_delimited_explode.go.tmpl | 66 - .../files/server/chi/handler.go.tmpl | 59 - .../files/server/chi/interface.go.tmpl | 24 - .../files/server/chi/receiver.go.tmpl | 95 - .../files/server/chi/wrapper.go.tmpl | 166 -- .../files/server/echo-v4/handler.go.tmpl | 34 - .../files/server/echo-v4/interface.go.tmpl | 24 - .../files/server/echo-v4/receiver.go.tmpl | 72 - .../files/server/echo-v4/wrapper.go.tmpl | 134 -- .../files/server/echo/handler.go.tmpl | 35 - .../files/server/echo/interface.go.tmpl | 24 - .../files/server/echo/receiver.go.tmpl | 72 - .../files/server/echo/wrapper.go.tmpl | 134 -- .../templates/files/server/errors.go.tmpl | 79 - .../files/server/fiber/handler.go.tmpl | 31 - .../files/server/fiber/interface.go.tmpl | 24 - .../files/server/fiber/receiver.go.tmpl | 71 - .../files/server/fiber/wrapper.go.tmpl | 137 -- .../files/server/gin/handler.go.tmpl | 37 - .../files/server/gin/interface.go.tmpl | 24 - .../files/server/gin/receiver.go.tmpl | 78 - .../files/server/gin/wrapper.go.tmpl | 161 -- .../files/server/gorilla/handler.go.tmpl | 57 - .../files/server/gorilla/interface.go.tmpl | 24 - .../files/server/gorilla/receiver.go.tmpl | 95 - .../files/server/gorilla/wrapper.go.tmpl | 169 -- .../files/server/iris/handler.go.tmpl | 28 - .../files/server/iris/interface.go.tmpl | 24 - .../files/server/iris/receiver.go.tmpl | 84 - .../files/server/iris/wrapper.go.tmpl | 160 -- .../files/server/param_types.go.tmpl | 24 - .../files/server/stdhttp/handler.go.tmpl | 63 - .../files/server/stdhttp/interface.go.tmpl | 13 - .../files/server/stdhttp/receiver.go.tmpl | 108 - .../files/server/stdhttp/wrapper.go.tmpl | 166 -- .../codegen/templates/files/types/date.tmpl | 38 - .../codegen/templates/files/types/email.tmpl | 43 - .../codegen/templates/files/types/file.tmpl | 64 - .../templates/files/types/nullable.tmpl | 104 - .../codegen/templates/files/types/uuid.tmpl | 3 - .../internal/codegen/templates/funcs.go | 151 -- .../internal/codegen/templates/registry.go | 909 ------- .../codegen/templates/test/types/date.gen.go | 46 - .../codegen/templates/test/types/date_test.go | 65 - .../codegen/templates/test/types/email.gen.go | 52 - .../templates/test/types/email_test.go | 176 -- .../codegen/templates/test/types/file.gen.go | 74 - .../codegen/templates/test/types/file_test.go | 54 - .../codegen/templates/test/types/generate.go | 3 - .../templates/test/types/gentypes/main.go | 109 - .../codegen/templates/test/types/uuid.gen.go | 10 - .../codegen/templates/test/types/uuid_test.go | 53 - .../codegen/test/comprehensive/doc.go | 3 - .../comprehensive/output/comprehensive.gen.go | 2098 ----------------- .../test/default_values/default_values.yaml | 158 -- .../output/default_values.gen.go | 514 ---- .../output/default_values_test.go | 324 --- .../codegen/test/external_ref/config.yaml | 5 - .../test/external_ref/external_ref_test.go | 62 - .../test/external_ref/packagea/config.yaml | 4 - .../test/external_ref/packagea/spec.gen.go | 22 - .../test/external_ref/packagea/spec.yaml | 14 - .../test/external_ref/packageb/config.yaml | 2 - .../test/external_ref/packageb/spec.gen.go | 14 - .../test/external_ref/packageb/spec.yaml | 12 - .../codegen/test/external_ref/spec.gen.go | 26 - .../codegen/test/external_ref/spec.yaml | 14 - .../codegen/test/files/comprehensive.yaml | 861 ------- .../codegen/test/issues/issue_1029/doc.go | 5 - .../issues/issue_1029/output/types.gen.go | 186 -- .../issues/issue_1029/output/types_test.go | 89 - .../codegen/test/issues/issue_1029/spec.yaml | 29 - .../codegen/test/issues/issue_1039/doc.go | 5 - .../issues/issue_1039/output/types.gen.go | 291 --- .../issues/issue_1039/output/types_test.go | 266 --- .../codegen/test/issues/issue_1039/spec.yaml | 86 - .../codegen/test/issues/issue_1397/doc.go | 5 - .../issues/issue_1397/output/types.gen.go | 114 - .../issues/issue_1397/output/types_test.go | 81 - .../codegen/test/issues/issue_1397/spec.yaml | 57 - .../codegen/test/issues/issue_1429/doc.go | 5 - .../issues/issue_1429/output/types.gen.go | 149 -- .../issues/issue_1429/output/types_test.go | 37 - .../codegen/test/issues/issue_1429/spec.yaml | 33 - .../codegen/test/issues/issue_1496/doc.go | 5 - .../issues/issue_1496/output/types.gen.go | 168 -- .../issues/issue_1496/output/types_test.go | 41 - .../codegen/test/issues/issue_1496/spec.yaml | 40 - .../codegen/test/issues/issue_1710/doc.go | 5 - .../issues/issue_1710/output/types.gen.go | 212 -- .../issues/issue_1710/output/types_test.go | 102 - .../codegen/test/issues/issue_1710/spec.yaml | 76 - .../codegen/test/issues/issue_193/doc.go | 5 - .../test/issues/issue_193/output/types.gen.go | 71 - .../issues/issue_193/output/types_test.go | 76 - .../codegen/test/issues/issue_193/spec.yaml | 27 - .../codegen/test/issues/issue_2102/doc.go | 5 - .../issues/issue_2102/output/types.gen.go | 82 - .../issues/issue_2102/output/types_test.go | 52 - .../codegen/test/issues/issue_2102/spec.yaml | 39 - .../codegen/test/issues/issue_312/doc.go | 6 - .../test/issues/issue_312/output/types.gen.go | 97 - .../issues/issue_312/output/types_test.go | 72 - .../codegen/test/issues/issue_312/spec.yaml | 86 - .../codegen/test/issues/issue_502/doc.go | 5 - .../test/issues/issue_502/output/types.gen.go | 228 -- .../issues/issue_502/output/types_test.go | 107 - .../codegen/test/issues/issue_502/spec.yaml | 29 - .../codegen/test/issues/issue_52/doc.go | 5 - .../test/issues/issue_52/output/types.gen.go | 87 - .../test/issues/issue_52/output/types_test.go | 64 - .../codegen/test/issues/issue_52/spec.yaml | 41 - .../codegen/test/issues/issue_579/doc.go | 5 - .../test/issues/issue_579/output/types.gen.go | 110 - .../issues/issue_579/output/types_test.go | 64 - .../codegen/test/issues/issue_579/spec.yaml | 30 - .../codegen/test/issues/issue_697/doc.go | 5 - .../test/issues/issue_697/output/types.gen.go | 81 - .../issues/issue_697/output/types_test.go | 58 - .../codegen/test/issues/issue_697/spec.yaml | 27 - .../codegen/test/issues/issue_775/doc.go | 5 - .../test/issues/issue_775/output/types.gen.go | 86 - .../issues/issue_775/output/types_test.go | 37 - .../codegen/test/issues/issue_775/spec.yaml | 22 - .../codegen/test/issues/issue_832/doc.go | 5 - .../test/issues/issue_832/output/types.gen.go | 91 - .../issues/issue_832/output/types_test.go | 62 - .../codegen/test/issues/issue_832/spec.yaml | 45 - .../issue_head_digit_operation_id/doc.go | 5 - .../output/types.gen.go | 69 - .../output/types_test.go | 50 - .../issue_head_digit_operation_id/spec.yaml | 20 - .../issues/issue_illegal_enum_names/doc.go | 5 - .../output/types.gen.go | 80 - .../output/types_test.go | 51 - .../issues/issue_illegal_enum_names/spec.yaml | 37 - .../codegen/test/nested_aggregate/doc.go | 3 - .../nested_aggregate/nested_aggregate.yaml | 62 - .../output/nested_aggregate.gen.go | 400 ---- .../output/nested_aggregate_test.go | 332 --- experimental/internal/codegen/typegen.go | 918 -------- experimental/internal/codegen/typemapping.go | 98 - experimental/internal/codegen/typenames.go | 1 - scripts/foreach-module.sh | 20 - 293 files changed, 7 insertions(+), 41797 deletions(-) delete mode 100644 experimental/Configuration.md delete mode 100644 experimental/Makefile delete mode 100644 experimental/README.md delete mode 100644 experimental/cmd/oapi-codegen/main.go delete mode 100644 experimental/examples/callback/client/main.go delete mode 100644 experimental/examples/callback/config.yaml delete mode 100644 experimental/examples/callback/doc.go delete mode 100644 experimental/examples/callback/server/main.go delete mode 100644 experimental/examples/callback/tree-farm.yaml delete mode 100644 experimental/examples/callback/treefarm.gen.go delete mode 100644 experimental/examples/petstore-expanded/chi/Makefile delete mode 100644 experimental/examples/petstore-expanded/chi/go.mod delete mode 100644 experimental/examples/petstore-expanded/chi/go.sum delete mode 100644 experimental/examples/petstore-expanded/chi/main.go delete mode 100644 experimental/examples/petstore-expanded/chi/server/petstore.go delete mode 100644 experimental/examples/petstore-expanded/chi/server/server.config.yaml delete mode 100644 experimental/examples/petstore-expanded/chi/server/server.gen.go delete mode 100644 experimental/examples/petstore-expanded/client/client.config.yaml delete mode 100644 experimental/examples/petstore-expanded/client/client.gen.go delete mode 100644 experimental/examples/petstore-expanded/client/client_test.go delete mode 100644 experimental/examples/petstore-expanded/client/validator/main.go delete mode 100644 experimental/examples/petstore-expanded/doc.go delete mode 100644 experimental/examples/petstore-expanded/echo-v4/Makefile delete mode 100644 experimental/examples/petstore-expanded/echo-v4/go.mod delete mode 100644 experimental/examples/petstore-expanded/echo-v4/go.sum delete mode 100644 experimental/examples/petstore-expanded/echo-v4/main.go delete mode 100644 experimental/examples/petstore-expanded/echo-v4/server/petstore.go delete mode 100644 experimental/examples/petstore-expanded/echo-v4/server/server.config.yaml delete mode 100644 experimental/examples/petstore-expanded/echo-v4/server/server.gen.go delete mode 100644 experimental/examples/petstore-expanded/echo/Makefile delete mode 100644 experimental/examples/petstore-expanded/echo/go.mod delete mode 100644 experimental/examples/petstore-expanded/echo/go.sum delete mode 100644 experimental/examples/petstore-expanded/echo/main.go delete mode 100644 experimental/examples/petstore-expanded/echo/server/petstore.go delete mode 100644 experimental/examples/petstore-expanded/echo/server/server.config.yaml delete mode 100644 experimental/examples/petstore-expanded/echo/server/server.gen.go delete mode 100644 experimental/examples/petstore-expanded/fiber/Makefile delete mode 100644 experimental/examples/petstore-expanded/fiber/go.mod delete mode 100644 experimental/examples/petstore-expanded/fiber/go.sum delete mode 100644 experimental/examples/petstore-expanded/fiber/main.go delete mode 100644 experimental/examples/petstore-expanded/fiber/server/petstore.go delete mode 100644 experimental/examples/petstore-expanded/fiber/server/server.config.yaml delete mode 100644 experimental/examples/petstore-expanded/fiber/server/server.gen.go delete mode 100644 experimental/examples/petstore-expanded/generate.go delete mode 100644 experimental/examples/petstore-expanded/gin/Makefile delete mode 100644 experimental/examples/petstore-expanded/gin/go.mod delete mode 100644 experimental/examples/petstore-expanded/gin/go.sum delete mode 100644 experimental/examples/petstore-expanded/gin/main.go delete mode 100644 experimental/examples/petstore-expanded/gin/server/petstore.go delete mode 100644 experimental/examples/petstore-expanded/gin/server/server.config.yaml delete mode 100644 experimental/examples/petstore-expanded/gin/server/server.gen.go delete mode 100644 experimental/examples/petstore-expanded/gorilla/Makefile delete mode 100644 experimental/examples/petstore-expanded/gorilla/go.mod delete mode 100644 experimental/examples/petstore-expanded/gorilla/go.sum delete mode 100644 experimental/examples/petstore-expanded/gorilla/main.go delete mode 100644 experimental/examples/petstore-expanded/gorilla/server/petstore.go delete mode 100644 experimental/examples/petstore-expanded/gorilla/server/server.config.yaml delete mode 100644 experimental/examples/petstore-expanded/gorilla/server/server.gen.go delete mode 100644 experimental/examples/petstore-expanded/iris/Makefile delete mode 100644 experimental/examples/petstore-expanded/iris/go.mod delete mode 100644 experimental/examples/petstore-expanded/iris/go.sum delete mode 100644 experimental/examples/petstore-expanded/iris/main.go delete mode 100644 experimental/examples/petstore-expanded/iris/server/petstore.go delete mode 100644 experimental/examples/petstore-expanded/iris/server/server.config.yaml delete mode 100644 experimental/examples/petstore-expanded/iris/server/server.gen.go delete mode 100644 experimental/examples/petstore-expanded/models.config.yaml delete mode 100644 experimental/examples/petstore-expanded/petstore-expanded.yaml delete mode 100644 experimental/examples/petstore-expanded/petstore.gen.go delete mode 100644 experimental/examples/petstore-expanded/stdhttp/Makefile delete mode 100644 experimental/examples/petstore-expanded/stdhttp/go.mod delete mode 100644 experimental/examples/petstore-expanded/stdhttp/go.sum delete mode 100644 experimental/examples/petstore-expanded/stdhttp/main.go delete mode 100644 experimental/examples/petstore-expanded/stdhttp/server/petstore.go delete mode 100644 experimental/examples/petstore-expanded/stdhttp/server/server.config.yaml delete mode 100644 experimental/examples/petstore-expanded/stdhttp/server/server.gen.go delete mode 100644 experimental/examples/webhook/client/main.go delete mode 100644 experimental/examples/webhook/config.yaml delete mode 100644 experimental/examples/webhook/doc.go delete mode 100644 experimental/examples/webhook/door-badge-reader.yaml delete mode 100644 experimental/examples/webhook/doorbadge.gen.go delete mode 100644 experimental/examples/webhook/server/main.go delete mode 100644 experimental/go.mod delete mode 100644 experimental/go.sum delete mode 100644 experimental/internal/codegen/clientgen.go delete mode 100644 experimental/internal/codegen/clientgen_test.go delete mode 100644 experimental/internal/codegen/codegen.go delete mode 100644 experimental/internal/codegen/configuration.go delete mode 100644 experimental/internal/codegen/configuration_test.go delete mode 100644 experimental/internal/codegen/extension.go delete mode 100644 experimental/internal/codegen/extension_integration_test.go delete mode 100644 experimental/internal/codegen/extension_test.go delete mode 100644 experimental/internal/codegen/gather.go delete mode 100644 experimental/internal/codegen/gather_operations.go delete mode 100644 experimental/internal/codegen/identifiers.go delete mode 100644 experimental/internal/codegen/identifiers_test.go delete mode 100644 experimental/internal/codegen/initiatorgen.go delete mode 100644 experimental/internal/codegen/inline.go delete mode 100644 experimental/internal/codegen/inline_test.go delete mode 100644 experimental/internal/codegen/namemangling.go delete mode 100644 experimental/internal/codegen/namemangling_test.go delete mode 100644 experimental/internal/codegen/operation.go delete mode 100644 experimental/internal/codegen/output.go delete mode 100644 experimental/internal/codegen/paramgen.go delete mode 100644 experimental/internal/codegen/paramgen_test.go delete mode 100644 experimental/internal/codegen/receivergen.go delete mode 100644 experimental/internal/codegen/schema.go delete mode 100644 experimental/internal/codegen/schemanames.go delete mode 100644 experimental/internal/codegen/servergen.go delete mode 100644 experimental/internal/codegen/skip_external_ref_test.go delete mode 100644 experimental/internal/codegen/structtags.go delete mode 100644 experimental/internal/codegen/templates/embed.go delete mode 100644 experimental/internal/codegen/templates/files/client/base.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/client/interface.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/client/methods.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/client/request_builders.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/client/simple.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/initiator/base.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/initiator/interface.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/initiator/methods.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/initiator/request_builders.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/initiator/simple.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/bind_deep_object.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/bind_form.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/bind_form_explode.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/bind_label.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/bind_label_explode.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/bind_matrix.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/bind_matrix_explode.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/bind_pipe_delimited.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/bind_pipe_delimited_explode.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/bind_simple.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/bind_simple_explode.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/bind_space_delimited.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/bind_space_delimited_explode.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/helpers.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/style_deep_object.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/style_form.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/style_form_explode.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/style_label.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/style_label_explode.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/style_matrix.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/style_matrix_explode.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/style_pipe_delimited.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/style_pipe_delimited_explode.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/style_simple.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/style_simple_explode.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/style_space_delimited.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/params/style_space_delimited_explode.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/chi/handler.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/chi/interface.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/chi/receiver.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/chi/wrapper.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/echo-v4/handler.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/echo-v4/interface.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/echo-v4/receiver.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/echo-v4/wrapper.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/echo/handler.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/echo/interface.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/echo/receiver.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/echo/wrapper.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/errors.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/fiber/handler.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/fiber/interface.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/fiber/receiver.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/fiber/wrapper.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/gin/handler.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/gin/interface.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/gin/receiver.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/gin/wrapper.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/gorilla/handler.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/gorilla/interface.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/gorilla/receiver.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/gorilla/wrapper.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/iris/handler.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/iris/interface.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/iris/receiver.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/iris/wrapper.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/param_types.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/stdhttp/handler.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/stdhttp/interface.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/stdhttp/receiver.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/server/stdhttp/wrapper.go.tmpl delete mode 100644 experimental/internal/codegen/templates/files/types/date.tmpl delete mode 100644 experimental/internal/codegen/templates/files/types/email.tmpl delete mode 100644 experimental/internal/codegen/templates/files/types/file.tmpl delete mode 100644 experimental/internal/codegen/templates/files/types/nullable.tmpl delete mode 100644 experimental/internal/codegen/templates/files/types/uuid.tmpl delete mode 100644 experimental/internal/codegen/templates/funcs.go delete mode 100644 experimental/internal/codegen/templates/registry.go delete mode 100644 experimental/internal/codegen/templates/test/types/date.gen.go delete mode 100644 experimental/internal/codegen/templates/test/types/date_test.go delete mode 100644 experimental/internal/codegen/templates/test/types/email.gen.go delete mode 100644 experimental/internal/codegen/templates/test/types/email_test.go delete mode 100644 experimental/internal/codegen/templates/test/types/file.gen.go delete mode 100644 experimental/internal/codegen/templates/test/types/file_test.go delete mode 100644 experimental/internal/codegen/templates/test/types/generate.go delete mode 100644 experimental/internal/codegen/templates/test/types/gentypes/main.go delete mode 100644 experimental/internal/codegen/templates/test/types/uuid.gen.go delete mode 100644 experimental/internal/codegen/templates/test/types/uuid_test.go delete mode 100644 experimental/internal/codegen/test/comprehensive/doc.go delete mode 100644 experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go delete mode 100644 experimental/internal/codegen/test/default_values/default_values.yaml delete mode 100644 experimental/internal/codegen/test/default_values/output/default_values.gen.go delete mode 100644 experimental/internal/codegen/test/default_values/output/default_values_test.go delete mode 100644 experimental/internal/codegen/test/external_ref/config.yaml delete mode 100644 experimental/internal/codegen/test/external_ref/external_ref_test.go delete mode 100644 experimental/internal/codegen/test/external_ref/packagea/config.yaml delete mode 100644 experimental/internal/codegen/test/external_ref/packagea/spec.gen.go delete mode 100644 experimental/internal/codegen/test/external_ref/packagea/spec.yaml delete mode 100644 experimental/internal/codegen/test/external_ref/packageb/config.yaml delete mode 100644 experimental/internal/codegen/test/external_ref/packageb/spec.gen.go delete mode 100644 experimental/internal/codegen/test/external_ref/packageb/spec.yaml delete mode 100644 experimental/internal/codegen/test/external_ref/spec.gen.go delete mode 100644 experimental/internal/codegen/test/external_ref/spec.yaml delete mode 100644 experimental/internal/codegen/test/files/comprehensive.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_1029/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1029/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1029/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_1039/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1039/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1039/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_1397/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1397/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1397/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_1429/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1429/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1429/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_1496/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1496/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1496/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_1710/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1710/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_1710/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_193/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_193/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_193/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_193/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_2102/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_2102/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_2102/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_312/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_312/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_312/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_312/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_502/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_502/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_502/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_502/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_52/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_52/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_52/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_52/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_579/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_579/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_579/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_579/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_697/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_697/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_697/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_697/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_775/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_775/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_775/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_775/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_832/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_832/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_832/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_832/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_head_digit_operation_id/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_head_digit_operation_id/spec.yaml delete mode 100644 experimental/internal/codegen/test/issues/issue_illegal_enum_names/doc.go delete mode 100644 experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go delete mode 100644 experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types_test.go delete mode 100644 experimental/internal/codegen/test/issues/issue_illegal_enum_names/spec.yaml delete mode 100644 experimental/internal/codegen/test/nested_aggregate/doc.go delete mode 100644 experimental/internal/codegen/test/nested_aggregate/nested_aggregate.yaml delete mode 100644 experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go delete mode 100644 experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate_test.go delete mode 100644 experimental/internal/codegen/typegen.go delete mode 100644 experimental/internal/codegen/typemapping.go delete mode 100644 experimental/internal/codegen/typenames.go delete mode 100755 scripts/foreach-module.sh diff --git a/Makefile b/Makefile index 89b6de0f6d..069527cf0e 100644 --- a/Makefile +++ b/Makefile @@ -19,34 +19,34 @@ lint: tools # run the root module explicitly, to prevent recursive calls by re-invoking `make ...` top-level $(GOBIN)/golangci-lint run ./... # then, for all child modules, use a module-managed `Makefile` - GOBIN=$(GOBIN) ./scripts/foreach-module.sh lint + git ls-files '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && env GOBIN=$(GOBIN) make lint' lint-ci: tools # for the root module, explicitly run the step, to prevent recursive calls $(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m # then, for all child modules, use a module-managed `Makefile` - GOBIN=$(GOBIN) ./scripts/foreach-module.sh lint-ci + git ls-files '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && env GOBIN=$(GOBIN) make lint-ci' generate: # for the root module, explicitly run the step, to prevent recursive calls go generate ./... # then, for all child modules, use a module-managed `Makefile` - GOBIN=$(GOBIN) ./scripts/foreach-module.sh generate + git ls-files '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && make generate' test: # for the root module, explicitly run the step, to prevent recursive calls go test -cover ./... # then, for all child modules, use a module-managed `Makefile` - GOBIN=$(GOBIN) ./scripts/foreach-module.sh test + git ls-files '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && make test' tidy: # for the root module, explicitly run the step, to prevent recursive calls go mod tidy # then, for all child modules, use a module-managed `Makefile` - GOBIN=$(GOBIN) ./scripts/foreach-module.sh tidy + git ls-files '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && make tidy' tidy-ci: # for the root module, explicitly run the step, to prevent recursive calls tidied -verbose # then, for all child modules, use a module-managed `Makefile` - GOBIN=$(GOBIN) ./scripts/foreach-module.sh tidy-ci + git ls-files '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && make tidy-ci' diff --git a/README.md b/README.md index ecf9a55e66..7bac0d2e98 100644 --- a/README.md +++ b/README.md @@ -393,9 +393,7 @@ We can see that this provides the best means to focus on the implementation of t - Single-file output - Support multiple OpenAPI files by having a package-per-OpenAPI file - Support of OpenAPI 3.0 - - OpenAPI 3.1 support is in experimental form, as a complete rewrite using [libopenapi](https://github.com/pb33f/libopenapi). Please look in the - `./experimental` directory. This is potentially the future V3 of `oapi-codegen` and is functionally complete, just not deeply tested yet. Many OpenAPI 3.1 - features are supported, such as webhooks and callbacks. + - OpenAPI 3.1 support is [awaiting upstream support](https://github.com/oapi-codegen/oapi-codegen/issues/373) - Note that this does not include OpenAPI 2.0 (aka Swagger) - Extract parameters from requests, to reduce work required by your implementation - Implicit `additionalProperties` are ignored by default ([more details](#additional-properties-additionalproperties)) diff --git a/experimental/Configuration.md b/experimental/Configuration.md deleted file mode 100644 index 55f550e2f3..0000000000 --- a/experimental/Configuration.md +++ /dev/null @@ -1,246 +0,0 @@ -# Configuration Reference - -`oapi-codegen` is configured using a YAML file. All sections are optional — you only need to include what you want to change from the defaults. - -Below is a fully annotated configuration file showing every option. - -```yaml -# The Go package name for generated code. -# Can also be set with -package flag. -package: myapi - -# Output file path. -# Can also be set with -output flag. -# Default: .gen.go -output: types.gen.go - -# Generation controls which parts of the code are generated. -generation: - # Server framework to generate code for. - # Supported: "std-http", "chi", "echo", "echo/v4", "gin", "gorilla", "fiber", "iris" - # Default: "" (no server code generated) - server: std-http - - # Generate an HTTP client that returns *http.Response. - # Default: false - client: true - - # Generate a SimpleClient wrapper with typed responses. - # Requires client: true. - # Default: false - simple-client: true - - # Use model types from an external package instead of generating them locally. - # When set, models are imported rather than generated. - # Default: not set (models are generated locally) - models-package: - path: github.com/org/project/models - alias: models # optional, defaults to last segment of path - -# Type mappings: OpenAPI type/format to Go type. -# User values are merged on top of defaults — you only need to specify overrides. -type-mapping: - integer: - default: - type: int # default - formats: - int32: - type: int32 # default - int64: - type: int64 # default - number: - default: - type: float32 # default - formats: - double: - type: float64 # default - boolean: - default: - type: bool # default - string: - default: - type: string # default - formats: - byte: - type: "[]byte" # default - date: - type: Date # default, custom template type - date-time: - type: time.Time # default - import: time - uuid: - type: UUID # default, custom template type - email: - type: Email # default, custom template type - binary: - type: File # default, custom template type - json: - type: json.RawMessage # default - import: encoding/json - # Add your own format mappings: - money: - type: decimal.Decimal - import: github.com/shopspring/decimal - -# Name mangling: controls how OpenAPI names become Go identifiers. -# User values are merged on top of defaults. -name-mangling: - # Prefix prepended when a name starts with a digit. - # Default: "N" (e.g., "123foo" becomes "N123foo") - numeric-prefix: "N" - - # Prefix prepended when a name conflicts with a Go keyword. - # Default: "" (uses keyword-suffix instead) - keyword-prefix: "" - - # Characters that mark word boundaries (next letter is capitalised). - # Default includes most punctuation: - # @ ! $ & = . + : ; _ ~ space ( ) { } [ ] | < > ? / \ - word-separators: "-#@!$&=.+:;_~ (){}[]|<>?/\\" - - # Words that should remain all-uppercase. - initialisms: - - ACL - - API - - ASCII - - CPU - - CSS - - DB - - DNS - - EOF - - GUID - - HTML - - HTTP - - HTTPS - - ID - - IP - - JSON - - QPS - - RAM - - RPC - - SLA - - SMTP - - SQL - - SSH - - TCP - - TLS - - TTL - - UDP - - UI - - UID - - GID - - URI - - URL - - UTF8 - - UUID - - VM - - XML - - XMPP - - XSRF - - XSS - - SIP - - RTP - - AMQP - - TS - - # Characters that get replaced with words when they appear at the start of a name. - character-substitutions: - "$": DollarSign - "-": Minus - "+": Plus - "&": And - "|": Or - "~": Tilde - "=": Equal - ">": GreaterThan - "<": LessThan - "#": Hash - ".": Dot - "*": Asterisk - "^": Caret - "%": Percent - "_": Underscore - "@": At - "!": Bang - "?": Question - "/": Slash - "\\": Backslash - ":": Colon - ";": Semicolon - "'": Apos - "\"": Quote - "`": Backtick - "(": LParen - ")": RParen - "[": LBracket - "]": RBracket - "{": LBrace - "}": RBrace - -# Name substitutions: direct overrides for specific generated names. -name-substitutions: - # Override type names during generation. - type-names: - foo: MyCustomFoo # Schema "foo" generates type "MyCustomFoo" instead of "Foo" - # Override property/field names during generation. - property-names: - bar: MyCustomBar # Property "bar" generates field "MyCustomBar" instead of "Bar" - -# Import mapping: resolve external $ref targets to Go packages. -# Required when your spec references schemas from other files. -import-mapping: - ../common/api.yaml: github.com/org/project/common - https://example.com/specs/shared.yaml: github.com/org/shared - # Use "-" to indicate types should stay in the current package - ./local-types.yaml: "-" - -# Content types: regexp patterns controlling which media types generate models. -# Only request/response bodies with matching content types will have types generated. -# Default: JSON types only. -content-types: - - "^application/json$" - - "^application/.*\\+json$" - # Add custom patterns as needed: - # - "^application/xml$" - # - "^text/plain$" - -# Content type short names: maps content type patterns to short names -# used in generated type names (e.g., "FindPetsJSONResponse"). -content-type-short-names: - - pattern: "^application/json$" - short-name: JSON - - pattern: "^application/xml$" - short-name: XML - - pattern: "^text/plain$" - short-name: Text - # ... defaults cover JSON, XML, Text, HTML, Binary, Multipart, Form - -# Struct tags: controls which struct tags are generated and their format. -# Uses Go text/template syntax. If specified, completely replaces the defaults. -# Default: json and form tags. -struct-tags: - tags: - - name: json - template: '{{if .JSONIgnore}}-{{else}}{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}{{if .OmitZero}},omitzero{{end}}{{end}}' - - name: form - template: '{{if .JSONIgnore}}-{{else}}{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}{{end}}' - # Add additional tags: - - name: yaml - template: '{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}' - - name: db - template: '{{ .FieldName }}' -``` - -## Struct tag template variables - -The struct tag templates have access to the following fields: - -| Variable | Type | Description | -|----------|------|-------------| -| `.FieldName` | `string` | The original field name from the OpenAPI spec | -| `.GoFieldName` | `string` | The Go identifier name after name mangling | -| `.IsOptional` | `bool` | Whether the field is optional in the schema | -| `.IsNullable` | `bool` | Whether the field is nullable | -| `.IsPointer` | `bool` | Whether the field is rendered as a pointer type | -| `.OmitEmpty` | `bool` | Whether `omitempty` should be applied (from extensions or optionality) | -| `.OmitZero` | `bool` | Whether `omitzero` should be applied (from `x-oapi-codegen-omitzero` extension) | -| `.JSONIgnore` | `bool` | Whether the field should be ignored in JSON (from `x-go-json-ignore` extension) | diff --git a/experimental/Makefile b/experimental/Makefile deleted file mode 100644 index a2ddf18fb1..0000000000 --- a/experimental/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -SHELL:=/bin/bash - -YELLOW := \e[0;33m -RESET := \e[0;0m - -GOVER := $(shell go env GOVERSION) -GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") - -define execute-if-go-124 -@{ \ -if [[ 24 -le $(GOMINOR) ]]; then \ - $1; \ -else \ - echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ -fi \ -} -endef - -lint: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) - -lint-ci: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) - -generate: - $(call execute-if-go-124,go generate ./...) - -test: - @echo "Skipping tests in experimental module" - -tidy: - $(call execute-if-go-124,go mod tidy) - -tidy-ci: - $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/README.md b/experimental/README.md deleted file mode 100644 index 4ce7d6310a..0000000000 --- a/experimental/README.md +++ /dev/null @@ -1,164 +0,0 @@ -# `oapi-codegen` V3 - -This is an experimental prototype of a V3 version of oapi-codegen. The generated code and command line options are not yet stable. Use at your -own risk. - -## What is new in Version 3 -This directory contains an experimental version of oapi-codegen's future V3 version, which is based on [libopenapi](https://github.com/pb33f/libopenapi), -instead of the prior [kin-openapi](https://github.com/getkin/kin-openapi). This change necessitated a nearly complete rewrite, but we strive to be as -compatible as possible. - -What is working: - - All model, client and server generation as in earlier versions. - - We have added Webhook and Callback support, please see `./examples`, which contains the ubiquitous OpenAPI pet shop implemented in all supported servers - and examples of webhooks and callbacks implemented on top of the `http.ServeMux` server, with no additional imports. - - Model generation of `allOf`, `anyOf`, `oneOf` is much more robust, since these were a source of many problems in earlier versions. - - Echo V5 support has been added (Go 1.25 required) - -What is missing: - - We have not yet created any runtime code like request validation middleware. - -## Differences in V3 - -V3 is a brand new implementation, and may (will) contain new bugs, but also strives to fix many current, existing bugs. We've run quite a few -conformance tests against specifications in old Issues, and we're looking pretty good! Please try this out, and if it failes in some way, please -file Issues. - -### Aggregate Schemas - -V3 implements `oneOf`, `anyOf`, `allOf` differently. Our prior versions had pretty good handling for `allOf`, where we merge all the constituent schemas -into a schema object that contains all the fields of the originals. It makes sense, since this is composition. - -`oneOf` and `anyOf` were handled by deferred parsing, where the JSON was captured into `json.RawMessage` and helper functions were created on each -type to parse the JSON as any constituent type with the user's help. - -Given the following schemas: - -```yaml -components: - schemas: - Cat: - type: object - properties: - name: - type: string - color: - type: string - Dog: - type: object - properties: - name: - type: string - color: - type: string - Fish: - type: object - properties: - species: - type: string - isSaltwater: - type: boolean - NamedPet: - anyOf: - - $ref: '#/components/schemas/Cat' - - $ref: '#/components/schemas/Dog' - SpecificPet: - oneOf: - - $ref: '#/components/schemas/Cat' - - $ref: '#/components/schemas/Dog' - - $ref: '#/components/schemas/Fish' -``` - -#### V2 output - -V2 generates both `NamedPet` (anyOf) and `SpecificPet` (oneOf) identically as opaque wrappers around `json.RawMessage`: - -```go -type NamedPet struct { - union json.RawMessage -} - -type SpecificPet struct { - union json.RawMessage -} -``` - -The actual variant types are invisible at the struct level. To access the underlying data, the user must call generated helper methods: - -```go -// NamedPet (anyOf) helpers -func (t NamedPet) AsCat() (Cat, error) -func (t *NamedPet) FromCat(v Cat) error -// - -// SpecificPet (oneOf) helpers - -func (t SpecificPet) AsFish() (Fish, error) -func (t *SpecificPet) FromFish(v Fish) error -func (t *SpecificPet) MergeFish(v Fish) error -// -``` - -Note that `anyOf` and `oneOf` produce identical types and method signatures; there is no semantic difference in the generated code. - -#### V3 output - -V3 generates structs with exported pointer fields for each variant, making the union members visible at the type level. Crucially, `anyOf` and `oneOf` now have different marshal/unmarshal semantics. - -**`anyOf` (NamedPet)** — `MarshalJSON` merges all non-nil variants into a single JSON object. `UnmarshalJSON` tries every variant and keeps whichever succeed: - -```go -type NamedPet struct { - Cat *Cat - Dog *Dog -} - -``` - -**`oneOf` (SpecificPet)** — `MarshalJSON` returns an error unless exactly one field is non-nil. `UnmarshalJSON` returns an error unless the JSON matches exactly one variant: - -```go -type SpecificPet struct { - Cat *Cat - Dog *Dog - Fish *Fish -} -``` - -### OpenAPI V3.1 Feature Support - -Thanks to [libopenapi](https://github.com/pb33f/libopenapi), we are able to parse OpenAPI 3.1 and 3.2 specifications. They are functionally similar, you can -read the differences between `nullable` fields yourself, but they add some new functionality, namely `webhooks` and `callbacks`. We support all of them in -this prototype. `callbacks` and `webhooks` are basically the inverse of `paths`. Webhooks contain no URL element in their definition, so we can't register handlers -for you in your http router of choice, you have to do that yourself. Callbacks support complex request URL's which may reference the original request. This is -something you need to pull out of the request body, and doing it generically is difficult, so we punt this problem, for now, to our users. - -Please see the [webhook example](examples/webhook/). It creates a little server that pretends to be a door badge reader, and it generates an event stream -about people coming and going. Any number of clients may subscribe to this event. See the [doc.go](examples/webhook/doc.go) for usage examples. - -The [callback example](examples/callback), creates a little server that pretends to plant trees. Each tree planting request contains a callback to be notified -when tree planting is complete. We invoke those in a random order via delays, and the client prints out callbacks as they happen. Please see [doc.go](examples/callback/doc.go) for usage. - -### Flexible Configuration - -oapi-codegen V3 tries to make no assumptions about which initialisms, struct tags, or name mangling that is correct for you. A very [flexible configuration file](Configuration.md) allows you to override anything. - - -### No runtime dependency - -V2 generated code relied on `github.com/oapi-codegen/runtime` for parameter binding and styling. This was a complaint from lots of people due to various -audit requirements. V3 embeds all necessary helper functions and helper types into the spec. There are no longer generic, parameterized functions that -handle arbitrary parameters, but rather very specific functions for each kind of parameter, and we call the correct little helper versus a generic -runtime helper. - - -## Installation - -Go 1.24 is required, install like so: - - go get -tool github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen@latest - -You can then run the code generator - - //go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen - diff --git a/experimental/cmd/oapi-codegen/main.go b/experimental/cmd/oapi-codegen/main.go deleted file mode 100644 index 24c5320c90..0000000000 --- a/experimental/cmd/oapi-codegen/main.go +++ /dev/null @@ -1,105 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/pb33f/libopenapi" - "github.com/pb33f/libopenapi/datamodel" - - "gopkg.in/yaml.v3" - - "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen" -) - -func main() { - configPath := flag.String("config", "", "path to configuration file") - flagPackage := flag.String("package", "", "Go package name for generated code") - flagOutput := flag.String("output", "", "output file path (default: .gen.go)") - flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage: %s [options] \n\n", os.Args[0]) - fmt.Fprintf(os.Stderr, "Arguments:\n") - fmt.Fprintf(os.Stderr, " spec-path path to OpenAPI spec file\n\n") - fmt.Fprintf(os.Stderr, "Options:\n") - flag.PrintDefaults() - } - flag.Parse() - - if flag.NArg() != 1 { - flag.Usage() - os.Exit(1) - } - - specPath := flag.Arg(0) - - // Parse the OpenAPI spec - specData, err := os.ReadFile(specPath) - if err != nil { - fmt.Fprintf(os.Stderr, "error reading spec: %v\n", err) - os.Exit(1) - } - - // Configure libopenapi to skip resolving external references. - // We handle external $refs via import mappings — the referenced specs - // don't need to be fetched or parsed. See pb33f/libopenapi#519. - docConfig := datamodel.NewDocumentConfiguration() - docConfig.SkipExternalRefResolution = true - - doc, err := libopenapi.NewDocumentWithConfiguration(specData, docConfig) - if err != nil { - fmt.Fprintf(os.Stderr, "error parsing spec: %v\n", err) - os.Exit(1) - } - - // Parse config if provided, otherwise use empty config - var cfg codegen.Configuration - if *configPath != "" { - configData, err := os.ReadFile(*configPath) - if err != nil { - fmt.Fprintf(os.Stderr, "error reading config: %v\n", err) - os.Exit(1) - } - if err := yaml.Unmarshal(configData, &cfg); err != nil { - fmt.Fprintf(os.Stderr, "error parsing config: %v\n", err) - os.Exit(1) - } - } - - // Flags override config file values - if *flagPackage != "" { - cfg.PackageName = *flagPackage - } - if *flagOutput != "" { - cfg.Output = *flagOutput - } - - // Default output to .gen.go - if cfg.Output == "" { - base := filepath.Base(specPath) - ext := filepath.Ext(base) - cfg.Output = strings.TrimSuffix(base, ext) + ".gen.go" - } - - // Default package name from output file - if cfg.PackageName == "" { - cfg.PackageName = "api" - } - - // Generate code - code, err := codegen.Generate(doc, specData, cfg) - if err != nil { - fmt.Fprintf(os.Stderr, "error generating code: %v\n", err) - os.Exit(1) - } - - // Write output - if err := os.WriteFile(cfg.Output, []byte(code), 0644); err != nil { - fmt.Fprintf(os.Stderr, "error writing output: %v\n", err) - os.Exit(1) - } - - fmt.Printf("Generated %s\n", cfg.Output) -} diff --git a/experimental/examples/callback/client/main.go b/experimental/examples/callback/client/main.go deleted file mode 100644 index 9e618448a3..0000000000 --- a/experimental/examples/callback/client/main.go +++ /dev/null @@ -1,144 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "flag" - "fmt" - "log" - "net" - "net/http" - "sync" - "sync/atomic" - - treefarm "github.com/oapi-codegen/oapi-codegen/experimental/examples/callback" -) - -// trees and cities for our planting requests -var treeKinds = []string{ - "oak", "maple", "pine", "birch", "willow", - "cedar", "elm", "ash", "cherry", "walnut", -} - -var cities = []string{ - "Providence", "Austin", "Denver", "Seattle", "Chicago", - "Boston", "Miami", "Nashville", "Savannah", "Mountain View", -} - -// CallbackReceiver implements treefarm.CallbackReceiverInterface. -type CallbackReceiver struct { - received atomic.Int32 - total int - done chan struct{} - once sync.Once - - mu sync.Mutex - ordinals map[string]int // UUID string -> 1-based planting order -} - -var _ treefarm.CallbackReceiverInterface = (*CallbackReceiver)(nil) - -func NewCallbackReceiver(total int) *CallbackReceiver { - return &CallbackReceiver{ - total: total, - done: make(chan struct{}), - ordinals: make(map[string]int), - } -} - -func (cr *CallbackReceiver) Register(id string, ordinal int) { - cr.mu.Lock() - cr.ordinals[id] = ordinal - cr.mu.Unlock() -} - -func (cr *CallbackReceiver) HandleTreePlantedCallback(w http.ResponseWriter, r *http.Request) { - var result treefarm.TreePlantingResult - if err := json.NewDecoder(r.Body).Decode(&result); err != nil { - log.Printf("Error decoding callback: %v", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - cr.mu.Lock() - ordinal := cr.ordinals[result.ID.String()] - cr.mu.Unlock() - - n := cr.received.Add(1) - log.Printf("Callback %d/%d received: tree #%d success=%v", n, cr.total, ordinal, result.Success) - - w.WriteHeader(http.StatusOK) - - if int(n) >= cr.total { - cr.once.Do(func() { close(cr.done) }) - } -} - -func main() { - serverAddr := flag.String("server", "http://localhost:8080", "Tree farm server address") - flag.Parse() - - const numTrees = 10 - - // Start callback receiver on an ephemeral port. - receiver := NewCallbackReceiver(numTrees) - - mux := http.NewServeMux() - mux.Handle("/tree_callback", treefarm.TreePlantedCallbackHandler(receiver, nil)) - - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - log.Fatalf("Failed to listen: %v", err) - } - callbackPort := listener.Addr().(*net.TCPAddr).Port - callbackURL := fmt.Sprintf("http://localhost:%d/tree_callback", callbackPort) - log.Printf("Callback receiver listening on port %d", callbackPort) - - go func() { - if err := http.Serve(listener, mux); err != nil { - log.Printf("Callback server stopped: %v", err) - } - }() - - // Send 10 tree planting requests. - client := &http.Client{} - for i := range numTrees { - req := treefarm.TreePlantingRequest{ - Kind: treeKinds[i], - Location: cities[i], - CallbackURL: callbackURL, - } - - body, err := json.Marshal(req) - if err != nil { - log.Fatalf("Failed to marshal request: %v", err) - } - - resp, err := client.Post( - *serverAddr+"/api/plant_tree", - "application/json", - bytes.NewReader(body), - ) - if err != nil { - log.Fatalf("Failed to plant tree %d: %v", i+1, err) - } - - var accepted treefarm.TreeWithID - if err := json.NewDecoder(resp.Body).Decode(&accepted); err != nil { - _ = resp.Body.Close() - log.Fatalf("Failed to decode response: %v", err) - } - _ = resp.Body.Close() - - receiver.Register(accepted.ID.String(), i+1) - log.Printf("Planted tree %d/%d: id=%s kind=%q location=%q", - i+1, numTrees, accepted.ID, accepted.Kind, accepted.Location) - } - - log.Printf("All %d trees planted, waiting for callbacks...", numTrees) - - // Wait for all callbacks. - <-receiver.done - - log.Printf("All %d callbacks received, done!", numTrees) -} diff --git a/experimental/examples/callback/config.yaml b/experimental/examples/callback/config.yaml deleted file mode 100644 index 29392d1127..0000000000 --- a/experimental/examples/callback/config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -package: treefarm -output: treefarm.gen.go -generation: - callback-initiator: true - callback-receiver: true - server: std-http diff --git a/experimental/examples/callback/doc.go b/experimental/examples/callback/doc.go deleted file mode 100644 index c441b88a3a..0000000000 --- a/experimental/examples/callback/doc.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config config.yaml tree-farm.yaml - -// Package treefarm provides an example of how to handle OpenAPI callbacks. -// We create a server which plants trees. The client asks the server to plant -// a tree and requests a callback when the planting is done. -// -// The server program will wait 1-5 seconds before notifying the client that a -// tree has been planted. -// -// You can run the example by running these two commands in parallel -// go run ./server --port 8080 -// go run ./client --server http://localhost:8080 -package treefarm diff --git a/experimental/examples/callback/server/main.go b/experimental/examples/callback/server/main.go deleted file mode 100644 index 4b255de3b6..0000000000 --- a/experimental/examples/callback/server/main.go +++ /dev/null @@ -1,116 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "flag" - "log" - "math/rand/v2" - "net" - "net/http" - "time" - - "github.com/google/uuid" - - treefarm "github.com/oapi-codegen/oapi-codegen/experimental/examples/callback" -) - -// TreeFarm implements treefarm.ServerInterface. -type TreeFarm struct { - initiator *treefarm.CallbackInitiator -} - -var _ treefarm.ServerInterface = (*TreeFarm)(nil) - -func NewTreeFarm() *TreeFarm { - initiator, err := treefarm.NewCallbackInitiator() - if err != nil { - log.Fatalf("Failed to create callback initiator: %v", err) - } - return &TreeFarm{initiator: initiator} -} - -func sendError(w http.ResponseWriter, code int, message string) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - _ = json.NewEncoder(w).Encode(treefarm.Error{ - Code: int32(code), - Message: message, - }) -} - -func (tf *TreeFarm) PlantTree(w http.ResponseWriter, r *http.Request) { - log.Printf("Received PlantTree request from %s", r.RemoteAddr) - - var req treefarm.TreePlantingRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - sendError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) - return - } - - if req.CallbackURL == "" { - sendError(w, http.StatusBadRequest, "callbackUrl is required") - return - } - - id := uuid.New() - - log.Printf("Accepted tree planting: id=%s kind=%q location=%q callbackUrl=%q", - id, req.Kind, req.Location, req.CallbackURL) - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusAccepted) - _ = json.NewEncoder(w).Encode(treefarm.TreeWithID{ - Location: req.Location, - Kind: req.Kind, - ID: id, - }) - - go tf.plantAndNotify(id, req) -} - -func (tf *TreeFarm) plantAndNotify(id uuid.UUID, req treefarm.TreePlantingRequest) { - delay := time.Duration(1+rand.IntN(5)) * time.Second - log.Printf("Planting tree %s (kind=%q, location=%q) — will complete in %s", - id, req.Kind, req.Location, delay) - - time.Sleep(delay) - - result := treefarm.TreePlantedJSONRequestBody{ - ID: id, - Kind: req.Kind, - Location: req.Location, - Success: true, - } - - log.Printf("Tree %s planted, invoking callback at %s", id, req.CallbackURL) - - resp, err := tf.initiator.TreePlanted(context.Background(), req.CallbackURL, result) - if err != nil { - log.Printf("Callback to %s failed: %v", req.CallbackURL, err) - return - } - defer func() { _ = resp.Body.Close() }() - - log.Printf("Callback to %s returned status %d", req.CallbackURL, resp.StatusCode) -} - -func main() { - port := flag.String("port", "8080", "Port for HTTP server") - flag.Parse() - - farm := NewTreeFarm() - - mux := http.NewServeMux() - treefarm.HandlerFromMux(farm, mux) - - // Wrap with request logging. - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) - mux.ServeHTTP(w, r) - }) - - addr := net.JoinHostPort("0.0.0.0", *port) - log.Printf("Tree Farm server listening on %s", addr) - log.Fatal(http.ListenAndServe(addr, handler)) -} diff --git a/experimental/examples/callback/tree-farm.yaml b/experimental/examples/callback/tree-farm.yaml deleted file mode 100644 index 4cad525c08..0000000000 --- a/experimental/examples/callback/tree-farm.yaml +++ /dev/null @@ -1,138 +0,0 @@ -openapi: "3.1.0" -info: - version: 1.0.0 - title: Tree Farm - description: | - A tree planting service that demonstrates OpenAPI callbacks. - The client submits a tree planting request along with a callback URL. - When the tree has been planted, the server sends a POST to the callback URL - to notify the client of the result. - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0.html -paths: - /api/plant_tree: - post: - summary: Request a tree planting - description: | - Submits a request to plant a tree at the specified location. - This is a long-running operation — the server accepts the request - and later notifies the caller via the provided callback URL. - operationId: PlantTree - requestBody: - $ref: '#/components/requestBodies/TreePlanting' - responses: - '202': - description: Tree planting request accepted - content: - application/json: - schema: - $ref: '#/components/schemas/TreeWithId' - default: - description: unexpected error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - callbacks: - treePlanted: - '{$request.body#/callbackUrl}': - post: - summary: Tree planting result notification - description: | - Sent by the server to the callback URL when the tree planting - operation completes (successfully or not). - operationId: TreePlanted - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/TreePlantingResult' - responses: - '200': - description: Callback received successfully - default: - description: unexpected error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' -components: - schemas: - Tree: - type: object - description: A tree to be planted - required: - - location - - kind - properties: - location: - type: string - description: Where to plant the tree (e.g. "north meadow") - kind: - type: string - description: What kind of tree to plant (e.g. "oak") - - TreePlantingRequest: - description: | - A tree planting request, combining the tree details with a callback URL - for completion notification. - allOf: - - $ref: '#/components/schemas/Tree' - - type: object - required: - - callbackUrl - properties: - callbackUrl: - type: string - format: uri - description: URL to receive the planting result callback - - TreeWithId: - description: A tree with a server-assigned identifier - allOf: - - $ref: '#/components/schemas/Tree' - - type: object - required: - - id - properties: - id: - type: string - format: uuid - description: Unique identifier for this planting request - - TreePlantingResult: - description: The result of a tree planting operation, sent via callback - allOf: - - $ref: '#/components/schemas/TreeWithId' - - type: object - required: - - success - properties: - success: - type: boolean - description: Whether the tree was successfully planted - - Error: - type: object - required: - - code - - message - properties: - code: - type: integer - format: int32 - description: Error code - message: - type: string - description: Error message - - requestBodies: - TreePlanting: - description: Tree planting request - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/TreePlantingRequest' diff --git a/experimental/examples/callback/treefarm.gen.go b/experimental/examples/callback/treefarm.gen.go deleted file mode 100644 index a1018c08e9..0000000000 --- a/experimental/examples/callback/treefarm.gen.go +++ /dev/null @@ -1,503 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package treefarm - -import ( - "bytes" - "compress/gzip" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "sync" - - "github.com/google/uuid" -) - -// #/components/schemas/Tree -// A tree to be planted -type Tree struct { - Location string `json:"location" form:"location"` // Where to plant the tree (e.g. "north meadow") - Kind string `json:"kind" form:"kind"` // What kind of tree to plant (e.g. "oak") -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Tree) ApplyDefaults() { -} - -// #/components/schemas/TreePlantingRequest -// A tree planting request, combining the tree details with a callback URL -// for completion notification. -type TreePlantingRequest struct { - Location string `json:"location" form:"location"` // Where to plant the tree (e.g. "north meadow") - Kind string `json:"kind" form:"kind"` // What kind of tree to plant (e.g. "oak") - CallbackURL string `json:"callbackUrl" form:"callbackUrl"` // URL to receive the planting result callback -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *TreePlantingRequest) ApplyDefaults() { -} - -// #/components/schemas/TreeWithId -// A tree with a server-assigned identifier -type TreeWithID struct { - Location string `json:"location" form:"location"` // Where to plant the tree (e.g. "north meadow") - Kind string `json:"kind" form:"kind"` // What kind of tree to plant (e.g. "oak") - ID UUID `json:"id" form:"id"` // Unique identifier for this planting request -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *TreeWithID) ApplyDefaults() { -} - -// #/components/schemas/TreePlantingResult -// The result of a tree planting operation, sent via callback -type TreePlantingResult struct { - Location string `json:"location" form:"location"` // Where to plant the tree (e.g. "north meadow") - Kind string `json:"kind" form:"kind"` // What kind of tree to plant (e.g. "oak") - ID UUID `json:"id" form:"id"` // Unique identifier for this planting request - Success bool `json:"success" form:"success"` // Whether the tree was successfully planted -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *TreePlantingResult) ApplyDefaults() { -} - -// #/components/schemas/Error -type Error struct { - Code int32 `json:"code" form:"code"` // Error code - Message string `json:"message" form:"message"` // Error message -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Error) ApplyDefaults() { -} - -type UUID = uuid.UUID - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/8RXwW7bOBC96ysGaQG3QCO76U23bJEFAhSbIHGQ44IWxxYbilTJUbzG7gL7EfuF+yUL", - "khJFy4rr9NKcopDzZvhm5s1EN6hYIwo4+5R/zBdnmVBrXWQAz2is0KqAj/kiX2QAJEhiAUuDCL8yU2cA", - "HG1pREP+3l8ZAMAlkLvQSKZIqA1YNM+iRKCKEXCstbJkGKGFmwbV5e01lEzKFSufbO4BlhVCKQUqAtuu", - "akEW2AjT4LcWLQGTWm1gK6gCFmHg4e5LQHqsUAFVGKwrZmGFqAIM8g/+yIWHBiwq7vzc3twvgbQ/SgE9", - "HmlQmsR6F45DjHrtvwzaVpLzK0WJymLhTRSrsYDLhpUVwoWnEaA1soCKqLHFfL7dbnPmz3NtNvPO2s6/", - "XH+++u3+6vwiX+QV1TJrGFXWoc5ZI+b+Eb+7hwVHjbYUfgOwbV0zsyvgrudpn7/u2kTy3M99JL2nmXQw", - "7XEYBeoaLMVaIAepS+Zg8giyrIQF4UBcis5Nq5RLnG7Q+Jvw3z//pvyzssSGbEeldxuxmOIgGaEJ7Au0", - "MT1o4Fkw/9kY/Sw48ok6gMHvNS/g1r3FVXF32Pn7RfNdEZ2+NbguYPZmXuq60QoV2flwUaCdO4TbjtFZ", - "hLKNdukbgGYXi4vZ8DmifTld154N5IlVqRWhohQIgDWNFIH6+Ver1f4pgC0rrNn4r9OPC3fDsx4FVdd8", - "lg0hr1kr6cVXtAr/aLAk5IDGaPMz4r5yjvuQo6QMGNSnC3kKPPvzbcd6vtJ892bemz4Y+fdsP4S0x2Ko", - "fa+NM+nkoCvY8NKR4QvdF7vQactql/bIhCzBdk/iRg0+/Ax955iT6OT3nW3LEq1dt1LuQPvuep9nLxi6", - "xlkOFI6uTbZQeigM8gLItHhwPFkhp9XJ8Wo5rdb7Fr7zGZsdPOygoZPGXsymvO5l9nOfLYMlimfkkNKe", - "HdoedNprO+4EXk9l9nvcntSRw4mD6Q4D4jKOLwDaNViAXn3FkqYGVLdXkIYV9iM8GxVYDPM8jqTkT09C", - "9RaNcYVNIs1rb5E+NgRlyew31V5gjxUaHGZkbMZ3mG9yOFPaUAU1Mq63Z+8jiAvm9Z4YeUO/dHRsBK+d", - "M82ezt5nkduhtn17FscH/+X0lvXBacZK+PkdH8eRmJB2aveKeGtterlxypNqYS8zTMqbdZq273XrLLk7", - "UTHT1RCuJ8KenExVQjpBHowcV/8LqeqeXDMqoDXimNw75SbdS0JYX0aTo/c/JDMM5eJIb3TJCOPinFkr", - "Ngo5CI7K703mJ7Au+AlkC/4jHLeCHyVZiW8tJq/3BUluMR2X+FTH2ESG91e2uO27Nhz/axIH5gf3PwX5", - "9TTm8gfpH69jr05CN3JOyER3czodK60lsqOLzGOFVLldpReKLbN7Ey9Kt0fxI+LIBJhS9lJzTD5rtJZt", - "8IiwO4NDqRWKcIPp4OwLSyj6dPGSBvuI92PoInitmgekPvwsXaJi+GlJThfjlGJnR/aug63g2CYwNf1P", - "36d8LLPs/wAAAP//y9dX5mEQAAA=", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} - -// ServerInterface represents all server handlers. -type ServerInterface interface { - // Request a tree planting - // (POST /api/plant_tree) - PlantTree(w http.ResponseWriter, r *http.Request) -} - -// ServerInterfaceWrapper converts HTTP requests to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -// MiddlewareFunc is a middleware function type. -type MiddlewareFunc func(http.Handler) http.Handler - -// PlantTree operation middleware -func (siw *ServerInterfaceWrapper) PlantTree(w http.ResponseWriter, r *http.Request) { - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.PlantTree(w, r) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// 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)) - ServeHTTP(w http.ResponseWriter, r *http.Request) -} - -// StdHTTPServerOptions configures the StdHTTP server. -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, - }) -} - -// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. -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) - } - } - - wrapper := ServerInterfaceWrapper{ - Handler: si, - HandlerMiddlewares: options.Middlewares, - ErrorHandlerFunc: options.ErrorHandlerFunc, - } - - m.HandleFunc("POST "+options.BaseURL+"/api/plant_tree", wrapper.PlantTree) - return m -} - -// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. -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 -} - -// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. -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 -} - -// RequiredParamError is returned when a required parameter is missing. -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -// RequiredHeaderError is returned when a required header is missing. -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 -} - -// InvalidParamFormatError is returned when a parameter has an invalid format. -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 -} - -// TooManyValuesForParamError is returned when a parameter has too many values. -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) -} - -type TreePlantedJSONRequestBody = TreePlantingResult - -// RequestEditorFn is the function signature for the RequestEditor callback function. -// It may already be defined if client code is also generated; this is a compatible redeclaration. -type RequestEditorFn func(ctx context.Context, req *http.Request) error - -// HttpRequestDoer performs HTTP requests. -// The standard http.Client implements this interface. -type HttpRequestDoer interface { - Do(req *http.Request) (*http.Response, error) -} - -// CallbackInitiator sends callback requests to target URLs. -// Unlike Client, it has no stored base URL — the full target URL is provided per-call. -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 request. This can be used to mutate the request. -func WithCallbackRequestEditorFn(fn RequestEditorFn) CallbackInitiatorOption { - return func(p *CallbackInitiator) error { - p.RequestEditors = append(p.RequestEditors, fn) - return nil - } -} - -func (p *CallbackInitiator) applyEditors(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 { - // TreePlantedWithBody sends a POST callback request - TreePlantedWithBody(ctx context.Context, targetURL string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - TreePlanted(ctx context.Context, targetURL string, body TreePlantedJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) -} - -// TreePlantedWithBody sends a POST callback request -// Tree planting result notification -func (p *CallbackInitiator) TreePlantedWithBody(ctx context.Context, targetURL string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewTreePlantedCallbackRequestWithBody(targetURL, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := p.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return p.Client.Do(req) -} - -// TreePlanted sends a POST callback request with JSON body -func (p *CallbackInitiator) TreePlanted(ctx context.Context, targetURL string, body TreePlantedJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewTreePlantedCallbackRequest(targetURL, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := p.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return p.Client.Do(req) -} - -// NewTreePlantedCallbackRequest creates a POST request for the callback with application/json body -func NewTreePlantedCallbackRequest(targetURL string, body TreePlantedJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewTreePlantedCallbackRequestWithBody(targetURL, "application/json", bodyReader) -} - -// NewTreePlantedCallbackRequestWithBody creates a POST request for the callback with any body -func NewTreePlantedCallbackRequestWithBody(targetURL string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - parsedURL, err := url.Parse(targetURL) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", parsedURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// CallbackHttpError represents an HTTP error response. -// The type parameter E is the type of the parsed error body. -type CallbackHttpError[E any] struct { - StatusCode int - Body E - RawBody []byte -} - -func (e *CallbackHttpError[E]) Error() string { - return fmt.Sprintf("HTTP %d", e.StatusCode) -} - -// SimpleCallbackInitiator wraps CallbackInitiator with typed responses for operations that have -// unambiguous response types. Methods return the success type directly, -// and HTTP errors are returned as *CallbackHttpError[E] where E is the error type. -type SimpleCallbackInitiator struct { - *CallbackInitiator -} - -// NewSimpleCallbackInitiator creates a new SimpleCallbackInitiator which wraps a CallbackInitiator. -func NewSimpleCallbackInitiator(opts ...CallbackInitiatorOption) (*SimpleCallbackInitiator, error) { - initiator, err := NewCallbackInitiator(opts...) - if err != nil { - return nil, err - } - return &SimpleCallbackInitiator{CallbackInitiator: initiator}, nil -} - -// CallbackReceiverInterface represents handlers for receiving callback requests. -type CallbackReceiverInterface interface { - // Tree planting result notification - // HandleTreePlantedCallback handles the POST callback request. - HandleTreePlantedCallback(w http.ResponseWriter, r *http.Request) -} - -// CallbackReceiverMiddlewareFunc is a middleware function for callback receiver handlers. -type CallbackReceiverMiddlewareFunc func(http.Handler) http.Handler - -// TreePlantedCallbackHandler returns an http.Handler for the TreePlanted callback. -// The caller is responsible for registering this handler at the appropriate path. -func TreePlantedCallbackHandler(si CallbackReceiverInterface, errHandler func(w http.ResponseWriter, r *http.Request, err error), middlewares ...CallbackReceiverMiddlewareFunc) http.Handler { - if errHandler == nil { - errHandler = func(w http.ResponseWriter, r *http.Request, err error) { - http.Error(w, err.Error(), http.StatusBadRequest) - } - } - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - si.HandleTreePlantedCallback(w, r) - })) - - for _, middleware := range middlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) - }) -} diff --git a/experimental/examples/petstore-expanded/chi/Makefile b/experimental/examples/petstore-expanded/chi/Makefile deleted file mode 100644 index 42389f4137..0000000000 --- a/experimental/examples/petstore-expanded/chi/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -SHELL:=/bin/bash - -YELLOW := \e[0;33m -RESET := \e[0;0m - -GOVER := $(shell go env GOVERSION) -GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") - -define execute-if-go-124 -@{ \ -if [[ 24 -le $(GOMINOR) ]]; then \ - $1; \ -else \ - echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ -fi \ -} -endef - -lint: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) - -lint-ci: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) - -generate: - $(call execute-if-go-124,go generate ./...) - -test: - $(call execute-if-go-124,go test -cover ./...) - -tidy: - $(call execute-if-go-124,go mod tidy) - -tidy-ci: - $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/chi/go.mod b/experimental/examples/petstore-expanded/chi/go.mod deleted file mode 100644 index 3c9d10741e..0000000000 --- a/experimental/examples/petstore-expanded/chi/go.mod +++ /dev/null @@ -1,11 +0,0 @@ -module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/chi - -go 1.24.0 - -require ( - github.com/go-chi/chi/v5 v5.2.0 - github.com/google/uuid v1.6.0 - github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 -) - -replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/chi/go.sum b/experimental/examples/petstore-expanded/chi/go.sum deleted file mode 100644 index ad495c8907..0000000000 --- a/experimental/examples/petstore-expanded/chi/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= -github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/experimental/examples/petstore-expanded/chi/main.go b/experimental/examples/petstore-expanded/chi/main.go deleted file mode 100644 index 8a4e0e3790..0000000000 --- a/experimental/examples/petstore-expanded/chi/main.go +++ /dev/null @@ -1,40 +0,0 @@ -//go:build go1.22 - -// This is an example of implementing the Pet Store from the OpenAPI documentation -// found at: -// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml - -package main - -import ( - "flag" - "log" - "net" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/chi/server" -) - -func main() { - port := flag.String("port", "8080", "Port for test HTTP server") - flag.Parse() - - // Create an instance of our handler which satisfies the generated interface - petStore := server.NewPetStore() - - r := chi.NewRouter() - - // We now register our petStore above as the handler for the interface - server.HandlerFromMux(petStore, r) - - s := &http.Server{ - Handler: r, - Addr: net.JoinHostPort("0.0.0.0", *port), - } - - log.Printf("Server listening on %s", s.Addr) - - // And we serve HTTP until the world ends. - log.Fatal(s.ListenAndServe()) -} diff --git a/experimental/examples/petstore-expanded/chi/server/petstore.go b/experimental/examples/petstore-expanded/chi/server/petstore.go deleted file mode 100644 index 2f52c8e271..0000000000 --- a/experimental/examples/petstore-expanded/chi/server/petstore.go +++ /dev/null @@ -1,135 +0,0 @@ -//go:build go1.22 - -package server - -import ( - "encoding/json" - "fmt" - "net/http" - "sync" - - petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" -) - -// PetStore implements the ServerInterface. -type PetStore struct { - Pets map[int64]petstore.Pet - NextId int64 - Lock sync.Mutex -} - -// Make sure we conform to ServerInterface -var _ ServerInterface = (*PetStore)(nil) - -// NewPetStore creates a new PetStore. -func NewPetStore() *PetStore { - return &PetStore{ - Pets: make(map[int64]petstore.Pet), - NextId: 1000, - } -} - -// sendPetStoreError wraps sending of an error in the Error format. -func sendPetStoreError(w http.ResponseWriter, code int, message string) { - petErr := petstore.Error{ - Code: int32(code), - Message: message, - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - _ = json.NewEncoder(w).Encode(petErr) -} - -// FindPets returns all pets, optionally filtered by tags and limited. -func (p *PetStore) FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) { - p.Lock.Lock() - defer p.Lock.Unlock() - - var result []petstore.Pet - - for _, pet := range p.Pets { - if params.Tags != nil { - // If we have tags, filter pets by tag - for _, t := range *params.Tags { - if pet.Tag != nil && (*pet.Tag == t) { - result = append(result, pet) - } - } - } else { - // Add all pets if we're not filtering - result = append(result, pet) - } - - if params.Limit != nil { - l := int(*params.Limit) - if len(result) >= l { - // We're at the limit - break - } - } - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(result) -} - -// AddPet creates a new pet. -func (p *PetStore) AddPet(w http.ResponseWriter, r *http.Request) { - // We expect a NewPet object in the request body. - var newPet petstore.NewPet - if err := json.NewDecoder(r.Body).Decode(&newPet); err != nil { - sendPetStoreError(w, http.StatusBadRequest, "Invalid format for NewPet") - return - } - - // We now have a pet, let's add it to our "database". - p.Lock.Lock() - defer p.Lock.Unlock() - - // We handle pets, not NewPets, which have an additional ID field - var pet petstore.Pet - pet.Name = newPet.Name - pet.Tag = newPet.Tag - pet.ID = p.NextId - p.NextId++ - - // Insert into map - p.Pets[pet.ID] = pet - - // Now, we have to return the Pet - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(pet) -} - -// FindPetByID returns a pet by ID. -func (p *PetStore) FindPetByID(w http.ResponseWriter, r *http.Request, id int64) { - p.Lock.Lock() - defer p.Lock.Unlock() - - pet, found := p.Pets[id] - if !found { - sendPetStoreError(w, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(pet) -} - -// DeletePet deletes a pet by ID. -func (p *PetStore) DeletePet(w http.ResponseWriter, r *http.Request, id int64) { - p.Lock.Lock() - defer p.Lock.Unlock() - - _, found := p.Pets[id] - if !found { - sendPetStoreError(w, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) - return - } - delete(p.Pets, id) - - w.WriteHeader(http.StatusNoContent) -} diff --git a/experimental/examples/petstore-expanded/chi/server/server.config.yaml b/experimental/examples/petstore-expanded/chi/server/server.config.yaml deleted file mode 100644 index b3b3509411..0000000000 --- a/experimental/examples/petstore-expanded/chi/server/server.config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -package: server -generation: - server: chi - models-package: - path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded - alias: petstore diff --git a/experimental/examples/petstore-expanded/chi/server/server.gen.go b/experimental/examples/petstore-expanded/chi/server/server.gen.go deleted file mode 100644 index 167ae93385..0000000000 --- a/experimental/examples/petstore-expanded/chi/server/server.gen.go +++ /dev/null @@ -1,1039 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package server - -import ( - "bytes" - "encoding" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "reflect" - "sort" - "strconv" - "strings" - "time" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" -) - -// ServerInterface represents all server handlers. -type ServerInterface interface { - // Returns all pets - // (GET /pets) - FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) - // Creates a new pet - // (POST /pets) - AddPet(w http.ResponseWriter, r *http.Request) - // Deletes a pet by ID - // (DELETE /pets/{id}) - DeletePet(w http.ResponseWriter, r *http.Request, id int64) - // Returns a pet by ID - // (GET /pets/{id}) - FindPetByID(w http.ResponseWriter, r *http.Request, id int64) -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. -type Unimplemented struct{} - -// Returns all pets -// (GET /pets) -func (_ Unimplemented) FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) { - w.WriteHeader(http.StatusNotImplemented) -} - -// Creates a new pet -// (POST /pets) -func (_ Unimplemented) AddPet(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotImplemented) -} - -// Deletes a pet by ID -// (DELETE /pets/{id}) -func (_ Unimplemented) DeletePet(w http.ResponseWriter, r *http.Request, id int64) { - w.WriteHeader(http.StatusNotImplemented) -} - -// Returns a pet by ID -// (GET /pets/{id}) -func (_ Unimplemented) FindPetByID(w http.ResponseWriter, r *http.Request, id int64) { - w.WriteHeader(http.StatusNotImplemented) -} - -// FindPetsParams defines parameters for FindPets. -type FindPetsParams struct { - // tags (optional) - Tags *[]string `form:"tags" json:"tags"` - // limit (optional) - Limit *int32 `form:"limit" json:"limit"` -} - -// ServerInterfaceWrapper converts HTTP requests to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -// MiddlewareFunc is a middleware function type. -type MiddlewareFunc func(http.Handler) http.Handler - -// FindPets operation middleware -func (siw *ServerInterfaceWrapper) FindPets(w http.ResponseWriter, r *http.Request) { - var err error - - // Parameter object where we will unmarshal all parameters from the context - var params FindPetsParams - - // ------------- Optional query parameter "tags" ------------- - err = BindFormExplodeParam("tags", false, r.URL.Query(), ¶ms.Tags) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tags", Err: err}) - return - } - - // ------------- Optional query parameter "limit" ------------- - err = BindFormExplodeParam("limit", false, r.URL.Query(), ¶ms.Limit) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.FindPets(w, r, params) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// AddPet operation middleware -func (siw *ServerInterfaceWrapper) AddPet(w http.ResponseWriter, r *http.Request) { - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.AddPet(w, r) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// DeletePet operation middleware -func (siw *ServerInterfaceWrapper) DeletePet(w http.ResponseWriter, r *http.Request) { - var err error - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, chi.URLParam(r, "id"), &id) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DeletePet(w, r, id) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// FindPetByID operation middleware -func (siw *ServerInterfaceWrapper) FindPetByID(w http.ResponseWriter, r *http.Request) { - var err error - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, chi.URLParam(r, "id"), &id) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.FindPetByID(w, r, id) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// Handler creates http.Handler with routing matching OpenAPI spec. -func Handler(si ServerInterface) http.Handler { - return HandlerWithOptions(si, ChiServerOptions{}) -} - -// ChiServerOptions configures the Chi server. -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, - }) -} - -// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. -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) - } - } - - wrapper := ServerInterfaceWrapper{ - Handler: si, - HandlerMiddlewares: options.Middlewares, - ErrorHandlerFunc: options.ErrorHandlerFunc, - } - - r.Group(func(r chi.Router) { - r.Get(options.BaseURL+"/pets", wrapper.FindPets) - }) - r.Group(func(r chi.Router) { - r.Post(options.BaseURL+"/pets", wrapper.AddPet) - }) - r.Group(func(r chi.Router) { - r.Delete(options.BaseURL+"/pets/{id}", wrapper.DeletePet) - }) - r.Group(func(r chi.Router) { - r.Get(options.BaseURL+"/pets/{id}", wrapper.FindPetByID) - }) - return r -} - -// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. -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 -} - -// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. -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 -} - -// RequiredParamError is returned when a required parameter is missing. -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -// RequiredHeaderError is returned when a required header is missing. -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 -} - -// InvalidParamFormatError is returned when a parameter has an invalid format. -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 -} - -// TooManyValuesForParamError is returned when a parameter has too many values. -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) -} - -// ParamLocation indicates where a parameter is located in an HTTP request. -type ParamLocation int - -const ( - ParamLocationUndefined ParamLocation = iota - ParamLocationQuery - ParamLocationPath - ParamLocationHeader - ParamLocationCookie -) - -// Binder is an interface for types that can bind themselves from a string value. -type Binder interface { - Bind(value string) error -} - -// DateFormat is the format used for date (without time) parameters. -const DateFormat = "2006-01-02" - -// Date represents a date (without time) for OpenAPI date format. -type Date struct { - time.Time -} - -// UnmarshalText implements encoding.TextUnmarshaler for Date. -func (d *Date) UnmarshalText(data []byte) error { - t, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = t - return nil -} - -// MarshalText implements encoding.TextMarshaler for Date. -func (d Date) MarshalText() ([]byte, error) { - return []byte(d.Format(DateFormat)), nil -} - -// Format returns the date formatted according to layout. -func (d Date) Format(layout string) string { - return d.Time.Format(layout) -} - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, types.Date, and types that implement -// json.Marshaler or fmt.Stringer. -func primitiveToString(value interface{}) (string, error) { - // Check for known types first (time, date, uuid) - if res, ok := marshalKnownTypes(value); ok { - return res, nil - } - - // Dereference pointers for optional values - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - kind := t.Kind() - - switch kind { - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: - return strconv.FormatInt(v.Int(), 10), nil - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: - return strconv.FormatUint(v.Uint(), 10), nil - case reflect.Float64: - return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil - case reflect.Float32: - return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil - case reflect.Bool: - if v.Bool() { - return "true", nil - } - return "false", nil - case reflect.String: - return v.String(), nil - case reflect.Struct: - // Check if it's a UUID - if u, ok := value.(uuid.UUID); ok { - return u.String(), nil - } - // Check if it implements json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - var i2 interface{} - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to decode JSON: %w", err) - } - return primitiveToString(i2) - } - fallthrough - default: - if s, ok := value.(fmt.Stringer); ok { - return s.String(), nil - } - return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) - } -} - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. -func marshalKnownTypes(value interface{}) (string, bool) { - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - tt := v.Convert(reflect.TypeOf(time.Time{})) - timeVal := tt.Interface().(time.Time) - return timeVal.Format(time.RFC3339Nano), true - } - - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - d := v.Convert(reflect.TypeOf(Date{})) - dateVal := d.Interface().(Date) - return dateVal.Format(DateFormat), true - } - - if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { - u := v.Convert(reflect.TypeOf(uuid.UUID{})) - uuidVal := u.Interface().(uuid.UUID) - return uuidVal.String(), true - } - - return "", false -} - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -func escapeParameterString(value string, paramLocation ParamLocation) string { - switch paramLocation { - case ParamLocationQuery: - return url.QueryEscape(value) - case ParamLocationPath: - return url.PathEscape(value) - default: - return value - } -} - -// unescapeParameterString unescapes a parameter value based on its location. -func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { - switch paramLocation { - case ParamLocationQuery, ParamLocationUndefined: - return url.QueryUnescape(value) - case ParamLocationPath: - return url.PathUnescape(value) - default: - return value, nil - } -} - -// sortedKeys returns the keys of a map in sorted order. -func sortedKeys(m map[string]string) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. -func BindStringToObject(src string, dst interface{}) error { - // Check for TextUnmarshaler - if tu, ok := dst.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(src)) - } - - // Check for Binder interface - if b, ok := dst.(Binder); ok { - return b.Bind(src) - } - - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr { - return fmt.Errorf("dst must be a pointer, got %T", dst) - } - v = v.Elem() - - switch v.Kind() { - case reflect.String: - v.SetString(src) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - i, err := strconv.ParseInt(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - v.SetInt(i) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - u, err := strconv.ParseUint(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse uint: %w", err) - } - v.SetUint(u) - case reflect.Float32, reflect.Float64: - f, err := strconv.ParseFloat(src, 64) - if err != nil { - return fmt.Errorf("failed to parse float: %w", err) - } - v.SetFloat(f) - case reflect.Bool: - b, err := strconv.ParseBool(src) - if err != nil { - return fmt.Errorf("failed to parse bool: %w", err) - } - v.SetBool(b) - default: - // Try JSON unmarshal as a fallback - return json.Unmarshal([]byte(src), dst) - } - return nil -} - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. -func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - newArray := reflect.MakeSlice(t, len(parts), len(parts)) - for i, p := range parts { - err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) - if err != nil { - return fmt.Errorf("error setting array element: %w", err) - } - } - v.Set(newArray) - return nil -} - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. -func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { - var fields []string - if explode { - fields = make([]string, len(parts)) - for i, property := range parts { - propertyParts := strings.Split(property, "=") - if len(propertyParts) != 2 { - return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) - } - fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" - } - } else { - if len(parts)%2 != 0 { - return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) - } - fields = make([]string, len(parts)/2) - for i := 0; i < len(parts); i += 2 { - key := parts[i] - value := parts[i+1] - fields[i/2] = "\"" + key + "\":\"" + value + "\"" - } - } - jsonParam := "{" + strings.Join(fields, ",") + "}" - return json.Unmarshal([]byte(jsonParam), dest) -} - -// BindFormExplodeParam binds a form-style parameter with explode to a destination. -// Form style is the default for query and cookie parameters. -// This handles the exploded case where arrays come as multiple query params. -// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) -// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) -func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { - dv := reflect.Indirect(reflect.ValueOf(dest)) - v := dv - var output interface{} - - if required { - output = dest - } else { - // For optional parameters, allocate if nil - if v.IsNil() { - t := v.Type() - newValue := reflect.New(t.Elem()) - output = newValue.Interface() - } else { - output = v.Interface() - } - v = reflect.Indirect(reflect.ValueOf(output)) - } - - t := v.Type() - k := t.Kind() - - values, found := queryParams[paramName] - - switch k { - case reflect.Slice: - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := bindSplitPartsToDestinationArray(values, output) - if err != nil { - return err - } - case reflect.Struct: - // For exploded objects, fields are spread across query params - fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) - if err != nil { - return err - } - if !fieldsPresent { - return nil - } - default: - // Primitive - if len(values) == 0 { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - if len(values) != 1 { - return fmt.Errorf("multiple values for single value parameter '%s'", paramName) - } - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := BindStringToObject(values[0], output) - if err != nil { - return err - } - } - - if !required { - dv.Set(reflect.ValueOf(output)) - } - return nil -} - -// bindParamsToExplodedObject binds query params to struct fields for exploded objects. -func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { - binder, v, t := indirectBinder(dest) - if binder != nil { - _, found := values[paramName] - if !found { - return false, nil - } - return true, BindStringToObject(values.Get(paramName), dest) - } - if t.Kind() != reflect.Struct { - return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) - } - - fieldsPresent := false - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - if !v.Field(i).CanSet() { - continue - } - - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - - fieldVal, found := values[fieldName] - if found { - if len(fieldVal) != 1 { - return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) - } - err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) - if err != nil { - return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) - } - fieldsPresent = true - } - } - return fieldsPresent, nil -} - -// indirectBinder checks if dest implements Binder and returns reflect values. -func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { - v := reflect.ValueOf(dest) - if v.Type().NumMethod() > 0 && v.CanInterface() { - if u, ok := v.Interface().(Binder); ok { - return u, reflect.Value{}, nil - } - } - v = reflect.Indirect(v) - t := v.Type() - // Handle special types like time.Time and Date - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - return dest, reflect.Value{}, nil - } - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - return dest, reflect.Value{}, nil - } - return nil, v, t -} - -// BindSimpleParam binds a simple-style parameter without explode to a destination. -// Simple style is the default for path and header parameters. -// Arrays: a,b,c -> []string{"a", "b", "c"} -// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} -func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // Split on comma and bind as key,value pairs - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} - -// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. -// Form style is the default for query and cookie parameters. -// Primitives: paramName=value -// Arrays: paramName=a¶mName=b¶mName=c -// Objects: key1=value1&key2=value2 -func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleFormExplodeSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleFormExplodeStruct(paramName, paramLocation, value) - case reflect.Map: - return styleFormExplodeMap(paramName, paramLocation, value) - default: - return styleFormExplodePrimitive(paramName, paramLocation, value) - } -} - -func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Form with explode: paramName=a¶mName=b¶mName=c - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, "&"+prefix), nil -} - -func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleFormExplodeParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. -// Simple style is the default for path and header parameters. -// Arrays are comma-separated: a,b,c -// Objects are key,value pairs: key1,value1,key2,value2 -func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return escapeParameterString(string(b), paramLocation), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleSimpleSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleSimpleStruct(paramName, paramLocation, value) - case reflect.Map: - return styleSimpleMap(paramName, paramLocation, value) - default: - return styleSimplePrimitive(paramLocation, value) - } -} - -func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return escapeParameterString(strVal, paramLocation), nil -} - -func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return escapeParameterString(timeVal, paramLocation), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleSimpleParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -// structToFieldDict converts a struct to a map of field names to string values. -func structToFieldDict(value interface{}) (map[string]string, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - fieldDict := make(map[string]string) - - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - f := v.Field(i) - - // Skip nil optional fields - if f.Type().Kind() == reflect.Ptr && f.IsNil() { - continue - } - str, err := primitiveToString(f.Interface()) - if err != nil { - return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) - } - fieldDict[fieldName] = str - } - return fieldDict, nil -} diff --git a/experimental/examples/petstore-expanded/client/client.config.yaml b/experimental/examples/petstore-expanded/client/client.config.yaml deleted file mode 100644 index eee5ae5073..0000000000 --- a/experimental/examples/petstore-expanded/client/client.config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -package: client -output: client.gen.go -generation: - client: true - simple-client: true - models-package: - path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded - alias: petstore diff --git a/experimental/examples/petstore-expanded/client/client.gen.go b/experimental/examples/petstore-expanded/client/client.gen.go deleted file mode 100644 index d095e9cfda..0000000000 --- a/experimental/examples/petstore-expanded/client/client.gen.go +++ /dev/null @@ -1,1225 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package client - -import ( - "bytes" - "context" - "encoding" - "encoding/json" - "errors" - "fmt" - "github.com/google/uuid" - petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" - "io" - "net/http" - "net/url" - "reflect" - "sort" - "strconv" - "strings" - "time" -) - -type addPetJSONRequestBody = petstore.NewPet - -// RequestEditorFn is the function signature for the RequestEditor callback function. -type RequestEditorFn func(ctx context.Context, req *http.Request) error - -// HttpRequestDoer performs HTTP requests. -// The standard http.Client implements this interface. -type HttpRequestDoer interface { - Do(req *http.Request) (*http.Response, error) -} - -// Client which conforms to the OpenAPI3 specification for this service. -type Client struct { - // The endpoint of the server conforming to this interface, with scheme, - // https://api.deepmap.com for example. This can contain a path relative - // to the server, such as https://api.deepmap.com/dev-test, and all the - // paths in the swagger spec will be appended to the server. - Server string - - // 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 -} - -// ClientOption allows setting custom parameters during construction. -type ClientOption func(*Client) error - -// NewClient creates a new Client with reasonable defaults. -func NewClient(server string, opts ...ClientOption) (*Client, error) { - client := Client{ - Server: server, - } - for _, o := range opts { - if err := o(&client); err != nil { - return nil, err - } - } - // Ensure the server URL always has a trailing slash - if !strings.HasSuffix(client.Server, "/") { - client.Server += "/" - } - // Create httpClient if not already present - if client.Client == nil { - client.Client = &http.Client{} - } - return &client, nil -} - -// WithHTTPClient allows overriding the default Doer, which is -// automatically created using http.Client. This is useful for tests. -func WithHTTPClient(doer HttpRequestDoer) ClientOption { - return func(c *Client) error { - c.Client = doer - return nil - } -} - -// WithRequestEditorFn allows setting up a callback function, which will be -// called right before sending the request. This can be used to mutate the request. -func WithRequestEditorFn(fn RequestEditorFn) ClientOption { - return func(c *Client) error { - c.RequestEditors = append(c.RequestEditors, fn) - return nil - } -} - -// WithBaseURL overrides the baseURL. -func WithBaseURL(baseURL string) ClientOption { - return func(c *Client) error { - newBaseURL, err := url.Parse(baseURL) - if err != nil { - return err - } - c.Server = newBaseURL.String() - return nil - } -} - -func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { - for _, r := range c.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 -} - -// ClientInterface is the interface specification for the client. -type ClientInterface interface { - // FindPets makes a GET request to /pets - FindPets(ctx context.Context, params *FindPetsParams, reqEditors ...RequestEditorFn) (*http.Response, error) - // AddPetWithBody makes a POST request to /pets - AddPetWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - AddPet(ctx context.Context, body addPetJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - // DeletePet makes a DELETE request to /pets/{id} - DeletePet(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error) - // FindPetByID makes a GET request to /pets/{id} - FindPetByID(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error) -} - -// FindPetsParams defines parameters for FindPets. -type FindPetsParams struct { - // tags (optional) - Tags *[]string `form:"tags" json:"tags"` - // limit (optional) - Limit *int32 `form:"limit" json:"limit"` -} - -// FindPets makes a GET request to /pets -// Returns all pets -func (c *Client) FindPets(ctx context.Context, params *FindPetsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewFindPetsRequest(c.Server, params) - 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) -} - -// AddPetWithBody makes a POST request to /pets -// Creates a new pet -func (c *Client) AddPetWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewAddPetRequestWithBody(c.Server, contentType, body) - 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) -} - -// AddPet makes a POST request to /pets with JSON body -func (c *Client) AddPet(ctx context.Context, body addPetJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewAddPetRequest(c.Server, body) - 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) -} - -// DeletePet makes a DELETE request to /pets/{id} -// Deletes a pet by ID -func (c *Client) DeletePet(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewDeletePetRequest(c.Server, id) - 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) -} - -// FindPetByID makes a GET request to /pets/{id} -// Returns a pet by ID -func (c *Client) FindPetByID(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewFindPetByIDRequest(c.Server, id) - 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) -} - -// NewFindPetsRequest creates a GET request for /pets -func NewFindPetsRequest(server string, params *FindPetsParams) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/pets") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - queryValues := queryURL.Query() - if params.Tags != nil { - if queryFrag, err := StyleFormExplodeParam("tags", ParamLocationQuery, *params.Tags); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { - return nil, err - } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } - } - } - } - if params.Limit != nil { - if queryFrag, err := StyleFormExplodeParam("limit", ParamLocationQuery, *params.Limit); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { - return nil, err - } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } - } - } - } - queryURL.RawQuery = queryValues.Encode() - } - - req, err := http.NewRequest("GET", queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewAddPetRequest creates a POST request for /pets with application/json body -func NewAddPetRequest(server string, body addPetJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewAddPetRequestWithBody(server, "application/json", bodyReader) -} - -// NewAddPetRequestWithBody creates a POST request for /pets with any body -func NewAddPetRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/pets") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewDeletePetRequest creates a DELETE request for /pets/{id} -func NewDeletePetRequest(server string, id int64) (*http.Request, error) { - var err error - - var pathParam0 string - pathParam0, err = StyleSimpleParam("id", ParamLocationPath, id) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/pets/%s", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("DELETE", queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewFindPetByIDRequest creates a GET request for /pets/{id} -func NewFindPetByIDRequest(server string, id int64) (*http.Request, error) { - var err error - - var pathParam0 string - pathParam0, err = StyleSimpleParam("id", ParamLocationPath, id) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/pets/%s", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// ClientHttpError represents an HTTP error response from the server. -// The type parameter E is the type of the parsed error body. -type ClientHttpError[E any] struct { - StatusCode int - Body E - RawBody []byte -} - -func (e *ClientHttpError[E]) Error() string { - return fmt.Sprintf("HTTP %d", e.StatusCode) -} - -// SimpleClient wraps Client with typed responses for operations that have -// unambiguous response types. Methods return the success type directly, -// and HTTP errors are returned as *ClientHttpError[E] where E is the error type. -type SimpleClient struct { - *Client -} - -// NewSimpleClient creates a new SimpleClient which wraps a Client. -func NewSimpleClient(server string, opts ...ClientOption) (*SimpleClient, error) { - client, err := NewClient(server, opts...) - if err != nil { - return nil, err - } - return &SimpleClient{Client: client}, nil -} - -// FindPets makes a GET request to /pets and returns the parsed response. -// Returns all pets -// On success, returns the response body. On HTTP error, returns *ClientHttpError[petstore.Error]. -func (c *SimpleClient) FindPets(ctx context.Context, params *FindPetsParams, reqEditors ...RequestEditorFn) ([]petstore.Pet, error) { - var result []petstore.Pet - resp, err := c.Client.FindPets(ctx, params, reqEditors...) - if err != nil { - return result, err - } - defer resp.Body.Close() - - rawBody, err := io.ReadAll(resp.Body) - if err != nil { - return result, err - } - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - if err := json.Unmarshal(rawBody, &result); err != nil { - return result, err - } - return result, nil - } - - // Parse error response - var errBody petstore.Error - _ = json.Unmarshal(rawBody, &errBody) // Best effort parse - return result, &ClientHttpError[petstore.Error]{ - StatusCode: resp.StatusCode, - Body: errBody, - RawBody: rawBody, - } -} - -// AddPet makes a POST request to /pets and returns the parsed response. -// Creates a new pet -// On success, returns the response body. On HTTP error, returns *ClientHttpError[petstore.Error]. -func (c *SimpleClient) AddPet(ctx context.Context, body addPetJSONRequestBody, reqEditors ...RequestEditorFn) (petstore.Pet, error) { - var result petstore.Pet - resp, err := c.Client.AddPet(ctx, body, reqEditors...) - if err != nil { - return result, err - } - defer resp.Body.Close() - - rawBody, err := io.ReadAll(resp.Body) - if err != nil { - return result, err - } - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - if err := json.Unmarshal(rawBody, &result); err != nil { - return result, err - } - return result, nil - } - - // Parse error response - var errBody petstore.Error - _ = json.Unmarshal(rawBody, &errBody) // Best effort parse - return result, &ClientHttpError[petstore.Error]{ - StatusCode: resp.StatusCode, - Body: errBody, - RawBody: rawBody, - } -} - -// FindPetByID makes a GET request to /pets/{id} and returns the parsed response. -// Returns a pet by ID -// On success, returns the response body. On HTTP error, returns *ClientHttpError[petstore.Error]. -func (c *SimpleClient) FindPetByID(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (petstore.Pet, error) { - var result petstore.Pet - resp, err := c.Client.FindPetByID(ctx, id, reqEditors...) - if err != nil { - return result, err - } - defer resp.Body.Close() - - rawBody, err := io.ReadAll(resp.Body) - if err != nil { - return result, err - } - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - if err := json.Unmarshal(rawBody, &result); err != nil { - return result, err - } - return result, nil - } - - // Parse error response - var errBody petstore.Error - _ = json.Unmarshal(rawBody, &errBody) // Best effort parse - return result, &ClientHttpError[petstore.Error]{ - StatusCode: resp.StatusCode, - Body: errBody, - RawBody: rawBody, - } -} - -// ParamLocation indicates where a parameter is located in an HTTP request. -type ParamLocation int - -const ( - ParamLocationUndefined ParamLocation = iota - ParamLocationQuery - ParamLocationPath - ParamLocationHeader - ParamLocationCookie -) - -// Binder is an interface for types that can bind themselves from a string value. -type Binder interface { - Bind(value string) error -} - -// DateFormat is the format used for date (without time) parameters. -const DateFormat = "2006-01-02" - -// Date represents a date (without time) for OpenAPI date format. -type Date struct { - time.Time -} - -// UnmarshalText implements encoding.TextUnmarshaler for Date. -func (d *Date) UnmarshalText(data []byte) error { - t, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = t - return nil -} - -// MarshalText implements encoding.TextMarshaler for Date. -func (d Date) MarshalText() ([]byte, error) { - return []byte(d.Format(DateFormat)), nil -} - -// Format returns the date formatted according to layout. -func (d Date) Format(layout string) string { - return d.Time.Format(layout) -} - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, types.Date, and types that implement -// json.Marshaler or fmt.Stringer. -func primitiveToString(value interface{}) (string, error) { - // Check for known types first (time, date, uuid) - if res, ok := marshalKnownTypes(value); ok { - return res, nil - } - - // Dereference pointers for optional values - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - kind := t.Kind() - - switch kind { - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: - return strconv.FormatInt(v.Int(), 10), nil - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: - return strconv.FormatUint(v.Uint(), 10), nil - case reflect.Float64: - return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil - case reflect.Float32: - return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil - case reflect.Bool: - if v.Bool() { - return "true", nil - } - return "false", nil - case reflect.String: - return v.String(), nil - case reflect.Struct: - // Check if it's a UUID - if u, ok := value.(uuid.UUID); ok { - return u.String(), nil - } - // Check if it implements json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - var i2 interface{} - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to decode JSON: %w", err) - } - return primitiveToString(i2) - } - fallthrough - default: - if s, ok := value.(fmt.Stringer); ok { - return s.String(), nil - } - return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) - } -} - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. -func marshalKnownTypes(value interface{}) (string, bool) { - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - tt := v.Convert(reflect.TypeOf(time.Time{})) - timeVal := tt.Interface().(time.Time) - return timeVal.Format(time.RFC3339Nano), true - } - - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - d := v.Convert(reflect.TypeOf(Date{})) - dateVal := d.Interface().(Date) - return dateVal.Format(DateFormat), true - } - - if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { - u := v.Convert(reflect.TypeOf(uuid.UUID{})) - uuidVal := u.Interface().(uuid.UUID) - return uuidVal.String(), true - } - - return "", false -} - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -func escapeParameterString(value string, paramLocation ParamLocation) string { - switch paramLocation { - case ParamLocationQuery: - return url.QueryEscape(value) - case ParamLocationPath: - return url.PathEscape(value) - default: - return value - } -} - -// unescapeParameterString unescapes a parameter value based on its location. -func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { - switch paramLocation { - case ParamLocationQuery, ParamLocationUndefined: - return url.QueryUnescape(value) - case ParamLocationPath: - return url.PathUnescape(value) - default: - return value, nil - } -} - -// sortedKeys returns the keys of a map in sorted order. -func sortedKeys(m map[string]string) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. -func BindStringToObject(src string, dst interface{}) error { - // Check for TextUnmarshaler - if tu, ok := dst.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(src)) - } - - // Check for Binder interface - if b, ok := dst.(Binder); ok { - return b.Bind(src) - } - - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr { - return fmt.Errorf("dst must be a pointer, got %T", dst) - } - v = v.Elem() - - switch v.Kind() { - case reflect.String: - v.SetString(src) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - i, err := strconv.ParseInt(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - v.SetInt(i) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - u, err := strconv.ParseUint(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse uint: %w", err) - } - v.SetUint(u) - case reflect.Float32, reflect.Float64: - f, err := strconv.ParseFloat(src, 64) - if err != nil { - return fmt.Errorf("failed to parse float: %w", err) - } - v.SetFloat(f) - case reflect.Bool: - b, err := strconv.ParseBool(src) - if err != nil { - return fmt.Errorf("failed to parse bool: %w", err) - } - v.SetBool(b) - default: - // Try JSON unmarshal as a fallback - return json.Unmarshal([]byte(src), dst) - } - return nil -} - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. -func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - newArray := reflect.MakeSlice(t, len(parts), len(parts)) - for i, p := range parts { - err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) - if err != nil { - return fmt.Errorf("error setting array element: %w", err) - } - } - v.Set(newArray) - return nil -} - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. -func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { - var fields []string - if explode { - fields = make([]string, len(parts)) - for i, property := range parts { - propertyParts := strings.Split(property, "=") - if len(propertyParts) != 2 { - return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) - } - fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" - } - } else { - if len(parts)%2 != 0 { - return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) - } - fields = make([]string, len(parts)/2) - for i := 0; i < len(parts); i += 2 { - key := parts[i] - value := parts[i+1] - fields[i/2] = "\"" + key + "\":\"" + value + "\"" - } - } - jsonParam := "{" + strings.Join(fields, ",") + "}" - return json.Unmarshal([]byte(jsonParam), dest) -} - -// BindFormExplodeParam binds a form-style parameter with explode to a destination. -// Form style is the default for query and cookie parameters. -// This handles the exploded case where arrays come as multiple query params. -// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) -// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) -func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { - dv := reflect.Indirect(reflect.ValueOf(dest)) - v := dv - var output interface{} - - if required { - output = dest - } else { - // For optional parameters, allocate if nil - if v.IsNil() { - t := v.Type() - newValue := reflect.New(t.Elem()) - output = newValue.Interface() - } else { - output = v.Interface() - } - v = reflect.Indirect(reflect.ValueOf(output)) - } - - t := v.Type() - k := t.Kind() - - values, found := queryParams[paramName] - - switch k { - case reflect.Slice: - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := bindSplitPartsToDestinationArray(values, output) - if err != nil { - return err - } - case reflect.Struct: - // For exploded objects, fields are spread across query params - fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) - if err != nil { - return err - } - if !fieldsPresent { - return nil - } - default: - // Primitive - if len(values) == 0 { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - if len(values) != 1 { - return fmt.Errorf("multiple values for single value parameter '%s'", paramName) - } - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := BindStringToObject(values[0], output) - if err != nil { - return err - } - } - - if !required { - dv.Set(reflect.ValueOf(output)) - } - return nil -} - -// bindParamsToExplodedObject binds query params to struct fields for exploded objects. -func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { - binder, v, t := indirectBinder(dest) - if binder != nil { - _, found := values[paramName] - if !found { - return false, nil - } - return true, BindStringToObject(values.Get(paramName), dest) - } - if t.Kind() != reflect.Struct { - return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) - } - - fieldsPresent := false - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - if !v.Field(i).CanSet() { - continue - } - - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - - fieldVal, found := values[fieldName] - if found { - if len(fieldVal) != 1 { - return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) - } - err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) - if err != nil { - return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) - } - fieldsPresent = true - } - } - return fieldsPresent, nil -} - -// indirectBinder checks if dest implements Binder and returns reflect values. -func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { - v := reflect.ValueOf(dest) - if v.Type().NumMethod() > 0 && v.CanInterface() { - if u, ok := v.Interface().(Binder); ok { - return u, reflect.Value{}, nil - } - } - v = reflect.Indirect(v) - t := v.Type() - // Handle special types like time.Time and Date - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - return dest, reflect.Value{}, nil - } - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - return dest, reflect.Value{}, nil - } - return nil, v, t -} - -// BindSimpleParam binds a simple-style parameter without explode to a destination. -// Simple style is the default for path and header parameters. -// Arrays: a,b,c -> []string{"a", "b", "c"} -// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} -func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // Split on comma and bind as key,value pairs - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} - -// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. -// Form style is the default for query and cookie parameters. -// Primitives: paramName=value -// Arrays: paramName=a¶mName=b¶mName=c -// Objects: key1=value1&key2=value2 -func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleFormExplodeSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleFormExplodeStruct(paramName, paramLocation, value) - case reflect.Map: - return styleFormExplodeMap(paramName, paramLocation, value) - default: - return styleFormExplodePrimitive(paramName, paramLocation, value) - } -} - -func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Form with explode: paramName=a¶mName=b¶mName=c - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, "&"+prefix), nil -} - -func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleFormExplodeParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. -// Simple style is the default for path and header parameters. -// Arrays are comma-separated: a,b,c -// Objects are key,value pairs: key1,value1,key2,value2 -func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return escapeParameterString(string(b), paramLocation), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleSimpleSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleSimpleStruct(paramName, paramLocation, value) - case reflect.Map: - return styleSimpleMap(paramName, paramLocation, value) - default: - return styleSimplePrimitive(paramLocation, value) - } -} - -func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return escapeParameterString(strVal, paramLocation), nil -} - -func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return escapeParameterString(timeVal, paramLocation), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleSimpleParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -// structToFieldDict converts a struct to a map of field names to string values. -func structToFieldDict(value interface{}) (map[string]string, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - fieldDict := make(map[string]string) - - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - f := v.Field(i) - - // Skip nil optional fields - if f.Type().Kind() == reflect.Ptr && f.IsNil() { - continue - } - str, err := primitiveToString(f.Interface()) - if err != nil { - return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) - } - fieldDict[fieldName] = str - } - return fieldDict, nil -} diff --git a/experimental/examples/petstore-expanded/client/client_test.go b/experimental/examples/petstore-expanded/client/client_test.go deleted file mode 100644 index 76ad5ef8e4..0000000000 --- a/experimental/examples/petstore-expanded/client/client_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package client - -import ( - "context" - "net/http" - "testing" - - petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestClientTypes verifies all generated types exist and have correct structure. -// If this test compiles, the type generation is correct. -func TestClientTypes(t *testing.T) { - // Core client types - var _ *Client - var _ *SimpleClient - var _ ClientInterface - var _ ClientOption - var _ RequestEditorFn - var _ HttpRequestDoer - - // Param types - var _ *FindPetsParams - - // Request body type alias - var _ addPetJSONRequestBody - - // Error type with generic parameter - var _ *ClientHttpError[petstore.Error] -} - -// TestClientStructure verifies Client struct has expected fields by accessing them. -func TestClientStructure(t *testing.T) { - c := &Client{} - - // Access fields - compiler validates they exist with correct types - var _ = c.Server - var _ = c.Client - var _ = c.RequestEditors -} - -// TestClientImplementsInterface verifies Client implements ClientInterface. -func TestClientImplementsInterface(t *testing.T) { - var _ ClientInterface = (*Client)(nil) -} - -// TestClientInterfaceMethods verifies ClientInterface methods exist with correct signatures. -// The fact that Client implements ClientInterface (tested above) validates all method signatures. -// Here we use method expressions to verify exact signatures without needing an instance. -func TestClientInterfaceMethods(t *testing.T) { - // Method expressions verify signatures at compile time - var _ = (*Client).FindPets - var _ = (*Client).AddPetWithBody - var _ = (*Client).AddPet - var _ = (*Client).DeletePet - var _ = (*Client).FindPetByID -} - -// TestSimpleClientMethods verifies SimpleClient methods return typed responses. -func TestSimpleClientMethods(t *testing.T) { - // Use method expressions to verify signatures without needing an instance - // Compiler validates return types - these use the petstore package types - var _ = (*SimpleClient).FindPets - var _ = (*SimpleClient).AddPet - var _ = (*SimpleClient).FindPetByID -} - -// TestFindPetsParamsStructure verifies param struct fields. -func TestFindPetsParamsStructure(t *testing.T) { - p := &FindPetsParams{} - - // Access fields - compiler validates they exist with correct types - var _ = p.Tags - var _ = p.Limit -} - -// TestRequestBodyTypeAlias verifies the type alias points to correct type. -func TestRequestBodyTypeAlias(t *testing.T) { - // Compiler validates the alias is compatible with petstore.NewPet - var body addPetJSONRequestBody - var newPet petstore.NewPet - - // Bidirectional assignment proves they're the same type - body = newPet - newPet = body - _ = body - _ = newPet -} - -// TestPetstoreTypes verifies that generated types use short names directly. -func TestPetstoreTypes(t *testing.T) { - // Types should be defined directly with short names (no SchemaComponent suffix) - var _ petstore.Pet - var _ petstore.NewPet - var _ petstore.Error -} - -// TestClientHttpErrorImplementsError verifies ClientHttpError implements error interface. -func TestClientHttpErrorImplementsError(t *testing.T) { - var _ error = (*ClientHttpError[petstore.Error])(nil) -} - -// TestClientHttpErrorStructure verifies error type fields. -func TestClientHttpErrorStructure(t *testing.T) { - e := &ClientHttpError[petstore.Error]{} - - // Access fields - compiler validates they exist with correct types - var _ = e.StatusCode - var _ = e.Body - var _ = e.RawBody -} - -// TestRequestBuilders verifies request builder functions exist with correct signatures. -func TestRequestBuilders(t *testing.T) { - var _ = NewFindPetsRequest - var _ = NewAddPetRequestWithBody - var _ = NewAddPetRequest - var _ = NewDeletePetRequest - var _ = NewFindPetByIDRequest -} - -// TestNewClientConstructor verifies the constructor works correctly. -func TestNewClientConstructor(t *testing.T) { - client, err := NewClient("https://api.example.com") - require.NoError(t, err) - require.NotNil(t, client) - - // Verify trailing slash is added - assert.Equal(t, "https://api.example.com/", client.Server) - // Verify default http.Client is created - assert.NotNil(t, client.Client) -} - -// TestClientOptions verifies client options work correctly. -func TestClientOptions(t *testing.T) { - customClient := &http.Client{} - - client, err := NewClient("https://api.example.com", - WithHTTPClient(customClient), - WithRequestEditorFn(func(ctx context.Context, req *http.Request) error { - req.Header.Set("X-Custom", "value") - return nil - }), - ) - require.NoError(t, err) - - assert.Equal(t, customClient, client.Client) - assert.Len(t, client.RequestEditors, 1) -} - -// TestClientHttpErrorMessage verifies the error message format. -func TestClientHttpErrorMessage(t *testing.T) { - err := &ClientHttpError[petstore.Error]{ - StatusCode: 404, - Body: petstore.Error{Code: 404, Message: "Not Found"}, - } - - assert.Contains(t, err.Error(), "404") -} diff --git a/experimental/examples/petstore-expanded/client/validator/main.go b/experimental/examples/petstore-expanded/client/validator/main.go deleted file mode 100644 index d84632f301..0000000000 --- a/experimental/examples/petstore-expanded/client/validator/main.go +++ /dev/null @@ -1,143 +0,0 @@ -package main - -import ( - "context" - "flag" - "log" - "os" - "strings" - - petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" - "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/client" -) - -func ptr[T any](v T) *T { return &v } - -func main() { - serverURL := flag.String("server", "http://localhost:8080", "Petstore server URL") - flag.Parse() - - log.SetFlags(0) - log.Printf("Petstore Validator") - log.Printf("==================") - log.Printf("Server: %s", *serverURL) - log.Println() - - c, err := client.NewSimpleClient(*serverURL) - if err != nil { - log.Fatalf("Failed to create client: %v", err) - } - ctx := context.Background() - - // Step 1: Add Fido the Dog and Sushi the Cat - log.Println("--- Step 1: Creating pets ---") - - fido, err := c.AddPet(ctx, petstore.NewPet{Name: "Fido", Tag: ptr("Dog")}) - if err != nil { - log.Fatalf("Failed to create Fido: %v", err) - } - log.Printf("Created pet: %s (tag=%s, id=%d)", fido.Name, derefTag(fido.Tag), fido.ID) - - sushi, err := c.AddPet(ctx, petstore.NewPet{Name: "Sushi", Tag: ptr("Cat")}) - if err != nil { - log.Fatalf("Failed to create Sushi: %v", err) - } - log.Printf("Created pet: %s (tag=%s, id=%d)", sushi.Name, derefTag(sushi.Tag), sushi.ID) - log.Println() - - // Step 2: List all pets - log.Println("--- Step 2: Listing all pets ---") - pets, err := c.FindPets(ctx, nil) - if err != nil { - log.Fatalf("Failed to list pets: %v", err) - } - printPets(pets) - log.Println() - - // Step 3: Delete Fido - log.Printf("--- Step 3: Deleting Fido (id=%d) ---", fido.ID) - resp, err := c.DeletePet(ctx, fido.ID) - if err != nil { - log.Fatalf("Failed to delete Fido: %v", err) - } - _ = resp.Body.Close() - if resp.StatusCode == 204 { - log.Printf("Deleted Fido successfully (HTTP %d)", resp.StatusCode) - } else { - log.Fatalf("Unexpected status deleting Fido: HTTP %d", resp.StatusCode) - } - log.Println() - - // Step 4: Add Slimy the Lizard - log.Println("--- Step 4: Creating Slimy the Lizard ---") - slimy, err := c.AddPet(ctx, petstore.NewPet{Name: "Slimy", Tag: ptr("Lizard")}) - if err != nil { - log.Fatalf("Failed to create Slimy: %v", err) - } - log.Printf("Created pet: %s (tag=%s, id=%d)", slimy.Name, derefTag(slimy.Tag), slimy.ID) - log.Println() - - // Step 5: List all pets again - log.Println("--- Step 5: Listing all pets (after changes) ---") - pets, err = c.FindPets(ctx, nil) - if err != nil { - log.Fatalf("Failed to list pets: %v", err) - } - printPets(pets) - log.Println() - - // Validate final state - log.Println("--- Validation ---") - ok := true - if len(pets) != 2 { - log.Printf("FAIL: expected 2 pets, got %d", len(pets)) - ok = false - } - names := map[string]bool{} - for _, p := range pets { - names[p.Name] = true - } - if !names["Sushi"] { - log.Printf("FAIL: Sushi not found in pet list") - ok = false - } - if !names["Slimy"] { - log.Printf("FAIL: Slimy not found in pet list") - ok = false - } - if names["Fido"] { - log.Printf("FAIL: Fido should have been deleted but is still present") - ok = false - } - - if ok { - log.Println("PASS: All validations passed!") - } else { - log.Println("FAIL: Some validations failed") - os.Exit(1) - } -} - -func derefTag(tag *string) string { - if tag == nil { - return "" - } - return *tag -} - -func printPets(pets []petstore.Pet) { - if len(pets) == 0 { - log.Println(" (no pets)") - return - } - maxName := 0 - for _, p := range pets { - if len(p.Name) > maxName { - maxName = len(p.Name) - } - } - for _, p := range pets { - padding := strings.Repeat(" ", maxName-len(p.Name)) - log.Printf(" - %s%s tag=%-8s id=%d", p.Name, padding, derefTag(p.Tag), p.ID) - } -} diff --git a/experimental/examples/petstore-expanded/doc.go b/experimental/examples/petstore-expanded/doc.go deleted file mode 100644 index d3e6eede3d..0000000000 --- a/experimental/examples/petstore-expanded/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package petstore provides generated types for the Petstore API. -package petstore diff --git a/experimental/examples/petstore-expanded/echo-v4/Makefile b/experimental/examples/petstore-expanded/echo-v4/Makefile deleted file mode 100644 index 42389f4137..0000000000 --- a/experimental/examples/petstore-expanded/echo-v4/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -SHELL:=/bin/bash - -YELLOW := \e[0;33m -RESET := \e[0;0m - -GOVER := $(shell go env GOVERSION) -GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") - -define execute-if-go-124 -@{ \ -if [[ 24 -le $(GOMINOR) ]]; then \ - $1; \ -else \ - echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ -fi \ -} -endef - -lint: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) - -lint-ci: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) - -generate: - $(call execute-if-go-124,go generate ./...) - -test: - $(call execute-if-go-124,go test -cover ./...) - -tidy: - $(call execute-if-go-124,go mod tidy) - -tidy-ci: - $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/echo-v4/go.mod b/experimental/examples/petstore-expanded/echo-v4/go.mod deleted file mode 100644 index 241f767a53..0000000000 --- a/experimental/examples/petstore-expanded/echo-v4/go.mod +++ /dev/null @@ -1,23 +0,0 @@ -module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/echo-v4 - -go 1.24.0 - -require ( - github.com/google/uuid v1.6.0 - github.com/labstack/echo/v4 v4.13.3 - github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 -) - -require ( - github.com/labstack/gommon v0.4.2 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.33.0 // indirect -) - -replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/echo-v4/go.sum b/experimental/examples/petstore-expanded/echo-v4/go.sum deleted file mode 100644 index d6b2e1f6c1..0000000000 --- a/experimental/examples/petstore-expanded/echo-v4/go.sum +++ /dev/null @@ -1,33 +0,0 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= -github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/experimental/examples/petstore-expanded/echo-v4/main.go b/experimental/examples/petstore-expanded/echo-v4/main.go deleted file mode 100644 index 5f89fc8cd3..0000000000 --- a/experimental/examples/petstore-expanded/echo-v4/main.go +++ /dev/null @@ -1,34 +0,0 @@ -//go:build go1.22 - -// This is an example of implementing the Pet Store from the OpenAPI documentation -// found at: -// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml - -package main - -import ( - "flag" - "log" - "net" - - "github.com/labstack/echo/v4" - "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/echo-v4/server" -) - -func main() { - port := flag.String("port", "8080", "Port for test HTTP server") - flag.Parse() - - // Create an instance of our handler which satisfies the generated interface - petStore := server.NewPetStore() - - e := echo.New() - - // We now register our petStore above as the handler for the interface - server.RegisterHandlers(e, petStore) - - log.Printf("Server listening on %s", net.JoinHostPort("0.0.0.0", *port)) - - // And we serve HTTP until the world ends. - log.Fatal(e.Start(net.JoinHostPort("0.0.0.0", *port))) -} diff --git a/experimental/examples/petstore-expanded/echo-v4/server/petstore.go b/experimental/examples/petstore-expanded/echo-v4/server/petstore.go deleted file mode 100644 index 9ee8d05a1b..0000000000 --- a/experimental/examples/petstore-expanded/echo-v4/server/petstore.go +++ /dev/null @@ -1,123 +0,0 @@ -//go:build go1.22 - -package server - -import ( - "net/http" - "sync" - - "github.com/labstack/echo/v4" - petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" -) - -// PetStore implements the ServerInterface. -type PetStore struct { - Pets map[int64]petstore.Pet - NextId int64 - Lock sync.Mutex -} - -// Make sure we conform to ServerInterface -var _ ServerInterface = (*PetStore)(nil) - -// NewPetStore creates a new PetStore. -func NewPetStore() *PetStore { - return &PetStore{ - Pets: make(map[int64]petstore.Pet), - NextId: 1000, - } -} - -// sendPetStoreError wraps sending of an error in the Error format. -func sendPetStoreError(ctx echo.Context, code int, message string) error { - petErr := petstore.Error{ - Code: int32(code), - Message: message, - } - return ctx.JSON(code, petErr) -} - -// FindPets returns all pets, optionally filtered by tags and limited. -func (p *PetStore) FindPets(ctx echo.Context, params FindPetsParams) error { - p.Lock.Lock() - defer p.Lock.Unlock() - - var result []petstore.Pet - - for _, pet := range p.Pets { - if params.Tags != nil { - // If we have tags, filter pets by tag - for _, t := range *params.Tags { - if pet.Tag != nil && (*pet.Tag == t) { - result = append(result, pet) - } - } - } else { - // Add all pets if we're not filtering - result = append(result, pet) - } - - if params.Limit != nil { - l := int(*params.Limit) - if len(result) >= l { - // We're at the limit - break - } - } - } - - return ctx.JSON(http.StatusOK, result) -} - -// AddPet creates a new pet. -func (p *PetStore) AddPet(ctx echo.Context) error { - // We expect a NewPet object in the request body. - var newPet petstore.NewPet - if err := ctx.Bind(&newPet); err != nil { - return sendPetStoreError(ctx, http.StatusBadRequest, "Invalid format for NewPet") - } - - // We now have a pet, let's add it to our "database". - p.Lock.Lock() - defer p.Lock.Unlock() - - // We handle pets, not NewPets, which have an additional ID field - var pet petstore.Pet - pet.Name = newPet.Name - pet.Tag = newPet.Tag - pet.ID = p.NextId - p.NextId++ - - // Insert into map - p.Pets[pet.ID] = pet - - // Now, we have to return the Pet - return ctx.JSON(http.StatusCreated, pet) -} - -// FindPetByID returns a pet by ID. -func (p *PetStore) FindPetByID(ctx echo.Context, id int64) error { - p.Lock.Lock() - defer p.Lock.Unlock() - - pet, found := p.Pets[id] - if !found { - return sendPetStoreError(ctx, http.StatusNotFound, "Could not find pet with ID") - } - - return ctx.JSON(http.StatusOK, pet) -} - -// DeletePet deletes a pet by ID. -func (p *PetStore) DeletePet(ctx echo.Context, id int64) error { - p.Lock.Lock() - defer p.Lock.Unlock() - - _, found := p.Pets[id] - if !found { - return sendPetStoreError(ctx, http.StatusNotFound, "Could not find pet with ID") - } - delete(p.Pets, id) - - return ctx.NoContent(http.StatusNoContent) -} diff --git a/experimental/examples/petstore-expanded/echo-v4/server/server.config.yaml b/experimental/examples/petstore-expanded/echo-v4/server/server.config.yaml deleted file mode 100644 index c4f40a9a65..0000000000 --- a/experimental/examples/petstore-expanded/echo-v4/server/server.config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -package: server -generation: - server: echo/v4 - models-package: - path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded - alias: petstore diff --git a/experimental/examples/petstore-expanded/echo-v4/server/server.gen.go b/experimental/examples/petstore-expanded/echo-v4/server/server.gen.go deleted file mode 100644 index af329680a5..0000000000 --- a/experimental/examples/petstore-expanded/echo-v4/server/server.gen.go +++ /dev/null @@ -1,976 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package server - -import ( - "bytes" - "encoding" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "reflect" - "sort" - "strconv" - "strings" - "time" - - "github.com/google/uuid" - "github.com/labstack/echo/v4" -) - -// ServerInterface represents all server handlers. -type ServerInterface interface { - // Returns all pets - // (GET /pets) - FindPets(ctx echo.Context, params FindPetsParams) error - // Creates a new pet - // (POST /pets) - AddPet(ctx echo.Context) error - // Deletes a pet by ID - // (DELETE /pets/{id}) - DeletePet(ctx echo.Context, id int64) error - // Returns a pet by ID - // (GET /pets/{id}) - FindPetByID(ctx echo.Context, id int64) error -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. -type Unimplemented struct{} - -// Returns all pets -// (GET /pets) -func (_ Unimplemented) FindPets(ctx echo.Context, params FindPetsParams) error { - return ctx.NoContent(http.StatusNotImplemented) -} - -// Creates a new pet -// (POST /pets) -func (_ Unimplemented) AddPet(ctx echo.Context) error { - return ctx.NoContent(http.StatusNotImplemented) -} - -// Deletes a pet by ID -// (DELETE /pets/{id}) -func (_ Unimplemented) DeletePet(ctx echo.Context, id int64) error { - return ctx.NoContent(http.StatusNotImplemented) -} - -// Returns a pet by ID -// (GET /pets/{id}) -func (_ Unimplemented) FindPetByID(ctx echo.Context, id int64) error { - return ctx.NoContent(http.StatusNotImplemented) -} - -// FindPetsParams defines parameters for FindPets. -type FindPetsParams struct { - // tags (optional) - Tags *[]string `form:"tags" json:"tags"` - // limit (optional) - Limit *int32 `form:"limit" json:"limit"` -} - -// ServerInterfaceWrapper converts echo contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface -} - -// FindPets converts echo context to params. -func (w *ServerInterfaceWrapper) FindPets(ctx echo.Context) error { - var err error - - // Parameter object where we will unmarshal all parameters from the context - var params FindPetsParams - - // ------------- Optional query parameter "tags" ------------- - err = BindFormExplodeParam("tags", false, ctx.QueryParams(), ¶ms.Tags) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter tags: %s", err)) - } - - // ------------- Optional query parameter "limit" ------------- - err = BindFormExplodeParam("limit", false, ctx.QueryParams(), ¶ms.Limit) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter limit: %s", err)) - } - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.FindPets(ctx, params) - return err -} - -// AddPet converts echo context to params. -func (w *ServerInterfaceWrapper) AddPet(ctx echo.Context) error { - var err error - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.AddPet(ctx) - return err -} - -// DeletePet converts echo context to params. -func (w *ServerInterfaceWrapper) DeletePet(ctx echo.Context) error { - var err error - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, ctx.Param("id"), &id) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) - } - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.DeletePet(ctx, id) - return err -} - -// FindPetByID converts echo context to params. -func (w *ServerInterfaceWrapper) FindPetByID(ctx echo.Context) error { - var err error - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, ctx.Param("id"), &id) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) - } - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.FindPetByID(ctx, id) - return err -} - -// EchoRouter is an interface for echo.Echo and echo.Group. -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 -} - -// RegisterHandlers adds each server route to the EchoRouter. -func RegisterHandlers(router EchoRouter, si ServerInterface) { - RegisterHandlersWithBaseURL(router, si, "") -} - -// RegisterHandlersWithBaseURL adds each server route to the EchoRouter with a base URL prefix. -func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { - - wrapper := ServerInterfaceWrapper{ - Handler: si, - } - - router.GET(baseURL+"/pets", wrapper.FindPets) - router.POST(baseURL+"/pets", wrapper.AddPet) - router.DELETE(baseURL+"/pets/:id", wrapper.DeletePet) - router.GET(baseURL+"/pets/:id", wrapper.FindPetByID) -} - -// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. -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 -} - -// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. -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 -} - -// RequiredParamError is returned when a required parameter is missing. -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -// RequiredHeaderError is returned when a required header is missing. -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 -} - -// InvalidParamFormatError is returned when a parameter has an invalid format. -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 -} - -// TooManyValuesForParamError is returned when a parameter has too many values. -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) -} - -// ParamLocation indicates where a parameter is located in an HTTP request. -type ParamLocation int - -const ( - ParamLocationUndefined ParamLocation = iota - ParamLocationQuery - ParamLocationPath - ParamLocationHeader - ParamLocationCookie -) - -// Binder is an interface for types that can bind themselves from a string value. -type Binder interface { - Bind(value string) error -} - -// DateFormat is the format used for date (without time) parameters. -const DateFormat = "2006-01-02" - -// Date represents a date (without time) for OpenAPI date format. -type Date struct { - time.Time -} - -// UnmarshalText implements encoding.TextUnmarshaler for Date. -func (d *Date) UnmarshalText(data []byte) error { - t, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = t - return nil -} - -// MarshalText implements encoding.TextMarshaler for Date. -func (d Date) MarshalText() ([]byte, error) { - return []byte(d.Format(DateFormat)), nil -} - -// Format returns the date formatted according to layout. -func (d Date) Format(layout string) string { - return d.Time.Format(layout) -} - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, types.Date, and types that implement -// json.Marshaler or fmt.Stringer. -func primitiveToString(value interface{}) (string, error) { - // Check for known types first (time, date, uuid) - if res, ok := marshalKnownTypes(value); ok { - return res, nil - } - - // Dereference pointers for optional values - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - kind := t.Kind() - - switch kind { - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: - return strconv.FormatInt(v.Int(), 10), nil - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: - return strconv.FormatUint(v.Uint(), 10), nil - case reflect.Float64: - return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil - case reflect.Float32: - return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil - case reflect.Bool: - if v.Bool() { - return "true", nil - } - return "false", nil - case reflect.String: - return v.String(), nil - case reflect.Struct: - // Check if it's a UUID - if u, ok := value.(uuid.UUID); ok { - return u.String(), nil - } - // Check if it implements json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - var i2 interface{} - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to decode JSON: %w", err) - } - return primitiveToString(i2) - } - fallthrough - default: - if s, ok := value.(fmt.Stringer); ok { - return s.String(), nil - } - return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) - } -} - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. -func marshalKnownTypes(value interface{}) (string, bool) { - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - tt := v.Convert(reflect.TypeOf(time.Time{})) - timeVal := tt.Interface().(time.Time) - return timeVal.Format(time.RFC3339Nano), true - } - - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - d := v.Convert(reflect.TypeOf(Date{})) - dateVal := d.Interface().(Date) - return dateVal.Format(DateFormat), true - } - - if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { - u := v.Convert(reflect.TypeOf(uuid.UUID{})) - uuidVal := u.Interface().(uuid.UUID) - return uuidVal.String(), true - } - - return "", false -} - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -func escapeParameterString(value string, paramLocation ParamLocation) string { - switch paramLocation { - case ParamLocationQuery: - return url.QueryEscape(value) - case ParamLocationPath: - return url.PathEscape(value) - default: - return value - } -} - -// unescapeParameterString unescapes a parameter value based on its location. -func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { - switch paramLocation { - case ParamLocationQuery, ParamLocationUndefined: - return url.QueryUnescape(value) - case ParamLocationPath: - return url.PathUnescape(value) - default: - return value, nil - } -} - -// sortedKeys returns the keys of a map in sorted order. -func sortedKeys(m map[string]string) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. -func BindStringToObject(src string, dst interface{}) error { - // Check for TextUnmarshaler - if tu, ok := dst.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(src)) - } - - // Check for Binder interface - if b, ok := dst.(Binder); ok { - return b.Bind(src) - } - - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr { - return fmt.Errorf("dst must be a pointer, got %T", dst) - } - v = v.Elem() - - switch v.Kind() { - case reflect.String: - v.SetString(src) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - i, err := strconv.ParseInt(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - v.SetInt(i) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - u, err := strconv.ParseUint(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse uint: %w", err) - } - v.SetUint(u) - case reflect.Float32, reflect.Float64: - f, err := strconv.ParseFloat(src, 64) - if err != nil { - return fmt.Errorf("failed to parse float: %w", err) - } - v.SetFloat(f) - case reflect.Bool: - b, err := strconv.ParseBool(src) - if err != nil { - return fmt.Errorf("failed to parse bool: %w", err) - } - v.SetBool(b) - default: - // Try JSON unmarshal as a fallback - return json.Unmarshal([]byte(src), dst) - } - return nil -} - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. -func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - newArray := reflect.MakeSlice(t, len(parts), len(parts)) - for i, p := range parts { - err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) - if err != nil { - return fmt.Errorf("error setting array element: %w", err) - } - } - v.Set(newArray) - return nil -} - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. -func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { - var fields []string - if explode { - fields = make([]string, len(parts)) - for i, property := range parts { - propertyParts := strings.Split(property, "=") - if len(propertyParts) != 2 { - return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) - } - fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" - } - } else { - if len(parts)%2 != 0 { - return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) - } - fields = make([]string, len(parts)/2) - for i := 0; i < len(parts); i += 2 { - key := parts[i] - value := parts[i+1] - fields[i/2] = "\"" + key + "\":\"" + value + "\"" - } - } - jsonParam := "{" + strings.Join(fields, ",") + "}" - return json.Unmarshal([]byte(jsonParam), dest) -} - -// BindFormExplodeParam binds a form-style parameter with explode to a destination. -// Form style is the default for query and cookie parameters. -// This handles the exploded case where arrays come as multiple query params. -// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) -// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) -func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { - dv := reflect.Indirect(reflect.ValueOf(dest)) - v := dv - var output interface{} - - if required { - output = dest - } else { - // For optional parameters, allocate if nil - if v.IsNil() { - t := v.Type() - newValue := reflect.New(t.Elem()) - output = newValue.Interface() - } else { - output = v.Interface() - } - v = reflect.Indirect(reflect.ValueOf(output)) - } - - t := v.Type() - k := t.Kind() - - values, found := queryParams[paramName] - - switch k { - case reflect.Slice: - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := bindSplitPartsToDestinationArray(values, output) - if err != nil { - return err - } - case reflect.Struct: - // For exploded objects, fields are spread across query params - fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) - if err != nil { - return err - } - if !fieldsPresent { - return nil - } - default: - // Primitive - if len(values) == 0 { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - if len(values) != 1 { - return fmt.Errorf("multiple values for single value parameter '%s'", paramName) - } - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := BindStringToObject(values[0], output) - if err != nil { - return err - } - } - - if !required { - dv.Set(reflect.ValueOf(output)) - } - return nil -} - -// bindParamsToExplodedObject binds query params to struct fields for exploded objects. -func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { - binder, v, t := indirectBinder(dest) - if binder != nil { - _, found := values[paramName] - if !found { - return false, nil - } - return true, BindStringToObject(values.Get(paramName), dest) - } - if t.Kind() != reflect.Struct { - return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) - } - - fieldsPresent := false - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - if !v.Field(i).CanSet() { - continue - } - - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - - fieldVal, found := values[fieldName] - if found { - if len(fieldVal) != 1 { - return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) - } - err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) - if err != nil { - return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) - } - fieldsPresent = true - } - } - return fieldsPresent, nil -} - -// indirectBinder checks if dest implements Binder and returns reflect values. -func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { - v := reflect.ValueOf(dest) - if v.Type().NumMethod() > 0 && v.CanInterface() { - if u, ok := v.Interface().(Binder); ok { - return u, reflect.Value{}, nil - } - } - v = reflect.Indirect(v) - t := v.Type() - // Handle special types like time.Time and Date - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - return dest, reflect.Value{}, nil - } - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - return dest, reflect.Value{}, nil - } - return nil, v, t -} - -// BindSimpleParam binds a simple-style parameter without explode to a destination. -// Simple style is the default for path and header parameters. -// Arrays: a,b,c -> []string{"a", "b", "c"} -// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} -func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // Split on comma and bind as key,value pairs - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} - -// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. -// Form style is the default for query and cookie parameters. -// Primitives: paramName=value -// Arrays: paramName=a¶mName=b¶mName=c -// Objects: key1=value1&key2=value2 -func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleFormExplodeSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleFormExplodeStruct(paramName, paramLocation, value) - case reflect.Map: - return styleFormExplodeMap(paramName, paramLocation, value) - default: - return styleFormExplodePrimitive(paramName, paramLocation, value) - } -} - -func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Form with explode: paramName=a¶mName=b¶mName=c - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, "&"+prefix), nil -} - -func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleFormExplodeParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. -// Simple style is the default for path and header parameters. -// Arrays are comma-separated: a,b,c -// Objects are key,value pairs: key1,value1,key2,value2 -func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return escapeParameterString(string(b), paramLocation), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleSimpleSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleSimpleStruct(paramName, paramLocation, value) - case reflect.Map: - return styleSimpleMap(paramName, paramLocation, value) - default: - return styleSimplePrimitive(paramLocation, value) - } -} - -func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return escapeParameterString(strVal, paramLocation), nil -} - -func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return escapeParameterString(timeVal, paramLocation), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleSimpleParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -// structToFieldDict converts a struct to a map of field names to string values. -func structToFieldDict(value interface{}) (map[string]string, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - fieldDict := make(map[string]string) - - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - f := v.Field(i) - - // Skip nil optional fields - if f.Type().Kind() == reflect.Ptr && f.IsNil() { - continue - } - str, err := primitiveToString(f.Interface()) - if err != nil { - return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) - } - fieldDict[fieldName] = str - } - return fieldDict, nil -} diff --git a/experimental/examples/petstore-expanded/echo/Makefile b/experimental/examples/petstore-expanded/echo/Makefile deleted file mode 100644 index 42389f4137..0000000000 --- a/experimental/examples/petstore-expanded/echo/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -SHELL:=/bin/bash - -YELLOW := \e[0;33m -RESET := \e[0;0m - -GOVER := $(shell go env GOVERSION) -GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") - -define execute-if-go-124 -@{ \ -if [[ 24 -le $(GOMINOR) ]]; then \ - $1; \ -else \ - echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ -fi \ -} -endef - -lint: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) - -lint-ci: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) - -generate: - $(call execute-if-go-124,go generate ./...) - -test: - $(call execute-if-go-124,go test -cover ./...) - -tidy: - $(call execute-if-go-124,go mod tidy) - -tidy-ci: - $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/echo/go.mod b/experimental/examples/petstore-expanded/echo/go.mod deleted file mode 100644 index 3576808d2d..0000000000 --- a/experimental/examples/petstore-expanded/echo/go.mod +++ /dev/null @@ -1,11 +0,0 @@ -module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/echo - -go 1.25.0 - -require ( - github.com/google/uuid v1.6.0 - github.com/labstack/echo/v5 v5.0.0 - github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 -) - -replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/echo/go.sum b/experimental/examples/petstore-expanded/echo/go.sum deleted file mode 100644 index 9dd4179c6c..0000000000 --- a/experimental/examples/petstore-expanded/echo/go.sum +++ /dev/null @@ -1,16 +0,0 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/labstack/echo/v5 v5.0.0 h1:JHKGrI0cbNsNMyKvranuY0C94O4hSM7yc/HtwcV3Na4= -github.com/labstack/echo/v5 v5.0.0/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/experimental/examples/petstore-expanded/echo/main.go b/experimental/examples/petstore-expanded/echo/main.go deleted file mode 100644 index 6f0483ac28..0000000000 --- a/experimental/examples/petstore-expanded/echo/main.go +++ /dev/null @@ -1,34 +0,0 @@ -//go:build go1.22 - -// This is an example of implementing the Pet Store from the OpenAPI documentation -// found at: -// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml - -package main - -import ( - "flag" - "log" - "net" - - "github.com/labstack/echo/v5" - "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/echo/server" -) - -func main() { - port := flag.String("port", "8080", "Port for test HTTP server") - flag.Parse() - - // Create an instance of our handler which satisfies the generated interface - petStore := server.NewPetStore() - - e := echo.New() - - // We now register our petStore above as the handler for the interface - server.RegisterHandlers(e, petStore) - - log.Printf("Server listening on %s", net.JoinHostPort("0.0.0.0", *port)) - - // And we serve HTTP until the world ends. - log.Fatal(e.Start(net.JoinHostPort("0.0.0.0", *port))) -} diff --git a/experimental/examples/petstore-expanded/echo/server/petstore.go b/experimental/examples/petstore-expanded/echo/server/petstore.go deleted file mode 100644 index 276030da81..0000000000 --- a/experimental/examples/petstore-expanded/echo/server/petstore.go +++ /dev/null @@ -1,123 +0,0 @@ -//go:build go1.22 - -package server - -import ( - "net/http" - "sync" - - "github.com/labstack/echo/v5" - petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" -) - -// PetStore implements the ServerInterface. -type PetStore struct { - Pets map[int64]petstore.Pet - NextId int64 - Lock sync.Mutex -} - -// Make sure we conform to ServerInterface -var _ ServerInterface = (*PetStore)(nil) - -// NewPetStore creates a new PetStore. -func NewPetStore() *PetStore { - return &PetStore{ - Pets: make(map[int64]petstore.Pet), - NextId: 1000, - } -} - -// sendPetStoreError wraps sending of an error in the Error format. -func sendPetStoreError(ctx *echo.Context, code int, message string) error { - petErr := petstore.Error{ - Code: int32(code), - Message: message, - } - return ctx.JSON(code, petErr) -} - -// FindPets returns all pets, optionally filtered by tags and limited. -func (p *PetStore) FindPets(ctx *echo.Context, params FindPetsParams) error { - p.Lock.Lock() - defer p.Lock.Unlock() - - var result []petstore.Pet - - for _, pet := range p.Pets { - if params.Tags != nil { - // If we have tags, filter pets by tag - for _, t := range *params.Tags { - if pet.Tag != nil && (*pet.Tag == t) { - result = append(result, pet) - } - } - } else { - // Add all pets if we're not filtering - result = append(result, pet) - } - - if params.Limit != nil { - l := int(*params.Limit) - if len(result) >= l { - // We're at the limit - break - } - } - } - - return ctx.JSON(http.StatusOK, result) -} - -// AddPet creates a new pet. -func (p *PetStore) AddPet(ctx *echo.Context) error { - // We expect a NewPet object in the request body. - var newPet petstore.NewPet - if err := ctx.Bind(&newPet); err != nil { - return sendPetStoreError(ctx, http.StatusBadRequest, "Invalid format for NewPet") - } - - // We now have a pet, let's add it to our "database". - p.Lock.Lock() - defer p.Lock.Unlock() - - // We handle pets, not NewPets, which have an additional ID field - var pet petstore.Pet - pet.Name = newPet.Name - pet.Tag = newPet.Tag - pet.ID = p.NextId - p.NextId++ - - // Insert into map - p.Pets[pet.ID] = pet - - // Now, we have to return the Pet - return ctx.JSON(http.StatusCreated, pet) -} - -// FindPetByID returns a pet by ID. -func (p *PetStore) FindPetByID(ctx *echo.Context, id int64) error { - p.Lock.Lock() - defer p.Lock.Unlock() - - pet, found := p.Pets[id] - if !found { - return sendPetStoreError(ctx, http.StatusNotFound, "Could not find pet with ID") - } - - return ctx.JSON(http.StatusOK, pet) -} - -// DeletePet deletes a pet by ID. -func (p *PetStore) DeletePet(ctx *echo.Context, id int64) error { - p.Lock.Lock() - defer p.Lock.Unlock() - - _, found := p.Pets[id] - if !found { - return sendPetStoreError(ctx, http.StatusNotFound, "Could not find pet with ID") - } - delete(p.Pets, id) - - return ctx.NoContent(http.StatusNoContent) -} diff --git a/experimental/examples/petstore-expanded/echo/server/server.config.yaml b/experimental/examples/petstore-expanded/echo/server/server.config.yaml deleted file mode 100644 index 16a0b6b82d..0000000000 --- a/experimental/examples/petstore-expanded/echo/server/server.config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -package: server -generation: - server: echo - models-package: - path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded - alias: petstore diff --git a/experimental/examples/petstore-expanded/echo/server/server.gen.go b/experimental/examples/petstore-expanded/echo/server/server.gen.go deleted file mode 100644 index da099a985f..0000000000 --- a/experimental/examples/petstore-expanded/echo/server/server.gen.go +++ /dev/null @@ -1,977 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package server - -import ( - "bytes" - "encoding" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "reflect" - "sort" - "strconv" - "strings" - "time" - - "github.com/google/uuid" - "github.com/labstack/echo/v5" -) - -// ServerInterface represents all server handlers. -type ServerInterface interface { - // Returns all pets - // (GET /pets) - FindPets(ctx *echo.Context, params FindPetsParams) error - // Creates a new pet - // (POST /pets) - AddPet(ctx *echo.Context) error - // Deletes a pet by ID - // (DELETE /pets/{id}) - DeletePet(ctx *echo.Context, id int64) error - // Returns a pet by ID - // (GET /pets/{id}) - FindPetByID(ctx *echo.Context, id int64) error -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. -type Unimplemented struct{} - -// Returns all pets -// (GET /pets) -func (_ Unimplemented) FindPets(ctx *echo.Context, params FindPetsParams) error { - return ctx.NoContent(http.StatusNotImplemented) -} - -// Creates a new pet -// (POST /pets) -func (_ Unimplemented) AddPet(ctx *echo.Context) error { - return ctx.NoContent(http.StatusNotImplemented) -} - -// Deletes a pet by ID -// (DELETE /pets/{id}) -func (_ Unimplemented) DeletePet(ctx *echo.Context, id int64) error { - return ctx.NoContent(http.StatusNotImplemented) -} - -// Returns a pet by ID -// (GET /pets/{id}) -func (_ Unimplemented) FindPetByID(ctx *echo.Context, id int64) error { - return ctx.NoContent(http.StatusNotImplemented) -} - -// FindPetsParams defines parameters for FindPets. -type FindPetsParams struct { - // tags (optional) - Tags *[]string `form:"tags" json:"tags"` - // limit (optional) - Limit *int32 `form:"limit" json:"limit"` -} - -// ServerInterfaceWrapper converts echo contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface -} - -// FindPets converts echo context to params. -func (w *ServerInterfaceWrapper) FindPets(ctx *echo.Context) error { - var err error - - // Parameter object where we will unmarshal all parameters from the context - var params FindPetsParams - - // ------------- Optional query parameter "tags" ------------- - err = BindFormExplodeParam("tags", false, ctx.QueryParams(), ¶ms.Tags) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter tags: %s", err)) - } - - // ------------- Optional query parameter "limit" ------------- - err = BindFormExplodeParam("limit", false, ctx.QueryParams(), ¶ms.Limit) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter limit: %s", err)) - } - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.FindPets(ctx, params) - return err -} - -// AddPet converts echo context to params. -func (w *ServerInterfaceWrapper) AddPet(ctx *echo.Context) error { - var err error - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.AddPet(ctx) - return err -} - -// DeletePet converts echo context to params. -func (w *ServerInterfaceWrapper) DeletePet(ctx *echo.Context) error { - var err error - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, ctx.Param("id"), &id) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) - } - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.DeletePet(ctx, id) - return err -} - -// FindPetByID converts echo context to params. -func (w *ServerInterfaceWrapper) FindPetByID(ctx *echo.Context) error { - var err error - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, ctx.Param("id"), &id) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) - } - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.FindPetByID(ctx, id) - return err -} - -// EchoRouter is an interface for echo.Echo and echo.Group. -type EchoRouter interface { - Add(method string, path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo - 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 -} - -// RegisterHandlers adds each server route to the EchoRouter. -func RegisterHandlers(router EchoRouter, si ServerInterface) { - RegisterHandlersWithBaseURL(router, si, "") -} - -// RegisterHandlersWithBaseURL adds each server route to the EchoRouter with a base URL prefix. -func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { - - wrapper := ServerInterfaceWrapper{ - Handler: si, - } - - router.GET(baseURL+"/pets", wrapper.FindPets) - router.POST(baseURL+"/pets", wrapper.AddPet) - router.DELETE(baseURL+"/pets/:id", wrapper.DeletePet) - router.GET(baseURL+"/pets/:id", wrapper.FindPetByID) -} - -// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. -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 -} - -// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. -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 -} - -// RequiredParamError is returned when a required parameter is missing. -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -// RequiredHeaderError is returned when a required header is missing. -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 -} - -// InvalidParamFormatError is returned when a parameter has an invalid format. -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 -} - -// TooManyValuesForParamError is returned when a parameter has too many values. -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) -} - -// ParamLocation indicates where a parameter is located in an HTTP request. -type ParamLocation int - -const ( - ParamLocationUndefined ParamLocation = iota - ParamLocationQuery - ParamLocationPath - ParamLocationHeader - ParamLocationCookie -) - -// Binder is an interface for types that can bind themselves from a string value. -type Binder interface { - Bind(value string) error -} - -// DateFormat is the format used for date (without time) parameters. -const DateFormat = "2006-01-02" - -// Date represents a date (without time) for OpenAPI date format. -type Date struct { - time.Time -} - -// UnmarshalText implements encoding.TextUnmarshaler for Date. -func (d *Date) UnmarshalText(data []byte) error { - t, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = t - return nil -} - -// MarshalText implements encoding.TextMarshaler for Date. -func (d Date) MarshalText() ([]byte, error) { - return []byte(d.Format(DateFormat)), nil -} - -// Format returns the date formatted according to layout. -func (d Date) Format(layout string) string { - return d.Time.Format(layout) -} - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, types.Date, and types that implement -// json.Marshaler or fmt.Stringer. -func primitiveToString(value interface{}) (string, error) { - // Check for known types first (time, date, uuid) - if res, ok := marshalKnownTypes(value); ok { - return res, nil - } - - // Dereference pointers for optional values - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - kind := t.Kind() - - switch kind { - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: - return strconv.FormatInt(v.Int(), 10), nil - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: - return strconv.FormatUint(v.Uint(), 10), nil - case reflect.Float64: - return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil - case reflect.Float32: - return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil - case reflect.Bool: - if v.Bool() { - return "true", nil - } - return "false", nil - case reflect.String: - return v.String(), nil - case reflect.Struct: - // Check if it's a UUID - if u, ok := value.(uuid.UUID); ok { - return u.String(), nil - } - // Check if it implements json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - var i2 interface{} - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to decode JSON: %w", err) - } - return primitiveToString(i2) - } - fallthrough - default: - if s, ok := value.(fmt.Stringer); ok { - return s.String(), nil - } - return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) - } -} - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. -func marshalKnownTypes(value interface{}) (string, bool) { - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - tt := v.Convert(reflect.TypeOf(time.Time{})) - timeVal := tt.Interface().(time.Time) - return timeVal.Format(time.RFC3339Nano), true - } - - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - d := v.Convert(reflect.TypeOf(Date{})) - dateVal := d.Interface().(Date) - return dateVal.Format(DateFormat), true - } - - if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { - u := v.Convert(reflect.TypeOf(uuid.UUID{})) - uuidVal := u.Interface().(uuid.UUID) - return uuidVal.String(), true - } - - return "", false -} - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -func escapeParameterString(value string, paramLocation ParamLocation) string { - switch paramLocation { - case ParamLocationQuery: - return url.QueryEscape(value) - case ParamLocationPath: - return url.PathEscape(value) - default: - return value - } -} - -// unescapeParameterString unescapes a parameter value based on its location. -func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { - switch paramLocation { - case ParamLocationQuery, ParamLocationUndefined: - return url.QueryUnescape(value) - case ParamLocationPath: - return url.PathUnescape(value) - default: - return value, nil - } -} - -// sortedKeys returns the keys of a map in sorted order. -func sortedKeys(m map[string]string) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. -func BindStringToObject(src string, dst interface{}) error { - // Check for TextUnmarshaler - if tu, ok := dst.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(src)) - } - - // Check for Binder interface - if b, ok := dst.(Binder); ok { - return b.Bind(src) - } - - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr { - return fmt.Errorf("dst must be a pointer, got %T", dst) - } - v = v.Elem() - - switch v.Kind() { - case reflect.String: - v.SetString(src) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - i, err := strconv.ParseInt(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - v.SetInt(i) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - u, err := strconv.ParseUint(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse uint: %w", err) - } - v.SetUint(u) - case reflect.Float32, reflect.Float64: - f, err := strconv.ParseFloat(src, 64) - if err != nil { - return fmt.Errorf("failed to parse float: %w", err) - } - v.SetFloat(f) - case reflect.Bool: - b, err := strconv.ParseBool(src) - if err != nil { - return fmt.Errorf("failed to parse bool: %w", err) - } - v.SetBool(b) - default: - // Try JSON unmarshal as a fallback - return json.Unmarshal([]byte(src), dst) - } - return nil -} - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. -func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - newArray := reflect.MakeSlice(t, len(parts), len(parts)) - for i, p := range parts { - err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) - if err != nil { - return fmt.Errorf("error setting array element: %w", err) - } - } - v.Set(newArray) - return nil -} - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. -func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { - var fields []string - if explode { - fields = make([]string, len(parts)) - for i, property := range parts { - propertyParts := strings.Split(property, "=") - if len(propertyParts) != 2 { - return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) - } - fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" - } - } else { - if len(parts)%2 != 0 { - return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) - } - fields = make([]string, len(parts)/2) - for i := 0; i < len(parts); i += 2 { - key := parts[i] - value := parts[i+1] - fields[i/2] = "\"" + key + "\":\"" + value + "\"" - } - } - jsonParam := "{" + strings.Join(fields, ",") + "}" - return json.Unmarshal([]byte(jsonParam), dest) -} - -// BindFormExplodeParam binds a form-style parameter with explode to a destination. -// Form style is the default for query and cookie parameters. -// This handles the exploded case where arrays come as multiple query params. -// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) -// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) -func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { - dv := reflect.Indirect(reflect.ValueOf(dest)) - v := dv - var output interface{} - - if required { - output = dest - } else { - // For optional parameters, allocate if nil - if v.IsNil() { - t := v.Type() - newValue := reflect.New(t.Elem()) - output = newValue.Interface() - } else { - output = v.Interface() - } - v = reflect.Indirect(reflect.ValueOf(output)) - } - - t := v.Type() - k := t.Kind() - - values, found := queryParams[paramName] - - switch k { - case reflect.Slice: - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := bindSplitPartsToDestinationArray(values, output) - if err != nil { - return err - } - case reflect.Struct: - // For exploded objects, fields are spread across query params - fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) - if err != nil { - return err - } - if !fieldsPresent { - return nil - } - default: - // Primitive - if len(values) == 0 { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - if len(values) != 1 { - return fmt.Errorf("multiple values for single value parameter '%s'", paramName) - } - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := BindStringToObject(values[0], output) - if err != nil { - return err - } - } - - if !required { - dv.Set(reflect.ValueOf(output)) - } - return nil -} - -// bindParamsToExplodedObject binds query params to struct fields for exploded objects. -func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { - binder, v, t := indirectBinder(dest) - if binder != nil { - _, found := values[paramName] - if !found { - return false, nil - } - return true, BindStringToObject(values.Get(paramName), dest) - } - if t.Kind() != reflect.Struct { - return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) - } - - fieldsPresent := false - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - if !v.Field(i).CanSet() { - continue - } - - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - - fieldVal, found := values[fieldName] - if found { - if len(fieldVal) != 1 { - return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) - } - err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) - if err != nil { - return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) - } - fieldsPresent = true - } - } - return fieldsPresent, nil -} - -// indirectBinder checks if dest implements Binder and returns reflect values. -func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { - v := reflect.ValueOf(dest) - if v.Type().NumMethod() > 0 && v.CanInterface() { - if u, ok := v.Interface().(Binder); ok { - return u, reflect.Value{}, nil - } - } - v = reflect.Indirect(v) - t := v.Type() - // Handle special types like time.Time and Date - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - return dest, reflect.Value{}, nil - } - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - return dest, reflect.Value{}, nil - } - return nil, v, t -} - -// BindSimpleParam binds a simple-style parameter without explode to a destination. -// Simple style is the default for path and header parameters. -// Arrays: a,b,c -> []string{"a", "b", "c"} -// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} -func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // Split on comma and bind as key,value pairs - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} - -// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. -// Form style is the default for query and cookie parameters. -// Primitives: paramName=value -// Arrays: paramName=a¶mName=b¶mName=c -// Objects: key1=value1&key2=value2 -func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleFormExplodeSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleFormExplodeStruct(paramName, paramLocation, value) - case reflect.Map: - return styleFormExplodeMap(paramName, paramLocation, value) - default: - return styleFormExplodePrimitive(paramName, paramLocation, value) - } -} - -func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Form with explode: paramName=a¶mName=b¶mName=c - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, "&"+prefix), nil -} - -func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleFormExplodeParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. -// Simple style is the default for path and header parameters. -// Arrays are comma-separated: a,b,c -// Objects are key,value pairs: key1,value1,key2,value2 -func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return escapeParameterString(string(b), paramLocation), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleSimpleSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleSimpleStruct(paramName, paramLocation, value) - case reflect.Map: - return styleSimpleMap(paramName, paramLocation, value) - default: - return styleSimplePrimitive(paramLocation, value) - } -} - -func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return escapeParameterString(strVal, paramLocation), nil -} - -func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return escapeParameterString(timeVal, paramLocation), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleSimpleParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -// structToFieldDict converts a struct to a map of field names to string values. -func structToFieldDict(value interface{}) (map[string]string, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - fieldDict := make(map[string]string) - - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - f := v.Field(i) - - // Skip nil optional fields - if f.Type().Kind() == reflect.Ptr && f.IsNil() { - continue - } - str, err := primitiveToString(f.Interface()) - if err != nil { - return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) - } - fieldDict[fieldName] = str - } - return fieldDict, nil -} diff --git a/experimental/examples/petstore-expanded/fiber/Makefile b/experimental/examples/petstore-expanded/fiber/Makefile deleted file mode 100644 index 42389f4137..0000000000 --- a/experimental/examples/petstore-expanded/fiber/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -SHELL:=/bin/bash - -YELLOW := \e[0;33m -RESET := \e[0;0m - -GOVER := $(shell go env GOVERSION) -GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") - -define execute-if-go-124 -@{ \ -if [[ 24 -le $(GOMINOR) ]]; then \ - $1; \ -else \ - echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ -fi \ -} -endef - -lint: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) - -lint-ci: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) - -generate: - $(call execute-if-go-124,go generate ./...) - -test: - $(call execute-if-go-124,go test -cover ./...) - -tidy: - $(call execute-if-go-124,go mod tidy) - -tidy-ci: - $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/fiber/go.mod b/experimental/examples/petstore-expanded/fiber/go.mod deleted file mode 100644 index aa60e0d81c..0000000000 --- a/experimental/examples/petstore-expanded/fiber/go.mod +++ /dev/null @@ -1,31 +0,0 @@ -module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/fiber - -go 1.24.0 - -require ( - github.com/gofiber/fiber/v3 v3.0.0-beta.4 - github.com/google/uuid v1.6.0 - github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 -) - -require ( - github.com/andybalholm/brotli v1.1.1 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/gofiber/schema v1.2.0 // indirect - github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect - github.com/tinylib/msgp v1.2.5 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.58.0 // indirect - github.com/valyala/tcplisten v1.0.0 // indirect - github.com/x448/float16 v0.8.4 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.33.0 // indirect -) - -replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/fiber/go.sum b/experimental/examples/petstore-expanded/fiber/go.sum deleted file mode 100644 index 00a56bd56e..0000000000 --- a/experimental/examples/petstore-expanded/fiber/go.sum +++ /dev/null @@ -1,51 +0,0 @@ -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/gofiber/fiber/v3 v3.0.0-beta.4 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0= -github.com/gofiber/fiber/v3 v3.0.0-beta.4/go.mod h1:/WFUoHRkZEsGHyy2+fYcdqi109IVOFbVwxv1n1RU+kk= -github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg= -github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= -github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ= -github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= -github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= -github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= -github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= -github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/experimental/examples/petstore-expanded/fiber/main.go b/experimental/examples/petstore-expanded/fiber/main.go deleted file mode 100644 index a429cc08f3..0000000000 --- a/experimental/examples/petstore-expanded/fiber/main.go +++ /dev/null @@ -1,35 +0,0 @@ -//go:build go1.22 - -// This is an example of implementing the Pet Store from the OpenAPI documentation -// found at: -// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml - -package main - -import ( - "flag" - "log" - "net" - - "github.com/gofiber/fiber/v3" - "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/fiber/server" -) - -func main() { - port := flag.String("port", "8080", "Port for test HTTP server") - flag.Parse() - - // Create an instance of our handler which satisfies the generated interface - petStore := server.NewPetStore() - - app := fiber.New() - - // We now register our petStore above as the handler for the interface - server.RegisterHandlers(app, petStore) - - addr := net.JoinHostPort("0.0.0.0", *port) - log.Printf("Server listening on %s", addr) - - // And we serve HTTP until the world ends. - log.Fatal(app.Listen(addr)) -} diff --git a/experimental/examples/petstore-expanded/fiber/server/petstore.go b/experimental/examples/petstore-expanded/fiber/server/petstore.go deleted file mode 100644 index 0692078618..0000000000 --- a/experimental/examples/petstore-expanded/fiber/server/petstore.go +++ /dev/null @@ -1,122 +0,0 @@ -//go:build go1.22 - -package server - -import ( - "sync" - - "github.com/gofiber/fiber/v3" - petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" -) - -// PetStore implements the ServerInterface. -type PetStore struct { - Pets map[int64]petstore.Pet - NextId int64 - Lock sync.Mutex -} - -// Make sure we conform to ServerInterface -var _ ServerInterface = (*PetStore)(nil) - -// NewPetStore creates a new PetStore. -func NewPetStore() *PetStore { - return &PetStore{ - Pets: make(map[int64]petstore.Pet), - NextId: 1000, - } -} - -// sendPetStoreError wraps sending of an error in the Error format. -func sendPetStoreError(c fiber.Ctx, code int, message string) error { - petErr := petstore.Error{ - Code: int32(code), - Message: message, - } - return c.Status(code).JSON(petErr) -} - -// FindPets returns all pets, optionally filtered by tags and limited. -func (p *PetStore) FindPets(c fiber.Ctx, params FindPetsParams) error { - p.Lock.Lock() - defer p.Lock.Unlock() - - var result []petstore.Pet - - for _, pet := range p.Pets { - if params.Tags != nil { - // If we have tags, filter pets by tag - for _, t := range *params.Tags { - if pet.Tag != nil && (*pet.Tag == t) { - result = append(result, pet) - } - } - } else { - // Add all pets if we're not filtering - result = append(result, pet) - } - - if params.Limit != nil { - l := int(*params.Limit) - if len(result) >= l { - // We're at the limit - break - } - } - } - - return c.Status(fiber.StatusOK).JSON(result) -} - -// AddPet creates a new pet. -func (p *PetStore) AddPet(c fiber.Ctx) error { - // We expect a NewPet object in the request body. - var newPet petstore.NewPet - if err := c.Bind().JSON(&newPet); err != nil { - return sendPetStoreError(c, fiber.StatusBadRequest, "Invalid format for NewPet") - } - - // We now have a pet, let's add it to our "database". - p.Lock.Lock() - defer p.Lock.Unlock() - - // We handle pets, not NewPets, which have an additional ID field - var pet petstore.Pet - pet.Name = newPet.Name - pet.Tag = newPet.Tag - pet.ID = p.NextId - p.NextId++ - - // Insert into map - p.Pets[pet.ID] = pet - - // Now, we have to return the Pet - return c.Status(fiber.StatusCreated).JSON(pet) -} - -// FindPetByID returns a pet by ID. -func (p *PetStore) FindPetByID(c fiber.Ctx, id int64) error { - p.Lock.Lock() - defer p.Lock.Unlock() - - pet, found := p.Pets[id] - if !found { - return sendPetStoreError(c, fiber.StatusNotFound, "Could not find pet with ID") - } - - return c.Status(fiber.StatusOK).JSON(pet) -} - -// DeletePet deletes a pet by ID. -func (p *PetStore) DeletePet(c fiber.Ctx, id int64) error { - p.Lock.Lock() - defer p.Lock.Unlock() - - _, found := p.Pets[id] - if !found { - return sendPetStoreError(c, fiber.StatusNotFound, "Could not find pet with ID") - } - delete(p.Pets, id) - - return c.SendStatus(fiber.StatusNoContent) -} diff --git a/experimental/examples/petstore-expanded/fiber/server/server.config.yaml b/experimental/examples/petstore-expanded/fiber/server/server.config.yaml deleted file mode 100644 index 1038e55a98..0000000000 --- a/experimental/examples/petstore-expanded/fiber/server/server.config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -package: server -generation: - server: fiber - models-package: - path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded - alias: petstore diff --git a/experimental/examples/petstore-expanded/fiber/server/server.gen.go b/experimental/examples/petstore-expanded/fiber/server/server.gen.go deleted file mode 100644 index a38d4cfb87..0000000000 --- a/experimental/examples/petstore-expanded/fiber/server/server.gen.go +++ /dev/null @@ -1,969 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package server - -import ( - "bytes" - "encoding" - "encoding/json" - "errors" - "fmt" - "net/url" - "reflect" - "sort" - "strconv" - "strings" - "time" - - "github.com/gofiber/fiber/v3" - "github.com/google/uuid" -) - -// ServerInterface represents all server handlers. -type ServerInterface interface { - // Returns all pets - // (GET /pets) - FindPets(c fiber.Ctx, params FindPetsParams) error - // Creates a new pet - // (POST /pets) - AddPet(c fiber.Ctx) error - // Deletes a pet by ID - // (DELETE /pets/{id}) - DeletePet(c fiber.Ctx, id int64) error - // Returns a pet by ID - // (GET /pets/{id}) - FindPetByID(c fiber.Ctx, id int64) error -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. -type Unimplemented struct{} - -// Returns all pets -// (GET /pets) -func (_ Unimplemented) FindPets(c fiber.Ctx, params FindPetsParams) error { - return c.SendStatus(fiber.StatusNotImplemented) -} - -// Creates a new pet -// (POST /pets) -func (_ Unimplemented) AddPet(c fiber.Ctx) error { - return c.SendStatus(fiber.StatusNotImplemented) -} - -// Deletes a pet by ID -// (DELETE /pets/{id}) -func (_ Unimplemented) DeletePet(c fiber.Ctx, id int64) error { - return c.SendStatus(fiber.StatusNotImplemented) -} - -// Returns a pet by ID -// (GET /pets/{id}) -func (_ Unimplemented) FindPetByID(c fiber.Ctx, id int64) error { - return c.SendStatus(fiber.StatusNotImplemented) -} - -// FindPetsParams defines parameters for FindPets. -type FindPetsParams struct { - // tags (optional) - Tags *[]string `form:"tags" json:"tags"` - // limit (optional) - Limit *int32 `form:"limit" json:"limit"` -} - -// ServerInterfaceWrapper converts contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface -} - -// FindPets operation middleware -func (siw *ServerInterfaceWrapper) FindPets(c fiber.Ctx) error { - var err error - - // Parameter object where we will unmarshal all parameters from the context - var params FindPetsParams - - var query url.Values - query, err = url.ParseQuery(string(c.Request().URI().QueryString())) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for query string: %s", err)) - } - - // ------------- Optional query parameter "tags" ------------- - err = BindFormExplodeParam("tags", false, query, ¶ms.Tags) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter tags: %s", err)) - } - - // ------------- Optional query parameter "limit" ------------- - err = BindFormExplodeParam("limit", false, query, ¶ms.Limit) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter limit: %s", err)) - } - - return siw.Handler.FindPets(c, params) -} - -// AddPet operation middleware -func (siw *ServerInterfaceWrapper) AddPet(c fiber.Ctx) error { - - return siw.Handler.AddPet(c) -} - -// DeletePet operation middleware -func (siw *ServerInterfaceWrapper) DeletePet(c fiber.Ctx) error { - var err error - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, c.Params("id"), &id) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) - } - - return siw.Handler.DeletePet(c, id) -} - -// FindPetByID operation middleware -func (siw *ServerInterfaceWrapper) FindPetByID(c fiber.Ctx) error { - var err error - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, c.Params("id"), &id) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) - } - - return siw.Handler.FindPetByID(c, id) -} - -// FiberServerOptions provides options for the Fiber server. -type FiberServerOptions struct { - BaseURL string - Middlewares []fiber.Handler -} - -// 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) { - - wrapper := ServerInterfaceWrapper{ - Handler: si, - } - - for _, m := range options.Middlewares { - router.Use(m) - } - - router.Get(options.BaseURL+"/pets", wrapper.FindPets) - router.Post(options.BaseURL+"/pets", wrapper.AddPet) - router.Delete(options.BaseURL+"/pets/:id", wrapper.DeletePet) - router.Get(options.BaseURL+"/pets/:id", wrapper.FindPetByID) -} - -// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. -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 -} - -// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. -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 -} - -// RequiredParamError is returned when a required parameter is missing. -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -// RequiredHeaderError is returned when a required header is missing. -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 -} - -// InvalidParamFormatError is returned when a parameter has an invalid format. -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 -} - -// TooManyValuesForParamError is returned when a parameter has too many values. -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) -} - -// ParamLocation indicates where a parameter is located in an HTTP request. -type ParamLocation int - -const ( - ParamLocationUndefined ParamLocation = iota - ParamLocationQuery - ParamLocationPath - ParamLocationHeader - ParamLocationCookie -) - -// Binder is an interface for types that can bind themselves from a string value. -type Binder interface { - Bind(value string) error -} - -// DateFormat is the format used for date (without time) parameters. -const DateFormat = "2006-01-02" - -// Date represents a date (without time) for OpenAPI date format. -type Date struct { - time.Time -} - -// UnmarshalText implements encoding.TextUnmarshaler for Date. -func (d *Date) UnmarshalText(data []byte) error { - t, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = t - return nil -} - -// MarshalText implements encoding.TextMarshaler for Date. -func (d Date) MarshalText() ([]byte, error) { - return []byte(d.Format(DateFormat)), nil -} - -// Format returns the date formatted according to layout. -func (d Date) Format(layout string) string { - return d.Time.Format(layout) -} - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, types.Date, and types that implement -// json.Marshaler or fmt.Stringer. -func primitiveToString(value interface{}) (string, error) { - // Check for known types first (time, date, uuid) - if res, ok := marshalKnownTypes(value); ok { - return res, nil - } - - // Dereference pointers for optional values - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - kind := t.Kind() - - switch kind { - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: - return strconv.FormatInt(v.Int(), 10), nil - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: - return strconv.FormatUint(v.Uint(), 10), nil - case reflect.Float64: - return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil - case reflect.Float32: - return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil - case reflect.Bool: - if v.Bool() { - return "true", nil - } - return "false", nil - case reflect.String: - return v.String(), nil - case reflect.Struct: - // Check if it's a UUID - if u, ok := value.(uuid.UUID); ok { - return u.String(), nil - } - // Check if it implements json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - var i2 interface{} - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to decode JSON: %w", err) - } - return primitiveToString(i2) - } - fallthrough - default: - if s, ok := value.(fmt.Stringer); ok { - return s.String(), nil - } - return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) - } -} - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. -func marshalKnownTypes(value interface{}) (string, bool) { - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - tt := v.Convert(reflect.TypeOf(time.Time{})) - timeVal := tt.Interface().(time.Time) - return timeVal.Format(time.RFC3339Nano), true - } - - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - d := v.Convert(reflect.TypeOf(Date{})) - dateVal := d.Interface().(Date) - return dateVal.Format(DateFormat), true - } - - if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { - u := v.Convert(reflect.TypeOf(uuid.UUID{})) - uuidVal := u.Interface().(uuid.UUID) - return uuidVal.String(), true - } - - return "", false -} - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -func escapeParameterString(value string, paramLocation ParamLocation) string { - switch paramLocation { - case ParamLocationQuery: - return url.QueryEscape(value) - case ParamLocationPath: - return url.PathEscape(value) - default: - return value - } -} - -// unescapeParameterString unescapes a parameter value based on its location. -func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { - switch paramLocation { - case ParamLocationQuery, ParamLocationUndefined: - return url.QueryUnescape(value) - case ParamLocationPath: - return url.PathUnescape(value) - default: - return value, nil - } -} - -// sortedKeys returns the keys of a map in sorted order. -func sortedKeys(m map[string]string) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. -func BindStringToObject(src string, dst interface{}) error { - // Check for TextUnmarshaler - if tu, ok := dst.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(src)) - } - - // Check for Binder interface - if b, ok := dst.(Binder); ok { - return b.Bind(src) - } - - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr { - return fmt.Errorf("dst must be a pointer, got %T", dst) - } - v = v.Elem() - - switch v.Kind() { - case reflect.String: - v.SetString(src) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - i, err := strconv.ParseInt(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - v.SetInt(i) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - u, err := strconv.ParseUint(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse uint: %w", err) - } - v.SetUint(u) - case reflect.Float32, reflect.Float64: - f, err := strconv.ParseFloat(src, 64) - if err != nil { - return fmt.Errorf("failed to parse float: %w", err) - } - v.SetFloat(f) - case reflect.Bool: - b, err := strconv.ParseBool(src) - if err != nil { - return fmt.Errorf("failed to parse bool: %w", err) - } - v.SetBool(b) - default: - // Try JSON unmarshal as a fallback - return json.Unmarshal([]byte(src), dst) - } - return nil -} - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. -func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - newArray := reflect.MakeSlice(t, len(parts), len(parts)) - for i, p := range parts { - err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) - if err != nil { - return fmt.Errorf("error setting array element: %w", err) - } - } - v.Set(newArray) - return nil -} - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. -func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { - var fields []string - if explode { - fields = make([]string, len(parts)) - for i, property := range parts { - propertyParts := strings.Split(property, "=") - if len(propertyParts) != 2 { - return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) - } - fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" - } - } else { - if len(parts)%2 != 0 { - return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) - } - fields = make([]string, len(parts)/2) - for i := 0; i < len(parts); i += 2 { - key := parts[i] - value := parts[i+1] - fields[i/2] = "\"" + key + "\":\"" + value + "\"" - } - } - jsonParam := "{" + strings.Join(fields, ",") + "}" - return json.Unmarshal([]byte(jsonParam), dest) -} - -// BindFormExplodeParam binds a form-style parameter with explode to a destination. -// Form style is the default for query and cookie parameters. -// This handles the exploded case where arrays come as multiple query params. -// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) -// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) -func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { - dv := reflect.Indirect(reflect.ValueOf(dest)) - v := dv - var output interface{} - - if required { - output = dest - } else { - // For optional parameters, allocate if nil - if v.IsNil() { - t := v.Type() - newValue := reflect.New(t.Elem()) - output = newValue.Interface() - } else { - output = v.Interface() - } - v = reflect.Indirect(reflect.ValueOf(output)) - } - - t := v.Type() - k := t.Kind() - - values, found := queryParams[paramName] - - switch k { - case reflect.Slice: - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := bindSplitPartsToDestinationArray(values, output) - if err != nil { - return err - } - case reflect.Struct: - // For exploded objects, fields are spread across query params - fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) - if err != nil { - return err - } - if !fieldsPresent { - return nil - } - default: - // Primitive - if len(values) == 0 { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - if len(values) != 1 { - return fmt.Errorf("multiple values for single value parameter '%s'", paramName) - } - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := BindStringToObject(values[0], output) - if err != nil { - return err - } - } - - if !required { - dv.Set(reflect.ValueOf(output)) - } - return nil -} - -// bindParamsToExplodedObject binds query params to struct fields for exploded objects. -func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { - binder, v, t := indirectBinder(dest) - if binder != nil { - _, found := values[paramName] - if !found { - return false, nil - } - return true, BindStringToObject(values.Get(paramName), dest) - } - if t.Kind() != reflect.Struct { - return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) - } - - fieldsPresent := false - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - if !v.Field(i).CanSet() { - continue - } - - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - - fieldVal, found := values[fieldName] - if found { - if len(fieldVal) != 1 { - return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) - } - err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) - if err != nil { - return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) - } - fieldsPresent = true - } - } - return fieldsPresent, nil -} - -// indirectBinder checks if dest implements Binder and returns reflect values. -func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { - v := reflect.ValueOf(dest) - if v.Type().NumMethod() > 0 && v.CanInterface() { - if u, ok := v.Interface().(Binder); ok { - return u, reflect.Value{}, nil - } - } - v = reflect.Indirect(v) - t := v.Type() - // Handle special types like time.Time and Date - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - return dest, reflect.Value{}, nil - } - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - return dest, reflect.Value{}, nil - } - return nil, v, t -} - -// BindSimpleParam binds a simple-style parameter without explode to a destination. -// Simple style is the default for path and header parameters. -// Arrays: a,b,c -> []string{"a", "b", "c"} -// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} -func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // Split on comma and bind as key,value pairs - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} - -// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. -// Form style is the default for query and cookie parameters. -// Primitives: paramName=value -// Arrays: paramName=a¶mName=b¶mName=c -// Objects: key1=value1&key2=value2 -func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleFormExplodeSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleFormExplodeStruct(paramName, paramLocation, value) - case reflect.Map: - return styleFormExplodeMap(paramName, paramLocation, value) - default: - return styleFormExplodePrimitive(paramName, paramLocation, value) - } -} - -func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Form with explode: paramName=a¶mName=b¶mName=c - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, "&"+prefix), nil -} - -func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleFormExplodeParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. -// Simple style is the default for path and header parameters. -// Arrays are comma-separated: a,b,c -// Objects are key,value pairs: key1,value1,key2,value2 -func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return escapeParameterString(string(b), paramLocation), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleSimpleSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleSimpleStruct(paramName, paramLocation, value) - case reflect.Map: - return styleSimpleMap(paramName, paramLocation, value) - default: - return styleSimplePrimitive(paramLocation, value) - } -} - -func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return escapeParameterString(strVal, paramLocation), nil -} - -func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return escapeParameterString(timeVal, paramLocation), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleSimpleParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -// structToFieldDict converts a struct to a map of field names to string values. -func structToFieldDict(value interface{}) (map[string]string, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - fieldDict := make(map[string]string) - - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - f := v.Field(i) - - // Skip nil optional fields - if f.Type().Kind() == reflect.Ptr && f.IsNil() { - continue - } - str, err := primitiveToString(f.Interface()) - if err != nil { - return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) - } - fieldDict[fieldName] = str - } - return fieldDict, nil -} diff --git a/experimental/examples/petstore-expanded/generate.go b/experimental/examples/petstore-expanded/generate.go deleted file mode 100644 index b8a15f94ef..0000000000 --- a/experimental/examples/petstore-expanded/generate.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config models.config.yaml petstore-expanded.yaml -//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config stdhttp/server/server.config.yaml -output stdhttp/server/server.gen.go petstore-expanded.yaml -//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config chi/server/server.config.yaml -output chi/server/server.gen.go petstore-expanded.yaml -//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config echo-v4/server/server.config.yaml -output echo-v4/server/server.gen.go petstore-expanded.yaml -//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config echo/server/server.config.yaml -output echo/server/server.gen.go petstore-expanded.yaml -//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config gin/server/server.config.yaml -output gin/server/server.gen.go petstore-expanded.yaml -//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config gorilla/server/server.config.yaml -output gorilla/server/server.gen.go petstore-expanded.yaml -//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config fiber/server/server.config.yaml -output fiber/server/server.gen.go petstore-expanded.yaml -//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config iris/server/server.config.yaml -output iris/server/server.gen.go petstore-expanded.yaml - -package petstore diff --git a/experimental/examples/petstore-expanded/gin/Makefile b/experimental/examples/petstore-expanded/gin/Makefile deleted file mode 100644 index 42389f4137..0000000000 --- a/experimental/examples/petstore-expanded/gin/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -SHELL:=/bin/bash - -YELLOW := \e[0;33m -RESET := \e[0;0m - -GOVER := $(shell go env GOVERSION) -GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") - -define execute-if-go-124 -@{ \ -if [[ 24 -le $(GOMINOR) ]]; then \ - $1; \ -else \ - echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ -fi \ -} -endef - -lint: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) - -lint-ci: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) - -generate: - $(call execute-if-go-124,go generate ./...) - -test: - $(call execute-if-go-124,go test -cover ./...) - -tidy: - $(call execute-if-go-124,go mod tidy) - -tidy-ci: - $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/gin/go.mod b/experimental/examples/petstore-expanded/gin/go.mod deleted file mode 100644 index eed1c070da..0000000000 --- a/experimental/examples/petstore-expanded/gin/go.mod +++ /dev/null @@ -1,40 +0,0 @@ -module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/gin - -go 1.24.0 - -require ( - github.com/gin-gonic/gin v1.10.0 - github.com/google/uuid v1.6.0 - github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 -) - -require ( - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.23.0 // indirect - golang.org/x/net v0.25.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.33.0 // indirect - google.golang.org/protobuf v1.34.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) - -replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/gin/go.sum b/experimental/examples/petstore-expanded/gin/go.sum deleted file mode 100644 index fa550cb75e..0000000000 --- a/experimental/examples/petstore-expanded/gin/go.sum +++ /dev/null @@ -1,92 +0,0 @@ -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/experimental/examples/petstore-expanded/gin/main.go b/experimental/examples/petstore-expanded/gin/main.go deleted file mode 100644 index 91148c8846..0000000000 --- a/experimental/examples/petstore-expanded/gin/main.go +++ /dev/null @@ -1,40 +0,0 @@ -//go:build go1.22 - -// This is an example of implementing the Pet Store from the OpenAPI documentation -// found at: -// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml - -package main - -import ( - "flag" - "log" - "net" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/gin/server" -) - -func main() { - port := flag.String("port", "8080", "Port for test HTTP server") - flag.Parse() - - // Create an instance of our handler which satisfies the generated interface - petStore := server.NewPetStore() - - r := gin.Default() - - // We now register our petStore above as the handler for the interface - server.RegisterHandlers(r, petStore) - - s := &http.Server{ - Handler: r, - Addr: net.JoinHostPort("0.0.0.0", *port), - } - - log.Printf("Server listening on %s", s.Addr) - - // And we serve HTTP until the world ends. - log.Fatal(s.ListenAndServe()) -} diff --git a/experimental/examples/petstore-expanded/gin/server/petstore.go b/experimental/examples/petstore-expanded/gin/server/petstore.go deleted file mode 100644 index 45f4801f25..0000000000 --- a/experimental/examples/petstore-expanded/gin/server/petstore.go +++ /dev/null @@ -1,126 +0,0 @@ -//go:build go1.22 - -package server - -import ( - "net/http" - "sync" - - "github.com/gin-gonic/gin" - petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" -) - -// PetStore implements the ServerInterface. -type PetStore struct { - Pets map[int64]petstore.Pet - NextId int64 - Lock sync.Mutex -} - -// Make sure we conform to ServerInterface -var _ ServerInterface = (*PetStore)(nil) - -// NewPetStore creates a new PetStore. -func NewPetStore() *PetStore { - return &PetStore{ - Pets: make(map[int64]petstore.Pet), - NextId: 1000, - } -} - -// sendPetStoreError wraps sending of an error in the Error format. -func sendPetStoreError(c *gin.Context, code int, message string) { - petErr := petstore.Error{ - Code: int32(code), - Message: message, - } - c.JSON(code, petErr) -} - -// FindPets returns all pets, optionally filtered by tags and limited. -func (p *PetStore) FindPets(c *gin.Context, params FindPetsParams) { - p.Lock.Lock() - defer p.Lock.Unlock() - - var result []petstore.Pet - - for _, pet := range p.Pets { - if params.Tags != nil { - // If we have tags, filter pets by tag - for _, t := range *params.Tags { - if pet.Tag != nil && (*pet.Tag == t) { - result = append(result, pet) - } - } - } else { - // Add all pets if we're not filtering - result = append(result, pet) - } - - if params.Limit != nil { - l := int(*params.Limit) - if len(result) >= l { - // We're at the limit - break - } - } - } - - c.JSON(http.StatusOK, result) -} - -// AddPet creates a new pet. -func (p *PetStore) AddPet(c *gin.Context) { - // We expect a NewPet object in the request body. - var newPet petstore.NewPet - if err := c.ShouldBindJSON(&newPet); err != nil { - sendPetStoreError(c, http.StatusBadRequest, "Invalid format for NewPet") - return - } - - // We now have a pet, let's add it to our "database". - p.Lock.Lock() - defer p.Lock.Unlock() - - // We handle pets, not NewPets, which have an additional ID field - var pet petstore.Pet - pet.Name = newPet.Name - pet.Tag = newPet.Tag - pet.ID = p.NextId - p.NextId++ - - // Insert into map - p.Pets[pet.ID] = pet - - // Now, we have to return the Pet - c.JSON(http.StatusCreated, pet) -} - -// FindPetByID returns a pet by ID. -func (p *PetStore) FindPetByID(c *gin.Context, id int64) { - p.Lock.Lock() - defer p.Lock.Unlock() - - pet, found := p.Pets[id] - if !found { - sendPetStoreError(c, http.StatusNotFound, "Could not find pet with ID") - return - } - - c.JSON(http.StatusOK, pet) -} - -// DeletePet deletes a pet by ID. -func (p *PetStore) DeletePet(c *gin.Context, id int64) { - p.Lock.Lock() - defer p.Lock.Unlock() - - _, found := p.Pets[id] - if !found { - sendPetStoreError(c, http.StatusNotFound, "Could not find pet with ID") - return - } - delete(p.Pets, id) - - c.Status(http.StatusNoContent) -} diff --git a/experimental/examples/petstore-expanded/gin/server/server.config.yaml b/experimental/examples/petstore-expanded/gin/server/server.config.yaml deleted file mode 100644 index 9b26a5e3b9..0000000000 --- a/experimental/examples/petstore-expanded/gin/server/server.config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -package: server -generation: - server: gin - models-package: - path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded - alias: petstore diff --git a/experimental/examples/petstore-expanded/gin/server/server.gen.go b/experimental/examples/petstore-expanded/gin/server/server.gen.go deleted file mode 100644 index 87d877c5c8..0000000000 --- a/experimental/examples/petstore-expanded/gin/server/server.gen.go +++ /dev/null @@ -1,1007 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package server - -import ( - "bytes" - "encoding" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "reflect" - "sort" - "strconv" - "strings" - "time" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" -) - -// ServerInterface represents all server handlers. -type ServerInterface interface { - // Returns all pets - // (GET /pets) - FindPets(c *gin.Context, params FindPetsParams) - // Creates a new pet - // (POST /pets) - AddPet(c *gin.Context) - // Deletes a pet by ID - // (DELETE /pets/{id}) - DeletePet(c *gin.Context, id int64) - // Returns a pet by ID - // (GET /pets/{id}) - FindPetByID(c *gin.Context, id int64) -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. -type Unimplemented struct{} - -// Returns all pets -// (GET /pets) -func (_ Unimplemented) FindPets(c *gin.Context, params FindPetsParams) { - c.Status(http.StatusNotImplemented) -} - -// Creates a new pet -// (POST /pets) -func (_ Unimplemented) AddPet(c *gin.Context) { - c.Status(http.StatusNotImplemented) -} - -// Deletes a pet by ID -// (DELETE /pets/{id}) -func (_ Unimplemented) DeletePet(c *gin.Context, id int64) { - c.Status(http.StatusNotImplemented) -} - -// Returns a pet by ID -// (GET /pets/{id}) -func (_ Unimplemented) FindPetByID(c *gin.Context, id int64) { - c.Status(http.StatusNotImplemented) -} - -// FindPetsParams defines parameters for FindPets. -type FindPetsParams struct { - // tags (optional) - Tags *[]string `form:"tags" json:"tags"` - // limit (optional) - Limit *int32 `form:"limit" json:"limit"` -} - -// ServerInterfaceWrapper converts contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandler func(*gin.Context, error, int) -} - -// MiddlewareFunc is a middleware function type. -type MiddlewareFunc func(c *gin.Context) - -// FindPets operation middleware -func (siw *ServerInterfaceWrapper) FindPets(c *gin.Context) { - var err error - - // Parameter object where we will unmarshal all parameters from the context - var params FindPetsParams - - // ------------- Optional query parameter "tags" ------------- - err = BindFormExplodeParam("tags", false, c.Request.URL.Query(), ¶ms.Tags) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter tags: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "limit" ------------- - err = BindFormExplodeParam("limit", false, c.Request.URL.Query(), ¶ms.Limit) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter limit: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.FindPets(c, params) -} - -// AddPet operation middleware -func (siw *ServerInterfaceWrapper) AddPet(c *gin.Context) { - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.AddPet(c) -} - -// DeletePet operation middleware -func (siw *ServerInterfaceWrapper) DeletePet(c *gin.Context) { - var err error - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, c.Param("id"), &id) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter id: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.DeletePet(c, id) -} - -// FindPetByID operation middleware -func (siw *ServerInterfaceWrapper) FindPetByID(c *gin.Context) { - var err error - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, c.Param("id"), &id) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter id: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.FindPetByID(c, id) -} - -// GinServerOptions provides options for the Gin server. -type GinServerOptions struct { - BaseURL string - Middlewares []MiddlewareFunc - ErrorHandler func(*gin.Context, error, int) -} - -// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. -func RegisterHandlers(router gin.IRouter, si ServerInterface) { - RegisterHandlersWithOptions(router, si, GinServerOptions{}) -} - -// RegisterHandlersWithOptions creates http.Handler with additional options. -func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options GinServerOptions) { - - errorHandler := options.ErrorHandler - if errorHandler == nil { - errorHandler = func(c *gin.Context, err error, statusCode int) { - c.JSON(statusCode, gin.H{"msg": err.Error()}) - } - } - - wrapper := ServerInterfaceWrapper{ - Handler: si, - HandlerMiddlewares: options.Middlewares, - ErrorHandler: errorHandler, - } - - router.GET(options.BaseURL+"/pets", wrapper.FindPets) - router.POST(options.BaseURL+"/pets", wrapper.AddPet) - router.DELETE(options.BaseURL+"/pets/:id", wrapper.DeletePet) - router.GET(options.BaseURL+"/pets/:id", wrapper.FindPetByID) -} - -// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. -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 -} - -// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. -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 -} - -// RequiredParamError is returned when a required parameter is missing. -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -// RequiredHeaderError is returned when a required header is missing. -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 -} - -// InvalidParamFormatError is returned when a parameter has an invalid format. -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 -} - -// TooManyValuesForParamError is returned when a parameter has too many values. -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) -} - -// ParamLocation indicates where a parameter is located in an HTTP request. -type ParamLocation int - -const ( - ParamLocationUndefined ParamLocation = iota - ParamLocationQuery - ParamLocationPath - ParamLocationHeader - ParamLocationCookie -) - -// Binder is an interface for types that can bind themselves from a string value. -type Binder interface { - Bind(value string) error -} - -// DateFormat is the format used for date (without time) parameters. -const DateFormat = "2006-01-02" - -// Date represents a date (without time) for OpenAPI date format. -type Date struct { - time.Time -} - -// UnmarshalText implements encoding.TextUnmarshaler for Date. -func (d *Date) UnmarshalText(data []byte) error { - t, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = t - return nil -} - -// MarshalText implements encoding.TextMarshaler for Date. -func (d Date) MarshalText() ([]byte, error) { - return []byte(d.Format(DateFormat)), nil -} - -// Format returns the date formatted according to layout. -func (d Date) Format(layout string) string { - return d.Time.Format(layout) -} - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, types.Date, and types that implement -// json.Marshaler or fmt.Stringer. -func primitiveToString(value interface{}) (string, error) { - // Check for known types first (time, date, uuid) - if res, ok := marshalKnownTypes(value); ok { - return res, nil - } - - // Dereference pointers for optional values - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - kind := t.Kind() - - switch kind { - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: - return strconv.FormatInt(v.Int(), 10), nil - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: - return strconv.FormatUint(v.Uint(), 10), nil - case reflect.Float64: - return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil - case reflect.Float32: - return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil - case reflect.Bool: - if v.Bool() { - return "true", nil - } - return "false", nil - case reflect.String: - return v.String(), nil - case reflect.Struct: - // Check if it's a UUID - if u, ok := value.(uuid.UUID); ok { - return u.String(), nil - } - // Check if it implements json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - var i2 interface{} - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to decode JSON: %w", err) - } - return primitiveToString(i2) - } - fallthrough - default: - if s, ok := value.(fmt.Stringer); ok { - return s.String(), nil - } - return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) - } -} - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. -func marshalKnownTypes(value interface{}) (string, bool) { - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - tt := v.Convert(reflect.TypeOf(time.Time{})) - timeVal := tt.Interface().(time.Time) - return timeVal.Format(time.RFC3339Nano), true - } - - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - d := v.Convert(reflect.TypeOf(Date{})) - dateVal := d.Interface().(Date) - return dateVal.Format(DateFormat), true - } - - if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { - u := v.Convert(reflect.TypeOf(uuid.UUID{})) - uuidVal := u.Interface().(uuid.UUID) - return uuidVal.String(), true - } - - return "", false -} - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -func escapeParameterString(value string, paramLocation ParamLocation) string { - switch paramLocation { - case ParamLocationQuery: - return url.QueryEscape(value) - case ParamLocationPath: - return url.PathEscape(value) - default: - return value - } -} - -// unescapeParameterString unescapes a parameter value based on its location. -func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { - switch paramLocation { - case ParamLocationQuery, ParamLocationUndefined: - return url.QueryUnescape(value) - case ParamLocationPath: - return url.PathUnescape(value) - default: - return value, nil - } -} - -// sortedKeys returns the keys of a map in sorted order. -func sortedKeys(m map[string]string) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. -func BindStringToObject(src string, dst interface{}) error { - // Check for TextUnmarshaler - if tu, ok := dst.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(src)) - } - - // Check for Binder interface - if b, ok := dst.(Binder); ok { - return b.Bind(src) - } - - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr { - return fmt.Errorf("dst must be a pointer, got %T", dst) - } - v = v.Elem() - - switch v.Kind() { - case reflect.String: - v.SetString(src) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - i, err := strconv.ParseInt(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - v.SetInt(i) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - u, err := strconv.ParseUint(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse uint: %w", err) - } - v.SetUint(u) - case reflect.Float32, reflect.Float64: - f, err := strconv.ParseFloat(src, 64) - if err != nil { - return fmt.Errorf("failed to parse float: %w", err) - } - v.SetFloat(f) - case reflect.Bool: - b, err := strconv.ParseBool(src) - if err != nil { - return fmt.Errorf("failed to parse bool: %w", err) - } - v.SetBool(b) - default: - // Try JSON unmarshal as a fallback - return json.Unmarshal([]byte(src), dst) - } - return nil -} - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. -func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - newArray := reflect.MakeSlice(t, len(parts), len(parts)) - for i, p := range parts { - err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) - if err != nil { - return fmt.Errorf("error setting array element: %w", err) - } - } - v.Set(newArray) - return nil -} - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. -func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { - var fields []string - if explode { - fields = make([]string, len(parts)) - for i, property := range parts { - propertyParts := strings.Split(property, "=") - if len(propertyParts) != 2 { - return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) - } - fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" - } - } else { - if len(parts)%2 != 0 { - return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) - } - fields = make([]string, len(parts)/2) - for i := 0; i < len(parts); i += 2 { - key := parts[i] - value := parts[i+1] - fields[i/2] = "\"" + key + "\":\"" + value + "\"" - } - } - jsonParam := "{" + strings.Join(fields, ",") + "}" - return json.Unmarshal([]byte(jsonParam), dest) -} - -// BindFormExplodeParam binds a form-style parameter with explode to a destination. -// Form style is the default for query and cookie parameters. -// This handles the exploded case where arrays come as multiple query params. -// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) -// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) -func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { - dv := reflect.Indirect(reflect.ValueOf(dest)) - v := dv - var output interface{} - - if required { - output = dest - } else { - // For optional parameters, allocate if nil - if v.IsNil() { - t := v.Type() - newValue := reflect.New(t.Elem()) - output = newValue.Interface() - } else { - output = v.Interface() - } - v = reflect.Indirect(reflect.ValueOf(output)) - } - - t := v.Type() - k := t.Kind() - - values, found := queryParams[paramName] - - switch k { - case reflect.Slice: - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := bindSplitPartsToDestinationArray(values, output) - if err != nil { - return err - } - case reflect.Struct: - // For exploded objects, fields are spread across query params - fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) - if err != nil { - return err - } - if !fieldsPresent { - return nil - } - default: - // Primitive - if len(values) == 0 { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - if len(values) != 1 { - return fmt.Errorf("multiple values for single value parameter '%s'", paramName) - } - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := BindStringToObject(values[0], output) - if err != nil { - return err - } - } - - if !required { - dv.Set(reflect.ValueOf(output)) - } - return nil -} - -// bindParamsToExplodedObject binds query params to struct fields for exploded objects. -func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { - binder, v, t := indirectBinder(dest) - if binder != nil { - _, found := values[paramName] - if !found { - return false, nil - } - return true, BindStringToObject(values.Get(paramName), dest) - } - if t.Kind() != reflect.Struct { - return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) - } - - fieldsPresent := false - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - if !v.Field(i).CanSet() { - continue - } - - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - - fieldVal, found := values[fieldName] - if found { - if len(fieldVal) != 1 { - return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) - } - err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) - if err != nil { - return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) - } - fieldsPresent = true - } - } - return fieldsPresent, nil -} - -// indirectBinder checks if dest implements Binder and returns reflect values. -func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { - v := reflect.ValueOf(dest) - if v.Type().NumMethod() > 0 && v.CanInterface() { - if u, ok := v.Interface().(Binder); ok { - return u, reflect.Value{}, nil - } - } - v = reflect.Indirect(v) - t := v.Type() - // Handle special types like time.Time and Date - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - return dest, reflect.Value{}, nil - } - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - return dest, reflect.Value{}, nil - } - return nil, v, t -} - -// BindSimpleParam binds a simple-style parameter without explode to a destination. -// Simple style is the default for path and header parameters. -// Arrays: a,b,c -> []string{"a", "b", "c"} -// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} -func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // Split on comma and bind as key,value pairs - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} - -// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. -// Form style is the default for query and cookie parameters. -// Primitives: paramName=value -// Arrays: paramName=a¶mName=b¶mName=c -// Objects: key1=value1&key2=value2 -func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleFormExplodeSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleFormExplodeStruct(paramName, paramLocation, value) - case reflect.Map: - return styleFormExplodeMap(paramName, paramLocation, value) - default: - return styleFormExplodePrimitive(paramName, paramLocation, value) - } -} - -func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Form with explode: paramName=a¶mName=b¶mName=c - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, "&"+prefix), nil -} - -func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleFormExplodeParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. -// Simple style is the default for path and header parameters. -// Arrays are comma-separated: a,b,c -// Objects are key,value pairs: key1,value1,key2,value2 -func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return escapeParameterString(string(b), paramLocation), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleSimpleSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleSimpleStruct(paramName, paramLocation, value) - case reflect.Map: - return styleSimpleMap(paramName, paramLocation, value) - default: - return styleSimplePrimitive(paramLocation, value) - } -} - -func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return escapeParameterString(strVal, paramLocation), nil -} - -func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return escapeParameterString(timeVal, paramLocation), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleSimpleParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -// structToFieldDict converts a struct to a map of field names to string values. -func structToFieldDict(value interface{}) (map[string]string, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - fieldDict := make(map[string]string) - - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - f := v.Field(i) - - // Skip nil optional fields - if f.Type().Kind() == reflect.Ptr && f.IsNil() { - continue - } - str, err := primitiveToString(f.Interface()) - if err != nil { - return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) - } - fieldDict[fieldName] = str - } - return fieldDict, nil -} diff --git a/experimental/examples/petstore-expanded/gorilla/Makefile b/experimental/examples/petstore-expanded/gorilla/Makefile deleted file mode 100644 index 42389f4137..0000000000 --- a/experimental/examples/petstore-expanded/gorilla/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -SHELL:=/bin/bash - -YELLOW := \e[0;33m -RESET := \e[0;0m - -GOVER := $(shell go env GOVERSION) -GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") - -define execute-if-go-124 -@{ \ -if [[ 24 -le $(GOMINOR) ]]; then \ - $1; \ -else \ - echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ -fi \ -} -endef - -lint: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) - -lint-ci: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) - -generate: - $(call execute-if-go-124,go generate ./...) - -test: - $(call execute-if-go-124,go test -cover ./...) - -tidy: - $(call execute-if-go-124,go mod tidy) - -tidy-ci: - $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/gorilla/go.mod b/experimental/examples/petstore-expanded/gorilla/go.mod deleted file mode 100644 index fa094711da..0000000000 --- a/experimental/examples/petstore-expanded/gorilla/go.mod +++ /dev/null @@ -1,11 +0,0 @@ -module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/gorilla - -go 1.24.0 - -require ( - github.com/google/uuid v1.6.0 - github.com/gorilla/mux v1.8.1 - github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 -) - -replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/gorilla/go.sum b/experimental/examples/petstore-expanded/gorilla/go.sum deleted file mode 100644 index c9af5271c5..0000000000 --- a/experimental/examples/petstore-expanded/gorilla/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/experimental/examples/petstore-expanded/gorilla/main.go b/experimental/examples/petstore-expanded/gorilla/main.go deleted file mode 100644 index 1bde88bcca..0000000000 --- a/experimental/examples/petstore-expanded/gorilla/main.go +++ /dev/null @@ -1,40 +0,0 @@ -//go:build go1.22 - -// This is an example of implementing the Pet Store from the OpenAPI documentation -// found at: -// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml - -package main - -import ( - "flag" - "log" - "net" - "net/http" - - "github.com/gorilla/mux" - "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/gorilla/server" -) - -func main() { - port := flag.String("port", "8080", "Port for test HTTP server") - flag.Parse() - - // Create an instance of our handler which satisfies the generated interface - petStore := server.NewPetStore() - - r := mux.NewRouter() - - // We now register our petStore above as the handler for the interface - server.HandlerFromMux(petStore, r) - - s := &http.Server{ - Handler: r, - Addr: net.JoinHostPort("0.0.0.0", *port), - } - - log.Printf("Server listening on %s", s.Addr) - - // And we serve HTTP until the world ends. - log.Fatal(s.ListenAndServe()) -} diff --git a/experimental/examples/petstore-expanded/gorilla/server/petstore.go b/experimental/examples/petstore-expanded/gorilla/server/petstore.go deleted file mode 100644 index 2f52c8e271..0000000000 --- a/experimental/examples/petstore-expanded/gorilla/server/petstore.go +++ /dev/null @@ -1,135 +0,0 @@ -//go:build go1.22 - -package server - -import ( - "encoding/json" - "fmt" - "net/http" - "sync" - - petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" -) - -// PetStore implements the ServerInterface. -type PetStore struct { - Pets map[int64]petstore.Pet - NextId int64 - Lock sync.Mutex -} - -// Make sure we conform to ServerInterface -var _ ServerInterface = (*PetStore)(nil) - -// NewPetStore creates a new PetStore. -func NewPetStore() *PetStore { - return &PetStore{ - Pets: make(map[int64]petstore.Pet), - NextId: 1000, - } -} - -// sendPetStoreError wraps sending of an error in the Error format. -func sendPetStoreError(w http.ResponseWriter, code int, message string) { - petErr := petstore.Error{ - Code: int32(code), - Message: message, - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - _ = json.NewEncoder(w).Encode(petErr) -} - -// FindPets returns all pets, optionally filtered by tags and limited. -func (p *PetStore) FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) { - p.Lock.Lock() - defer p.Lock.Unlock() - - var result []petstore.Pet - - for _, pet := range p.Pets { - if params.Tags != nil { - // If we have tags, filter pets by tag - for _, t := range *params.Tags { - if pet.Tag != nil && (*pet.Tag == t) { - result = append(result, pet) - } - } - } else { - // Add all pets if we're not filtering - result = append(result, pet) - } - - if params.Limit != nil { - l := int(*params.Limit) - if len(result) >= l { - // We're at the limit - break - } - } - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(result) -} - -// AddPet creates a new pet. -func (p *PetStore) AddPet(w http.ResponseWriter, r *http.Request) { - // We expect a NewPet object in the request body. - var newPet petstore.NewPet - if err := json.NewDecoder(r.Body).Decode(&newPet); err != nil { - sendPetStoreError(w, http.StatusBadRequest, "Invalid format for NewPet") - return - } - - // We now have a pet, let's add it to our "database". - p.Lock.Lock() - defer p.Lock.Unlock() - - // We handle pets, not NewPets, which have an additional ID field - var pet petstore.Pet - pet.Name = newPet.Name - pet.Tag = newPet.Tag - pet.ID = p.NextId - p.NextId++ - - // Insert into map - p.Pets[pet.ID] = pet - - // Now, we have to return the Pet - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(pet) -} - -// FindPetByID returns a pet by ID. -func (p *PetStore) FindPetByID(w http.ResponseWriter, r *http.Request, id int64) { - p.Lock.Lock() - defer p.Lock.Unlock() - - pet, found := p.Pets[id] - if !found { - sendPetStoreError(w, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(pet) -} - -// DeletePet deletes a pet by ID. -func (p *PetStore) DeletePet(w http.ResponseWriter, r *http.Request, id int64) { - p.Lock.Lock() - defer p.Lock.Unlock() - - _, found := p.Pets[id] - if !found { - sendPetStoreError(w, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) - return - } - delete(p.Pets, id) - - w.WriteHeader(http.StatusNoContent) -} diff --git a/experimental/examples/petstore-expanded/gorilla/server/server.config.yaml b/experimental/examples/petstore-expanded/gorilla/server/server.config.yaml deleted file mode 100644 index 5de1d21f15..0000000000 --- a/experimental/examples/petstore-expanded/gorilla/server/server.config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -package: server -generation: - server: gorilla - models-package: - path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded - alias: petstore diff --git a/experimental/examples/petstore-expanded/gorilla/server/server.gen.go b/experimental/examples/petstore-expanded/gorilla/server/server.gen.go deleted file mode 100644 index 957f88149e..0000000000 --- a/experimental/examples/petstore-expanded/gorilla/server/server.gen.go +++ /dev/null @@ -1,1035 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package server - -import ( - "bytes" - "encoding" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "reflect" - "sort" - "strconv" - "strings" - "time" - - "github.com/google/uuid" - "github.com/gorilla/mux" -) - -// ServerInterface represents all server handlers. -type ServerInterface interface { - // Returns all pets - // (GET /pets) - FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) - // Creates a new pet - // (POST /pets) - AddPet(w http.ResponseWriter, r *http.Request) - // Deletes a pet by ID - // (DELETE /pets/{id}) - DeletePet(w http.ResponseWriter, r *http.Request, id int64) - // Returns a pet by ID - // (GET /pets/{id}) - FindPetByID(w http.ResponseWriter, r *http.Request, id int64) -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. -type Unimplemented struct{} - -// Returns all pets -// (GET /pets) -func (_ Unimplemented) FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) { - w.WriteHeader(http.StatusNotImplemented) -} - -// Creates a new pet -// (POST /pets) -func (_ Unimplemented) AddPet(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotImplemented) -} - -// Deletes a pet by ID -// (DELETE /pets/{id}) -func (_ Unimplemented) DeletePet(w http.ResponseWriter, r *http.Request, id int64) { - w.WriteHeader(http.StatusNotImplemented) -} - -// Returns a pet by ID -// (GET /pets/{id}) -func (_ Unimplemented) FindPetByID(w http.ResponseWriter, r *http.Request, id int64) { - w.WriteHeader(http.StatusNotImplemented) -} - -// FindPetsParams defines parameters for FindPets. -type FindPetsParams struct { - // tags (optional) - Tags *[]string `form:"tags" json:"tags"` - // limit (optional) - Limit *int32 `form:"limit" json:"limit"` -} - -// ServerInterfaceWrapper converts HTTP requests to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -// MiddlewareFunc is a middleware function type. -type MiddlewareFunc func(http.Handler) http.Handler - -// FindPets operation middleware -func (siw *ServerInterfaceWrapper) FindPets(w http.ResponseWriter, r *http.Request) { - var err error - - // Parameter object where we will unmarshal all parameters from the context - var params FindPetsParams - - // ------------- Optional query parameter "tags" ------------- - err = BindFormExplodeParam("tags", false, r.URL.Query(), ¶ms.Tags) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tags", Err: err}) - return - } - - // ------------- Optional query parameter "limit" ------------- - err = BindFormExplodeParam("limit", false, r.URL.Query(), ¶ms.Limit) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.FindPets(w, r, params) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// AddPet operation middleware -func (siw *ServerInterfaceWrapper) AddPet(w http.ResponseWriter, r *http.Request) { - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.AddPet(w, r) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// DeletePet operation middleware -func (siw *ServerInterfaceWrapper) DeletePet(w http.ResponseWriter, r *http.Request) { - var err error - - pathParams := mux.Vars(r) - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, pathParams["id"], &id) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DeletePet(w, r, id) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// FindPetByID operation middleware -func (siw *ServerInterfaceWrapper) FindPetByID(w http.ResponseWriter, r *http.Request) { - var err error - - pathParams := mux.Vars(r) - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, pathParams["id"], &id) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.FindPetByID(w, r, id) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// Handler creates http.Handler with routing matching OpenAPI spec. -func Handler(si ServerInterface) http.Handler { - return HandlerWithOptions(si, GorillaServerOptions{}) -} - -// GorillaServerOptions configures the Gorilla server. -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, - }) -} - -// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. -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) - } - } - - wrapper := ServerInterfaceWrapper{ - Handler: si, - HandlerMiddlewares: options.Middlewares, - ErrorHandlerFunc: options.ErrorHandlerFunc, - } - - r.HandleFunc(options.BaseURL+"/pets", wrapper.FindPets).Methods("GET") - r.HandleFunc(options.BaseURL+"/pets", wrapper.AddPet).Methods("POST") - r.HandleFunc(options.BaseURL+"/pets/{id}", wrapper.DeletePet).Methods("DELETE") - r.HandleFunc(options.BaseURL+"/pets/{id}", wrapper.FindPetByID).Methods("GET") - return r -} - -// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. -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 -} - -// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. -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 -} - -// RequiredParamError is returned when a required parameter is missing. -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -// RequiredHeaderError is returned when a required header is missing. -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 -} - -// InvalidParamFormatError is returned when a parameter has an invalid format. -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 -} - -// TooManyValuesForParamError is returned when a parameter has too many values. -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) -} - -// ParamLocation indicates where a parameter is located in an HTTP request. -type ParamLocation int - -const ( - ParamLocationUndefined ParamLocation = iota - ParamLocationQuery - ParamLocationPath - ParamLocationHeader - ParamLocationCookie -) - -// Binder is an interface for types that can bind themselves from a string value. -type Binder interface { - Bind(value string) error -} - -// DateFormat is the format used for date (without time) parameters. -const DateFormat = "2006-01-02" - -// Date represents a date (without time) for OpenAPI date format. -type Date struct { - time.Time -} - -// UnmarshalText implements encoding.TextUnmarshaler for Date. -func (d *Date) UnmarshalText(data []byte) error { - t, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = t - return nil -} - -// MarshalText implements encoding.TextMarshaler for Date. -func (d Date) MarshalText() ([]byte, error) { - return []byte(d.Format(DateFormat)), nil -} - -// Format returns the date formatted according to layout. -func (d Date) Format(layout string) string { - return d.Time.Format(layout) -} - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, types.Date, and types that implement -// json.Marshaler or fmt.Stringer. -func primitiveToString(value interface{}) (string, error) { - // Check for known types first (time, date, uuid) - if res, ok := marshalKnownTypes(value); ok { - return res, nil - } - - // Dereference pointers for optional values - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - kind := t.Kind() - - switch kind { - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: - return strconv.FormatInt(v.Int(), 10), nil - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: - return strconv.FormatUint(v.Uint(), 10), nil - case reflect.Float64: - return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil - case reflect.Float32: - return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil - case reflect.Bool: - if v.Bool() { - return "true", nil - } - return "false", nil - case reflect.String: - return v.String(), nil - case reflect.Struct: - // Check if it's a UUID - if u, ok := value.(uuid.UUID); ok { - return u.String(), nil - } - // Check if it implements json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - var i2 interface{} - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to decode JSON: %w", err) - } - return primitiveToString(i2) - } - fallthrough - default: - if s, ok := value.(fmt.Stringer); ok { - return s.String(), nil - } - return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) - } -} - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. -func marshalKnownTypes(value interface{}) (string, bool) { - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - tt := v.Convert(reflect.TypeOf(time.Time{})) - timeVal := tt.Interface().(time.Time) - return timeVal.Format(time.RFC3339Nano), true - } - - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - d := v.Convert(reflect.TypeOf(Date{})) - dateVal := d.Interface().(Date) - return dateVal.Format(DateFormat), true - } - - if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { - u := v.Convert(reflect.TypeOf(uuid.UUID{})) - uuidVal := u.Interface().(uuid.UUID) - return uuidVal.String(), true - } - - return "", false -} - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -func escapeParameterString(value string, paramLocation ParamLocation) string { - switch paramLocation { - case ParamLocationQuery: - return url.QueryEscape(value) - case ParamLocationPath: - return url.PathEscape(value) - default: - return value - } -} - -// unescapeParameterString unescapes a parameter value based on its location. -func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { - switch paramLocation { - case ParamLocationQuery, ParamLocationUndefined: - return url.QueryUnescape(value) - case ParamLocationPath: - return url.PathUnescape(value) - default: - return value, nil - } -} - -// sortedKeys returns the keys of a map in sorted order. -func sortedKeys(m map[string]string) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. -func BindStringToObject(src string, dst interface{}) error { - // Check for TextUnmarshaler - if tu, ok := dst.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(src)) - } - - // Check for Binder interface - if b, ok := dst.(Binder); ok { - return b.Bind(src) - } - - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr { - return fmt.Errorf("dst must be a pointer, got %T", dst) - } - v = v.Elem() - - switch v.Kind() { - case reflect.String: - v.SetString(src) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - i, err := strconv.ParseInt(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - v.SetInt(i) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - u, err := strconv.ParseUint(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse uint: %w", err) - } - v.SetUint(u) - case reflect.Float32, reflect.Float64: - f, err := strconv.ParseFloat(src, 64) - if err != nil { - return fmt.Errorf("failed to parse float: %w", err) - } - v.SetFloat(f) - case reflect.Bool: - b, err := strconv.ParseBool(src) - if err != nil { - return fmt.Errorf("failed to parse bool: %w", err) - } - v.SetBool(b) - default: - // Try JSON unmarshal as a fallback - return json.Unmarshal([]byte(src), dst) - } - return nil -} - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. -func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - newArray := reflect.MakeSlice(t, len(parts), len(parts)) - for i, p := range parts { - err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) - if err != nil { - return fmt.Errorf("error setting array element: %w", err) - } - } - v.Set(newArray) - return nil -} - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. -func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { - var fields []string - if explode { - fields = make([]string, len(parts)) - for i, property := range parts { - propertyParts := strings.Split(property, "=") - if len(propertyParts) != 2 { - return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) - } - fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" - } - } else { - if len(parts)%2 != 0 { - return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) - } - fields = make([]string, len(parts)/2) - for i := 0; i < len(parts); i += 2 { - key := parts[i] - value := parts[i+1] - fields[i/2] = "\"" + key + "\":\"" + value + "\"" - } - } - jsonParam := "{" + strings.Join(fields, ",") + "}" - return json.Unmarshal([]byte(jsonParam), dest) -} - -// BindFormExplodeParam binds a form-style parameter with explode to a destination. -// Form style is the default for query and cookie parameters. -// This handles the exploded case where arrays come as multiple query params. -// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) -// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) -func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { - dv := reflect.Indirect(reflect.ValueOf(dest)) - v := dv - var output interface{} - - if required { - output = dest - } else { - // For optional parameters, allocate if nil - if v.IsNil() { - t := v.Type() - newValue := reflect.New(t.Elem()) - output = newValue.Interface() - } else { - output = v.Interface() - } - v = reflect.Indirect(reflect.ValueOf(output)) - } - - t := v.Type() - k := t.Kind() - - values, found := queryParams[paramName] - - switch k { - case reflect.Slice: - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := bindSplitPartsToDestinationArray(values, output) - if err != nil { - return err - } - case reflect.Struct: - // For exploded objects, fields are spread across query params - fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) - if err != nil { - return err - } - if !fieldsPresent { - return nil - } - default: - // Primitive - if len(values) == 0 { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - if len(values) != 1 { - return fmt.Errorf("multiple values for single value parameter '%s'", paramName) - } - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := BindStringToObject(values[0], output) - if err != nil { - return err - } - } - - if !required { - dv.Set(reflect.ValueOf(output)) - } - return nil -} - -// bindParamsToExplodedObject binds query params to struct fields for exploded objects. -func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { - binder, v, t := indirectBinder(dest) - if binder != nil { - _, found := values[paramName] - if !found { - return false, nil - } - return true, BindStringToObject(values.Get(paramName), dest) - } - if t.Kind() != reflect.Struct { - return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) - } - - fieldsPresent := false - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - if !v.Field(i).CanSet() { - continue - } - - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - - fieldVal, found := values[fieldName] - if found { - if len(fieldVal) != 1 { - return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) - } - err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) - if err != nil { - return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) - } - fieldsPresent = true - } - } - return fieldsPresent, nil -} - -// indirectBinder checks if dest implements Binder and returns reflect values. -func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { - v := reflect.ValueOf(dest) - if v.Type().NumMethod() > 0 && v.CanInterface() { - if u, ok := v.Interface().(Binder); ok { - return u, reflect.Value{}, nil - } - } - v = reflect.Indirect(v) - t := v.Type() - // Handle special types like time.Time and Date - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - return dest, reflect.Value{}, nil - } - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - return dest, reflect.Value{}, nil - } - return nil, v, t -} - -// BindSimpleParam binds a simple-style parameter without explode to a destination. -// Simple style is the default for path and header parameters. -// Arrays: a,b,c -> []string{"a", "b", "c"} -// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} -func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // Split on comma and bind as key,value pairs - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} - -// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. -// Form style is the default for query and cookie parameters. -// Primitives: paramName=value -// Arrays: paramName=a¶mName=b¶mName=c -// Objects: key1=value1&key2=value2 -func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleFormExplodeSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleFormExplodeStruct(paramName, paramLocation, value) - case reflect.Map: - return styleFormExplodeMap(paramName, paramLocation, value) - default: - return styleFormExplodePrimitive(paramName, paramLocation, value) - } -} - -func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Form with explode: paramName=a¶mName=b¶mName=c - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, "&"+prefix), nil -} - -func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleFormExplodeParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. -// Simple style is the default for path and header parameters. -// Arrays are comma-separated: a,b,c -// Objects are key,value pairs: key1,value1,key2,value2 -func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return escapeParameterString(string(b), paramLocation), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleSimpleSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleSimpleStruct(paramName, paramLocation, value) - case reflect.Map: - return styleSimpleMap(paramName, paramLocation, value) - default: - return styleSimplePrimitive(paramLocation, value) - } -} - -func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return escapeParameterString(strVal, paramLocation), nil -} - -func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return escapeParameterString(timeVal, paramLocation), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleSimpleParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -// structToFieldDict converts a struct to a map of field names to string values. -func structToFieldDict(value interface{}) (map[string]string, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - fieldDict := make(map[string]string) - - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - f := v.Field(i) - - // Skip nil optional fields - if f.Type().Kind() == reflect.Ptr && f.IsNil() { - continue - } - str, err := primitiveToString(f.Interface()) - if err != nil { - return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) - } - fieldDict[fieldName] = str - } - return fieldDict, nil -} diff --git a/experimental/examples/petstore-expanded/iris/Makefile b/experimental/examples/petstore-expanded/iris/Makefile deleted file mode 100644 index 42389f4137..0000000000 --- a/experimental/examples/petstore-expanded/iris/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -SHELL:=/bin/bash - -YELLOW := \e[0;33m -RESET := \e[0;0m - -GOVER := $(shell go env GOVERSION) -GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") - -define execute-if-go-124 -@{ \ -if [[ 24 -le $(GOMINOR) ]]; then \ - $1; \ -else \ - echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ -fi \ -} -endef - -lint: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) - -lint-ci: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) - -generate: - $(call execute-if-go-124,go generate ./...) - -test: - $(call execute-if-go-124,go test -cover ./...) - -tidy: - $(call execute-if-go-124,go mod tidy) - -tidy-ci: - $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/iris/go.mod b/experimental/examples/petstore-expanded/iris/go.mod deleted file mode 100644 index b13819f2a5..0000000000 --- a/experimental/examples/petstore-expanded/iris/go.mod +++ /dev/null @@ -1,55 +0,0 @@ -module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/iris - -go 1.24.0 - -require ( - github.com/google/uuid v1.6.0 - github.com/kataras/iris/v12 v12.2.11 - github.com/oapi-codegen/oapi-codegen/experimental v0.0.0 -) - -require ( - github.com/BurntSushi/toml v1.3.2 // indirect - github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect - github.com/CloudyKit/jet/v6 v6.2.0 // indirect - github.com/Joker/jade v1.1.3 // indirect - github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect - github.com/andybalholm/brotli v1.1.0 // indirect - github.com/aymerick/douceur v0.2.0 // indirect - github.com/fatih/structs v1.1.0 // indirect - github.com/flosch/pongo2/v4 v4.0.2 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 // indirect - github.com/gorilla/css v1.0.0 // indirect - github.com/iris-contrib/schema v0.0.6 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/kataras/blocks v0.0.8 // indirect - github.com/kataras/golog v0.1.11 // indirect - github.com/kataras/pio v0.0.13 // indirect - github.com/kataras/sitemap v0.0.6 // indirect - github.com/kataras/tunnel v0.0.4 // indirect - github.com/klauspost/compress v1.17.7 // indirect - github.com/mailgun/raymond/v2 v2.0.48 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/microcosm-cc/bluemonday v1.0.26 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/schollz/closestmatch v2.1.0+incompatible // indirect - github.com/sirupsen/logrus v1.8.1 // indirect - github.com/tdewolff/minify/v2 v2.20.19 // indirect - github.com/tdewolff/parse/v2 v2.7.12 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect - github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/yosssi/ace v0.0.5 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.33.0 // indirect - golang.org/x/time v0.5.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) - -replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/iris/go.sum b/experimental/examples/petstore-expanded/iris/go.sum deleted file mode 100644 index a06439c9fc..0000000000 --- a/experimental/examples/petstore-expanded/iris/go.sum +++ /dev/null @@ -1,178 +0,0 @@ -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= -github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= -github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= -github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= -github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= -github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= -github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= -github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= -github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= -github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= -github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= -github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= -github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= -github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 h1:4gjrh/PN2MuWCCElk8/I4OCKRKWCCo2zEct3VKCbibU= -github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= -github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= -github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= -github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= -github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= -github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= -github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= -github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= -github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= -github.com/kataras/golog v0.1.11 h1:dGkcCVsIpqiAMWTlebn/ZULHxFvfG4K43LF1cNWSh20= -github.com/kataras/golog v0.1.11/go.mod h1:mAkt1vbPowFUuUGvexyQ5NFW6djEgGyxQBIARJ0AH4A= -github.com/kataras/iris/v12 v12.2.11 h1:sGgo43rMPfzDft8rjVhPs6L3qDJy3TbBrMD/zGL1pzk= -github.com/kataras/iris/v12 v12.2.11/go.mod h1:uMAeX8OqG9vqdhyrIPv8Lajo/wXTtAF43wchP9WHt2w= -github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= -github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= -github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= -github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= -github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= -github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= -github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= -github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= -github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= -github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= -github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= -github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= -github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= -github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= -github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo= -github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM= -github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ= -github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= -github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= -github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= -github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= -github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= -github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= -github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= -github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= -github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= -golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= -moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= diff --git a/experimental/examples/petstore-expanded/iris/main.go b/experimental/examples/petstore-expanded/iris/main.go deleted file mode 100644 index 48d035a84d..0000000000 --- a/experimental/examples/petstore-expanded/iris/main.go +++ /dev/null @@ -1,35 +0,0 @@ -//go:build go1.22 - -// This is an example of implementing the Pet Store from the OpenAPI documentation -// found at: -// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml - -package main - -import ( - "flag" - "log" - "net" - - "github.com/kataras/iris/v12" - "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/iris/server" -) - -func main() { - port := flag.String("port", "8080", "Port for test HTTP server") - flag.Parse() - - // Create an instance of our handler which satisfies the generated interface - petStore := server.NewPetStore() - - app := iris.New() - - // We now register our petStore above as the handler for the interface - server.RegisterHandlers(app, petStore) - - addr := net.JoinHostPort("0.0.0.0", *port) - log.Printf("Server listening on %s", addr) - - // And we serve HTTP until the world ends. - log.Fatal(app.Listen(addr)) -} diff --git a/experimental/examples/petstore-expanded/iris/server/petstore.go b/experimental/examples/petstore-expanded/iris/server/petstore.go deleted file mode 100644 index cab647706c..0000000000 --- a/experimental/examples/petstore-expanded/iris/server/petstore.go +++ /dev/null @@ -1,130 +0,0 @@ -//go:build go1.22 - -package server - -import ( - "net/http" - "sync" - - "github.com/kataras/iris/v12" - petstore "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded" -) - -// PetStore implements the ServerInterface. -type PetStore struct { - Pets map[int64]petstore.Pet - NextId int64 - Lock sync.Mutex -} - -// Make sure we conform to ServerInterface -var _ ServerInterface = (*PetStore)(nil) - -// NewPetStore creates a new PetStore. -func NewPetStore() *PetStore { - return &PetStore{ - Pets: make(map[int64]petstore.Pet), - NextId: 1000, - } -} - -// sendPetStoreError wraps sending of an error in the Error format. -func sendPetStoreError(ctx iris.Context, code int, message string) { - petErr := petstore.Error{ - Code: int32(code), - Message: message, - } - ctx.StatusCode(code) - _ = ctx.JSON(petErr) -} - -// FindPets returns all pets, optionally filtered by tags and limited. -func (p *PetStore) FindPets(ctx iris.Context, params FindPetsParams) { - p.Lock.Lock() - defer p.Lock.Unlock() - - var result []petstore.Pet - - for _, pet := range p.Pets { - if params.Tags != nil { - // If we have tags, filter pets by tag - for _, t := range *params.Tags { - if pet.Tag != nil && (*pet.Tag == t) { - result = append(result, pet) - } - } - } else { - // Add all pets if we're not filtering - result = append(result, pet) - } - - if params.Limit != nil { - l := int(*params.Limit) - if len(result) >= l { - // We're at the limit - break - } - } - } - - ctx.StatusCode(http.StatusOK) - _ = ctx.JSON(result) -} - -// AddPet creates a new pet. -func (p *PetStore) AddPet(ctx iris.Context) { - // We expect a NewPet object in the request body. - var newPet petstore.NewPet - if err := ctx.ReadJSON(&newPet); err != nil { - sendPetStoreError(ctx, http.StatusBadRequest, "Invalid format for NewPet") - return - } - - // We now have a pet, let's add it to our "database". - p.Lock.Lock() - defer p.Lock.Unlock() - - // We handle pets, not NewPets, which have an additional ID field - var pet petstore.Pet - pet.Name = newPet.Name - pet.Tag = newPet.Tag - pet.ID = p.NextId - p.NextId++ - - // Insert into map - p.Pets[pet.ID] = pet - - // Now, we have to return the Pet - ctx.StatusCode(http.StatusCreated) - _ = ctx.JSON(pet) -} - -// FindPetByID returns a pet by ID. -func (p *PetStore) FindPetByID(ctx iris.Context, id int64) { - p.Lock.Lock() - defer p.Lock.Unlock() - - pet, found := p.Pets[id] - if !found { - sendPetStoreError(ctx, http.StatusNotFound, "Could not find pet with ID") - return - } - - ctx.StatusCode(http.StatusOK) - _ = ctx.JSON(pet) -} - -// DeletePet deletes a pet by ID. -func (p *PetStore) DeletePet(ctx iris.Context, id int64) { - p.Lock.Lock() - defer p.Lock.Unlock() - - _, found := p.Pets[id] - if !found { - sendPetStoreError(ctx, http.StatusNotFound, "Could not find pet with ID") - return - } - delete(p.Pets, id) - - ctx.StatusCode(http.StatusNoContent) -} diff --git a/experimental/examples/petstore-expanded/iris/server/server.config.yaml b/experimental/examples/petstore-expanded/iris/server/server.config.yaml deleted file mode 100644 index 443b44a84b..0000000000 --- a/experimental/examples/petstore-expanded/iris/server/server.config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -package: server -generation: - server: iris - models-package: - path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded - alias: petstore diff --git a/experimental/examples/petstore-expanded/iris/server/server.gen.go b/experimental/examples/petstore-expanded/iris/server/server.gen.go deleted file mode 100644 index 1c59478225..0000000000 --- a/experimental/examples/petstore-expanded/iris/server/server.gen.go +++ /dev/null @@ -1,973 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package server - -import ( - "bytes" - "encoding" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "reflect" - "sort" - "strconv" - "strings" - "time" - - "github.com/google/uuid" - "github.com/kataras/iris/v12" -) - -// ServerInterface represents all server handlers. -type ServerInterface interface { - // Returns all pets - // (GET /pets) - FindPets(ctx iris.Context, params FindPetsParams) - // Creates a new pet - // (POST /pets) - AddPet(ctx iris.Context) - // Deletes a pet by ID - // (DELETE /pets/{id}) - DeletePet(ctx iris.Context, id int64) - // Returns a pet by ID - // (GET /pets/{id}) - FindPetByID(ctx iris.Context, id int64) -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. -type Unimplemented struct{} - -// Returns all pets -// (GET /pets) -func (_ Unimplemented) FindPets(ctx iris.Context, params FindPetsParams) { - ctx.StatusCode(http.StatusNotImplemented) -} - -// Creates a new pet -// (POST /pets) -func (_ Unimplemented) AddPet(ctx iris.Context) { - ctx.StatusCode(http.StatusNotImplemented) -} - -// Deletes a pet by ID -// (DELETE /pets/{id}) -func (_ Unimplemented) DeletePet(ctx iris.Context, id int64) { - ctx.StatusCode(http.StatusNotImplemented) -} - -// Returns a pet by ID -// (GET /pets/{id}) -func (_ Unimplemented) FindPetByID(ctx iris.Context, id int64) { - ctx.StatusCode(http.StatusNotImplemented) -} - -// FindPetsParams defines parameters for FindPets. -type FindPetsParams struct { - // tags (optional) - Tags *[]string `form:"tags" json:"tags"` - // limit (optional) - Limit *int32 `form:"limit" json:"limit"` -} - -// ServerInterfaceWrapper converts iris contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface -} - -// FindPets converts iris context to params. -func (w *ServerInterfaceWrapper) FindPets(ctx iris.Context) { - var err error - - // Parameter object where we will unmarshal all parameters from the context - var params FindPetsParams - - // ------------- Optional query parameter "tags" ------------- - err = BindFormExplodeParam("tags", false, ctx.Request().URL.Query(), ¶ms.Tags) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString(fmt.Sprintf("Invalid format for parameter tags: %s", err)) - return - } - - // ------------- Optional query parameter "limit" ------------- - err = BindFormExplodeParam("limit", false, ctx.Request().URL.Query(), ¶ms.Limit) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString(fmt.Sprintf("Invalid format for parameter limit: %s", err)) - return - } - - // Invoke the callback with all the unmarshaled arguments - w.Handler.FindPets(ctx, params) -} - -// AddPet converts iris context to params. -func (w *ServerInterfaceWrapper) AddPet(ctx iris.Context) { - - // Invoke the callback with all the unmarshaled arguments - w.Handler.AddPet(ctx) -} - -// DeletePet converts iris context to params. -func (w *ServerInterfaceWrapper) DeletePet(ctx iris.Context) { - var err error - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, ctx.Params().Get("id"), &id) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString(fmt.Sprintf("Invalid format for parameter id: %s", err)) - return - } - - // Invoke the callback with all the unmarshaled arguments - w.Handler.DeletePet(ctx, id) -} - -// FindPetByID converts iris context to params. -func (w *ServerInterfaceWrapper) FindPetByID(ctx iris.Context) { - var err error - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, ctx.Params().Get("id"), &id) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString(fmt.Sprintf("Invalid format for parameter id: %s", err)) - return - } - - // Invoke the callback with all the unmarshaled arguments - w.Handler.FindPetByID(ctx, id) -} - -// IrisServerOptions is the option for iris server. -type IrisServerOptions struct { - BaseURL string - Middlewares []iris.Handler -} - -// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. -func RegisterHandlers(router *iris.Application, si ServerInterface) { - RegisterHandlersWithOptions(router, si, IrisServerOptions{}) -} - -// RegisterHandlersWithOptions creates http.Handler with additional options. -func RegisterHandlersWithOptions(router *iris.Application, si ServerInterface, options IrisServerOptions) { - - wrapper := ServerInterfaceWrapper{ - Handler: si, - } - - router.Get(options.BaseURL+"/pets", wrapper.FindPets) - router.Post(options.BaseURL+"/pets", wrapper.AddPet) - router.Delete(options.BaseURL+"/pets/:id", wrapper.DeletePet) - router.Get(options.BaseURL+"/pets/:id", wrapper.FindPetByID) - router.Build() -} - -// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. -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 -} - -// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. -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 -} - -// RequiredParamError is returned when a required parameter is missing. -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -// RequiredHeaderError is returned when a required header is missing. -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 -} - -// InvalidParamFormatError is returned when a parameter has an invalid format. -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 -} - -// TooManyValuesForParamError is returned when a parameter has too many values. -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) -} - -// ParamLocation indicates where a parameter is located in an HTTP request. -type ParamLocation int - -const ( - ParamLocationUndefined ParamLocation = iota - ParamLocationQuery - ParamLocationPath - ParamLocationHeader - ParamLocationCookie -) - -// Binder is an interface for types that can bind themselves from a string value. -type Binder interface { - Bind(value string) error -} - -// DateFormat is the format used for date (without time) parameters. -const DateFormat = "2006-01-02" - -// Date represents a date (without time) for OpenAPI date format. -type Date struct { - time.Time -} - -// UnmarshalText implements encoding.TextUnmarshaler for Date. -func (d *Date) UnmarshalText(data []byte) error { - t, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = t - return nil -} - -// MarshalText implements encoding.TextMarshaler for Date. -func (d Date) MarshalText() ([]byte, error) { - return []byte(d.Format(DateFormat)), nil -} - -// Format returns the date formatted according to layout. -func (d Date) Format(layout string) string { - return d.Time.Format(layout) -} - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, types.Date, and types that implement -// json.Marshaler or fmt.Stringer. -func primitiveToString(value interface{}) (string, error) { - // Check for known types first (time, date, uuid) - if res, ok := marshalKnownTypes(value); ok { - return res, nil - } - - // Dereference pointers for optional values - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - kind := t.Kind() - - switch kind { - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: - return strconv.FormatInt(v.Int(), 10), nil - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: - return strconv.FormatUint(v.Uint(), 10), nil - case reflect.Float64: - return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil - case reflect.Float32: - return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil - case reflect.Bool: - if v.Bool() { - return "true", nil - } - return "false", nil - case reflect.String: - return v.String(), nil - case reflect.Struct: - // Check if it's a UUID - if u, ok := value.(uuid.UUID); ok { - return u.String(), nil - } - // Check if it implements json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - var i2 interface{} - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to decode JSON: %w", err) - } - return primitiveToString(i2) - } - fallthrough - default: - if s, ok := value.(fmt.Stringer); ok { - return s.String(), nil - } - return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) - } -} - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. -func marshalKnownTypes(value interface{}) (string, bool) { - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - tt := v.Convert(reflect.TypeOf(time.Time{})) - timeVal := tt.Interface().(time.Time) - return timeVal.Format(time.RFC3339Nano), true - } - - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - d := v.Convert(reflect.TypeOf(Date{})) - dateVal := d.Interface().(Date) - return dateVal.Format(DateFormat), true - } - - if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { - u := v.Convert(reflect.TypeOf(uuid.UUID{})) - uuidVal := u.Interface().(uuid.UUID) - return uuidVal.String(), true - } - - return "", false -} - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -func escapeParameterString(value string, paramLocation ParamLocation) string { - switch paramLocation { - case ParamLocationQuery: - return url.QueryEscape(value) - case ParamLocationPath: - return url.PathEscape(value) - default: - return value - } -} - -// unescapeParameterString unescapes a parameter value based on its location. -func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { - switch paramLocation { - case ParamLocationQuery, ParamLocationUndefined: - return url.QueryUnescape(value) - case ParamLocationPath: - return url.PathUnescape(value) - default: - return value, nil - } -} - -// sortedKeys returns the keys of a map in sorted order. -func sortedKeys(m map[string]string) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. -func BindStringToObject(src string, dst interface{}) error { - // Check for TextUnmarshaler - if tu, ok := dst.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(src)) - } - - // Check for Binder interface - if b, ok := dst.(Binder); ok { - return b.Bind(src) - } - - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr { - return fmt.Errorf("dst must be a pointer, got %T", dst) - } - v = v.Elem() - - switch v.Kind() { - case reflect.String: - v.SetString(src) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - i, err := strconv.ParseInt(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - v.SetInt(i) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - u, err := strconv.ParseUint(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse uint: %w", err) - } - v.SetUint(u) - case reflect.Float32, reflect.Float64: - f, err := strconv.ParseFloat(src, 64) - if err != nil { - return fmt.Errorf("failed to parse float: %w", err) - } - v.SetFloat(f) - case reflect.Bool: - b, err := strconv.ParseBool(src) - if err != nil { - return fmt.Errorf("failed to parse bool: %w", err) - } - v.SetBool(b) - default: - // Try JSON unmarshal as a fallback - return json.Unmarshal([]byte(src), dst) - } - return nil -} - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. -func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - newArray := reflect.MakeSlice(t, len(parts), len(parts)) - for i, p := range parts { - err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) - if err != nil { - return fmt.Errorf("error setting array element: %w", err) - } - } - v.Set(newArray) - return nil -} - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. -func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { - var fields []string - if explode { - fields = make([]string, len(parts)) - for i, property := range parts { - propertyParts := strings.Split(property, "=") - if len(propertyParts) != 2 { - return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) - } - fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" - } - } else { - if len(parts)%2 != 0 { - return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) - } - fields = make([]string, len(parts)/2) - for i := 0; i < len(parts); i += 2 { - key := parts[i] - value := parts[i+1] - fields[i/2] = "\"" + key + "\":\"" + value + "\"" - } - } - jsonParam := "{" + strings.Join(fields, ",") + "}" - return json.Unmarshal([]byte(jsonParam), dest) -} - -// BindFormExplodeParam binds a form-style parameter with explode to a destination. -// Form style is the default for query and cookie parameters. -// This handles the exploded case where arrays come as multiple query params. -// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) -// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) -func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { - dv := reflect.Indirect(reflect.ValueOf(dest)) - v := dv - var output interface{} - - if required { - output = dest - } else { - // For optional parameters, allocate if nil - if v.IsNil() { - t := v.Type() - newValue := reflect.New(t.Elem()) - output = newValue.Interface() - } else { - output = v.Interface() - } - v = reflect.Indirect(reflect.ValueOf(output)) - } - - t := v.Type() - k := t.Kind() - - values, found := queryParams[paramName] - - switch k { - case reflect.Slice: - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := bindSplitPartsToDestinationArray(values, output) - if err != nil { - return err - } - case reflect.Struct: - // For exploded objects, fields are spread across query params - fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) - if err != nil { - return err - } - if !fieldsPresent { - return nil - } - default: - // Primitive - if len(values) == 0 { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - if len(values) != 1 { - return fmt.Errorf("multiple values for single value parameter '%s'", paramName) - } - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := BindStringToObject(values[0], output) - if err != nil { - return err - } - } - - if !required { - dv.Set(reflect.ValueOf(output)) - } - return nil -} - -// bindParamsToExplodedObject binds query params to struct fields for exploded objects. -func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { - binder, v, t := indirectBinder(dest) - if binder != nil { - _, found := values[paramName] - if !found { - return false, nil - } - return true, BindStringToObject(values.Get(paramName), dest) - } - if t.Kind() != reflect.Struct { - return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) - } - - fieldsPresent := false - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - if !v.Field(i).CanSet() { - continue - } - - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - - fieldVal, found := values[fieldName] - if found { - if len(fieldVal) != 1 { - return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) - } - err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) - if err != nil { - return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) - } - fieldsPresent = true - } - } - return fieldsPresent, nil -} - -// indirectBinder checks if dest implements Binder and returns reflect values. -func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { - v := reflect.ValueOf(dest) - if v.Type().NumMethod() > 0 && v.CanInterface() { - if u, ok := v.Interface().(Binder); ok { - return u, reflect.Value{}, nil - } - } - v = reflect.Indirect(v) - t := v.Type() - // Handle special types like time.Time and Date - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - return dest, reflect.Value{}, nil - } - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - return dest, reflect.Value{}, nil - } - return nil, v, t -} - -// BindSimpleParam binds a simple-style parameter without explode to a destination. -// Simple style is the default for path and header parameters. -// Arrays: a,b,c -> []string{"a", "b", "c"} -// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} -func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // Split on comma and bind as key,value pairs - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} - -// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. -// Form style is the default for query and cookie parameters. -// Primitives: paramName=value -// Arrays: paramName=a¶mName=b¶mName=c -// Objects: key1=value1&key2=value2 -func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleFormExplodeSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleFormExplodeStruct(paramName, paramLocation, value) - case reflect.Map: - return styleFormExplodeMap(paramName, paramLocation, value) - default: - return styleFormExplodePrimitive(paramName, paramLocation, value) - } -} - -func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Form with explode: paramName=a¶mName=b¶mName=c - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, "&"+prefix), nil -} - -func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleFormExplodeParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. -// Simple style is the default for path and header parameters. -// Arrays are comma-separated: a,b,c -// Objects are key,value pairs: key1,value1,key2,value2 -func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return escapeParameterString(string(b), paramLocation), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleSimpleSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleSimpleStruct(paramName, paramLocation, value) - case reflect.Map: - return styleSimpleMap(paramName, paramLocation, value) - default: - return styleSimplePrimitive(paramLocation, value) - } -} - -func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return escapeParameterString(strVal, paramLocation), nil -} - -func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return escapeParameterString(timeVal, paramLocation), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleSimpleParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -// structToFieldDict converts a struct to a map of field names to string values. -func structToFieldDict(value interface{}) (map[string]string, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - fieldDict := make(map[string]string) - - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - f := v.Field(i) - - // Skip nil optional fields - if f.Type().Kind() == reflect.Ptr && f.IsNil() { - continue - } - str, err := primitiveToString(f.Interface()) - if err != nil { - return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) - } - fieldDict[fieldName] = str - } - return fieldDict, nil -} diff --git a/experimental/examples/petstore-expanded/models.config.yaml b/experimental/examples/petstore-expanded/models.config.yaml deleted file mode 100644 index 47d99a649f..0000000000 --- a/experimental/examples/petstore-expanded/models.config.yaml +++ /dev/null @@ -1,2 +0,0 @@ -package: petstore -output: petstore.gen.go diff --git a/experimental/examples/petstore-expanded/petstore-expanded.yaml b/experimental/examples/petstore-expanded/petstore-expanded.yaml deleted file mode 100644 index f9f84f2854..0000000000 --- a/experimental/examples/petstore-expanded/petstore-expanded.yaml +++ /dev/null @@ -1,164 +0,0 @@ -openapi: "3.0.0" -info: - version: 1.0.0 - title: Swagger Petstore - description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification - termsOfService: https://swagger.io/terms/ - contact: - name: Swagger API Team - email: apiteam@swagger.io - url: https://swagger.io - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0.html -servers: - - url: https://petstore.swagger.io/api -paths: - /pets: - get: - summary: Returns all pets - description: | - Returns all pets from the system that the user has access to - Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. - - Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. - operationId: findPets - parameters: - - name: tags - in: query - description: tags to filter by - required: false - style: form - schema: - type: array - items: - type: string - - name: limit - in: query - description: maximum number of results to return - required: false - schema: - type: integer - format: int32 - responses: - '200': - description: pet response - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Pet' - default: - description: unexpected error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - post: - summary: Creates a new pet - description: Creates a new pet in the store. Duplicates are allowed - operationId: addPet - requestBody: - description: Pet to add to the store - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NewPet' - responses: - '200': - description: pet response - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - default: - description: unexpected error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /pets/{id}: - get: - summary: Returns a pet by ID - description: Returns a pet based on a single ID - operationId: findPetByID - parameters: - - name: id - in: path - description: ID of pet to fetch - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: pet response - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - default: - description: unexpected error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - delete: - summary: Deletes a pet by ID - description: deletes a single pet based on the ID supplied - operationId: deletePet - parameters: - - name: id - in: path - description: ID of pet to delete - required: true - schema: - type: integer - format: int64 - responses: - '204': - description: pet deleted - default: - description: unexpected error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' -components: - schemas: - Pet: - allOf: - - $ref: '#/components/schemas/NewPet' - - required: - - id - properties: - id: - type: integer - format: int64 - description: Unique id of the pet - - NewPet: - required: - - name - properties: - name: - type: string - description: Name of the pet - tag: - type: string - description: Type of the pet - - Error: - required: - - code - - message - properties: - code: - type: integer - format: int32 - description: Error code - message: - type: string - description: Error message diff --git a/experimental/examples/petstore-expanded/petstore.gen.go b/experimental/examples/petstore-expanded/petstore.gen.go deleted file mode 100644 index 4149df2b00..0000000000 --- a/experimental/examples/petstore-expanded/petstore.gen.go +++ /dev/null @@ -1,117 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package petstore - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/Pet -type Pet struct { - Name string `json:"name" form:"name"` // Name of the pet - Tag *string `json:"tag,omitempty" form:"tag,omitempty"` // Type of the pet - ID int64 `json:"id" form:"id"` // Unique id of the pet -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Pet) ApplyDefaults() { -} - -// #/components/schemas/NewPet -type NewPet struct { - Name string `json:"name" form:"name"` // Name of the pet - Tag *string `json:"tag,omitempty" form:"tag,omitempty"` // Type of the pet -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *NewPet) ApplyDefaults() { -} - -// #/components/schemas/Error -type Error struct { - Code int32 `json:"code" form:"code"` // Error code - Message string `json:"message" form:"message"` // Error message -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Error) ApplyDefaults() { -} - -// #/paths//pets/get/responses/200/content/application/json/schema -type FindPetsJSONResponse = []Pet - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/+xXXW9byQ19168gtgXyoly5yaIPemo2TgED3cStd/tOz6UkLubLQ44coe1/Lzj3Srqy", - "ZG+CYvuF9YMNz3A4hzyHHN6UKWLmJXzztrvqrr6ZcVyl5QxgS0U4xSX8ztZnAMrqaQl3j7heU4FbUtFU", - "aAbQk7jCWZv5OxAM2RO8u70B3aBCFRJAyOMBQAGMQJ8HM03QU0hRtKASrAi1FhLgCLoh+JQpmqe33RVI", - "Jscrdmg3GSIqQT6t7qhs2dESNqpZlouFDBA7TotmspgBuBQVnVpkABHDJBJz/wNhaFsUkP0SMLMShj8c", - "XbXdWvyla2YAnh1Foan/dxndhuBNy96Ts4+Pjx22/S6V9WI8LYs/3bz/8PHuw+s33VW30eBnQsWYML+v", - "T13s89lNwsXMs4y6afbNYgC0pjFyAKkhYNkt4S+ktUQB9L5xM+6fkPn3cRHOrGFVUmgMyU6UwkC1/V+F", - "CmyMZOdIBDQdnHzEAEK9kdFzoKg1AIl28D2So4gCSiGnAoJrVmUBwcwU5xDJQdmk6KqAUJgYsAIG0g7e", - "USSMgArrglvuEbCuK80BHTC66rkd7eB9LXjPWguknhP4VCjMIZWIhYDWpECeRnSR3BxcLVIFuAdPTqt0", - "cF1ZIDBoLZllDrn6LUcsdheVZLHPQTk67mtU2GLhKvBTFU0d3ETYoIONgUARguxRCaFnpzVYOm6ikikT", - "FbDnzOI4rgGjWjTH2D2vq8dD5HmDhbTgPolmDyF5EmUCDplKz5apv/IWwxAQen6oGKBntMwUFHiw2Lbk", - "WSGmCJqKpmIp4RXF/nB7B7cFSSiqwaTI4QigloiwTb5qRoUtRYpogIfk2q+AtZiPm3j0vKIyZn2Fjj3L", - "ySXtBvs1P/LrQFKPnozYfm55dFRQLTD728FdlUyxZ8uyRxNPn3wqc1OgkFMTdYuyScWinsOWNuyqR+Co", - "VPoawPM9ldTB96ncM1BlCamf0mDbTdgeHUfGbnaQ/B31jY8qsCKToE/3qbRjlI66KVVLDV2rkIDN7UgB", - "i58D1ZOaGYgHX02NptEObjco5P1QHpnKeLwlu5FMCiusju/rkHbc32N20/Nb8iOBvKVScH56tVULcD8/", - "lGPk+00HPypk8p6ikjxUgpykktXTvpS6lgrc14KV3j6je0/7sFo+5w3IQRyxRgdaWNRigS0rUgd/rOII", - "SFtP6CsfasH6hTjyVLjBGVS8PxBMMxWbhFwNghECri1k8iNbHfy5DkdD8sbbwB7VQUFHKPNDCwKszkpl", - "sBxFOoQ9SmRsNYeaNMkYwcBxfoQylm9k4T1gMQyOtfZsUEUQqu7VNhI53HSStHZfB7dTYlrmRoy5kHIN", - "k/41iKbOJyq3BtyNek7Z6opTvOmXsOLY3x4fjozFsjA+VsPP6/EpVFzLYRGA4xIeKpXdZO3k3TF7mwxW", - "7JUK3E8NCz1ULmQA0AtNdkR3NqGsUgnTVbehgMvJCoDuMi0BS8HdyTorBTk13RuLFo7rs8g8B9avCS3g", - "Zw72stRwTwXSCgpJ9driLe2V/bJgnw2Lh9fjZMdygtr23r6Z7X1LtjY4CfjVm6urV8vnoGfSw6GJjY1W", - "FPUUCubsx0lt8ZOk+DSpl+C/xMyz7NjPbwutlvDqNwuXQk6RospiuEAWt6SvZsdwVli9PhthjfQ527tg", - "D0hJ5ZeK8iXAH+ziAXJOcj63vS+E2ibqSI/GyaXB7cxoP08PIyNc1wG4mdhI7n16pP5SlWNvRT47ypFE", - "v0v9bjm7mMFbUhMy9r39Odw4Oxe0lnpcvpDdl3N7ObMv5fUjPU608F8p/v9PHbfvkMXfuP/HF3yMtDzf", - "7+Dm+pKsn5ihPaA2vYFwXHs6nrr0UH23O2y/9FZx/6Sd2wfVc4m+ubYOngfRr0jd5mLzPtH6v9C7f//t", - "r/L997fhnjwpnWn2ui3/rGb7g9ko0hPpWoe8uQapFsLlDjw4ODbhX0a8wy3/SfV++7J6B4D9/6KIjjt2", - "fNwcPN0emyF6/2k1pfTLXzOzPvB1gvD1qSRyMWkp05M5ivvLU+8lRi9zepGDH2P7WuPehGZSt3GlHRjw", - "L2dPtPZE0LPnUTe9z35mSn8C5yMGmiI5nMX117r6YZfpLKhG90sxudTT5N9AIrh+KUo7cA7tnJVL8/0Z", - "5gbvFMOI4GujHzzt4f8zAAD//6ZcwVZEFgAA", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/examples/petstore-expanded/stdhttp/Makefile b/experimental/examples/petstore-expanded/stdhttp/Makefile deleted file mode 100644 index 42389f4137..0000000000 --- a/experimental/examples/petstore-expanded/stdhttp/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -SHELL:=/bin/bash - -YELLOW := \e[0;33m -RESET := \e[0;0m - -GOVER := $(shell go env GOVERSION) -GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") - -define execute-if-go-124 -@{ \ -if [[ 24 -le $(GOMINOR) ]]; then \ - $1; \ -else \ - echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ -fi \ -} -endef - -lint: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) - -lint-ci: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) - -generate: - $(call execute-if-go-124,go generate ./...) - -test: - $(call execute-if-go-124,go test -cover ./...) - -tidy: - $(call execute-if-go-124,go mod tidy) - -tidy-ci: - $(call execute-if-go-124,tidied -verbose) diff --git a/experimental/examples/petstore-expanded/stdhttp/go.mod b/experimental/examples/petstore-expanded/stdhttp/go.mod deleted file mode 100644 index e211ad75fb..0000000000 --- a/experimental/examples/petstore-expanded/stdhttp/go.mod +++ /dev/null @@ -1,7 +0,0 @@ -module github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/stdhttp - -go 1.24.0 - -require github.com/google/uuid v1.6.0 - -replace github.com/oapi-codegen/oapi-codegen/experimental => ../../../ diff --git a/experimental/examples/petstore-expanded/stdhttp/go.sum b/experimental/examples/petstore-expanded/stdhttp/go.sum deleted file mode 100644 index 7790d7c3e0..0000000000 --- a/experimental/examples/petstore-expanded/stdhttp/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/experimental/examples/petstore-expanded/stdhttp/main.go b/experimental/examples/petstore-expanded/stdhttp/main.go deleted file mode 100644 index 9f438a128d..0000000000 --- a/experimental/examples/petstore-expanded/stdhttp/main.go +++ /dev/null @@ -1,39 +0,0 @@ -//go:build go1.22 - -// This is an example of implementing the Pet Store from the OpenAPI documentation -// found at: -// https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml - -package main - -import ( - "flag" - "log" - "net" - "net/http" - - "github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded/stdhttp/server" -) - -func main() { - port := flag.String("port", "8080", "Port for test HTTP server") - flag.Parse() - - // Create an instance of our handler which satisfies the generated interface - petStore := server.NewPetStore() - - r := http.NewServeMux() - - // We now register our petStore above as the handler for the interface - server.HandlerFromMux(petStore, r) - - s := &http.Server{ - Handler: r, - Addr: net.JoinHostPort("0.0.0.0", *port), - } - - log.Printf("Server listening on %s", s.Addr) - - // And we serve HTTP until the world ends. - log.Fatal(s.ListenAndServe()) -} diff --git a/experimental/examples/petstore-expanded/stdhttp/server/petstore.go b/experimental/examples/petstore-expanded/stdhttp/server/petstore.go deleted file mode 100644 index f05a50f1e6..0000000000 --- a/experimental/examples/petstore-expanded/stdhttp/server/petstore.go +++ /dev/null @@ -1,163 +0,0 @@ -//go:build go1.22 - -package server - -import ( - "encoding/json" - "fmt" - "net/http" - "sync" -) - -// Pet defines model for Pet. -type Pet struct { - // Id Unique id of the pet - Id int64 `json:"id"` - - // Name Name of the pet - Name string `json:"name"` - - // Tag Type of the pet - Tag *string `json:"tag,omitempty"` -} - -// NewPet defines model for NewPet. -type NewPet struct { - // Name Name of the pet - Name string `json:"name"` - - // Tag Type of the pet - Tag *string `json:"tag,omitempty"` -} - -// Error defines model for Error. -type Error struct { - // Code Error code - Code int32 `json:"code"` - - // Message Error message - Message string `json:"message"` -} - -// PetStore implements the ServerInterface. -type PetStore struct { - Pets map[int64]Pet - NextId int64 - Lock sync.Mutex -} - -// Make sure we conform to ServerInterface -var _ ServerInterface = (*PetStore)(nil) - -// NewPetStore creates a new PetStore. -func NewPetStore() *PetStore { - return &PetStore{ - Pets: make(map[int64]Pet), - NextId: 1000, - } -} - -// sendPetStoreError wraps sending of an error in the Error format. -func sendPetStoreError(w http.ResponseWriter, code int, message string) { - petErr := Error{ - Code: int32(code), - Message: message, - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - _ = json.NewEncoder(w).Encode(petErr) -} - -// FindPets returns all pets, optionally filtered by tags and limited. -func (p *PetStore) FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) { - p.Lock.Lock() - defer p.Lock.Unlock() - - var result []Pet - - for _, pet := range p.Pets { - if params.Tags != nil { - // If we have tags, filter pets by tag - for _, t := range *params.Tags { - if pet.Tag != nil && (*pet.Tag == t) { - result = append(result, pet) - } - } - } else { - // Add all pets if we're not filtering - result = append(result, pet) - } - - if params.Limit != nil { - l := int(*params.Limit) - if len(result) >= l { - // We're at the limit - break - } - } - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(result) -} - -// AddPet creates a new pet. -func (p *PetStore) AddPet(w http.ResponseWriter, r *http.Request) { - // We expect a NewPet object in the request body. - var newPet NewPet - if err := json.NewDecoder(r.Body).Decode(&newPet); err != nil { - sendPetStoreError(w, http.StatusBadRequest, "Invalid format for NewPet") - return - } - - // We now have a pet, let's add it to our "database". - p.Lock.Lock() - defer p.Lock.Unlock() - - // We handle pets, not NewPets, which have an additional ID field - var pet Pet - pet.Name = newPet.Name - pet.Tag = newPet.Tag - pet.Id = p.NextId - p.NextId++ - - // Insert into map - p.Pets[pet.Id] = pet - - // Now, we have to return the Pet - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(pet) -} - -// FindPetByID returns a pet by ID. -func (p *PetStore) FindPetByID(w http.ResponseWriter, r *http.Request, id int64) { - p.Lock.Lock() - defer p.Lock.Unlock() - - pet, found := p.Pets[id] - if !found { - sendPetStoreError(w, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(pet) -} - -// DeletePet deletes a pet by ID. -func (p *PetStore) DeletePet(w http.ResponseWriter, r *http.Request, id int64) { - p.Lock.Lock() - defer p.Lock.Unlock() - - _, found := p.Pets[id] - if !found { - sendPetStoreError(w, http.StatusNotFound, fmt.Sprintf("Could not find pet with ID %d", id)) - return - } - delete(p.Pets, id) - - w.WriteHeader(http.StatusNoContent) -} diff --git a/experimental/examples/petstore-expanded/stdhttp/server/server.config.yaml b/experimental/examples/petstore-expanded/stdhttp/server/server.config.yaml deleted file mode 100644 index 4bb9655ead..0000000000 --- a/experimental/examples/petstore-expanded/stdhttp/server/server.config.yaml +++ /dev/null @@ -1,7 +0,0 @@ -package: server -output: server.gen.go -generation: - server: std-http - models-package: - path: github.com/oapi-codegen/oapi-codegen/experimental/examples/petstore-expanded - alias: petstore diff --git a/experimental/examples/petstore-expanded/stdhttp/server/server.gen.go b/experimental/examples/petstore-expanded/stdhttp/server/server.gen.go deleted file mode 100644 index a65e2e57e1..0000000000 --- a/experimental/examples/petstore-expanded/stdhttp/server/server.gen.go +++ /dev/null @@ -1,1009 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package server - -import ( - "bytes" - "encoding" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "reflect" - "sort" - "strconv" - "strings" - "time" - - "github.com/google/uuid" -) - -// ServerInterface represents all server handlers. -type ServerInterface interface { - // Returns all pets - // (GET /pets) - FindPets(w http.ResponseWriter, r *http.Request, params FindPetsParams) - // Creates a new pet - // (POST /pets) - AddPet(w http.ResponseWriter, r *http.Request) - // Deletes a pet by ID - // (DELETE /pets/{id}) - DeletePet(w http.ResponseWriter, r *http.Request, id int64) - // Returns a pet by ID - // (GET /pets/{id}) - FindPetByID(w http.ResponseWriter, r *http.Request, id int64) -} - -// FindPetsParams defines parameters for FindPets. -type FindPetsParams struct { - // tags (optional) - Tags *[]string `form:"tags" json:"tags"` - // limit (optional) - Limit *int32 `form:"limit" json:"limit"` -} - -// ServerInterfaceWrapper converts HTTP requests to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -// MiddlewareFunc is a middleware function type. -type MiddlewareFunc func(http.Handler) http.Handler - -// FindPets operation middleware -func (siw *ServerInterfaceWrapper) FindPets(w http.ResponseWriter, r *http.Request) { - var err error - - // Parameter object where we will unmarshal all parameters from the context - var params FindPetsParams - - // ------------- Optional query parameter "tags" ------------- - err = BindFormExplodeParam("tags", false, r.URL.Query(), ¶ms.Tags) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tags", Err: err}) - return - } - - // ------------- Optional query parameter "limit" ------------- - err = BindFormExplodeParam("limit", false, r.URL.Query(), ¶ms.Limit) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.FindPets(w, r, params) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// AddPet operation middleware -func (siw *ServerInterfaceWrapper) AddPet(w http.ResponseWriter, r *http.Request) { - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.AddPet(w, r) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// DeletePet operation middleware -func (siw *ServerInterfaceWrapper) DeletePet(w http.ResponseWriter, r *http.Request) { - var err error - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, r.PathValue("id"), &id) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DeletePet(w, r, id) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// FindPetByID operation middleware -func (siw *ServerInterfaceWrapper) FindPetByID(w http.ResponseWriter, r *http.Request) { - var err error - - // ------------- Path parameter "id" ------------- - var id int64 - - err = BindSimpleParam("id", ParamLocationPath, r.PathValue("id"), &id) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.FindPetByID(w, r, id) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// 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)) - ServeHTTP(w http.ResponseWriter, r *http.Request) -} - -// StdHTTPServerOptions configures the StdHTTP server. -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, - }) -} - -// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. -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) - } - } - - wrapper := ServerInterfaceWrapper{ - Handler: si, - HandlerMiddlewares: options.Middlewares, - ErrorHandlerFunc: options.ErrorHandlerFunc, - } - - m.HandleFunc("GET "+options.BaseURL+"/pets", wrapper.FindPets) - m.HandleFunc("POST "+options.BaseURL+"/pets", wrapper.AddPet) - m.HandleFunc("DELETE "+options.BaseURL+"/pets/{id}", wrapper.DeletePet) - m.HandleFunc("GET "+options.BaseURL+"/pets/{id}", wrapper.FindPetByID) - return m -} - -// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. -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 -} - -// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. -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 -} - -// RequiredParamError is returned when a required parameter is missing. -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -// RequiredHeaderError is returned when a required header is missing. -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 -} - -// InvalidParamFormatError is returned when a parameter has an invalid format. -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 -} - -// TooManyValuesForParamError is returned when a parameter has too many values. -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) -} - -// ParamLocation indicates where a parameter is located in an HTTP request. -type ParamLocation int - -const ( - ParamLocationUndefined ParamLocation = iota - ParamLocationQuery - ParamLocationPath - ParamLocationHeader - ParamLocationCookie -) - -// Binder is an interface for types that can bind themselves from a string value. -type Binder interface { - Bind(value string) error -} - -// DateFormat is the format used for date (without time) parameters. -const DateFormat = "2006-01-02" - -// Date represents a date (without time) for OpenAPI date format. -type Date struct { - time.Time -} - -// UnmarshalText implements encoding.TextUnmarshaler for Date. -func (d *Date) UnmarshalText(data []byte) error { - t, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = t - return nil -} - -// MarshalText implements encoding.TextMarshaler for Date. -func (d Date) MarshalText() ([]byte, error) { - return []byte(d.Format(DateFormat)), nil -} - -// Format returns the date formatted according to layout. -func (d Date) Format(layout string) string { - return d.Time.Format(layout) -} - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, types.Date, and types that implement -// json.Marshaler or fmt.Stringer. -func primitiveToString(value interface{}) (string, error) { - // Check for known types first (time, date, uuid) - if res, ok := marshalKnownTypes(value); ok { - return res, nil - } - - // Dereference pointers for optional values - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - kind := t.Kind() - - switch kind { - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: - return strconv.FormatInt(v.Int(), 10), nil - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: - return strconv.FormatUint(v.Uint(), 10), nil - case reflect.Float64: - return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil - case reflect.Float32: - return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil - case reflect.Bool: - if v.Bool() { - return "true", nil - } - return "false", nil - case reflect.String: - return v.String(), nil - case reflect.Struct: - // Check if it's a UUID - if u, ok := value.(uuid.UUID); ok { - return u.String(), nil - } - // Check if it implements json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - var i2 interface{} - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to decode JSON: %w", err) - } - return primitiveToString(i2) - } - fallthrough - default: - if s, ok := value.(fmt.Stringer); ok { - return s.String(), nil - } - return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) - } -} - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. -func marshalKnownTypes(value interface{}) (string, bool) { - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - tt := v.Convert(reflect.TypeOf(time.Time{})) - timeVal := tt.Interface().(time.Time) - return timeVal.Format(time.RFC3339Nano), true - } - - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - d := v.Convert(reflect.TypeOf(Date{})) - dateVal := d.Interface().(Date) - return dateVal.Format(DateFormat), true - } - - if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { - u := v.Convert(reflect.TypeOf(uuid.UUID{})) - uuidVal := u.Interface().(uuid.UUID) - return uuidVal.String(), true - } - - return "", false -} - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -func escapeParameterString(value string, paramLocation ParamLocation) string { - switch paramLocation { - case ParamLocationQuery: - return url.QueryEscape(value) - case ParamLocationPath: - return url.PathEscape(value) - default: - return value - } -} - -// unescapeParameterString unescapes a parameter value based on its location. -func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { - switch paramLocation { - case ParamLocationQuery, ParamLocationUndefined: - return url.QueryUnescape(value) - case ParamLocationPath: - return url.PathUnescape(value) - default: - return value, nil - } -} - -// sortedKeys returns the keys of a map in sorted order. -func sortedKeys(m map[string]string) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. -func BindStringToObject(src string, dst interface{}) error { - // Check for TextUnmarshaler - if tu, ok := dst.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(src)) - } - - // Check for Binder interface - if b, ok := dst.(Binder); ok { - return b.Bind(src) - } - - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr { - return fmt.Errorf("dst must be a pointer, got %T", dst) - } - v = v.Elem() - - switch v.Kind() { - case reflect.String: - v.SetString(src) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - i, err := strconv.ParseInt(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - v.SetInt(i) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - u, err := strconv.ParseUint(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse uint: %w", err) - } - v.SetUint(u) - case reflect.Float32, reflect.Float64: - f, err := strconv.ParseFloat(src, 64) - if err != nil { - return fmt.Errorf("failed to parse float: %w", err) - } - v.SetFloat(f) - case reflect.Bool: - b, err := strconv.ParseBool(src) - if err != nil { - return fmt.Errorf("failed to parse bool: %w", err) - } - v.SetBool(b) - default: - // Try JSON unmarshal as a fallback - return json.Unmarshal([]byte(src), dst) - } - return nil -} - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. -func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - newArray := reflect.MakeSlice(t, len(parts), len(parts)) - for i, p := range parts { - err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) - if err != nil { - return fmt.Errorf("error setting array element: %w", err) - } - } - v.Set(newArray) - return nil -} - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. -func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { - var fields []string - if explode { - fields = make([]string, len(parts)) - for i, property := range parts { - propertyParts := strings.Split(property, "=") - if len(propertyParts) != 2 { - return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) - } - fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" - } - } else { - if len(parts)%2 != 0 { - return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) - } - fields = make([]string, len(parts)/2) - for i := 0; i < len(parts); i += 2 { - key := parts[i] - value := parts[i+1] - fields[i/2] = "\"" + key + "\":\"" + value + "\"" - } - } - jsonParam := "{" + strings.Join(fields, ",") + "}" - return json.Unmarshal([]byte(jsonParam), dest) -} - -// BindFormExplodeParam binds a form-style parameter with explode to a destination. -// Form style is the default for query and cookie parameters. -// This handles the exploded case where arrays come as multiple query params. -// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) -// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) -func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { - dv := reflect.Indirect(reflect.ValueOf(dest)) - v := dv - var output interface{} - - if required { - output = dest - } else { - // For optional parameters, allocate if nil - if v.IsNil() { - t := v.Type() - newValue := reflect.New(t.Elem()) - output = newValue.Interface() - } else { - output = v.Interface() - } - v = reflect.Indirect(reflect.ValueOf(output)) - } - - t := v.Type() - k := t.Kind() - - values, found := queryParams[paramName] - - switch k { - case reflect.Slice: - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := bindSplitPartsToDestinationArray(values, output) - if err != nil { - return err - } - case reflect.Struct: - // For exploded objects, fields are spread across query params - fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) - if err != nil { - return err - } - if !fieldsPresent { - return nil - } - default: - // Primitive - if len(values) == 0 { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - if len(values) != 1 { - return fmt.Errorf("multiple values for single value parameter '%s'", paramName) - } - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := BindStringToObject(values[0], output) - if err != nil { - return err - } - } - - if !required { - dv.Set(reflect.ValueOf(output)) - } - return nil -} - -// bindParamsToExplodedObject binds query params to struct fields for exploded objects. -func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { - binder, v, t := indirectBinder(dest) - if binder != nil { - _, found := values[paramName] - if !found { - return false, nil - } - return true, BindStringToObject(values.Get(paramName), dest) - } - if t.Kind() != reflect.Struct { - return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) - } - - fieldsPresent := false - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - if !v.Field(i).CanSet() { - continue - } - - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - - fieldVal, found := values[fieldName] - if found { - if len(fieldVal) != 1 { - return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) - } - err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) - if err != nil { - return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) - } - fieldsPresent = true - } - } - return fieldsPresent, nil -} - -// indirectBinder checks if dest implements Binder and returns reflect values. -func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { - v := reflect.ValueOf(dest) - if v.Type().NumMethod() > 0 && v.CanInterface() { - if u, ok := v.Interface().(Binder); ok { - return u, reflect.Value{}, nil - } - } - v = reflect.Indirect(v) - t := v.Type() - // Handle special types like time.Time and Date - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - return dest, reflect.Value{}, nil - } - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - return dest, reflect.Value{}, nil - } - return nil, v, t -} - -// BindSimpleParam binds a simple-style parameter without explode to a destination. -// Simple style is the default for path and header parameters. -// Arrays: a,b,c -> []string{"a", "b", "c"} -// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} -func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // Split on comma and bind as key,value pairs - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} - -// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. -// Form style is the default for query and cookie parameters. -// Primitives: paramName=value -// Arrays: paramName=a¶mName=b¶mName=c -// Objects: key1=value1&key2=value2 -func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleFormExplodeSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleFormExplodeStruct(paramName, paramLocation, value) - case reflect.Map: - return styleFormExplodeMap(paramName, paramLocation, value) - default: - return styleFormExplodePrimitive(paramName, paramLocation, value) - } -} - -func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Form with explode: paramName=a¶mName=b¶mName=c - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, "&"+prefix), nil -} - -func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleFormExplodeParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. -// Simple style is the default for path and header parameters. -// Arrays are comma-separated: a,b,c -// Objects are key,value pairs: key1,value1,key2,value2 -func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return escapeParameterString(string(b), paramLocation), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleSimpleSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleSimpleStruct(paramName, paramLocation, value) - case reflect.Map: - return styleSimpleMap(paramName, paramLocation, value) - default: - return styleSimplePrimitive(paramLocation, value) - } -} - -func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return escapeParameterString(strVal, paramLocation), nil -} - -func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return escapeParameterString(timeVal, paramLocation), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleSimpleParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -// structToFieldDict converts a struct to a map of field names to string values. -func structToFieldDict(value interface{}) (map[string]string, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - fieldDict := make(map[string]string) - - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - f := v.Field(i) - - // Skip nil optional fields - if f.Type().Kind() == reflect.Ptr && f.IsNil() { - continue - } - str, err := primitiveToString(f.Interface()) - if err != nil { - return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) - } - fieldDict[fieldName] = str - } - return fieldDict, nil -} diff --git a/experimental/examples/webhook/client/main.go b/experimental/examples/webhook/client/main.go deleted file mode 100644 index 2d12c19acc..0000000000 --- a/experimental/examples/webhook/client/main.go +++ /dev/null @@ -1,150 +0,0 @@ -package main - -import ( - "bytes" - "context" - "encoding/json" - "flag" - "fmt" - "log" - "net" - "net/http" - "time" - - "github.com/google/uuid" - - doorbadge "github.com/oapi-codegen/oapi-codegen/experimental/examples/webhook" -) - -// WebhookReceiver implements doorbadge.WebhookReceiverInterface. -type WebhookReceiver struct{} - -var _ doorbadge.WebhookReceiverInterface = (*WebhookReceiver)(nil) - -func (wr *WebhookReceiver) HandleEnterEventWebhook(w http.ResponseWriter, r *http.Request) { - var person doorbadge.Person - if err := json.NewDecoder(r.Body).Decode(&person); err != nil { - log.Printf("Error decoding enter event: %v", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - log.Printf("ENTER: %s", person.Name) - w.WriteHeader(http.StatusOK) -} - -func (wr *WebhookReceiver) HandleExitEventWebhook(w http.ResponseWriter, r *http.Request) { - var person doorbadge.Person - if err := json.NewDecoder(r.Body).Decode(&person); err != nil { - log.Printf("Error decoding exit event: %v", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - log.Printf("EXIT: %s", person.Name) - w.WriteHeader(http.StatusOK) -} - -func registerWebhook(client *http.Client, serverAddr, kind, url string) (uuid.UUID, error) { - body, err := json.Marshal(doorbadge.WebhookRegistration{URL: url}) - if err != nil { - return uuid.UUID{}, err - } - - resp, err := client.Post( - serverAddr+"/api/webhook/"+kind, - "application/json", - bytes.NewReader(body), - ) - if err != nil { - return uuid.UUID{}, err - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusCreated { - return uuid.UUID{}, fmt.Errorf("unexpected status %d", resp.StatusCode) - } - - var regResp doorbadge.WebhookRegistrationResponse - if err := json.NewDecoder(resp.Body).Decode(®Resp); err != nil { - return uuid.UUID{}, err - } - return regResp.ID, nil -} - -func deregisterWebhook(client *http.Client, serverAddr string, id uuid.UUID) error { - req, err := http.NewRequest(http.MethodDelete, serverAddr+"/api/webhook/"+id.String(), nil) - if err != nil { - return err - } - resp, err := client.Do(req) - if err != nil { - return err - } - _ = resp.Body.Close() - return nil -} - -func main() { - serverAddr := flag.String("server", "http://localhost:8080", "Badge reader server address") - duration := flag.Duration("duration", 30*time.Second, "How long to listen for events") - flag.Parse() - - // Start the webhook receiver on an ephemeral port. - receiver := &WebhookReceiver{} - - mux := http.NewServeMux() - mux.Handle("POST /enter", doorbadge.EnterEventWebhookHandler(receiver, nil)) - mux.Handle("POST /exit", doorbadge.ExitEventWebhookHandler(receiver, nil)) - - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - log.Fatalf("Failed to listen: %v", err) - } - callbackPort := listener.Addr().(*net.TCPAddr).Port - baseURL := fmt.Sprintf("http://localhost:%d", callbackPort) - log.Printf("Webhook receiver listening on port %d", callbackPort) - - srv := &http.Server{Handler: mux} - go func() { - if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { - log.Printf("Webhook server stopped: %v", err) - } - }() - - // Register webhooks for both event kinds. - client := &http.Client{} - - kinds := [2]string{"enterEvent", "exitEvent"} - urls := [2]string{baseURL + "/enter", baseURL + "/exit"} - var registrationIDs [2]uuid.UUID - - for i, kind := range kinds { - id, err := registerWebhook(client, *serverAddr, kind, urls[i]) - if err != nil { - log.Fatalf("Failed to register %s webhook: %v", kind, err) - } - registrationIDs[i] = id - log.Printf("Registered %s webhook: id=%s url=%s", kind, id, urls[i]) - } - - log.Printf("Listening for events for %s...", *duration) - - // Wait for the specified duration. - time.Sleep(*duration) - - // Deregister webhooks cleanly. - log.Printf("Duration elapsed, deregistering webhooks...") - for i, id := range registrationIDs { - if err := deregisterWebhook(client, *serverAddr, id); err != nil { - log.Printf("Failed to deregister %s webhook: %v", kinds[i], err) - continue - } - log.Printf("Deregistered %s webhook: id=%s", kinds[i], id) - } - - // Shut down the local webhook server. - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - _ = srv.Shutdown(ctx) - - log.Printf("Done!") -} diff --git a/experimental/examples/webhook/config.yaml b/experimental/examples/webhook/config.yaml deleted file mode 100644 index 6a95d87ee1..0000000000 --- a/experimental/examples/webhook/config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -package: doorbadge -output: doorbadge.gen.go -generation: - webhook-initiator: true - webhook-receiver: true - server: std-http diff --git a/experimental/examples/webhook/doc.go b/experimental/examples/webhook/doc.go deleted file mode 100644 index ee6d3b6920..0000000000 --- a/experimental/examples/webhook/doc.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:generate go run github.com/oapi-codegen/oapi-codegen/experimental/cmd/oapi-codegen -config config.yaml door-badge-reader.yaml - -// Package doorbadge provides an example of OpenAPI 3.1 webhooks. -// A door badge reader server generates random enter/exit events and -// notifies registered webhook listeners. -// -// You can run the example by running these two commands in parallel: -// -// go run ./server --port 8080 -// go run ./client --server http://localhost:8080 -// -// You can run multiple clients and they will all get the notifications -package doorbadge diff --git a/experimental/examples/webhook/door-badge-reader.yaml b/experimental/examples/webhook/door-badge-reader.yaml deleted file mode 100644 index ac37ce40dd..0000000000 --- a/experimental/examples/webhook/door-badge-reader.yaml +++ /dev/null @@ -1,139 +0,0 @@ -openapi: "3.1.0" -info: - version: 1.0.0 - title: Door Badge Reader - description: | - A door badge reader service that demonstrates OpenAPI 3.1 webhooks. - Clients register for enterEvent and exitEvent webhooks. The server - randomly generates badge events and notifies all registered listeners. - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0.html -paths: - /api/webhook/{kind}: - post: - summary: Register a webhook - description: | - Registers a webhook for the given event kind. The server will POST - to the provided URL whenever an event of that kind occurs. - operationId: RegisterWebhook - parameters: - - name: kind - in: path - required: true - schema: - type: string - enum: - - enterEvent - - exitEvent - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/WebhookRegistration' - responses: - '201': - description: Webhook registered - content: - application/json: - schema: - $ref: '#/components/schemas/WebhookRegistrationResponse' - default: - description: unexpected error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /api/webhook/{id}: - delete: - summary: Deregister a webhook - description: Removes a previously registered webhook by its ID. - operationId: DeregisterWebhook - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - responses: - '204': - description: Webhook deregistered - default: - description: unexpected error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' -webhooks: - enterEvent: - post: - summary: Person entered the building - operationId: EnterEvent - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Person' - responses: - '200': - description: Event received - exitEvent: - post: - summary: Person exited the building - operationId: ExitEvent - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Person' - responses: - '200': - description: Event received -components: - schemas: - WebhookRegistration: - type: object - required: - - url - properties: - url: - type: string - format: uri - description: URL to receive webhook events - - WebhookRegistrationResponse: - type: object - required: - - id - properties: - id: - type: string - format: uuid - description: Unique identifier for this webhook registration - - Person: - type: object - required: - - name - properties: - name: - type: string - description: Name of the person who badged in or out - - Error: - type: object - required: - - code - - message - properties: - code: - type: integer - format: int32 - description: Error code - message: - type: string - description: Error message diff --git a/experimental/examples/webhook/doorbadge.gen.go b/experimental/examples/webhook/doorbadge.gen.go deleted file mode 100644 index 702d74db62..0000000000 --- a/experimental/examples/webhook/doorbadge.gen.go +++ /dev/null @@ -1,1069 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package doorbadge - -import ( - "bytes" - "compress/gzip" - "context" - "encoding" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "reflect" - "sort" - "strconv" - "strings" - "sync" - "time" - - "github.com/google/uuid" -) - -// #/components/schemas/WebhookRegistration -type WebhookRegistration struct { - URL string `json:"url" form:"url"` // URL to receive webhook events -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *WebhookRegistration) ApplyDefaults() { -} - -// #/components/schemas/WebhookRegistrationResponse -type WebhookRegistrationResponse struct { - ID UUID `json:"id" form:"id"` // Unique identifier for this webhook registration -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *WebhookRegistrationResponse) ApplyDefaults() { -} - -// #/components/schemas/Person -type Person struct { - Name string `json:"name" form:"name"` // Name of the person who badged in or out -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Person) ApplyDefaults() { -} - -// #/components/schemas/Error -type Error struct { - Code int32 `json:"code" form:"code"` // Error code - Message string `json:"message" form:"message"` // Error message -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Error) ApplyDefaults() { -} - -// #/paths//api/webhook/{kind}/post/parameters/0/schema -type PostAPIWebhookKindParameter string - -const ( - PostAPIWebhookKindParameter_enterEvent PostAPIWebhookKindParameter = "enterEvent" - PostAPIWebhookKindParameter_exitEvent PostAPIWebhookKindParameter = "exitEvent" -) - -type UUID = uuid.UUID - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/9RWTY/bNhC961cM0gI+RfLu9qTbJuvDAkGycFL0TItjaxKJZIaUvEbb/16Q1GdX3l0D", - "RZv6ZEnDmfceh4+jDSphKIc3N+lVun6TkNrrPAFokS1plcNVuk7XCYAjV2EOd1ozvBPygLBFIZETAIm2", - "YDIuxP+RAADcgvSBuxDIIRAscksFgiuFA4m1VtaxcGjhk0F1+3APN+kVHHFXav3NpiHP+4pQOQuMB7IO", - "GfaaAZVD3rSoHAglAR/JxadhLXwpMdQL+ABYKKnr6gQHVBhrRmjYhvQ+jdKO9oQWRFUN9VBC5f8o5ICo", - "ogKVxTxkVaLGHG6NKEqE66ASQMNVDqVzxuZZdjweUxG+p5oPWbfaZh/u328+ft68vU7XaenqKjHCldZn", - "zYShrOOR/f6NlPwzFjPauvgPwDZ1LfiUw7aXRfTcu4iFLfG/Pt6OC4KirkQ4UIsqCgK+7lREOFJVwcOn", - "z1+GVE6HVYZ1SxIl/Lr9AMcSFfpw0SfS+7jdPiHoomi421gAbfxOkFb3ciTy24yFESxq9Hjzoe7bTnaf", - "cXgJQCoHL+LkFeP3hhhlDo4bnHywRYm1yCdvANzJYA7WManD7AOqpp6HegxjCz791LdjMsJA695peRrz", - "nMFWaOVQuWk9YUxFRRAq+2q1mmNZogLwM+M+h9VPWaFro5Vv8SxG2qxTOAoeN2A1ILVG+/4c862u11er", - "afpZY3W5JqdlErnA5SU25/hczGjbMVklI+69aCp3lkqj8NFg4VACMmv+L4hsfOHVExOg3gIkVujwiQnc", - "Ib/KBrZY69b7GxjGlnRjq9PU6HpD2J2AnIX7u8WTOlZ7/Vmlf+Wk7jXXwuXQNEO95Y7+5eWOlrjQ0/+j", - "HupvQr949KpnbpIHZKtVjEUZrH3XUCVHkWddsPm7//1gLhfpPG9s6/NtEMcJxgKpDQ0wmPorJHwk9xoF", - "f+xr4p8VcCzgV3Q14uIF9+6zxpOud1+xmKoUBJl4TMNV70HsJXY0xennseRF9xi8g+kcJT/gON1zGtwy", - "TpDJOS79TXQhp8HBliiRvIRRM3PfOSVF3xsEkqjC7MvdMEh2oMcTLpFk7IwL+fh74BlG4Zp4mdMM/EdR", - "Y5wvEUw8esdSx7leAinQDLpxEXSwxQsxF1ri5LFGa8XhORZ+wVMWpBwekBe2hpS7uT57gjziOYYOwaVC", - "xUw9/L8CAAD//+XREy3yDQAA", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} - -// ServerInterface represents all server handlers. -type ServerInterface interface { - // Deregister a webhook - // (DELETE /api/webhook/{id}) - DeregisterWebhook(w http.ResponseWriter, r *http.Request, id uuid.UUID) - // Register a webhook - // (POST /api/webhook/{kind}) - RegisterWebhook(w http.ResponseWriter, r *http.Request, kind string) -} - -// ServerInterfaceWrapper converts HTTP requests to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -// MiddlewareFunc is a middleware function type. -type MiddlewareFunc func(http.Handler) http.Handler - -// DeregisterWebhook operation middleware -func (siw *ServerInterfaceWrapper) DeregisterWebhook(w http.ResponseWriter, r *http.Request) { - var err error - - // ------------- Path parameter "id" ------------- - var id uuid.UUID - - err = BindSimpleParam("id", ParamLocationPath, r.PathValue("id"), &id) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DeregisterWebhook(w, r, id) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// RegisterWebhook operation middleware -func (siw *ServerInterfaceWrapper) RegisterWebhook(w http.ResponseWriter, r *http.Request) { - var err error - - // ------------- Path parameter "kind" ------------- - var kind string - - err = BindSimpleParam("kind", ParamLocationPath, r.PathValue("kind"), &kind) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "kind", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.RegisterWebhook(w, r, kind) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// 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)) - ServeHTTP(w http.ResponseWriter, r *http.Request) -} - -// StdHTTPServerOptions configures the StdHTTP server. -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, - }) -} - -// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. -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) - } - } - - wrapper := ServerInterfaceWrapper{ - Handler: si, - HandlerMiddlewares: options.Middlewares, - ErrorHandlerFunc: options.ErrorHandlerFunc, - } - - m.HandleFunc("DELETE "+options.BaseURL+"/api/webhook/{id}", wrapper.DeregisterWebhook) - m.HandleFunc("POST "+options.BaseURL+"/api/webhook/{kind}", wrapper.RegisterWebhook) - return m -} - -// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. -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 -} - -// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. -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 -} - -// RequiredParamError is returned when a required parameter is missing. -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -// RequiredHeaderError is returned when a required header is missing. -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 -} - -// InvalidParamFormatError is returned when a parameter has an invalid format. -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 -} - -// TooManyValuesForParamError is returned when a parameter has too many values. -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) -} - -// ParamLocation indicates where a parameter is located in an HTTP request. -type ParamLocation int - -const ( - ParamLocationUndefined ParamLocation = iota - ParamLocationQuery - ParamLocationPath - ParamLocationHeader - ParamLocationCookie -) - -// Binder is an interface for types that can bind themselves from a string value. -type Binder interface { - Bind(value string) error -} - -// DateFormat is the format used for date (without time) parameters. -const DateFormat = "2006-01-02" - -// Date represents a date (without time) for OpenAPI date format. -type Date struct { - time.Time -} - -// UnmarshalText implements encoding.TextUnmarshaler for Date. -func (d *Date) UnmarshalText(data []byte) error { - t, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = t - return nil -} - -// MarshalText implements encoding.TextMarshaler for Date. -func (d Date) MarshalText() ([]byte, error) { - return []byte(d.Format(DateFormat)), nil -} - -// Format returns the date formatted according to layout. -func (d Date) Format(layout string) string { - return d.Time.Format(layout) -} - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, types.Date, and types that implement -// json.Marshaler or fmt.Stringer. -func primitiveToString(value interface{}) (string, error) { - // Check for known types first (time, date, uuid) - if res, ok := marshalKnownTypes(value); ok { - return res, nil - } - - // Dereference pointers for optional values - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - kind := t.Kind() - - switch kind { - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: - return strconv.FormatInt(v.Int(), 10), nil - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: - return strconv.FormatUint(v.Uint(), 10), nil - case reflect.Float64: - return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil - case reflect.Float32: - return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil - case reflect.Bool: - if v.Bool() { - return "true", nil - } - return "false", nil - case reflect.String: - return v.String(), nil - case reflect.Struct: - // Check if it's a UUID - if u, ok := value.(uuid.UUID); ok { - return u.String(), nil - } - // Check if it implements json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - var i2 interface{} - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to decode JSON: %w", err) - } - return primitiveToString(i2) - } - fallthrough - default: - if s, ok := value.(fmt.Stringer); ok { - return s.String(), nil - } - return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) - } -} - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. -func marshalKnownTypes(value interface{}) (string, bool) { - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - tt := v.Convert(reflect.TypeOf(time.Time{})) - timeVal := tt.Interface().(time.Time) - return timeVal.Format(time.RFC3339Nano), true - } - - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - d := v.Convert(reflect.TypeOf(Date{})) - dateVal := d.Interface().(Date) - return dateVal.Format(DateFormat), true - } - - if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { - u := v.Convert(reflect.TypeOf(uuid.UUID{})) - uuidVal := u.Interface().(uuid.UUID) - return uuidVal.String(), true - } - - return "", false -} - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -func escapeParameterString(value string, paramLocation ParamLocation) string { - switch paramLocation { - case ParamLocationQuery: - return url.QueryEscape(value) - case ParamLocationPath: - return url.PathEscape(value) - default: - return value - } -} - -// unescapeParameterString unescapes a parameter value based on its location. -func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { - switch paramLocation { - case ParamLocationQuery, ParamLocationUndefined: - return url.QueryUnescape(value) - case ParamLocationPath: - return url.PathUnescape(value) - default: - return value, nil - } -} - -// sortedKeys returns the keys of a map in sorted order. -func sortedKeys(m map[string]string) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. -func BindStringToObject(src string, dst interface{}) error { - // Check for TextUnmarshaler - if tu, ok := dst.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(src)) - } - - // Check for Binder interface - if b, ok := dst.(Binder); ok { - return b.Bind(src) - } - - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr { - return fmt.Errorf("dst must be a pointer, got %T", dst) - } - v = v.Elem() - - switch v.Kind() { - case reflect.String: - v.SetString(src) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - i, err := strconv.ParseInt(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - v.SetInt(i) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - u, err := strconv.ParseUint(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse uint: %w", err) - } - v.SetUint(u) - case reflect.Float32, reflect.Float64: - f, err := strconv.ParseFloat(src, 64) - if err != nil { - return fmt.Errorf("failed to parse float: %w", err) - } - v.SetFloat(f) - case reflect.Bool: - b, err := strconv.ParseBool(src) - if err != nil { - return fmt.Errorf("failed to parse bool: %w", err) - } - v.SetBool(b) - default: - // Try JSON unmarshal as a fallback - return json.Unmarshal([]byte(src), dst) - } - return nil -} - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. -func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - newArray := reflect.MakeSlice(t, len(parts), len(parts)) - for i, p := range parts { - err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) - if err != nil { - return fmt.Errorf("error setting array element: %w", err) - } - } - v.Set(newArray) - return nil -} - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. -func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { - var fields []string - if explode { - fields = make([]string, len(parts)) - for i, property := range parts { - propertyParts := strings.Split(property, "=") - if len(propertyParts) != 2 { - return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) - } - fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" - } - } else { - if len(parts)%2 != 0 { - return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) - } - fields = make([]string, len(parts)/2) - for i := 0; i < len(parts); i += 2 { - key := parts[i] - value := parts[i+1] - fields[i/2] = "\"" + key + "\":\"" + value + "\"" - } - } - jsonParam := "{" + strings.Join(fields, ",") + "}" - return json.Unmarshal([]byte(jsonParam), dest) -} - -// BindSimpleParam binds a simple-style parameter without explode to a destination. -// Simple style is the default for path and header parameters. -// Arrays: a,b,c -> []string{"a", "b", "c"} -// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} -func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // Split on comma and bind as key,value pairs - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} - -// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. -// Simple style is the default for path and header parameters. -// Arrays are comma-separated: a,b,c -// Objects are key,value pairs: key1,value1,key2,value2 -func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return escapeParameterString(string(b), paramLocation), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleSimpleSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleSimpleStruct(paramName, paramLocation, value) - case reflect.Map: - return styleSimpleMap(paramName, paramLocation, value) - default: - return styleSimplePrimitive(paramLocation, value) - } -} - -func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return escapeParameterString(strVal, paramLocation), nil -} - -func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return escapeParameterString(timeVal, paramLocation), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleSimpleParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -// structToFieldDict converts a struct to a map of field names to string values. -func structToFieldDict(value interface{}) (map[string]string, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - fieldDict := make(map[string]string) - - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - f := v.Field(i) - - // Skip nil optional fields - if f.Type().Kind() == reflect.Ptr && f.IsNil() { - continue - } - str, err := primitiveToString(f.Interface()) - if err != nil { - return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) - } - fieldDict[fieldName] = str - } - return fieldDict, nil -} - -type EnterEventJSONRequestBody = Person - -type ExitEventJSONRequestBody = Person - -// RequestEditorFn is the function signature for the RequestEditor callback function. -// It may already be defined if client code is also generated; this is a compatible redeclaration. -type RequestEditorFn func(ctx context.Context, req *http.Request) error - -// HttpRequestDoer performs HTTP requests. -// The standard http.Client implements this interface. -type HttpRequestDoer interface { - Do(req *http.Request) (*http.Response, error) -} - -// WebhookInitiator sends webhook requests to target URLs. -// Unlike Client, it has no stored base URL — the full target URL is provided per-call. -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 request. This can be used to mutate the request. -func WithWebhookRequestEditorFn(fn RequestEditorFn) WebhookInitiatorOption { - return func(p *WebhookInitiator) error { - p.RequestEditors = append(p.RequestEditors, fn) - return nil - } -} - -func (p *WebhookInitiator) applyEditors(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 { - // EnterEventWithBody sends a POST webhook request - EnterEventWithBody(ctx context.Context, targetURL string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - EnterEvent(ctx context.Context, targetURL string, body EnterEventJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - // ExitEventWithBody sends a POST webhook request - ExitEventWithBody(ctx context.Context, targetURL string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - ExitEvent(ctx context.Context, targetURL string, body ExitEventJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) -} - -// EnterEventWithBody sends a POST webhook request -// Person entered the building -func (p *WebhookInitiator) EnterEventWithBody(ctx context.Context, targetURL string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewEnterEventWebhookRequestWithBody(targetURL, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := p.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return p.Client.Do(req) -} - -// EnterEvent sends a POST webhook request with JSON body -func (p *WebhookInitiator) EnterEvent(ctx context.Context, targetURL string, body EnterEventJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewEnterEventWebhookRequest(targetURL, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := p.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return p.Client.Do(req) -} - -// ExitEventWithBody sends a POST webhook request -// Person exited the building -func (p *WebhookInitiator) ExitEventWithBody(ctx context.Context, targetURL string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewExitEventWebhookRequestWithBody(targetURL, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := p.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return p.Client.Do(req) -} - -// ExitEvent sends a POST webhook request with JSON body -func (p *WebhookInitiator) ExitEvent(ctx context.Context, targetURL string, body ExitEventJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewExitEventWebhookRequest(targetURL, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := p.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return p.Client.Do(req) -} - -// NewEnterEventWebhookRequest creates a POST request for the webhook with application/json body -func NewEnterEventWebhookRequest(targetURL string, body EnterEventJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewEnterEventWebhookRequestWithBody(targetURL, "application/json", bodyReader) -} - -// NewEnterEventWebhookRequestWithBody creates a POST request for the webhook with any body -func NewEnterEventWebhookRequestWithBody(targetURL string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - parsedURL, err := url.Parse(targetURL) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", parsedURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewExitEventWebhookRequest creates a POST request for the webhook with application/json body -func NewExitEventWebhookRequest(targetURL string, body ExitEventJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewExitEventWebhookRequestWithBody(targetURL, "application/json", bodyReader) -} - -// NewExitEventWebhookRequestWithBody creates a POST request for the webhook with any body -func NewExitEventWebhookRequestWithBody(targetURL string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - parsedURL, err := url.Parse(targetURL) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", parsedURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// WebhookHttpError represents an HTTP error response. -// The type parameter E is the type of the parsed error body. -type WebhookHttpError[E any] struct { - StatusCode int - Body E - RawBody []byte -} - -func (e *WebhookHttpError[E]) Error() string { - return fmt.Sprintf("HTTP %d", e.StatusCode) -} - -// SimpleWebhookInitiator wraps WebhookInitiator with typed responses for operations that have -// unambiguous response types. Methods return the success type directly, -// and HTTP errors are returned as *WebhookHttpError[E] where E is the error type. -type SimpleWebhookInitiator struct { - *WebhookInitiator -} - -// NewSimpleWebhookInitiator creates a new SimpleWebhookInitiator which wraps a WebhookInitiator. -func NewSimpleWebhookInitiator(opts ...WebhookInitiatorOption) (*SimpleWebhookInitiator, error) { - initiator, err := NewWebhookInitiator(opts...) - if err != nil { - return nil, err - } - return &SimpleWebhookInitiator{WebhookInitiator: initiator}, nil -} - -// WebhookReceiverInterface represents handlers for receiving webhook requests. -type WebhookReceiverInterface interface { - // Person entered the building - // HandleEnterEventWebhook handles the POST webhook request. - HandleEnterEventWebhook(w http.ResponseWriter, r *http.Request) - // Person exited the building - // HandleExitEventWebhook handles the POST webhook request. - HandleExitEventWebhook(w http.ResponseWriter, r *http.Request) -} - -// WebhookReceiverMiddlewareFunc is a middleware function for webhook receiver handlers. -type WebhookReceiverMiddlewareFunc func(http.Handler) http.Handler - -// EnterEventWebhookHandler returns an http.Handler for the EnterEvent webhook. -// The caller is responsible for registering this handler at the appropriate path. -func EnterEventWebhookHandler(si WebhookReceiverInterface, errHandler func(w http.ResponseWriter, r *http.Request, err error), middlewares ...WebhookReceiverMiddlewareFunc) http.Handler { - if errHandler == nil { - errHandler = func(w http.ResponseWriter, r *http.Request, err error) { - http.Error(w, err.Error(), http.StatusBadRequest) - } - } - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - si.HandleEnterEventWebhook(w, r) - })) - - for _, middleware := range middlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) - }) -} - -// ExitEventWebhookHandler returns an http.Handler for the ExitEvent webhook. -// The caller is responsible for registering this handler at the appropriate path. -func ExitEventWebhookHandler(si WebhookReceiverInterface, errHandler func(w http.ResponseWriter, r *http.Request, err error), middlewares ...WebhookReceiverMiddlewareFunc) http.Handler { - if errHandler == nil { - errHandler = func(w http.ResponseWriter, r *http.Request, err error) { - http.Error(w, err.Error(), http.StatusBadRequest) - } - } - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - si.HandleExitEventWebhook(w, r) - })) - - for _, middleware := range middlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) - }) -} diff --git a/experimental/examples/webhook/server/main.go b/experimental/examples/webhook/server/main.go deleted file mode 100644 index 8476aab7a8..0000000000 --- a/experimental/examples/webhook/server/main.go +++ /dev/null @@ -1,186 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "flag" - "log" - "math/rand/v2" - "net" - "net/http" - "sync" - "time" - - "github.com/google/uuid" - - doorbadge "github.com/oapi-codegen/oapi-codegen/experimental/examples/webhook" -) - -var names = []string{ - "Alice", "Bob", "Charlie", "Diana", "Eve", - "Frank", "Grace", "Hank", "Iris", "Jack", -} - -type webhookEntry struct { - id uuid.UUID - url string - kind string -} - -// BadgeReader implements doorbadge.ServerInterface. -type BadgeReader struct { - initiator *doorbadge.WebhookInitiator - - mu sync.Mutex - webhooks map[uuid.UUID]webhookEntry -} - -var _ doorbadge.ServerInterface = (*BadgeReader)(nil) - -func NewBadgeReader() *BadgeReader { - initiator, err := doorbadge.NewWebhookInitiator() - if err != nil { - log.Fatalf("Failed to create webhook initiator: %v", err) - } - return &BadgeReader{ - initiator: initiator, - webhooks: make(map[uuid.UUID]webhookEntry), - } -} - -func sendError(w http.ResponseWriter, code int, message string) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - _ = json.NewEncoder(w).Encode(doorbadge.Error{ - Code: int32(code), - Message: message, - }) -} - -func (br *BadgeReader) RegisterWebhook(w http.ResponseWriter, r *http.Request, kind string) { - var req doorbadge.WebhookRegistration - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - sendError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) - return - } - - if kind != "enterEvent" && kind != "exitEvent" { - sendError(w, http.StatusBadRequest, "Invalid webhook kind: "+kind) - return - } - - id := uuid.New() - entry := webhookEntry{id: id, url: req.URL, kind: kind} - - br.mu.Lock() - br.webhooks[id] = entry - br.mu.Unlock() - - log.Printf("Registered webhook: id=%s kind=%s url=%s", id, kind, req.URL) - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(doorbadge.WebhookRegistrationResponse{ID: id}) -} - -func (br *BadgeReader) DeregisterWebhook(w http.ResponseWriter, r *http.Request, id uuid.UUID) { - br.mu.Lock() - entry, ok := br.webhooks[id] - delete(br.webhooks, id) - br.mu.Unlock() - - if !ok { - sendError(w, http.StatusNotFound, "Webhook not found: "+id.String()) - return - } - - log.Printf("Deregistered webhook: id=%s kind=%s url=%s", id, entry.kind, entry.url) - w.WriteHeader(http.StatusNoContent) -} - -// generateEvents picks a random name and event kind every second and notifies webhooks. -func (br *BadgeReader) generateEvents(ctx context.Context) { - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - name := names[rand.IntN(len(names))] - kind := "enterEvent" - if rand.IntN(2) == 0 { - kind = "exitEvent" - } - - person := doorbadge.Person{Name: name} - - br.mu.Lock() - targets := make([]webhookEntry, 0) - for _, entry := range br.webhooks { - if entry.kind == kind { - targets = append(targets, entry) - } - } - br.mu.Unlock() - - if len(targets) == 0 { - continue - } - - log.Printf("Event: %s %s (%d webhooks)", kind, name, len(targets)) - - for _, target := range targets { - var resp *http.Response - var err error - - switch kind { - case "enterEvent": - resp, err = br.initiator.EnterEvent(ctx, target.url, person) - case "exitEvent": - resp, err = br.initiator.ExitEvent(ctx, target.url, person) - } - - if err != nil { - log.Printf("Webhook %s failed: %v — removing", target.id, err) - br.mu.Lock() - delete(br.webhooks, target.id) - br.mu.Unlock() - continue - } - _ = resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - log.Printf("Webhook %s returned %d — removing", target.id, resp.StatusCode) - br.mu.Lock() - delete(br.webhooks, target.id) - br.mu.Unlock() - } - } - } - } -} - -func main() { - port := flag.String("port", "8080", "Port for HTTP server") - flag.Parse() - - reader := NewBadgeReader() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - go reader.generateEvents(ctx) - - mux := http.NewServeMux() - doorbadge.HandlerFromMux(reader, mux) - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) - mux.ServeHTTP(w, r) - }) - - addr := net.JoinHostPort("0.0.0.0", *port) - log.Printf("Door Badge Reader server listening on %s", addr) - log.Fatal(http.ListenAndServe(addr, handler)) -} diff --git a/experimental/go.mod b/experimental/go.mod deleted file mode 100644 index 70784976bf..0000000000 --- a/experimental/go.mod +++ /dev/null @@ -1,27 +0,0 @@ -module github.com/oapi-codegen/oapi-codegen/experimental - -go 1.24.0 - -require ( - github.com/google/uuid v1.6.0 - github.com/pb33f/libopenapi v0.33.5 - github.com/stretchr/testify v1.11.1 - go.yaml.in/yaml/v4 v4.0.0-rc.4 // required by libopenapi for Extensions type - gopkg.in/yaml.v3 v3.0.1 -) - -require ( - golang.org/x/text v0.33.0 - golang.org/x/tools v0.41.0 -) - -require ( - github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/buger/jsonparser v1.1.1 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pb33f/jsonpath v0.7.1 // indirect - github.com/pb33f/ordered-map/v2 v2.3.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/mod v0.32.0 // indirect - golang.org/x/sync v0.19.0 // indirect -) diff --git a/experimental/go.sum b/experimental/go.sum deleted file mode 100644 index 28075cb106..0000000000 --- a/experimental/go.sum +++ /dev/null @@ -1,41 +0,0 @@ -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pb33f/jsonpath v0.7.1 h1:dEp6oIZuJbpDSyuHAl9m7GonoDW4M20BcD5vT0tPYRE= -github.com/pb33f/jsonpath v0.7.1/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo= -github.com/pb33f/libopenapi v0.33.5 h1:AzILVrOzMaawLFhQENmwmn7h/TIDH2QEgUd0PfxS2xE= -github.com/pb33f/libopenapi v0.33.5/go.mod h1:e/dmd2Pf1nkjqkI0r7guFSyt9T5V0IIQKgs0L6B/3b0= -github.com/pb33f/ordered-map/v2 v2.3.0 h1:k2OhVEQkhTCQMhAicQ3Z6iInzoZNQ7L9MVomwKBZ5WQ= -github.com/pb33f/ordered-map/v2 v2.3.0/go.mod h1:oe5ue+6ZNhy7QN9cPZvPA23Hx0vMHnNVeMg4fGdCANw= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= -go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/experimental/internal/codegen/clientgen.go b/experimental/internal/codegen/clientgen.go deleted file mode 100644 index 759d9e2da1..0000000000 --- a/experimental/internal/codegen/clientgen.go +++ /dev/null @@ -1,349 +0,0 @@ -package codegen - -import ( - "bytes" - "fmt" - "strings" - "text/template" - - "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/templates" -) - -// ClientGenerator generates client code from operation descriptors. -type ClientGenerator struct { - tmpl *template.Template - schemaIndex map[string]*SchemaDescriptor - generateSimple bool - modelsPackage *ModelsPackage -} - -// NewClientGenerator creates a new client generator. -// modelsPackage can be nil if models are in the same package. -func NewClientGenerator(schemaIndex map[string]*SchemaDescriptor, generateSimple bool, modelsPackage *ModelsPackage) (*ClientGenerator, error) { - tmpl := template.New("client").Funcs(templates.Funcs()).Funcs(clientFuncs(schemaIndex, modelsPackage)) - - // Parse client templates - for _, ct := range templates.ClientTemplates { - content, err := templates.TemplateFS.ReadFile("files/" + ct.Template) - if err != nil { - return nil, err - } - _, err = tmpl.New(ct.Name).Parse(string(content)) - if err != nil { - return nil, err - } - } - - // Parse shared templates (param_types is shared with server) - for _, st := range templates.SharedServerTemplates { - content, err := templates.TemplateFS.ReadFile("files/" + st.Template) - if err != nil { - return nil, err - } - _, err = tmpl.New(st.Name).Parse(string(content)) - if err != nil { - return nil, err - } - } - - return &ClientGenerator{ - tmpl: tmpl, - schemaIndex: schemaIndex, - generateSimple: generateSimple, - modelsPackage: modelsPackage, - }, nil -} - -// clientFuncs returns template functions specific to client generation. -func clientFuncs(schemaIndex map[string]*SchemaDescriptor, modelsPackage *ModelsPackage) template.FuncMap { - return template.FuncMap{ - "pathFmt": pathFmt, - "isSimpleOperation": isSimpleOperation, - "simpleOperationSuccessResponse": simpleOperationSuccessResponse, - "errorResponseForOperation": errorResponseForOperation, - "goTypeForContent": func(content *ResponseContentDescriptor) string { - return goTypeForContent(content, schemaIndex, modelsPackage) - }, - "modelsPkg": func() string { - return modelsPackage.Prefix() - }, - } -} - -// pathFmt converts a path with {param} placeholders to a format string. -// Example: "/pets/{petId}" -> "/pets/%s" -func pathFmt(path string) string { - result := path - for { - start := strings.Index(result, "{") - if start == -1 { - break - } - end := strings.Index(result, "}") - if end == -1 { - break - } - result = result[:start] + "%s" + result[end+1:] - } - return result -} - -// isSimpleOperation returns true if an operation has a single JSON success response type. -// "Simple" operations can have typed wrapper methods in SimpleClient. -func isSimpleOperation(op *OperationDescriptor) bool { - // Must have responses - if len(op.Responses) == 0 { - return false - } - - // Count success responses (2xx or default that could be success) - var successResponses []*ResponseDescriptor - for _, r := range op.Responses { - if strings.HasPrefix(r.StatusCode, "2") { - successResponses = append(successResponses, r) - } - } - - // Must have exactly one success response - if len(successResponses) != 1 { - return false - } - - success := successResponses[0] - - // Must have at least one content type and exactly one JSON content type - // (i.e., if there are multiple content types, we can't have a simple typed response) - if len(success.Contents) == 0 { - return false - } - if len(success.Contents) != 1 { - return false - } - - // The single content type must be JSON - return success.Contents[0].IsJSON -} - -// simpleOperationSuccessResponse returns the single success response for a simple operation. -func simpleOperationSuccessResponse(op *OperationDescriptor) *ResponseDescriptor { - for _, r := range op.Responses { - if strings.HasPrefix(r.StatusCode, "2") { - return r - } - } - return nil -} - -// errorResponseForOperation returns the error response (default or 4xx/5xx) if one exists. -func errorResponseForOperation(op *OperationDescriptor) *ResponseDescriptor { - // First, look for a default response - for _, r := range op.Responses { - if r.StatusCode == "default" { - if len(r.Contents) > 0 && r.Contents[0].IsJSON { - return r - } - } - } - // Then look for a 4xx or 5xx response - for _, r := range op.Responses { - if strings.HasPrefix(r.StatusCode, "4") || strings.HasPrefix(r.StatusCode, "5") { - if len(r.Contents) > 0 && r.Contents[0].IsJSON { - return r - } - } - } - return nil -} - -// goTypeForContent returns the Go type for a response content descriptor. -// If modelsPackage is set, type names are prefixed with the package name. -func goTypeForContent(content *ResponseContentDescriptor, schemaIndex map[string]*SchemaDescriptor, modelsPackage *ModelsPackage) string { - if content == nil || content.Schema == nil { - return "interface{}" - } - - pkgPrefix := modelsPackage.Prefix() - - // If the schema has a reference, look it up - if content.Schema.Ref != "" { - if target, ok := schemaIndex[content.Schema.Ref]; ok { - return pkgPrefix + target.ShortName - } - } - - // Check if this is an array schema with items that have a reference - if content.Schema.Schema != nil && content.Schema.Schema.Items != nil { - itemProxy := content.Schema.Schema.Items.A - if itemProxy != nil && itemProxy.IsReference() { - ref := itemProxy.GetReference() - if target, ok := schemaIndex[ref]; ok { - return "[]" + pkgPrefix + target.ShortName - } - } - } - - // If the schema has a short name, use it - if content.Schema.ShortName != "" { - return pkgPrefix + content.Schema.ShortName - } - - // Fall back to the stable name - if content.Schema.StableName != "" { - return pkgPrefix + content.Schema.StableName - } - - // Try to derive from the schema itself - if content.Schema.Schema != nil { - return schemaToGoType(content.Schema.Schema) - } - - return "interface{}" -} - -// GenerateBase generates the base client types and helpers. -func (g *ClientGenerator) GenerateBase() (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "base", nil); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateInterface generates the ClientInterface. -func (g *ClientGenerator) GenerateInterface(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "interface", ops); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateMethods generates the Client methods. -func (g *ClientGenerator) GenerateMethods(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "methods", ops); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateRequestBuilders generates the request builder functions. -func (g *ClientGenerator) GenerateRequestBuilders(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "request_builders", ops); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateSimple generates the SimpleClient with typed responses. -func (g *ClientGenerator) GenerateSimple(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "simple", ops); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateParamTypes generates the parameter struct types. -func (g *ClientGenerator) GenerateParamTypes(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "param_types", ops); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateRequestBodyTypes generates type aliases for request bodies. -func (g *ClientGenerator) GenerateRequestBodyTypes(ops []*OperationDescriptor) string { - var buf bytes.Buffer - pkgPrefix := g.modelsPackage.Prefix() - - for _, op := range ops { - for _, body := range op.Bodies { - if !body.IsJSON { - continue - } - // Get the underlying type for this request body - var targetType string - if body.Schema != nil { - if body.Schema.Ref != "" { - // Reference to a component schema - if target, ok := g.schemaIndex[body.Schema.Ref]; ok { - targetType = pkgPrefix + target.ShortName - } - } else if body.Schema.ShortName != "" { - targetType = pkgPrefix + body.Schema.ShortName - } - } - if targetType == "" { - targetType = "interface{}" - } - - // Generate type alias: type addPetJSONRequestBody = models.NewPet - buf.WriteString(fmt.Sprintf("type %s = %s\n\n", body.GoTypeName, targetType)) - } - } - - return buf.String() -} - -// GenerateClient generates the complete client code. -func (g *ClientGenerator) GenerateClient(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - - // Generate request body type aliases first - bodyTypes := g.GenerateRequestBodyTypes(ops) - buf.WriteString(bodyTypes) - - // Generate base client - base, err := g.GenerateBase() - if err != nil { - return "", fmt.Errorf("generating base client: %w", err) - } - buf.WriteString(base) - buf.WriteString("\n") - - // Generate interface - iface, err := g.GenerateInterface(ops) - if err != nil { - return "", fmt.Errorf("generating client interface: %w", err) - } - buf.WriteString(iface) - buf.WriteString("\n") - - // Generate param types - paramTypes, err := g.GenerateParamTypes(ops) - if err != nil { - return "", fmt.Errorf("generating param types: %w", err) - } - buf.WriteString(paramTypes) - buf.WriteString("\n") - - // Generate methods - methods, err := g.GenerateMethods(ops) - if err != nil { - return "", fmt.Errorf("generating client methods: %w", err) - } - buf.WriteString(methods) - buf.WriteString("\n") - - // Generate request builders - builders, err := g.GenerateRequestBuilders(ops) - if err != nil { - return "", fmt.Errorf("generating request builders: %w", err) - } - buf.WriteString(builders) - buf.WriteString("\n") - - // Generate simple client if requested - if g.generateSimple { - simple, err := g.GenerateSimple(ops) - if err != nil { - return "", fmt.Errorf("generating simple client: %w", err) - } - buf.WriteString(simple) - } - - return buf.String(), nil -} diff --git a/experimental/internal/codegen/clientgen_test.go b/experimental/internal/codegen/clientgen_test.go deleted file mode 100644 index cb5d6961b5..0000000000 --- a/experimental/internal/codegen/clientgen_test.go +++ /dev/null @@ -1,188 +0,0 @@ -package codegen - -import ( - "os" - "testing" - - "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/require" -) - -func TestClientGenerator(t *testing.T) { - // Read the petstore spec - specPath := "../../examples/petstore-expanded/petstore-expanded.yaml" - specData, err := os.ReadFile(specPath) - require.NoError(t, err, "Failed to read petstore spec") - - // Parse the spec - doc, err := libopenapi.NewDocument(specData) - require.NoError(t, err, "Failed to parse petstore spec") - - // Gather schemas to build schema index - contentTypeMatcher := NewContentTypeMatcher(DefaultContentTypes()) - schemas, err := GatherSchemas(doc, contentTypeMatcher) - require.NoError(t, err, "Failed to gather schemas") - - // Compute names for schemas - converter := NewNameConverter(NameMangling{}, NameSubstitutions{}) - contentTypeNamer := NewContentTypeShortNamer(DefaultContentTypeShortNames()) - ComputeSchemaNames(schemas, converter, contentTypeNamer) - - // Build schema index - key by Path.String() for component schemas - schemaIndex := make(map[string]*SchemaDescriptor) - for _, s := range schemas { - schemaIndex[s.Path.String()] = s - } - - // Create param tracker - paramTracker := NewParamUsageTracker() - - // Gather operations - ops, err := GatherOperations(doc, paramTracker) - require.NoError(t, err, "Failed to gather operations") - require.Len(t, ops, 4, "Expected 4 operations") - - // Log operations for debugging - // Verify we have the expected operations - operationIDs := make([]string, 0, len(ops)) - for _, op := range ops { - operationIDs = append(operationIDs, op.GoOperationID) - } - t.Logf("Operations: %v", operationIDs) - - // Generate client code - gen, err := NewClientGenerator(schemaIndex, true, nil) - require.NoError(t, err, "Failed to create client generator") - - clientCode, err := gen.GenerateClient(ops) - require.NoError(t, err, "Failed to generate client code") - require.NotEmpty(t, clientCode, "Generated client code should not be empty") - - t.Logf("Generated client code:\n%s", clientCode) - - // Verify key components are present - require.Contains(t, clientCode, "type Client struct") - require.Contains(t, clientCode, "NewClient") - require.Contains(t, clientCode, "type ClientInterface interface") - require.Contains(t, clientCode, "FindPets") - require.Contains(t, clientCode, "AddPet") - require.Contains(t, clientCode, "DeletePet") - require.Contains(t, clientCode, "FindPetByID") - - // Verify request builders - require.Contains(t, clientCode, "NewFindPetsRequest") - require.Contains(t, clientCode, "NewAddPetRequest") - require.Contains(t, clientCode, "NewDeletePetRequest") - require.Contains(t, clientCode, "NewFindPetByIDRequest") - - // Verify SimpleClient - require.Contains(t, clientCode, "type SimpleClient struct") - require.Contains(t, clientCode, "NewSimpleClient") -} - -func TestIsSimpleOperation(t *testing.T) { - tests := []struct { - name string - op *OperationDescriptor - expected bool - }{ - { - name: "simple operation with single JSON 200 response", - op: &OperationDescriptor{ - Responses: []*ResponseDescriptor{ - { - StatusCode: "200", - Contents: []*ResponseContentDescriptor{ - {ContentType: "application/json", IsJSON: true}, - }, - }, - }, - }, - expected: true, - }, - { - name: "not simple - multiple success responses", - op: &OperationDescriptor{ - Responses: []*ResponseDescriptor{ - { - StatusCode: "200", - Contents: []*ResponseContentDescriptor{ - {ContentType: "application/json", IsJSON: true}, - }, - }, - { - StatusCode: "201", - Contents: []*ResponseContentDescriptor{ - {ContentType: "application/json", IsJSON: true}, - }, - }, - }, - }, - expected: false, - }, - { - name: "not simple - multiple content types", - op: &OperationDescriptor{ - Responses: []*ResponseDescriptor{ - { - StatusCode: "200", - Contents: []*ResponseContentDescriptor{ - {ContentType: "application/json", IsJSON: true}, - {ContentType: "application/xml", IsJSON: false}, - }, - }, - }, - }, - expected: false, - }, - { - name: "not simple - no JSON content", - op: &OperationDescriptor{ - Responses: []*ResponseDescriptor{ - { - StatusCode: "200", - Contents: []*ResponseContentDescriptor{ - {ContentType: "text/plain", IsJSON: false}, - }, - }, - }, - }, - expected: false, - }, - { - name: "not simple - no responses", - op: &OperationDescriptor{}, - expected: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result := isSimpleOperation(tc.op) - if result != tc.expected { - t.Errorf("isSimpleOperation() = %v, expected %v", result, tc.expected) - } - }) - } -} - -func TestPathFmt(t *testing.T) { - tests := []struct { - path string - expected string - }{ - {"/pets", "/pets"}, - {"/pets/{petId}", "/pets/%s"}, - {"/pets/{petId}/photos/{photoId}", "/pets/%s/photos/%s"}, - {"/users/{userId}/posts/{postId}/comments/{commentId}", "/users/%s/posts/%s/comments/%s"}, - } - - for _, tc := range tests { - t.Run(tc.path, func(t *testing.T) { - result := pathFmt(tc.path) - if result != tc.expected { - t.Errorf("pathFmt(%q) = %q, expected %q", tc.path, result, tc.expected) - } - }) - } -} diff --git a/experimental/internal/codegen/codegen.go b/experimental/internal/codegen/codegen.go deleted file mode 100644 index 5b6fdff894..0000000000 --- a/experimental/internal/codegen/codegen.go +++ /dev/null @@ -1,1101 +0,0 @@ -// Package codegen generates Go code from parsed OpenAPI specs. -package codegen - -import ( - "fmt" - "maps" - "slices" - "strings" - "text/template" - - "github.com/pb33f/libopenapi" - "github.com/pb33f/libopenapi/datamodel/high/base" - - "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/templates" -) - -// Generate produces Go code from the parsed OpenAPI document. -// specData is the raw spec bytes used to embed the spec in the generated code. -func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (string, error) { - cfg.ApplyDefaults() - - // Create content type matcher for filtering request/response bodies - contentTypeMatcher := NewContentTypeMatcher(cfg.ContentTypes) - - // Create content type short namer for friendly type names - contentTypeNamer := NewContentTypeShortNamer(cfg.ContentTypeShortNames) - - // Pass 1: Gather all schemas that need types - schemas, err := GatherSchemas(doc, contentTypeMatcher) - if err != nil { - return "", fmt.Errorf("gathering schemas: %w", err) - } - - // Pass 2: Compute names for all schemas - converter := NewNameConverter(cfg.NameMangling, cfg.NameSubstitutions) - ComputeSchemaNames(schemas, converter, contentTypeNamer) - - // Build schema index for type resolution - schemaIndex := make(map[string]*SchemaDescriptor) - for _, s := range schemas { - schemaIndex[s.Path.String()] = s - } - - // Pass 3: Generate Go code - importResolver := NewImportResolver(cfg.ImportMapping) - tagGenerator := NewStructTagGenerator(cfg.StructTags) - gen := NewTypeGenerator(cfg.TypeMapping, converter, importResolver, tagGenerator) - gen.IndexSchemas(schemas) - - output := NewOutput(cfg.PackageName) - // Note: encoding/json and fmt imports are added by generateType when needed - - // Generate models (types for schemas) unless using external models package - if cfg.Generation.ModelsPackage == nil { - for _, desc := range schemas { - code := generateType(gen, desc) - if code != "" { - output.AddType(code) - } - } - - // Add imports collected during generation - output.AddImports(gen.Imports()) - - // Add custom type templates (Date, Email, UUID, File, etc.) - // Sort template names for deterministic output ordering. - templateNames := slices.Sorted(maps.Keys(gen.RequiredTemplates())) - for _, templateName := range templateNames { - typeCode, typeImports := loadCustomType(templateName) - if typeCode != "" { - output.AddType(typeCode) - for path, alias := range typeImports { - output.AddImport(path, alias) - } - } - } - - // Embed the raw OpenAPI spec if specData was provided - if len(specData) > 0 { - embeddedCode, err := generateEmbeddedSpec(specData) - if err != nil { - return "", fmt.Errorf("generating embedded spec: %w", err) - } - output.AddType(embeddedCode) - output.AddImport("bytes", "") - output.AddImport("compress/gzip", "") - output.AddImport("encoding/base64", "") - output.AddImport("fmt", "") - output.AddImport("strings", "") - output.AddImport("sync", "") - } - } - - // Generate client code if requested - if cfg.Generation.Client { - // Create param tracker for tracking which param functions are needed - paramTracker := NewParamUsageTracker() - - // Gather operations - ops, err := GatherOperations(doc, paramTracker) - if err != nil { - return "", fmt.Errorf("gathering operations: %w", err) - } - - // Generate client - clientGen, err := NewClientGenerator(schemaIndex, cfg.Generation.SimpleClient, cfg.Generation.ModelsPackage) - if err != nil { - return "", fmt.Errorf("creating client generator: %w", err) - } - - clientCode, err := clientGen.GenerateClient(ops) - if err != nil { - return "", fmt.Errorf("generating client code: %w", err) - } - output.AddType(clientCode) - - // Add client imports - for _, ct := range templates.ClientTemplates { - for _, imp := range ct.Imports { - output.AddImport(imp.Path, imp.Alias) - } - } - - // Add models package import if using external models - if cfg.Generation.ModelsPackage != nil && cfg.Generation.ModelsPackage.Path != "" { - output.AddImport(cfg.Generation.ModelsPackage.Path, cfg.Generation.ModelsPackage.Alias) - } - - // Generate param functions - paramFuncs, err := generateParamFunctionsFromTracker(paramTracker) - if err != nil { - return "", fmt.Errorf("generating param functions: %w", err) - } - if paramFuncs != "" { - output.AddType(paramFuncs) - } - - // Add param function imports - for _, imp := range paramTracker.GetRequiredImports() { - output.AddImport(imp.Path, imp.Alias) - } - } - - // Track whether shared error types have been generated to avoid duplication. - // Both server and receiver generation emit the same error types. - generatedErrors := false - - // Generate server code for path operations if a server framework is set. - // Only generate if there are actual path operations — setting server solely - // for receiver use should not produce path-operation server code. - if cfg.Generation.Server != "" { - // Create param tracker for tracking which param functions are needed - paramTracker := NewParamUsageTracker() - - // Gather operations - ops, err := GatherOperations(doc, paramTracker) - if err != nil { - return "", fmt.Errorf("gathering operations: %w", err) - } - - if len(ops) > 0 { - // Generate server - serverGen, err := NewServerGenerator(cfg.Generation.Server) - if err != nil { - return "", fmt.Errorf("creating server generator: %w", err) - } - - serverCode, err := serverGen.GenerateServer(ops) - if err != nil { - return "", fmt.Errorf("generating server code: %w", err) - } - output.AddType(serverCode) - generatedErrors = true - - // Add server imports based on server type - serverTemplates, err := getServerTemplates(cfg.Generation.Server) - if err != nil { - return "", fmt.Errorf("getting server templates: %w", err) - } - for _, st := range serverTemplates { - for _, imp := range st.Imports { - output.AddImport(imp.Path, imp.Alias) - } - } - - // Note: Server interfaces don't use external models directly. - // Models are used in the hand-written implementation (petstore.go), - // not in the generated server interface code. - - // Generate param functions - paramFuncs, err := generateParamFunctionsFromTracker(paramTracker) - if err != nil { - return "", fmt.Errorf("generating param functions: %w", err) - } - if paramFuncs != "" { - output.AddType(paramFuncs) - } - - // Add param function imports - for _, imp := range paramTracker.GetRequiredImports() { - output.AddImport(imp.Path, imp.Alias) - } - } - } - - // Generate webhook initiator code if requested - if cfg.Generation.WebhookInitiator { - paramTracker := NewParamUsageTracker() - - webhookOps, err := GatherWebhookOperations(doc, paramTracker) - if err != nil { - return "", fmt.Errorf("gathering webhook operations: %w", err) - } - - if len(webhookOps) > 0 { - initiatorGen, err := NewInitiatorGenerator("Webhook", schemaIndex, true, cfg.Generation.ModelsPackage) - if err != nil { - return "", fmt.Errorf("creating webhook initiator generator: %w", err) - } - - initiatorCode, err := initiatorGen.GenerateInitiator(webhookOps) - if err != nil { - return "", fmt.Errorf("generating webhook initiator code: %w", err) - } - output.AddType(initiatorCode) - - for _, pt := range templates.InitiatorTemplates { - for _, imp := range pt.Imports { - output.AddImport(imp.Path, imp.Alias) - } - } - - if cfg.Generation.ModelsPackage != nil && cfg.Generation.ModelsPackage.Path != "" { - output.AddImport(cfg.Generation.ModelsPackage.Path, cfg.Generation.ModelsPackage.Alias) - } - - paramFuncs, err := generateParamFunctionsFromTracker(paramTracker) - if err != nil { - return "", fmt.Errorf("generating param functions: %w", err) - } - if paramFuncs != "" { - output.AddType(paramFuncs) - } - for _, imp := range paramTracker.GetRequiredImports() { - output.AddImport(imp.Path, imp.Alias) - } - } - } - - // Generate callback initiator code if requested - if cfg.Generation.CallbackInitiator { - paramTracker := NewParamUsageTracker() - - callbackOps, err := GatherCallbackOperations(doc, paramTracker) - if err != nil { - return "", fmt.Errorf("gathering callback operations: %w", err) - } - - if len(callbackOps) > 0 { - initiatorGen, err := NewInitiatorGenerator("Callback", schemaIndex, true, cfg.Generation.ModelsPackage) - if err != nil { - return "", fmt.Errorf("creating callback initiator generator: %w", err) - } - - initiatorCode, err := initiatorGen.GenerateInitiator(callbackOps) - if err != nil { - return "", fmt.Errorf("generating callback initiator code: %w", err) - } - output.AddType(initiatorCode) - - for _, pt := range templates.InitiatorTemplates { - for _, imp := range pt.Imports { - output.AddImport(imp.Path, imp.Alias) - } - } - - if cfg.Generation.ModelsPackage != nil && cfg.Generation.ModelsPackage.Path != "" { - output.AddImport(cfg.Generation.ModelsPackage.Path, cfg.Generation.ModelsPackage.Alias) - } - - paramFuncs, err := generateParamFunctionsFromTracker(paramTracker) - if err != nil { - return "", fmt.Errorf("generating param functions: %w", err) - } - if paramFuncs != "" { - output.AddType(paramFuncs) - } - for _, imp := range paramTracker.GetRequiredImports() { - output.AddImport(imp.Path, imp.Alias) - } - } - } - - // Generate webhook receiver code if requested - if cfg.Generation.WebhookReceiver { - if cfg.Generation.Server == "" { - return "", fmt.Errorf("webhook-receiver requires server to be set") - } - - paramTracker := NewParamUsageTracker() - - webhookOps, err := GatherWebhookOperations(doc, paramTracker) - if err != nil { - return "", fmt.Errorf("gathering webhook operations: %w", err) - } - - if len(webhookOps) > 0 { - receiverGen, err := NewReceiverGenerator("Webhook", cfg.Generation.Server) - if err != nil { - return "", fmt.Errorf("creating webhook receiver generator: %w", err) - } - - receiverCode, err := receiverGen.GenerateReceiver(webhookOps) - if err != nil { - return "", fmt.Errorf("generating webhook receiver code: %w", err) - } - output.AddType(receiverCode) - - // Add param types - paramTypes, err := receiverGen.GenerateParamTypes(webhookOps) - if err != nil { - return "", fmt.Errorf("generating webhook receiver param types: %w", err) - } - output.AddType(paramTypes) - - // Add error types (only if not already generated by server) - if !generatedErrors { - errors, err := receiverGen.GenerateErrors() - if err != nil { - return "", fmt.Errorf("generating webhook receiver errors: %w", err) - } - output.AddType(errors) - generatedErrors = true - } - - receiverTemplates, err := getReceiverTemplates(cfg.Generation.Server) - if err != nil { - return "", fmt.Errorf("getting receiver templates: %w", err) - } - for _, ct := range receiverTemplates { - for _, imp := range ct.Imports { - output.AddImport(imp.Path, imp.Alias) - } - } - for _, st := range templates.SharedServerTemplates { - for _, imp := range st.Imports { - output.AddImport(imp.Path, imp.Alias) - } - } - - paramFuncs, err := generateParamFunctionsFromTracker(paramTracker) - if err != nil { - return "", fmt.Errorf("generating param functions: %w", err) - } - if paramFuncs != "" { - output.AddType(paramFuncs) - } - for _, imp := range paramTracker.GetRequiredImports() { - output.AddImport(imp.Path, imp.Alias) - } - } - } - - // Generate callback receiver code if requested - if cfg.Generation.CallbackReceiver { - if cfg.Generation.Server == "" { - return "", fmt.Errorf("callback-receiver requires server to be set") - } - - paramTracker := NewParamUsageTracker() - - callbackOps, err := GatherCallbackOperations(doc, paramTracker) - if err != nil { - return "", fmt.Errorf("gathering callback operations: %w", err) - } - - if len(callbackOps) > 0 { - receiverGen, err := NewReceiverGenerator("Callback", cfg.Generation.Server) - if err != nil { - return "", fmt.Errorf("creating callback receiver generator: %w", err) - } - - receiverCode, err := receiverGen.GenerateReceiver(callbackOps) - if err != nil { - return "", fmt.Errorf("generating callback receiver code: %w", err) - } - output.AddType(receiverCode) - - paramTypes, err := receiverGen.GenerateParamTypes(callbackOps) - if err != nil { - return "", fmt.Errorf("generating callback receiver param types: %w", err) - } - output.AddType(paramTypes) - - // Add error types (only if not already generated by server or another receiver) - if !generatedErrors { - errors, err := receiverGen.GenerateErrors() - if err != nil { - return "", fmt.Errorf("generating callback receiver errors: %w", err) - } - output.AddType(errors) - generatedErrors = true //nolint:ineffassign // kept for symmetry with webhook loop below - } - - receiverTemplates, err := getReceiverTemplates(cfg.Generation.Server) - if err != nil { - return "", fmt.Errorf("getting receiver templates: %w", err) - } - for _, ct := range receiverTemplates { - for _, imp := range ct.Imports { - output.AddImport(imp.Path, imp.Alias) - } - } - for _, st := range templates.SharedServerTemplates { - for _, imp := range st.Imports { - output.AddImport(imp.Path, imp.Alias) - } - } - - paramFuncs, err := generateParamFunctionsFromTracker(paramTracker) - if err != nil { - return "", fmt.Errorf("generating param functions: %w", err) - } - if paramFuncs != "" { - output.AddType(paramFuncs) - } - for _, imp := range paramTracker.GetRequiredImports() { - output.AddImport(imp.Path, imp.Alias) - } - } - } - - return output.Format() -} - -// generateParamFunctionsFromTracker generates the parameter styling/binding functions based on usage. -func generateParamFunctionsFromTracker(tracker *ParamUsageTracker) (string, error) { - if !tracker.HasAnyUsage() { - return "", nil - } - - var result strings.Builder - - // Get required templates - requiredTemplates := tracker.GetRequiredTemplates() - - for _, tmplInfo := range requiredTemplates { - content, err := templates.TemplateFS.ReadFile("files/" + tmplInfo.Template) - if err != nil { - return "", fmt.Errorf("reading param template %s: %w", tmplInfo.Template, err) - } - - // Parse and execute as a Go template - tmpl, err := template.New(tmplInfo.Name).Parse(string(content)) - if err != nil { - return "", fmt.Errorf("parsing param template %s: %w", tmplInfo.Template, err) - } - - if err := tmpl.Execute(&result, nil); err != nil { - return "", fmt.Errorf("executing param template %s: %w", tmplInfo.Template, err) - } - result.WriteString("\n") - } - - return result.String(), nil -} - -// generateType generates Go code for a single schema descriptor. -func generateType(gen *TypeGenerator, desc *SchemaDescriptor) string { - kind := GetSchemaKind(desc) - - // If schema has TypeOverride extension, generate a type alias to the external type - // instead of generating the full type definition - if desc.Extensions != nil && desc.Extensions.TypeOverride != nil { - return generateTypeOverrideAlias(gen, desc) - } - - var code string - switch kind { - case KindReference: - // References don't generate new types; they use the referenced type's name - return "" - - case KindStruct: - code = generateStructType(gen, desc) - - case KindMap: - code = generateMapAlias(gen, desc) - - case KindEnum: - code = generateEnumType(gen, desc) - - case KindAllOf: - code = generateAllOfType(gen, desc) - - case KindAnyOf: - code = generateAnyOfType(gen, desc) - - case KindOneOf: - code = generateOneOfType(gen, desc) - - case KindAlias: - code = generateTypeAlias(gen, desc) - - default: - return "" - } - - if code == "" { - return "" - } - - // Prepend schema path comment - return schemaPathComment(desc.Path) + code -} - -// schemaPathComment returns a comment line showing the schema path. -func schemaPathComment(path SchemaPath) string { - return fmt.Sprintf("// %s\n", path.String()) -} - -// generateStructType generates a struct type for an object schema. -func generateStructType(gen *TypeGenerator, desc *SchemaDescriptor) string { - fields := gen.GenerateStructFields(desc) - doc := extractDescription(desc.Schema) - - // Check if we need additionalProperties handling - if gen.HasAdditionalProperties(desc) { - // Mixed properties need encoding/json for marshal/unmarshal (but not fmt) - gen.AddJSONImport() - - addPropsType := gen.AdditionalPropertiesType(desc) - structCode := GenerateStructWithAdditionalProps(desc.ShortName, fields, addPropsType, doc, gen.TagGenerator()) - - // Generate marshal/unmarshal methods - marshalCode := GenerateMixedPropertiesMarshal(desc.ShortName, fields) - unmarshalCode := GenerateMixedPropertiesUnmarshal(desc.ShortName, fields, addPropsType) - - code := structCode + "\n" + marshalCode + "\n" + unmarshalCode - - // Generate ApplyDefaults method if needed - if applyDefaults := GenerateApplyDefaults(desc.ShortName, fields); applyDefaults != "" { - code += "\n" + applyDefaults - } - - return code - } - - code := GenerateStruct(desc.ShortName, fields, doc, gen.TagGenerator()) - - // Generate ApplyDefaults method if needed - if applyDefaults := GenerateApplyDefaults(desc.ShortName, fields); applyDefaults != "" { - code += "\n" + applyDefaults - } - - return code -} - -// generateMapAlias generates a type alias for a pure map schema. -func generateMapAlias(gen *TypeGenerator, desc *SchemaDescriptor) string { - mapType := gen.GoTypeExpr(desc) - doc := extractDescription(desc.Schema) - return GenerateTypeAlias(desc.ShortName, mapType, doc) -} - -// generateEnumType generates an enum type with const values. -func generateEnumType(gen *TypeGenerator, desc *SchemaDescriptor) string { - schema := desc.Schema - if schema == nil { - return "" - } - - // Determine base type - baseType := "string" - primaryType := getPrimaryType(schema) - if primaryType == "integer" { - baseType = "int" - } - - // Extract enum values as strings - var values []string - for _, v := range schema.Enum { - values = append(values, fmt.Sprintf("%v", v.Value)) - } - - // Check for custom enum variable names from extensions - var customNames []string - if desc.Extensions != nil && len(desc.Extensions.EnumVarNames) > 0 { - customNames = desc.Extensions.EnumVarNames - } - - doc := extractDescription(schema) - return GenerateEnumWithConstPrefix(desc.ShortName, desc.ShortName, baseType, values, customNames, doc) -} - -// generateTypeAlias generates a simple type alias. -func generateTypeAlias(gen *TypeGenerator, desc *SchemaDescriptor) string { - goType := gen.GoTypeExpr(desc) - doc := extractDescription(desc.Schema) - return GenerateTypeAlias(desc.ShortName, goType, doc) -} - -// generateTypeOverrideAlias generates a type alias to an external type specified via x-oapi-codegen-type-override. -func generateTypeOverrideAlias(gen *TypeGenerator, desc *SchemaDescriptor) string { - override := desc.Extensions.TypeOverride - - // Register the import - if override.ImportPath != "" { - if override.ImportAlias != "" { - gen.AddImportAlias(override.ImportPath, override.ImportAlias) - } else { - gen.AddImport(override.ImportPath) - } - } - - doc := extractDescription(desc.Schema) - return GenerateTypeAlias(desc.ShortName, override.TypeName, doc) -} - -// AllOfMergeError represents a conflict when merging allOf schemas. -type AllOfMergeError struct { - SchemaName string - PropertyName string - Type1 string - Type2 string -} - -func (e AllOfMergeError) Error() string { - return fmt.Sprintf("allOf merge conflict in %s: property %q has conflicting types %s and %s", - e.SchemaName, e.PropertyName, e.Type1, e.Type2) -} - -// allOfMemberInfo holds information about an allOf member for merging. -type allOfMemberInfo struct { - fields []StructField // flattened fields from object schemas - unionType string // non-empty if this member is a oneOf/anyOf union - unionDesc *SchemaDescriptor - required []string // required fields from this allOf member -} - -// generateAllOfType generates a struct with flattened properties from all allOf members. -// Object schema properties are merged into flat fields. -// oneOf/anyOf members become union fields with json:"-" tag. -func generateAllOfType(gen *TypeGenerator, desc *SchemaDescriptor) string { - schema := desc.Schema - if schema == nil { - return "" - } - - // Merge all fields, checking for conflicts - mergedFields := make(map[string]StructField) // keyed by JSONName - var fieldOrder []string // preserve order - var unionFields []StructField - - // First, collect fields from properties defined directly on the schema - // (Issue 2102: properties at same level as allOf were being ignored) - if schema.Properties != nil && schema.Properties.Len() > 0 { - directFields := gen.GenerateStructFields(desc) - for _, field := range directFields { - mergedFields[field.JSONName] = field - fieldOrder = append(fieldOrder, field.JSONName) - } - } - - // Collect info about each allOf member - var members []allOfMemberInfo - for i, proxy := range schema.AllOf { - info := allOfMemberInfo{} - - memberSchema := proxy.Schema() - if memberSchema == nil { - continue - } - - // Check if this member is a oneOf/anyOf (union type) - if len(memberSchema.OneOf) > 0 || len(memberSchema.AnyOf) > 0 { - // This is a union - keep as a union field - if desc.AllOf != nil && i < len(desc.AllOf) { - info.unionType = desc.AllOf[i].ShortName - info.unionDesc = desc.AllOf[i] - } - } else if proxy.IsReference() { - // Reference to another schema - get its fields - ref := proxy.GetReference() - if target, ok := gen.schemaIndex[ref]; ok { - info.fields = gen.collectFieldsRecursive(target) - } - } else if memberSchema.Properties != nil && memberSchema.Properties.Len() > 0 { - // Inline object schema - get its fields - if desc.AllOf != nil && i < len(desc.AllOf) { - info.fields = gen.GenerateStructFields(desc.AllOf[i]) - } - } - - // Also check for required array in allOf members (may mark fields as required) - info.required = memberSchema.Required - - members = append(members, info) - } - - // Merge fields from allOf members - for _, member := range members { - if member.unionType != "" { - // Add union as a special field - unionFields = append(unionFields, StructField{ - Name: member.unionType, - Type: "*" + member.unionType, - JSONName: "-", // will use json:"-" - }) - continue - } - - for _, field := range member.fields { - if existing, ok := mergedFields[field.JSONName]; ok { - // Check for type conflict - if existing.Type != field.Type { - // Type conflict - generate error comment in output - // In a real implementation, this should be a proper error - // For now, we'll include a comment and use the first type - field.Doc = fmt.Sprintf("CONFLICT: type %s vs %s", existing.Type, field.Type) - } - // If same type, keep existing (first wins for required/nullable) - continue - } - mergedFields[field.JSONName] = field - fieldOrder = append(fieldOrder, field.JSONName) - } - - // Apply required array from this allOf member to update pointer/omitempty - for _, reqName := range member.required { - if field, ok := mergedFields[reqName]; ok { - if !field.Required { - field.Required = true - field.OmitEmpty = false - // Update pointer status - required non-nullable fields are not pointers - if !field.Nullable && !strings.HasPrefix(field.Type, "[]") && !strings.HasPrefix(field.Type, "map[") { - field.Type = strings.TrimPrefix(field.Type, "*") - field.Pointer = false - } - mergedFields[reqName] = field - } - } - } - } - - // Build final field list in order - var finalFields []StructField - for _, jsonName := range fieldOrder { - finalFields = append(finalFields, mergedFields[jsonName]) - } - - doc := extractDescription(schema) - - // Generate struct - var code string - if len(unionFields) > 0 { - // Has union members - need custom marshal/unmarshal - gen.AddJSONImport() - code = generateAllOfStructWithUnions(desc.ShortName, finalFields, unionFields, doc, gen.TagGenerator()) - } else { - // Simple case - just flattened fields - code = GenerateStruct(desc.ShortName, finalFields, doc, gen.TagGenerator()) - } - - // Generate ApplyDefaults method if needed - if applyDefaults := GenerateApplyDefaults(desc.ShortName, finalFields); applyDefaults != "" { - code += "\n" + applyDefaults - } - - return code -} - -// generateAllOfStructWithUnions generates an allOf struct that contains union fields. -func generateAllOfStructWithUnions(name string, fields []StructField, unionFields []StructField, doc string, tagGen *StructTagGenerator) string { - b := NewCodeBuilder() - - if doc != "" { - for _, line := range strings.Split(doc, "\n") { - b.Line("// %s", line) - } - } - - b.Line("type %s struct {", name) - b.Indent() - - // Regular fields - for _, f := range fields { - tag := generateFieldTag(f, tagGen) - b.Line("%s %s %s", f.Name, f.Type, tag) - } - - // Union fields with json:"-" - for _, f := range unionFields { - b.Line("%s %s `json:\"-\"`", f.Name, f.Type) - } - - b.Dedent() - b.Line("}") - - // Generate MarshalJSON - b.BlankLine() - b.Line("func (s %s) MarshalJSON() ([]byte, error) {", name) - b.Indent() - b.Line("result := make(map[string]any)") - b.BlankLine() - - // Marshal regular fields - for _, f := range fields { - if f.Pointer { - b.Line("if s.%s != nil {", f.Name) - b.Indent() - b.Line("result[%q] = s.%s", f.JSONName, f.Name) - b.Dedent() - b.Line("}") - } else if strings.HasPrefix(f.Type, "[]") || strings.HasPrefix(f.Type, "map[") { - // Slices and maps - only include if not nil - b.Line("if s.%s != nil {", f.Name) - b.Indent() - b.Line("result[%q] = s.%s", f.JSONName, f.Name) - b.Dedent() - b.Line("}") - } else { - b.Line("result[%q] = s.%s", f.JSONName, f.Name) - } - } - - // Marshal and merge union fields - for _, f := range unionFields { - b.BlankLine() - b.Line("if s.%s != nil {", f.Name) - b.Indent() - b.Line("unionData, err := json.Marshal(s.%s)", f.Name) - b.Line("if err != nil {") - b.Indent() - b.Line("return nil, err") - b.Dedent() - b.Line("}") - b.Line("var unionMap map[string]any") - b.Line("if err := json.Unmarshal(unionData, &unionMap); err == nil {") - b.Indent() - b.Line("for k, v := range unionMap {") - b.Indent() - b.Line("result[k] = v") - b.Dedent() - b.Line("}") - b.Dedent() - b.Line("}") - b.Dedent() - b.Line("}") - } - - b.BlankLine() - b.Line("return json.Marshal(result)") - b.Dedent() - b.Line("}") - - // Generate UnmarshalJSON - b.BlankLine() - b.Line("func (s *%s) UnmarshalJSON(data []byte) error {", name) - b.Indent() - - // Unmarshal into raw map for field extraction - b.Line("var raw map[string]json.RawMessage") - b.Line("if err := json.Unmarshal(data, &raw); err != nil {") - b.Indent() - b.Line("return err") - b.Dedent() - b.Line("}") - b.BlankLine() - - // Unmarshal known fields - for _, f := range fields { - b.Line("if v, ok := raw[%q]; ok {", f.JSONName) - b.Indent() - if f.Pointer { - baseType := strings.TrimPrefix(f.Type, "*") - b.Line("var val %s", baseType) - b.Line("if err := json.Unmarshal(v, &val); err != nil {") - b.Indent() - b.Line("return err") - b.Dedent() - b.Line("}") - b.Line("s.%s = &val", f.Name) - } else { - b.Line("if err := json.Unmarshal(v, &s.%s); err != nil {", f.Name) - b.Indent() - b.Line("return err") - b.Dedent() - b.Line("}") - } - b.Dedent() - b.Line("}") - } - - // Unmarshal union fields from the full data - for _, f := range unionFields { - b.BlankLine() - baseType := strings.TrimPrefix(f.Type, "*") - b.Line("var %sVal %s", f.Name, baseType) - b.Line("if err := json.Unmarshal(data, &%sVal); err != nil {", f.Name) - b.Indent() - b.Line("return err") - b.Dedent() - b.Line("}") - b.Line("s.%s = &%sVal", f.Name, f.Name) - } - - b.BlankLine() - b.Line("return nil") - b.Dedent() - b.Line("}") - - return b.String() -} - -// generateAnyOfType generates a union type for anyOf schemas. -func generateAnyOfType(gen *TypeGenerator, desc *SchemaDescriptor) string { - members := collectUnionMembers(gen, desc, desc.AnyOf, desc.Schema.AnyOf) - if len(members) == 0 { - return "" - } - - // anyOf types only need encoding/json (not fmt like oneOf) - gen.AddJSONImport() - - doc := extractDescription(desc.Schema) - code := GenerateUnionType(desc.ShortName, members, false, doc) - - marshalCode := GenerateUnionMarshalAnyOf(desc.ShortName, members) - unmarshalCode := GenerateUnionUnmarshalAnyOf(desc.ShortName, members) - applyDefaultsCode := GenerateUnionApplyDefaults(desc.ShortName, members) - - code += "\n" + marshalCode + "\n" + unmarshalCode + "\n" + applyDefaultsCode - - return code -} - -// generateOneOfType generates a union type for oneOf schemas. -func generateOneOfType(gen *TypeGenerator, desc *SchemaDescriptor) string { - members := collectUnionMembers(gen, desc, desc.OneOf, desc.Schema.OneOf) - if len(members) == 0 { - return "" - } - - // Union types need encoding/json and fmt for marshal/unmarshal - gen.AddJSONImports() - - doc := extractDescription(desc.Schema) - code := GenerateUnionType(desc.ShortName, members, true, doc) - - marshalCode := GenerateUnionMarshalOneOf(desc.ShortName, members) - unmarshalCode := GenerateUnionUnmarshalOneOf(desc.ShortName, members) - applyDefaultsCode := GenerateUnionApplyDefaults(desc.ShortName, members) - - code += "\n" + marshalCode + "\n" + unmarshalCode + "\n" + applyDefaultsCode - - return code -} - -// loadCustomType loads a custom type template and returns its code and imports. -func loadCustomType(templateName string) (string, map[string]string) { - // Lookup the type definition - typeName := strings.TrimSuffix(templateName, ".tmpl") - - // Map template name to type info from registry - var typeDef templates.TypeTemplate - var found bool - - for name, def := range templates.TypeTemplates { - if def.Template == "types/"+templateName || strings.ToLower(name) == typeName { - typeDef = def - found = true - break - } - } - - if !found { - return "", nil - } - - // Read the template file - content, err := templates.TemplateFS.ReadFile("files/" + typeDef.Template) - if err != nil { - return "", nil - } - - // Remove the template comment header - code := string(content) - if idx := strings.Index(code, "}}"); idx != -1 { - code = strings.TrimLeft(code[idx+2:], "\n") - } - - // Build imports map - imports := make(map[string]string) - for _, imp := range typeDef.Imports { - imports[imp.Path] = imp.Alias - } - - return code, imports -} - -// schemaHasApplyDefaults returns true if the schema will have an ApplyDefaults method generated. -// This is true for: -// - Object types with properties -// - Union types (oneOf/anyOf) -// - AllOf types (merged structs) -// This is false for: -// - Primitive types (string, integer, boolean, number) -// - Enum types (without object properties) -// - Arrays -// - Maps (additionalProperties only) -func schemaHasApplyDefaults(schema *base.Schema) bool { - if schema == nil { - return false - } - - // Has properties -> object type with ApplyDefaults - if schema.Properties != nil && schema.Properties.Len() > 0 { - return true - } - - // Has oneOf/anyOf -> union type with ApplyDefaults - if len(schema.OneOf) > 0 || len(schema.AnyOf) > 0 { - return true - } - - // Has allOf -> merged struct with ApplyDefaults - if len(schema.AllOf) > 0 { - return true - } - - return false -} - -// collectUnionMembers gathers union member information for anyOf/oneOf. -func collectUnionMembers(gen *TypeGenerator, parentDesc *SchemaDescriptor, memberDescs []*SchemaDescriptor, memberProxies []*base.SchemaProxy) []UnionMember { - var members []UnionMember - - // Build a map of schema paths to descriptors for lookup - descByPath := make(map[string]*SchemaDescriptor) - for _, desc := range memberDescs { - if desc != nil { - descByPath[desc.Path.String()] = desc - } - } - - for i, proxy := range memberProxies { - var memberType string - var fieldName string - var hasApplyDefaults bool - - if proxy.IsReference() { - ref := proxy.GetReference() - if target, ok := gen.schemaIndex[ref]; ok { - memberType = target.ShortName - fieldName = target.ShortName - hasApplyDefaults = schemaHasApplyDefaults(target.Schema) - } else { - continue - } - } else { - // Check if this inline schema has a descriptor - schema := proxy.Schema() - if schema == nil { - continue - } - - // Determine the path for this member to look up its descriptor - var memberPath SchemaPath - if parentDesc != nil { - // Try to find a descriptor by constructing the expected path - memberPath = parentDesc.Path.Append("anyOf", fmt.Sprintf("%d", i)) - if _, ok := descByPath[memberPath.String()]; !ok { - memberPath = parentDesc.Path.Append("oneOf", fmt.Sprintf("%d", i)) - } - } - - if desc, ok := descByPath[memberPath.String()]; ok && desc.ShortName != "" { - memberType = desc.ShortName - fieldName = desc.ShortName - hasApplyDefaults = schemaHasApplyDefaults(desc.Schema) - } else { - // This is a primitive type that doesn't have a named type - goType := gen.goTypeForSchema(schema, nil) - memberType = goType - // Create a field name based on the type - fieldName = gen.converter.ToTypeName(goType) + fmt.Sprintf("%d", i) - hasApplyDefaults = false // Primitive types don't have ApplyDefaults - } - } - - members = append(members, UnionMember{ - FieldName: fieldName, - TypeName: memberType, - Index: i, - HasApplyDefaults: hasApplyDefaults, - }) - } - - return members -} diff --git a/experimental/internal/codegen/configuration.go b/experimental/internal/codegen/configuration.go deleted file mode 100644 index c8d1b87388..0000000000 --- a/experimental/internal/codegen/configuration.go +++ /dev/null @@ -1,317 +0,0 @@ -package codegen - -import ( - "crypto/sha256" - "encoding/hex" - "regexp" - "sort" - "strings" -) - -type Configuration struct { - // PackageName which will be used in all generated files - PackageName string `yaml:"package"` - // Output specifies the output file path - Output string `yaml:"output"` - // Generation controls which parts of the code are generated - Generation GenerationOptions `yaml:"generation,omitempty"` - // TypeMapping allows customizing OpenAPI type/format to Go type mappings - TypeMapping TypeMapping `yaml:"type-mapping,omitempty"` - // NameMangling configures how OpenAPI names are converted to Go identifiers - NameMangling NameMangling `yaml:"name-mangling,omitempty"` - // NameSubstitutions allows direct overrides of generated names - NameSubstitutions NameSubstitutions `yaml:"name-substitutions,omitempty"` - // ImportMapping maps external spec file paths to Go package import paths. - // Example: {"../common/api.yaml": "github.com/org/project/common"} - // Use "-" as the value to indicate types should be in the current package. - ImportMapping map[string]string `yaml:"import-mapping,omitempty"` - // ContentTypes is a list of regexp patterns for media types to generate models for. - // Only request/response bodies with matching content types will have types generated. - // Defaults to common JSON and YAML types if not specified. - ContentTypes []string `yaml:"content-types,omitempty"` - // ContentTypeShortNames maps content type regex patterns to short names for use in type names. - // Example: {"^application/json$": "JSON", "^application/xml$": "XML"} - // These are used when generating response/request type names like "FindPetsJSONResponse". - ContentTypeShortNames []ContentTypeShortName `yaml:"content-type-short-names,omitempty"` - // StructTags configures how struct tags are generated for fields. - // By default, only json tags are generated. - StructTags StructTagsConfig `yaml:"struct-tags,omitempty"` -} - -// ModelsPackage specifies an external package containing the model types. -type ModelsPackage struct { - // Path is the import path for the models package (e.g., "github.com/org/project/models") - Path string `yaml:"path"` - // Alias is an optional import alias. If empty, the last segment of the path is used. - Alias string `yaml:"alias,omitempty"` -} - -// Name returns the package name/alias to use for qualifying types. -// Returns the Alias if set, otherwise derives from the Path. -func (m *ModelsPackage) Name() string { - if m == nil || m.Path == "" { - return "" - } - if m.Alias != "" { - return m.Alias - } - // Derive from path - take last segment - parts := strings.Split(m.Path, "/") - return parts[len(parts)-1] -} - -// Prefix returns the package prefix for qualifying types (e.g., "models."). -// Returns empty string if models are in the same package. -func (m *ModelsPackage) Prefix() string { - name := m.Name() - if name == "" { - return "" - } - return name + "." -} - -// ContentTypeShortName maps a content type pattern to a short name. -type ContentTypeShortName struct { - // Pattern is a regex pattern to match against content types - Pattern string `yaml:"pattern"` - // ShortName is the short name to use in generated type names (e.g., "JSON", "XML") - ShortName string `yaml:"short-name"` -} - -// GenerationOptions controls which parts of the code are generated. -type GenerationOptions struct { - // Server specifies which server framework to generate code for. - // Supported values: "std-http" - // Empty string (default) means no server code is generated. - Server string `yaml:"server,omitempty"` - - // Client enables generation of the HTTP client. - // When true, generates a base Client that returns *http.Response. - Client bool `yaml:"client,omitempty"` - - // SimpleClient enables generation of the SimpleClient wrapper. - // SimpleClient wraps the base Client with typed responses for - // operations that have unambiguous response types. - // Requires Client to also be enabled. - SimpleClient bool `yaml:"simple-client,omitempty"` - - // WebhookInitiator enables generation of webhook initiator code (sends webhook requests). - // Generates a framework-agnostic client that takes the full target URL per-call. - WebhookInitiator bool `yaml:"webhook-initiator,omitempty"` - - // WebhookReceiver enables generation of webhook receiver code (receives webhook requests). - // Generates framework-specific handler functions. Requires Server to be set. - WebhookReceiver bool `yaml:"webhook-receiver,omitempty"` - - // CallbackInitiator enables generation of callback initiator code (sends callback requests). - // Generates a framework-agnostic client that takes the full target URL per-call. - CallbackInitiator bool `yaml:"callback-initiator,omitempty"` - - // CallbackReceiver enables generation of callback receiver code (receives callback requests). - // Generates framework-specific handler functions. Requires Server to be set. - CallbackReceiver bool `yaml:"callback-receiver,omitempty"` - - // ModelsPackage specifies an external package containing the model types. - // When set, models are NOT generated locally - instead, generated code - // imports and references types from this package. - // Example: {path: "github.com/org/project/models"} - ModelsPackage *ModelsPackage `yaml:"models-package,omitempty"` -} - -// ServerType constants for supported server frameworks. -const ( - ServerTypeStdHTTP = "std-http" - ServerTypeChi = "chi" - ServerTypeEcho = "echo" - ServerTypeEchoV4 = "echo/v4" - ServerTypeGin = "gin" - ServerTypeGorilla = "gorilla" - ServerTypeFiber = "fiber" - ServerTypeIris = "iris" -) - -// DefaultContentTypes returns the default list of content type patterns. -// These match common JSON and YAML media types. -func DefaultContentTypes() []string { - return []string{ - `^application/json$`, - `^application/.*\+json$`, - } -} - -// DefaultContentTypeShortNames returns the default content type to short name mappings. -func DefaultContentTypeShortNames() []ContentTypeShortName { - return []ContentTypeShortName{ - {Pattern: `^application/json$`, ShortName: "JSON"}, - {Pattern: `^application/.*\+json$`, ShortName: "JSON"}, - {Pattern: `^application/xml$`, ShortName: "XML"}, - {Pattern: `^application/.*\+xml$`, ShortName: "XML"}, - {Pattern: `^text/xml$`, ShortName: "XML"}, - {Pattern: `^text/plain$`, ShortName: "Text"}, - {Pattern: `^text/html$`, ShortName: "HTML"}, - {Pattern: `^application/octet-stream$`, ShortName: "Binary"}, - {Pattern: `^multipart/form-data$`, ShortName: "Multipart"}, - {Pattern: `^application/x-www-form-urlencoded$`, ShortName: "Form"}, - } -} - -// ApplyDefaults merges user configuration on top of default values. -func (c *Configuration) ApplyDefaults() { - c.TypeMapping = DefaultTypeMapping.Merge(c.TypeMapping) - c.NameMangling = DefaultNameMangling().Merge(c.NameMangling) - if len(c.ContentTypes) == 0 { - c.ContentTypes = DefaultContentTypes() - } - if len(c.ContentTypeShortNames) == 0 { - c.ContentTypeShortNames = DefaultContentTypeShortNames() - } - c.StructTags = DefaultStructTagsConfig().Merge(c.StructTags) -} - -// ContentTypeMatcher checks if content types match configured patterns. -type ContentTypeMatcher struct { - patterns []*regexp.Regexp -} - -// NewContentTypeMatcher creates a matcher from a list of regexp patterns. -// Invalid patterns are silently ignored. -func NewContentTypeMatcher(patterns []string) *ContentTypeMatcher { - m := &ContentTypeMatcher{ - patterns: make([]*regexp.Regexp, 0, len(patterns)), - } - for _, p := range patterns { - if re, err := regexp.Compile(p); err == nil { - m.patterns = append(m.patterns, re) - } - } - return m -} - -// Matches returns true if the content type matches any of the configured patterns. -func (m *ContentTypeMatcher) Matches(contentType string) bool { - for _, re := range m.patterns { - if re.MatchString(contentType) { - return true - } - } - return false -} - -// ContentTypeShortNamer resolves content types to short names for use in type names. -type ContentTypeShortNamer struct { - patterns []*regexp.Regexp - shortNames []string -} - -// NewContentTypeShortNamer creates a short namer from configuration. -func NewContentTypeShortNamer(mappings []ContentTypeShortName) *ContentTypeShortNamer { - n := &ContentTypeShortNamer{ - patterns: make([]*regexp.Regexp, 0, len(mappings)), - shortNames: make([]string, 0, len(mappings)), - } - for _, m := range mappings { - if re, err := regexp.Compile(m.Pattern); err == nil { - n.patterns = append(n.patterns, re) - n.shortNames = append(n.shortNames, m.ShortName) - } - } - return n -} - -// ShortName returns the short name for a content type, or a fallback derived from the content type. -func (n *ContentTypeShortNamer) ShortName(contentType string) string { - for i, re := range n.patterns { - if re.MatchString(contentType) { - return n.shortNames[i] - } - } - // Fallback: derive from content type (e.g., "application/vnd.api+json" -> "VndApiJson") - return deriveContentTypeShortName(contentType) -} - -// deriveContentTypeShortName creates a short name from an unmatched content type. -func deriveContentTypeShortName(contentType string) string { - // Remove "application/", "text/", etc. prefix - if idx := strings.Index(contentType, "/"); idx >= 0 { - contentType = contentType[idx+1:] - } - // Replace non-alphanumeric with spaces for word splitting - var result strings.Builder - capitalizeNext := true - for _, r := range contentType { - if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' { - if capitalizeNext { - if r >= 'a' && r <= 'z' { - r = r - 'a' + 'A' - } - capitalizeNext = false - } - result.WriteRune(r) - } else { - capitalizeNext = true - } - } - return result.String() -} - -// ExternalImport represents an external package import with its alias. -type ExternalImport struct { - Alias string // Short alias for use in generated code (e.g., "ext_a1b2c3") - Path string // Full import path (e.g., "github.com/org/project/common") -} - -// ImportResolver resolves external references to Go package imports. -type ImportResolver struct { - mapping map[string]ExternalImport // spec file path -> import info -} - -// NewImportResolver creates an ImportResolver from the configuration's import mapping. -func NewImportResolver(importMapping map[string]string) *ImportResolver { - resolver := &ImportResolver{ - mapping: make(map[string]ExternalImport), - } - - for specPath, pkgPath := range importMapping { - if pkgPath == "-" { - // "-" means current package, no import needed - resolver.mapping[specPath] = ExternalImport{Alias: "", Path: ""} - } else { - resolver.mapping[specPath] = ExternalImport{ - Alias: hashImportAlias(pkgPath), - Path: pkgPath, - } - } - } - - return resolver -} - -// Resolve looks up an external spec file path and returns its import info. -// Returns nil if the path is not in the mapping. -func (r *ImportResolver) Resolve(specPath string) *ExternalImport { - if imp, ok := r.mapping[specPath]; ok { - return &imp - } - return nil -} - -// AllImports returns all external imports sorted by alias. -func (r *ImportResolver) AllImports() []ExternalImport { - var imports []ExternalImport - for _, imp := range r.mapping { - if imp.Path != "" { // Skip current package markers - imports = append(imports, imp) - } - } - sort.Slice(imports, func(i, j int) bool { - return imports[i].Alias < imports[j].Alias - }) - return imports -} - -// hashImportAlias generates a short, deterministic alias from an import path. -// Uses first 8 characters of SHA256 hash prefixed with "ext_". -func hashImportAlias(importPath string) string { - h := sha256.Sum256([]byte(importPath)) - return "ext_" + hex.EncodeToString(h[:])[:8] -} diff --git a/experimental/internal/codegen/configuration_test.go b/experimental/internal/codegen/configuration_test.go deleted file mode 100644 index 700c65505a..0000000000 --- a/experimental/internal/codegen/configuration_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package codegen - -import ( - "testing" - - "gopkg.in/yaml.v3" -) - -func TestContentTypeMatcher(t *testing.T) { - tests := []struct { - name string - patterns []string - contentType string - want bool - }{ - // Default patterns - JSON only (YAML not supported without custom unmarshalers) - {"json exact", DefaultContentTypes(), "application/json", true}, - {"json+suffix", DefaultContentTypes(), "application/vnd.api+json", true}, - {"problem+json", DefaultContentTypes(), "application/problem+json", true}, - - // YAML not in defaults (would need custom unmarshalers) - {"yaml not default", DefaultContentTypes(), "application/yaml", false}, - {"text/yaml not default", DefaultContentTypes(), "text/yaml", false}, - - // Non-matching - {"text/plain", DefaultContentTypes(), "text/plain", false}, - {"text/html", DefaultContentTypes(), "text/html", false}, - {"application/xml", DefaultContentTypes(), "application/xml", false}, - {"application/octet-stream", DefaultContentTypes(), "application/octet-stream", false}, - {"multipart/form-data", DefaultContentTypes(), "multipart/form-data", false}, - {"image/png", DefaultContentTypes(), "image/png", false}, - - // Custom patterns - {"custom xml", []string{`^application/xml$`}, "application/xml", true}, - {"custom xml no match", []string{`^application/xml$`}, "application/json", false}, - {"custom wildcard", []string{`^text/.*`}, "text/plain", true}, - {"custom wildcard html", []string{`^text/.*`}, "text/html", true}, - {"custom yaml", []string{`^application/yaml$`}, "application/yaml", true}, - - // Empty patterns - {"empty patterns", []string{}, "application/json", false}, - - // Invalid pattern (silently ignored) - {"invalid pattern", []string{`[invalid`}, "application/json", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := NewContentTypeMatcher(tt.patterns) - got := m.Matches(tt.contentType) - if got != tt.want { - t.Errorf("Matches(%q) = %v, want %v", tt.contentType, got, tt.want) - } - }) - } -} - -func TestDefaultContentTypes(t *testing.T) { - defaults := DefaultContentTypes() - if len(defaults) == 0 { - t.Error("DefaultContentTypes() returned empty slice") - } - - // Verify all patterns are valid regexps - m := NewContentTypeMatcher(defaults) - if len(m.patterns) != len(defaults) { - t.Errorf("Some default patterns failed to compile: got %d patterns, want %d", - len(m.patterns), len(defaults)) - } -} - -func TestGenerationOptions_ServerYAML(t *testing.T) { - t.Run("unmarshal server field", func(t *testing.T) { - yamlContent := ` -generation: - server: std-http -` - var cfg Configuration - err := yaml.Unmarshal([]byte(yamlContent), &cfg) - if err != nil { - t.Fatalf("yaml.Unmarshal failed: %v", err) - } - if cfg.Generation.Server != ServerTypeStdHTTP { - t.Errorf("Server = %q, want %q", cfg.Generation.Server, ServerTypeStdHTTP) - } - }) - - t.Run("unmarshal empty server field", func(t *testing.T) { - yamlContent := ` -generation: - no-models: true -` - var cfg Configuration - err := yaml.Unmarshal([]byte(yamlContent), &cfg) - if err != nil { - t.Fatalf("yaml.Unmarshal failed: %v", err) - } - if cfg.Generation.Server != "" { - t.Errorf("Server = %q, want empty string", cfg.Generation.Server) - } - }) - - t.Run("marshal server field", func(t *testing.T) { - cfg := Configuration{ - PackageName: "test", - Generation: GenerationOptions{ - Server: ServerTypeStdHTTP, - }, - } - data, err := yaml.Marshal(&cfg) - if err != nil { - t.Fatalf("yaml.Marshal failed: %v", err) - } - if got := string(data); !contains(got, "server: std-http") { - t.Errorf("Marshaled YAML does not contain 'server: std-http':\n%s", got) - } - }) - - t.Run("omit empty server field", func(t *testing.T) { - cfg := Configuration{ - PackageName: "test", - Generation: GenerationOptions{}, - } - data, err := yaml.Marshal(&cfg) - if err != nil { - t.Fatalf("yaml.Marshal failed: %v", err) - } - if got := string(data); contains(got, "server:") { - t.Errorf("Marshaled YAML should not contain 'server:' when empty:\n%s", got) - } - }) -} - -func TestServerTypeConstants(t *testing.T) { - if ServerTypeStdHTTP != "std-http" { - t.Errorf("ServerTypeStdHTTP = %q, want %q", ServerTypeStdHTTP, "std-http") - } -} - -// contains is a simple helper for string containment check -func contains(s, substr string) bool { - return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsAt(s, substr)) -} - -func containsAt(s, substr string) bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -} diff --git a/experimental/internal/codegen/extension.go b/experimental/internal/codegen/extension.go deleted file mode 100644 index f3f48dcbb3..0000000000 --- a/experimental/internal/codegen/extension.go +++ /dev/null @@ -1,337 +0,0 @@ -// Package codegen provides extension handling for OpenAPI x- properties. -package codegen - -import ( - "fmt" - "strings" - - "github.com/pb33f/libopenapi/orderedmap" - "go.yaml.in/yaml/v4" -) - -// Extension names - new naming convention with x-oapi-codegen- prefix -const ( - // ExtTypeOverride specifies an external type to use instead of generating one. - // Format: "TypeName" or "TypeName;import/path" or "TypeName;alias import/path" - ExtTypeOverride = "x-oapi-codegen-type-override" - - // ExtNameOverride overrides the generated field name. - ExtNameOverride = "x-oapi-codegen-name-override" - - // ExtTypeNameOverride overrides the generated type name. - ExtTypeNameOverride = "x-oapi-codegen-type-name-override" - - // ExtSkipOptionalPointer skips pointer wrapping for optional fields. - ExtSkipOptionalPointer = "x-oapi-codegen-skip-optional-pointer" - - // ExtJSONIgnore excludes the field from JSON marshaling (json:"-"). - ExtJSONIgnore = "x-oapi-codegen-json-ignore" - - // ExtOmitEmpty explicitly controls the omitempty JSON tag. - ExtOmitEmpty = "x-oapi-codegen-omitempty" - - // ExtOmitZero adds omitzero to the JSON tag (Go 1.24+ encoding/json/v2). - ExtOmitZero = "x-oapi-codegen-omitzero" - - // ExtEnumVarNames overrides the generated enum constant names. - ExtEnumVarNames = "x-oapi-codegen-enum-varnames" - - // ExtDeprecatedReason provides a deprecation reason for documentation. - ExtDeprecatedReason = "x-oapi-codegen-deprecated-reason" - - // ExtOrder controls field ordering in generated structs. - ExtOrder = "x-oapi-codegen-order" -) - -// Legacy extension names for backwards compatibility -const ( - legacyExtGoType = "x-go-type" - legacyExtGoTypeImport = "x-go-type-import" - legacyExtGoName = "x-go-name" - legacyExtGoTypeName = "x-go-type-name" - legacyExtGoTypeSkipOptionalPtr = "x-go-type-skip-optional-pointer" - legacyExtGoJSONIgnore = "x-go-json-ignore" - legacyExtOmitEmpty = "x-omitempty" - legacyExtOmitZero = "x-omitzero" - legacyExtEnumVarNames = "x-enum-varnames" - legacyExtEnumNames = "x-enumNames" // Alternative name - legacyExtDeprecatedReason = "x-deprecated-reason" - legacyExtOrder = "x-order" -) - -// TypeOverride represents an external type override with optional import. -type TypeOverride struct { - TypeName string // The Go type name (e.g., "uuid.UUID") - ImportPath string // Import path (e.g., "github.com/google/uuid") - ImportAlias string // Optional import alias (e.g., "foo" for `import foo "..."`) -} - -// Extensions holds parsed extension values for a schema or property. -type Extensions struct { - TypeOverride *TypeOverride // External type to use - NameOverride string // Override field name - TypeNameOverride string // Override generated type name - SkipOptionalPointer *bool // Skip pointer for optional fields - JSONIgnore *bool // Exclude from JSON - OmitEmpty *bool // Control omitempty - OmitZero *bool // Control omitzero - EnumVarNames []string // Override enum constant names - DeprecatedReason string // Deprecation reason - Order *int // Field ordering -} - -// ParseExtensions extracts extension values from a schema's extensions map. -// It supports both new (x-oapi-codegen-*) and legacy (x-go-*) extension names, -// logging deprecation warnings for legacy names. -func ParseExtensions(extensions *orderedmap.Map[string, *yaml.Node], path string) (*Extensions, error) { - if extensions == nil { - return &Extensions{}, nil - } - - ext := &Extensions{} - - // Legacy type override needs special handling: x-go-type and x-go-type-import - // are separate extensions that must be combined - var legacyGoType string - var legacyGoTypeImport any - - for pair := extensions.First(); pair != nil; pair = pair.Next() { - key := pair.Key() - node := pair.Value() - if node == nil { - continue - } - - val := decodeYAMLNode(node) - - switch key { - case ExtTypeOverride: - override, err := parseTypeOverride(val) - if err != nil { - return nil, fmt.Errorf("parsing %s: %w", key, err) - } - ext.TypeOverride = override - - case legacyExtGoType: - if s, ok := val.(string); ok { - legacyGoType = s - } - - case legacyExtGoTypeImport: - legacyGoTypeImport = val - - case ExtNameOverride, legacyExtGoName: - s, err := asString(val, key) - if err != nil { - return nil, err - } - ext.NameOverride = s - - case ExtTypeNameOverride, legacyExtGoTypeName: - s, err := asString(val, key) - if err != nil { - return nil, err - } - ext.TypeNameOverride = s - - case ExtSkipOptionalPointer, legacyExtGoTypeSkipOptionalPtr: - b, err := asBool(val, key) - if err != nil { - return nil, err - } - ext.SkipOptionalPointer = &b - - case ExtJSONIgnore, legacyExtGoJSONIgnore: - b, err := asBool(val, key) - if err != nil { - return nil, err - } - ext.JSONIgnore = &b - - case ExtOmitEmpty, legacyExtOmitEmpty: - b, err := asBool(val, key) - if err != nil { - return nil, err - } - ext.OmitEmpty = &b - - case ExtOmitZero, legacyExtOmitZero: - b, err := asBool(val, key) - if err != nil { - return nil, err - } - ext.OmitZero = &b - - case ExtEnumVarNames, legacyExtEnumVarNames, legacyExtEnumNames: - s, err := asStringSlice(val, key) - if err != nil { - return nil, err - } - ext.EnumVarNames = s - - case ExtDeprecatedReason, legacyExtDeprecatedReason: - s, err := asString(val, key) - if err != nil { - return nil, err - } - ext.DeprecatedReason = s - - case ExtOrder, legacyExtOrder: - i, err := asInt(val, key) - if err != nil { - return nil, err - } - ext.Order = &i - - default: - // Unknown extension - ignore - } - } - - // Combine legacy x-go-type and x-go-type-import if no new-style override was set - if ext.TypeOverride == nil && legacyGoType != "" { - ext.TypeOverride = buildLegacyTypeOverride(legacyGoType, legacyGoTypeImport) - } - - return ext, nil -} - -// hasExtension checks if an extension exists by either the new or legacy name. -// This is used to check for extensions before fully parsing them. -func hasExtension(extensions *orderedmap.Map[string, *yaml.Node], newName, legacyName string) bool { - if extensions == nil { - return false - } - - for pair := extensions.First(); pair != nil; pair = pair.Next() { - key := pair.Key() - if key == newName || key == legacyName { - return true - } - } - return false -} - -// decodeYAMLNode converts a yaml.Node to a Go value. -func decodeYAMLNode(node *yaml.Node) any { - if node == nil { - return nil - } - - var result any - if err := node.Decode(&result); err != nil { - return nil - } - return result -} - -// parseTypeOverride parses the new combined type override format. -// Format: "TypeName" or "TypeName;import/path" or "TypeName;alias import/path" -func parseTypeOverride(val any) (*TypeOverride, error) { - str, ok := val.(string) - if !ok { - return nil, fmt.Errorf("expected string, got %T", val) - } - - override := &TypeOverride{} - parts := strings.SplitN(str, ";", 2) - override.TypeName = strings.TrimSpace(parts[0]) - - if len(parts) == 2 { - importPart := strings.TrimSpace(parts[1]) - importParts := strings.SplitN(importPart, " ", 2) - if len(importParts) == 2 { - override.ImportAlias = strings.TrimSpace(importParts[0]) - override.ImportPath = strings.TrimSpace(importParts[1]) - } else { - override.ImportPath = importPart - } - } - - return override, nil -} - -// buildLegacyTypeOverride combines legacy x-go-type and x-go-type-import values. -func buildLegacyTypeOverride(typeName string, importVal any) *TypeOverride { - override := &TypeOverride{ - TypeName: typeName, - } - - if importVal == nil { - return override - } - - // Legacy import can be a string or an object with path/name - switch v := importVal.(type) { - case string: - override.ImportPath = v - case map[string]any: - if p, ok := v["path"].(string); ok { - override.ImportPath = p - } - if name, ok := v["name"].(string); ok { - override.ImportAlias = name - } - } - - return override -} - -// Type conversion helpers that include the extension name in error messages - -func asString(val any, extName string) (string, error) { - if val == nil { - return "", nil - } - str, ok := val.(string) - if !ok { - return "", fmt.Errorf("parsing %s: expected string, got %T", extName, val) - } - return str, nil -} - -func asBool(val any, extName string) (bool, error) { - if val == nil { - return false, nil - } - b, ok := val.(bool) - if !ok { - return false, fmt.Errorf("parsing %s: expected bool, got %T", extName, val) - } - return b, nil -} - -func asInt(val any, extName string) (int, error) { - if val == nil { - return 0, nil - } - switch v := val.(type) { - case int: - return v, nil - case int64: - return int(v), nil - case float64: - return int(v), nil - default: - return 0, fmt.Errorf("parsing %s: expected int, got %T", extName, val) - } -} - -func asStringSlice(val any, extName string) ([]string, error) { - if val == nil { - return nil, nil - } - slice, ok := val.([]any) - if !ok { - return nil, fmt.Errorf("parsing %s: expected array, got %T", extName, val) - } - result := make([]string, len(slice)) - for i, v := range slice { - str, ok := v.(string) - if !ok { - return nil, fmt.Errorf("parsing %s: expected string at index %d, got %T", extName, i, v) - } - result[i] = str - } - return result, nil -} diff --git a/experimental/internal/codegen/extension_integration_test.go b/experimental/internal/codegen/extension_integration_test.go deleted file mode 100644 index c100af7333..0000000000 --- a/experimental/internal/codegen/extension_integration_test.go +++ /dev/null @@ -1,201 +0,0 @@ -package codegen - -import ( - "strings" - "testing" - - "github.com/pb33f/libopenapi" -) - -func TestExtensionIntegration(t *testing.T) { - spec := ` -openapi: "3.1.0" -info: - title: Extension Test API - version: "1.0" -paths: {} -components: - schemas: - # Test type-name-override - MySchema: - type: object - x-oapi-codegen-type-name-override: CustomTypeName - properties: - id: - type: string - - # Test type-override at schema level - ExternalType: - type: string - x-oapi-codegen-type-override: "uuid.UUID;github.com/google/uuid" - - # Test field-level extensions - User: - type: object - properties: - # Test name override - user_id: - type: string - x-oapi-codegen-name-override: UserID - # Test type override - created_at: - type: string - x-oapi-codegen-type-override: "time.Time;time" - # Test skip optional pointer - description: - type: string - x-oapi-codegen-skip-optional-pointer: true - # Test omitempty control - status: - type: string - x-oapi-codegen-omitempty: false - # Test omitzero - count: - type: integer - x-oapi-codegen-omitzero: true - # Test order (count should come before status) - age: - type: integer - x-oapi-codegen-order: 1 - name: - type: string - x-oapi-codegen-order: 0 - # Test deprecated reason - old_field: - type: string - x-oapi-codegen-deprecated-reason: "Use new_field instead" - - # Test enum with custom var names - Status: - type: string - enum: - - active - - inactive - - pending - x-oapi-codegen-enum-varnames: - - Active - - Inactive - - Pending -` - - doc, err := libopenapi.NewDocument([]byte(spec)) - if err != nil { - t.Fatalf("Failed to parse spec: %v", err) - } - - cfg := Configuration{ - PackageName: "output", - } - - code, err := Generate(doc, nil, cfg) - if err != nil { - t.Fatalf("Generate failed: %v", err) - } - - t.Logf("Generated code:\n%s", code) - - // Verify type-name-override - if !strings.Contains(code, "type CustomTypeName") { - t.Error("Expected CustomTypeName type from type-name-override") - } - - // Verify type-override at schema level creates alias - if !strings.Contains(code, "= uuid.UUID") { - t.Error("Expected type alias to uuid.UUID from type-override") - } - if !strings.Contains(code, `"github.com/google/uuid"`) { - t.Error("Expected uuid import") - } - - // Verify name override - if !strings.Contains(code, "UserID") { - t.Error("Expected UserID field from name-override") - } - - // Verify type override on field - if !strings.Contains(code, "time.Time") { - t.Error("Expected time.Time from field type-override") - } - if !strings.Contains(code, `"time"`) { - t.Error("Expected time import") - } - - // Verify skip-optional-pointer (description should be string, not *string) - // The field should appear as just "string", not "*string" - if strings.Contains(code, "Description *string") || strings.Contains(code, "Description *string") { - t.Error("Expected description to not be a pointer due to skip-optional-pointer") - } - if !strings.Contains(code, "Description string") && !strings.Contains(code, "Description string") { - t.Error("Expected description to be a non-pointer string") - } - - // Verify omitzero - if !strings.Contains(code, "omitzero") { - t.Error("Expected omitzero in struct tags") - } - - // Verify deprecated reason in doc - if !strings.Contains(code, "Deprecated:") { - t.Error("Expected Deprecated: in documentation") - } - - // Verify enum with custom var names - if !strings.Contains(code, "Status_Active") { - t.Error("Expected Status_Active from custom enum var names") - } - if !strings.Contains(code, "Status_Inactive") { - t.Error("Expected Status_Inactive from custom enum var names") - } - if !strings.Contains(code, "Status_Pending") { - t.Error("Expected Status_Pending from custom enum var names") - } -} - -func TestLegacyExtensionIntegration(t *testing.T) { - spec := ` -openapi: "3.1.0" -info: - title: Legacy Extension Test API - version: "1.0" -paths: {} -components: - schemas: - User: - type: object - properties: - # Test legacy x-go-type - id: - type: string - x-go-type: mypackage.ID - # Test legacy x-go-name - user_name: - type: string - x-go-name: Username -` - - doc, err := libopenapi.NewDocument([]byte(spec)) - if err != nil { - t.Fatalf("Failed to parse spec: %v", err) - } - - cfg := Configuration{ - PackageName: "output", - } - - code, err := Generate(doc, nil, cfg) - if err != nil { - t.Fatalf("Generate failed: %v", err) - } - - t.Logf("Generated code:\n%s", code) - - // Verify legacy x-go-type works - if !strings.Contains(code, "mypackage.ID") { - t.Error("Expected mypackage.ID from legacy x-go-type") - } - - // Verify legacy x-go-name works - if !strings.Contains(code, "Username") { - t.Error("Expected Username from legacy x-go-name") - } -} diff --git a/experimental/internal/codegen/extension_test.go b/experimental/internal/codegen/extension_test.go deleted file mode 100644 index c0f615fc41..0000000000 --- a/experimental/internal/codegen/extension_test.go +++ /dev/null @@ -1,180 +0,0 @@ -package codegen - -import ( - "testing" - - "github.com/pb33f/libopenapi/orderedmap" - "go.yaml.in/yaml/v4" -) - -func TestParseTypeOverride(t *testing.T) { - tests := []struct { - name string - input string - wantType string - wantPath string - wantAlias string - }{ - { - name: "simple type", - input: "int64", - wantType: "int64", - }, - { - name: "type with import", - input: "uuid.UUID;github.com/google/uuid", - wantType: "uuid.UUID", - wantPath: "github.com/google/uuid", - }, - { - name: "type with aliased import", - input: "foo.Type;foo github.com/bar/foo/v2", - wantType: "foo.Type", - wantPath: "github.com/bar/foo/v2", - wantAlias: "foo", - }, - { - name: "type with spaces", - input: " decimal.Decimal ; github.com/shopspring/decimal ", - wantType: "decimal.Decimal", - wantPath: "github.com/shopspring/decimal", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseTypeOverride(tt.input) - if err != nil { - t.Fatalf("parseTypeOverride() error = %v", err) - } - if got.TypeName != tt.wantType { - t.Errorf("TypeName = %q, want %q", got.TypeName, tt.wantType) - } - if got.ImportPath != tt.wantPath { - t.Errorf("ImportPath = %q, want %q", got.ImportPath, tt.wantPath) - } - if got.ImportAlias != tt.wantAlias { - t.Errorf("ImportAlias = %q, want %q", got.ImportAlias, tt.wantAlias) - } - }) - } -} - -func TestParseExtensions(t *testing.T) { - // Create a test extensions map - extensions := orderedmap.New[string, *yaml.Node]() - - // Add a type override extension - typeOverrideNode := &yaml.Node{} - typeOverrideNode.SetString("uuid.UUID;github.com/google/uuid") - extensions.Set(ExtTypeOverride, typeOverrideNode) - - // Add a name override extension - nameOverrideNode := &yaml.Node{} - nameOverrideNode.SetString("CustomFieldName") - extensions.Set(ExtNameOverride, nameOverrideNode) - - // Add omitempty extension - omitEmptyNode := &yaml.Node{} - if err := omitEmptyNode.Encode(true); err != nil { - t.Fatalf("Failed to encode omitEmptyNode: %v", err) - } - extensions.Set(ExtOmitEmpty, omitEmptyNode) - - ext, err := ParseExtensions(extensions, "#/test/path") - if err != nil { - t.Fatalf("ParseExtensions() error = %v", err) - } - - // Check type override - if ext.TypeOverride == nil { - t.Fatal("TypeOverride should not be nil") - } - if ext.TypeOverride.TypeName != "uuid.UUID" { - t.Errorf("TypeOverride.TypeName = %q, want %q", ext.TypeOverride.TypeName, "uuid.UUID") - } - if ext.TypeOverride.ImportPath != "github.com/google/uuid" { - t.Errorf("TypeOverride.ImportPath = %q, want %q", ext.TypeOverride.ImportPath, "github.com/google/uuid") - } - - // Check name override - if ext.NameOverride != "CustomFieldName" { - t.Errorf("NameOverride = %q, want %q", ext.NameOverride, "CustomFieldName") - } - - // Check omitempty - if ext.OmitEmpty == nil || *ext.OmitEmpty != true { - t.Errorf("OmitEmpty = %v, want true", ext.OmitEmpty) - } -} - -func TestParseExtensionsLegacy(t *testing.T) { - // Create a test extensions map with legacy names - extensions := orderedmap.New[string, *yaml.Node]() - - // Add legacy x-go-type extension - goTypeNode := &yaml.Node{} - goTypeNode.SetString("time.Time") - extensions.Set("x-go-type", goTypeNode) - - // Add legacy x-go-type-import extension - goImportNode := &yaml.Node{} - goImportNode.SetString("time") - extensions.Set("x-go-type-import", goImportNode) - - // Add legacy x-go-name extension - goNameNode := &yaml.Node{} - goNameNode.SetString("LegacyFieldName") - extensions.Set("x-go-name", goNameNode) - - ext, err := ParseExtensions(extensions, "#/test/path") - if err != nil { - t.Fatalf("ParseExtensions() error = %v", err) - } - - // Check type override (from legacy) - if ext.TypeOverride == nil { - t.Fatal("TypeOverride should not be nil") - } - if ext.TypeOverride.TypeName != "time.Time" { - t.Errorf("TypeOverride.TypeName = %q, want %q", ext.TypeOverride.TypeName, "time.Time") - } - if ext.TypeOverride.ImportPath != "time" { - t.Errorf("TypeOverride.ImportPath = %q, want %q", ext.TypeOverride.ImportPath, "time") - } - - // Check name override (from legacy) - if ext.NameOverride != "LegacyFieldName" { - t.Errorf("NameOverride = %q, want %q", ext.NameOverride, "LegacyFieldName") - } -} - -func TestParseExtensionsEnumVarNames(t *testing.T) { - extensions := orderedmap.New[string, *yaml.Node]() - - // Add enum var names as a sequence - enumNamesNode := &yaml.Node{ - Kind: yaml.SequenceNode, - Content: []*yaml.Node{ - {Kind: yaml.ScalarNode, Value: "Active"}, - {Kind: yaml.ScalarNode, Value: "Inactive"}, - {Kind: yaml.ScalarNode, Value: "Pending"}, - }, - } - extensions.Set(ExtEnumVarNames, enumNamesNode) - - ext, err := ParseExtensions(extensions, "#/test/path") - if err != nil { - t.Fatalf("ParseExtensions() error = %v", err) - } - - if len(ext.EnumVarNames) != 3 { - t.Fatalf("EnumVarNames length = %d, want 3", len(ext.EnumVarNames)) - } - expected := []string{"Active", "Inactive", "Pending"} - for i, name := range ext.EnumVarNames { - if name != expected[i] { - t.Errorf("EnumVarNames[%d] = %q, want %q", i, name, expected[i]) - } - } -} diff --git a/experimental/internal/codegen/gather.go b/experimental/internal/codegen/gather.go deleted file mode 100644 index 0644ed7897..0000000000 --- a/experimental/internal/codegen/gather.go +++ /dev/null @@ -1,621 +0,0 @@ -package codegen - -import ( - "fmt" - "log/slog" - - "github.com/pb33f/libopenapi" - "github.com/pb33f/libopenapi/datamodel/high/base" - v3 "github.com/pb33f/libopenapi/datamodel/high/v3" -) - -// GatherResult contains the results of gathering from an OpenAPI document. -type GatherResult struct { - Schemas []*SchemaDescriptor - ParamTracker *ParamUsageTracker -} - -// GatherSchemas traverses an OpenAPI document and collects all schemas into a list. -func GatherSchemas(doc libopenapi.Document, contentTypeMatcher *ContentTypeMatcher) ([]*SchemaDescriptor, error) { - result, err := GatherAll(doc, contentTypeMatcher) - if err != nil { - return nil, err - } - return result.Schemas, nil -} - -// GatherAll traverses an OpenAPI document and collects all schemas and parameter usage. -func GatherAll(doc libopenapi.Document, contentTypeMatcher *ContentTypeMatcher) (*GatherResult, error) { - model, err := doc.BuildV3Model() - if err != nil { - return nil, fmt.Errorf("building v3 model: %w", err) - } - if model == nil { - return nil, fmt.Errorf("failed to build v3 model") - } - - g := &gatherer{ - schemas: make([]*SchemaDescriptor, 0), - contentTypeMatcher: contentTypeMatcher, - paramTracker: NewParamUsageTracker(), - } - - g.gatherFromDocument(&model.Model) - return &GatherResult{ - Schemas: g.schemas, - ParamTracker: g.paramTracker, - }, nil -} - -type gatherer struct { - schemas []*SchemaDescriptor - contentTypeMatcher *ContentTypeMatcher - paramTracker *ParamUsageTracker - // Context for the current operation being gathered (for nicer naming) - currentOperationID string - currentContentType string -} - -func (g *gatherer) gatherFromDocument(doc *v3.Document) { - // Gather from components/schemas - if doc.Components != nil && doc.Components.Schemas != nil { - for pair := doc.Components.Schemas.First(); pair != nil; pair = pair.Next() { - name := pair.Key() - schemaProxy := pair.Value() - path := SchemaPath{"components", "schemas", name} - g.gatherFromSchemaProxy(schemaProxy, path, nil) - } - } - - // Gather from paths - if doc.Paths != nil && doc.Paths.PathItems != nil { - for pair := doc.Paths.PathItems.First(); pair != nil; pair = pair.Next() { - pathStr := pair.Key() - pathItem := pair.Value() - g.gatherFromPathItem(pathItem, SchemaPath{"paths", pathStr}) - } - } - - // Gather from webhooks (3.1+) - if doc.Webhooks != nil { - for pair := doc.Webhooks.First(); pair != nil; pair = pair.Next() { - name := pair.Key() - pathItem := pair.Value() - g.gatherFromPathItem(pathItem, SchemaPath{"webhooks", name}) - } - } -} - -func (g *gatherer) gatherFromPathItem(pathItem *v3.PathItem, basePath SchemaPath) { - if pathItem == nil { - return - } - - // Path-level parameters - for i, param := range pathItem.Parameters { - g.gatherFromParameter(param, basePath.Append("parameters", fmt.Sprintf("%d", i))) - } - - // Operations - ops := pathItem.GetOperations() - if ops != nil { - for pair := ops.First(); pair != nil; pair = pair.Next() { - method := pair.Key() - op := pair.Value() - g.gatherFromOperation(op, basePath.Append(method)) - } - } -} - -func (g *gatherer) gatherFromOperation(op *v3.Operation, basePath SchemaPath) { - if op == nil { - return - } - - // Set operation context for nicer naming - prevOperationID := g.currentOperationID - if op.OperationId != "" { - g.currentOperationID = op.OperationId - } - - // Parameters - for i, param := range op.Parameters { - g.gatherFromParameter(param, basePath.Append("parameters", fmt.Sprintf("%d", i))) - } - - // Request body - if op.RequestBody != nil { - g.gatherFromRequestBody(op.RequestBody, basePath.Append("requestBody")) - } - - // Responses - if op.Responses != nil && op.Responses.Codes != nil { - for pair := op.Responses.Codes.First(); pair != nil; pair = pair.Next() { - code := pair.Key() - response := pair.Value() - g.gatherFromResponse(response, basePath.Append("responses", code)) - } - } - - // Callbacks - if op.Callbacks != nil { - for pair := op.Callbacks.First(); pair != nil; pair = pair.Next() { - name := pair.Key() - callback := pair.Value() - g.gatherFromCallback(callback, basePath.Append("callbacks", name)) - } - } - - // Restore previous operation context - g.currentOperationID = prevOperationID -} - -func (g *gatherer) gatherFromParameter(param *v3.Parameter, basePath SchemaPath) { - if param == nil { - return - } - - // Track parameter styling usage for code generation - if g.paramTracker != nil && param.Schema != nil { - // Determine style (with defaults based on location) - style := param.Style - if style == "" { - style = DefaultParamStyle(param.In) - } - - // Determine explode (with defaults based on location) - explode := DefaultParamExplode(param.In) - if param.Explode != nil { - explode = *param.Explode - } - - // Record both style (client) and bind (server) usage - g.paramTracker.RecordParam(style, explode) - } - - if param.Schema != nil { - g.gatherFromSchemaProxy(param.Schema, basePath.Append("schema"), nil) - } - - // Parameter can also have content with schemas - if param.Content != nil { - for pair := param.Content.First(); pair != nil; pair = pair.Next() { - contentType := pair.Key() - mediaType := pair.Value() - g.gatherFromMediaType(mediaType, basePath.Append("content", contentType)) - } - } -} - -func (g *gatherer) gatherFromRequestBody(rb *v3.RequestBody, basePath SchemaPath) { - if rb == nil || rb.Content == nil { - return - } - - for pair := rb.Content.First(); pair != nil; pair = pair.Next() { - contentType := pair.Key() - // Skip content types that don't match the configured patterns - if g.contentTypeMatcher != nil && !g.contentTypeMatcher.Matches(contentType) { - continue - } - // Set content type context - prevContentType := g.currentContentType - g.currentContentType = contentType - - mediaType := pair.Value() - g.gatherFromMediaType(mediaType, basePath.Append("content", contentType)) - - g.currentContentType = prevContentType - } -} - -func (g *gatherer) gatherFromResponse(response *v3.Response, basePath SchemaPath) { - if response == nil { - return - } - - if response.Content != nil { - for pair := response.Content.First(); pair != nil; pair = pair.Next() { - contentType := pair.Key() - // Skip content types that don't match the configured patterns - if g.contentTypeMatcher != nil && !g.contentTypeMatcher.Matches(contentType) { - continue - } - // Set content type context - prevContentType := g.currentContentType - g.currentContentType = contentType - - mediaType := pair.Value() - g.gatherFromMediaType(mediaType, basePath.Append("content", contentType)) - - g.currentContentType = prevContentType - } - } - - // Response headers can have schemas - if response.Headers != nil { - for pair := response.Headers.First(); pair != nil; pair = pair.Next() { - name := pair.Key() - header := pair.Value() - if header != nil && header.Schema != nil { - g.gatherFromSchemaProxy(header.Schema, basePath.Append("headers", name, "schema"), nil) - } - } - } -} - -func (g *gatherer) gatherFromMediaType(mt *v3.MediaType, basePath SchemaPath) { - if mt == nil || mt.Schema == nil { - return - } - g.gatherFromSchemaProxy(mt.Schema, basePath.Append("schema"), nil) -} - -func (g *gatherer) gatherFromCallback(callback *v3.Callback, basePath SchemaPath) { - if callback == nil || callback.Expression == nil { - return - } - - for pair := callback.Expression.First(); pair != nil; pair = pair.Next() { - expr := pair.Key() - pathItem := pair.Value() - g.gatherFromPathItem(pathItem, basePath.Append(expr)) - } -} - -func (g *gatherer) gatherFromSchemaProxy(proxy *base.SchemaProxy, path SchemaPath, parent *SchemaDescriptor) *SchemaDescriptor { - if proxy == nil { - return nil - } - - // Check if this is a reference - isRef := proxy.IsReference() - ref := "" - if isRef { - ref = proxy.GetReference() - } - - // Get the resolved schema - schema := proxy.Schema() - - // Check if schema has extensions that require type generation - hasTypeOverride := schema != nil && schema.Extensions != nil && hasExtension(schema.Extensions, ExtTypeOverride, legacyExtGoType) - hasTypeNameOverride := schema != nil && schema.Extensions != nil && hasExtension(schema.Extensions, ExtTypeNameOverride, legacyExtGoTypeName) - - // Only gather schemas that need a generated type - // References are always gathered (they point to real schemas) - // Simple types (primitives without enum) are skipped - // Inline nullable primitives (under properties/) don't need types - they use Nullable[T] directly - // Schemas with type-override or type-name-override extensions always need types - isInlineProperty := path.ContainsProperties() - skipInlineNullablePrimitive := isInlineProperty && isNullablePrimitive(schema) - needsType := isRef || needsGeneratedType(schema) || hasTypeOverride || hasTypeNameOverride - if needsType && !skipInlineNullablePrimitive { - desc := &SchemaDescriptor{ - Path: path, - Parent: parent, - Ref: ref, - Schema: schema, - OperationID: g.currentOperationID, - ContentType: g.currentContentType, - } - - // Parse extensions from the schema - if schema != nil && schema.Extensions != nil { - ext, err := ParseExtensions(schema.Extensions, path.String()) - if err != nil { - slog.Warn("failed to parse extensions", - "path", path.String(), - "error", err) - } else { - desc.Extensions = ext - } - } - - g.schemas = append(g.schemas, desc) - - // Don't recurse into references - they point to schemas we'll gather elsewhere - if isRef { - return desc - } - - // Recurse into schema structure - if schema != nil { - g.gatherFromSchema(schema, path, desc) - } - return desc - } else if schema != nil { - // Even if we don't gather this schema, we still need to recurse - // to find any nested complex schemas (e.g., array items that are objects) - g.gatherFromSchema(schema, path, nil) - } - return nil -} - -// gatherSchemaDescriptorOnly creates a descriptor for field extraction without adding it -// to the schemas list (i.e., no type will be generated for it). -// This is used for inline allOf members whose fields are flattened into the parent. -func (g *gatherer) gatherSchemaDescriptorOnly(proxy *base.SchemaProxy, path SchemaPath, parent *SchemaDescriptor) *SchemaDescriptor { - if proxy == nil { - return nil - } - - schema := proxy.Schema() - if schema == nil { - return nil - } - - desc := &SchemaDescriptor{ - Path: path, - Parent: parent, - Schema: schema, - } - - // Parse extensions from the schema - if schema.Extensions != nil { - ext, err := ParseExtensions(schema.Extensions, path.String()) - if err != nil { - slog.Warn("failed to parse extensions", - "path", path.String(), - "error", err) - } else { - desc.Extensions = ext - } - } - - // Still recurse to gather any nested complex schemas that DO need types - // (e.g., nested objects within properties) - g.gatherFromSchema(schema, path, desc) - - return desc -} - -// needsGeneratedType returns true if a schema requires a generated Go type. -// Simple primitive types (string, integer, number, boolean) without enums -// don't need generated types - they map directly to Go builtins. -// However, nullable primitives DO need generated types (Nullable[T]). -func needsGeneratedType(schema *base.Schema) bool { - if schema == nil { - return false - } - - // Nullable primitives need a generated type (Nullable[T]) - if isNullablePrimitive(schema) { - return true - } - - // Enums always need a generated type - if len(schema.Enum) > 0 { - return true - } - - // Objects need a generated type - if schema.Properties != nil && schema.Properties.Len() > 0 { - return true - } - - // Check explicit type - types := schema.Type - for _, t := range types { - if t == "object" { - return true - } - } - - // Composition types need generated types - if len(schema.AllOf) > 0 || len(schema.AnyOf) > 0 || len(schema.OneOf) > 0 { - return true - } - - // Arrays with complex items need generated types for the array type itself - // But we handle items separately in gatherFromSchema - if schema.Items != nil && schema.Items.A != nil { - itemSchema := schema.Items.A.Schema() - if needsGeneratedType(itemSchema) { - return true - } - } - - // AdditionalProperties with complex schema needs a type - if schema.AdditionalProperties != nil && schema.AdditionalProperties.A != nil { - addSchema := schema.AdditionalProperties.A.Schema() - if needsGeneratedType(addSchema) { - return true - } - } - - // Simple primitives (string, integer, number, boolean) without enum - // don't need generated types - return false -} - -// isNullablePrimitive returns true if the schema is a nullable primitive type. -// Nullable primitives need Nullable[T] wrapper types. -func isNullablePrimitive(schema *base.Schema) bool { - if schema == nil { - return false - } - - // Check for nullable - isNullable := false - // OpenAPI 3.1 style: type array includes "null" - for _, t := range schema.Type { - if t == "null" { - isNullable = true - break - } - } - // OpenAPI 3.0 style: nullable: true - if schema.Nullable != nil && *schema.Nullable { - isNullable = true - } - - if !isNullable { - return false - } - - // Check if it's a primitive type (not object, array, or composition) - if schema.Properties != nil && schema.Properties.Len() > 0 { - return false // object with properties - } - if len(schema.AllOf) > 0 || len(schema.AnyOf) > 0 || len(schema.OneOf) > 0 { - return false // composition type - } - if schema.Items != nil { - return false // array - } - - // Get the primary type - for _, t := range schema.Type { - switch t { - case "string", "integer", "number", "boolean": - return true - case "object": - return false - case "array": - return false - } - } - - return false -} - -func (g *gatherer) gatherFromSchema(schema *base.Schema, basePath SchemaPath, parent *SchemaDescriptor) { - if schema == nil { - return - } - - // Properties - if schema.Properties != nil { - if parent != nil { - parent.Properties = make(map[string]*SchemaDescriptor) - } - for pair := schema.Properties.First(); pair != nil; pair = pair.Next() { - propName := pair.Key() - propProxy := pair.Value() - propPath := basePath.Append("properties", propName) - propDesc := g.gatherFromSchemaProxy(propProxy, propPath, parent) - if parent != nil && propDesc != nil { - parent.Properties[propName] = propDesc - } - } - } - - // Items (array element schema) - if schema.Items != nil && schema.Items.A != nil { - itemsPath := basePath.Append("items") - itemsDesc := g.gatherFromSchemaProxy(schema.Items.A, itemsPath, parent) - if parent != nil && itemsDesc != nil { - parent.Items = itemsDesc - } - } - - // AllOf - inline object members don't need separate types since fields are flattened into parent - // However, inline oneOf/anyOf members DO need union types generated - for i, proxy := range schema.AllOf { - allOfPath := basePath.Append("allOf", fmt.Sprintf("%d", i)) - var allOfDesc *SchemaDescriptor - if proxy.IsReference() { - // References still need to be gathered normally - allOfDesc = g.gatherFromSchemaProxy(proxy, allOfPath, parent) - } else { - memberSchema := proxy.Schema() - // If the allOf member is itself a oneOf/anyOf, we need to generate a union type - if memberSchema != nil && (len(memberSchema.OneOf) > 0 || len(memberSchema.AnyOf) > 0) { - allOfDesc = g.gatherFromSchemaProxy(proxy, allOfPath, parent) - } else { - // Simple inline objects: create descriptor for field extraction but don't generate a type - allOfDesc = g.gatherSchemaDescriptorOnly(proxy, allOfPath, parent) - } - } - if parent != nil && allOfDesc != nil { - parent.AllOf = append(parent.AllOf, allOfDesc) - } - } - - // AnyOf - for i, proxy := range schema.AnyOf { - anyOfPath := basePath.Append("anyOf", fmt.Sprintf("%d", i)) - anyOfDesc := g.gatherFromSchemaProxy(proxy, anyOfPath, parent) - if parent != nil && anyOfDesc != nil { - parent.AnyOf = append(parent.AnyOf, anyOfDesc) - } - } - - // OneOf - for i, proxy := range schema.OneOf { - oneOfPath := basePath.Append("oneOf", fmt.Sprintf("%d", i)) - oneOfDesc := g.gatherFromSchemaProxy(proxy, oneOfPath, parent) - if parent != nil && oneOfDesc != nil { - parent.OneOf = append(parent.OneOf, oneOfDesc) - } - } - - // AdditionalProperties (if it's a schema, not a boolean) - if schema.AdditionalProperties != nil && schema.AdditionalProperties.A != nil { - addPropsPath := basePath.Append("additionalProperties") - addPropsDesc := g.gatherFromSchemaProxy(schema.AdditionalProperties.A, addPropsPath, parent) - if parent != nil && addPropsDesc != nil { - parent.AdditionalProps = addPropsDesc - } - } - - // Not - if schema.Not != nil { - g.gatherFromSchemaProxy(schema.Not, basePath.Append("not"), parent) - } - - // PrefixItems (3.1 tuple validation) - for i, proxy := range schema.PrefixItems { - g.gatherFromSchemaProxy(proxy, basePath.Append("prefixItems", fmt.Sprintf("%d", i)), parent) - } - - // Contains (3.1) - if schema.Contains != nil { - g.gatherFromSchemaProxy(schema.Contains, basePath.Append("contains"), parent) - } - - // If/Then/Else (3.1) - if schema.If != nil { - g.gatherFromSchemaProxy(schema.If, basePath.Append("if"), parent) - } - if schema.Then != nil { - g.gatherFromSchemaProxy(schema.Then, basePath.Append("then"), parent) - } - if schema.Else != nil { - g.gatherFromSchemaProxy(schema.Else, basePath.Append("else"), parent) - } - - // DependentSchemas (3.1) - if schema.DependentSchemas != nil { - for pair := schema.DependentSchemas.First(); pair != nil; pair = pair.Next() { - name := pair.Key() - proxy := pair.Value() - g.gatherFromSchemaProxy(proxy, basePath.Append("dependentSchemas", name), parent) - } - } - - // PatternProperties (3.1) - if schema.PatternProperties != nil { - for pair := schema.PatternProperties.First(); pair != nil; pair = pair.Next() { - pattern := pair.Key() - proxy := pair.Value() - g.gatherFromSchemaProxy(proxy, basePath.Append("patternProperties", pattern), parent) - } - } - - // PropertyNames (3.1) - if schema.PropertyNames != nil { - g.gatherFromSchemaProxy(schema.PropertyNames, basePath.Append("propertyNames"), parent) - } - - // UnevaluatedItems (3.1) - if schema.UnevaluatedItems != nil { - g.gatherFromSchemaProxy(schema.UnevaluatedItems, basePath.Append("unevaluatedItems"), parent) - } - - // UnevaluatedProperties (3.1) - can be schema or bool - if schema.UnevaluatedProperties != nil && schema.UnevaluatedProperties.A != nil { - g.gatherFromSchemaProxy(schema.UnevaluatedProperties.A, basePath.Append("unevaluatedProperties"), parent) - } -} diff --git a/experimental/internal/codegen/gather_operations.go b/experimental/internal/codegen/gather_operations.go deleted file mode 100644 index 0492bbb433..0000000000 --- a/experimental/internal/codegen/gather_operations.go +++ /dev/null @@ -1,864 +0,0 @@ -package codegen - -import ( - "fmt" - "sort" - "strings" - - "github.com/pb33f/libopenapi" - "github.com/pb33f/libopenapi/datamodel/high/base" - v3 "github.com/pb33f/libopenapi/datamodel/high/v3" -) - -// GatherOperations traverses an OpenAPI document and collects all operations. -func GatherOperations(doc libopenapi.Document, paramTracker *ParamUsageTracker) ([]*OperationDescriptor, error) { - model, err := doc.BuildV3Model() - if err != nil { - return nil, fmt.Errorf("building v3 model: %w", err) - } - if model == nil { - return nil, fmt.Errorf("failed to build v3 model") - } - - g := &operationGatherer{ - paramTracker: paramTracker, - } - - return g.gatherFromDocument(&model.Model) -} - -type operationGatherer struct { - paramTracker *ParamUsageTracker -} - -func (g *operationGatherer) gatherFromDocument(doc *v3.Document) ([]*OperationDescriptor, error) { - var operations []*OperationDescriptor - - if doc.Paths == nil || doc.Paths.PathItems == nil { - return operations, nil - } - - // Collect paths in sorted order for deterministic output - var paths []string - for pair := doc.Paths.PathItems.First(); pair != nil; pair = pair.Next() { - paths = append(paths, pair.Key()) - } - sort.Strings(paths) - - for _, pathStr := range paths { - pathItem := doc.Paths.PathItems.GetOrZero(pathStr) - if pathItem == nil { - continue - } - - // Gather path-level parameters (shared by all operations on this path) - globalParams, err := g.gatherParameters(pathItem.Parameters) - if err != nil { - return nil, fmt.Errorf("error gathering path-level parameters for %s: %w", pathStr, err) - } - - // Process each operation on this path - ops := pathItem.GetOperations() - if ops == nil { - continue - } - - // Collect methods in sorted order - var methods []string - for pair := ops.First(); pair != nil; pair = pair.Next() { - methods = append(methods, pair.Key()) - } - sort.Strings(methods) - - for _, method := range methods { - op := ops.GetOrZero(method) - if op == nil { - continue - } - - opDesc, err := g.gatherOperation(method, pathStr, op, globalParams) - if err != nil { - return nil, fmt.Errorf("error gathering operation %s %s: %w", method, pathStr, err) - } - operations = append(operations, opDesc) - } - } - - return operations, nil -} - -func (g *operationGatherer) gatherOperation(method, path string, op *v3.Operation, globalParams []*ParameterDescriptor) (*OperationDescriptor, error) { - // Determine operation ID - operationID := op.OperationId - if operationID == "" { - operationID = generateOperationID(method, path) - } - goOperationID := ToGoIdentifier(operationID) - - // Gather operation-level parameters - localParams, err := g.gatherParameters(op.Parameters) - if err != nil { - return nil, fmt.Errorf("error gathering parameters: %w", err) - } - - // Combine global and local parameters (local overrides global) - allParams := combineParameters(globalParams, localParams) - - // Sort path params to match order in path - pathParams := filterParamsByLocation(allParams, "path") - pathParams, err = sortPathParamsByPath(path, pathParams) - if err != nil { - return nil, fmt.Errorf("error sorting path params: %w", err) - } - - // Gather request bodies - bodies, err := g.gatherRequestBodies(operationID, op.RequestBody) - if err != nil { - return nil, fmt.Errorf("error gathering request bodies: %w", err) - } - - // Gather responses - responses, err := g.gatherResponses(operationID, op.Responses) - if err != nil { - return nil, fmt.Errorf("error gathering responses: %w", err) - } - - // Gather security requirements - security := g.gatherSecurity(op.Security) - - queryParams := filterParamsByLocation(allParams, "query") - headerParams := filterParamsByLocation(allParams, "header") - cookieParams := filterParamsByLocation(allParams, "cookie") - - hasParams := len(queryParams)+len(headerParams)+len(cookieParams) > 0 - - desc := &OperationDescriptor{ - OperationID: operationID, - GoOperationID: goOperationID, - Method: strings.ToUpper(method), - Path: path, - Summary: op.Summary, - Description: op.Description, - - PathParams: pathParams, - QueryParams: queryParams, - HeaderParams: headerParams, - CookieParams: cookieParams, - - Bodies: bodies, - Responses: responses, - Security: security, - - HasBody: len(bodies) > 0, - HasParams: hasParams, - ParamsTypeName: goOperationID + "Params", - - Spec: op, - } - - return desc, nil -} - -func (g *operationGatherer) gatherParameters(params []*v3.Parameter) ([]*ParameterDescriptor, error) { - var result []*ParameterDescriptor - - for _, param := range params { - if param == nil { - continue - } - - desc, err := g.gatherParameter(param) - if err != nil { - return nil, fmt.Errorf("error gathering parameter %s: %w", param.Name, err) - } - result = append(result, desc) - } - - return result, nil -} - -func (g *operationGatherer) gatherParameter(param *v3.Parameter) (*ParameterDescriptor, error) { - // Determine style and explode (with defaults based on location) - style := param.Style - if style == "" { - style = DefaultParamStyle(param.In) - } - - explode := DefaultParamExplode(param.In) - if param.Explode != nil { - explode = *param.Explode - } - - // Record param usage for function generation - if g.paramTracker != nil { - g.paramTracker.RecordParam(style, explode) - } - - // Determine encoding mode - isStyled := param.Schema != nil - isJSON := false - isPassThrough := false - - if param.Content != nil && param.Content.Len() > 0 { - // Parameter uses content encoding - isStyled = false - for pair := param.Content.First(); pair != nil; pair = pair.Next() { - contentType := pair.Key() - if IsMediaTypeJSON(contentType) { - isJSON = true - break - } - } - if !isJSON { - isPassThrough = true - } - } - - // Get type declaration from schema - typeDecl := "string" // Default - var schemaDesc *SchemaDescriptor - if param.Schema != nil { - schema := param.Schema.Schema() - if schema != nil { - schemaDesc = &SchemaDescriptor{ - Schema: schema, - } - typeDecl = schemaToGoType(schema) - } - } - - goName := ToCamelCase(param.Name) - - // Handle *bool for Required - required := false - if param.Required != nil { - required = *param.Required - } - - desc := &ParameterDescriptor{ - Name: param.Name, - GoName: goName, - Location: param.In, - Required: required, - - Style: style, - Explode: explode, - - Schema: schemaDesc, - TypeDecl: typeDecl, - - StyleFunc: ComputeStyleFunc(style, explode), - BindFunc: ComputeBindFunc(style, explode), - - IsStyled: isStyled, - IsPassThrough: isPassThrough, - IsJSON: isJSON, - - Spec: param, - } - - return desc, nil -} - -func (g *operationGatherer) gatherRequestBodies(operationID string, bodyRef *v3.RequestBody) ([]*RequestBodyDescriptor, error) { - if bodyRef == nil { - return nil, nil - } - - var bodies []*RequestBodyDescriptor - - if bodyRef.Content == nil { - return bodies, nil - } - - // Collect content types in sorted order - var contentTypes []string - for pair := bodyRef.Content.First(); pair != nil; pair = pair.Next() { - contentTypes = append(contentTypes, pair.Key()) - } - sort.Strings(contentTypes) - - // Determine which is the default (application/json if present) - hasApplicationJSON := false - for _, ct := range contentTypes { - if ct == "application/json" { - hasApplicationJSON = true - break - } - } - - for _, contentType := range contentTypes { - mediaType := bodyRef.Content.GetOrZero(contentType) - if mediaType == nil { - continue - } - - nameTag := ComputeBodyNameTag(contentType) - isDefault := contentType == "application/json" || (!hasApplicationJSON && contentType == contentTypes[0]) - - var schemaDesc *SchemaDescriptor - if mediaType.Schema != nil { - schemaDesc = schemaProxyToDescriptor(mediaType.Schema) - } - - funcSuffix := "" - if !isDefault && nameTag != "" { - funcSuffix = "With" + nameTag + "Body" - } - - goTypeName := operationID + nameTag + "RequestBody" - if nameTag == "" { - goTypeName = operationID + "RequestBody" - } - - // Handle *bool for Required - bodyRequired := false - if bodyRef.Required != nil { - bodyRequired = *bodyRef.Required - } - - desc := &RequestBodyDescriptor{ - ContentType: contentType, - Required: bodyRequired, - Schema: schemaDesc, - - NameTag: nameTag, - GoTypeName: goTypeName, - FuncSuffix: funcSuffix, - IsDefault: isDefault, - IsJSON: IsMediaTypeJSON(contentType), - } - - // Gather encoding options for form data - if mediaType.Encoding != nil && mediaType.Encoding.Len() > 0 { - desc.Encoding = make(map[string]RequestBodyEncoding) - for pair := mediaType.Encoding.First(); pair != nil; pair = pair.Next() { - enc := pair.Value() - desc.Encoding[pair.Key()] = RequestBodyEncoding{ - ContentType: enc.ContentType, - Style: enc.Style, - Explode: enc.Explode, - } - } - } - - bodies = append(bodies, desc) - } - - return bodies, nil -} - -func (g *operationGatherer) gatherResponses(operationID string, responses *v3.Responses) ([]*ResponseDescriptor, error) { - if responses == nil { - return nil, nil - } - - var result []*ResponseDescriptor - - // Gather default response - if responses.Default != nil { - desc, err := g.gatherResponse(operationID, "default", responses.Default) - if err != nil { - return nil, err - } - if desc != nil { - result = append(result, desc) - } - } - - // Gather status code responses - if responses.Codes != nil { - var codes []string - for pair := responses.Codes.First(); pair != nil; pair = pair.Next() { - codes = append(codes, pair.Key()) - } - sort.Strings(codes) - - for _, code := range codes { - respRef := responses.Codes.GetOrZero(code) - if respRef == nil { - continue - } - - desc, err := g.gatherResponse(operationID, code, respRef) - if err != nil { - return nil, err - } - if desc != nil { - result = append(result, desc) - } - } - } - - return result, nil -} - -func (g *operationGatherer) gatherResponse(operationID, statusCode string, resp *v3.Response) (*ResponseDescriptor, error) { - if resp == nil { - return nil, nil - } - - var contents []*ResponseContentDescriptor - if resp.Content != nil { - var contentTypes []string - for pair := resp.Content.First(); pair != nil; pair = pair.Next() { - contentTypes = append(contentTypes, pair.Key()) - } - sort.Strings(contentTypes) - - for _, contentType := range contentTypes { - mediaType := resp.Content.GetOrZero(contentType) - if mediaType == nil { - continue - } - - var schemaDesc *SchemaDescriptor - if mediaType.Schema != nil { - schemaDesc = schemaProxyToDescriptor(mediaType.Schema) - } - - nameTag := ComputeBodyNameTag(contentType) - - contents = append(contents, &ResponseContentDescriptor{ - ContentType: contentType, - Schema: schemaDesc, - NameTag: nameTag, - IsJSON: IsMediaTypeJSON(contentType), - }) - } - } - - var headers []*ResponseHeaderDescriptor - if resp.Headers != nil { - var headerNames []string - for pair := resp.Headers.First(); pair != nil; pair = pair.Next() { - headerNames = append(headerNames, pair.Key()) - } - sort.Strings(headerNames) - - for _, name := range headerNames { - header := resp.Headers.GetOrZero(name) - if header == nil { - continue - } - - var schemaDesc *SchemaDescriptor - if header.Schema != nil { - schemaDesc = schemaProxyToDescriptor(header.Schema) - } - - headers = append(headers, &ResponseHeaderDescriptor{ - Name: name, - GoName: ToCamelCase(name), - Required: header.Required, - Schema: schemaDesc, - }) - } - } - - description := "" - if resp.Description != "" { - description = resp.Description - } - - return &ResponseDescriptor{ - StatusCode: statusCode, - Description: description, - Contents: contents, - Headers: headers, - }, nil -} - -func (g *operationGatherer) gatherSecurity(security []*base.SecurityRequirement) []SecurityRequirement { - if security == nil { - return nil - } - - var result []SecurityRequirement - for _, req := range security { - if req == nil || req.Requirements == nil { - continue - } - for pair := req.Requirements.First(); pair != nil; pair = pair.Next() { - result = append(result, SecurityRequirement{ - Name: pair.Key(), - Scopes: pair.Value(), - }) - } - } - return result -} - -// Helper functions - -func generateOperationID(method, path string) string { - // Generate operation ID from method and path - // GET /users/{id} -> GetUsersId - id := strings.ToLower(method) - for _, part := range strings.Split(path, "/") { - if part == "" { - continue - } - // Remove path parameter braces - part = strings.TrimPrefix(part, "{") - part = strings.TrimSuffix(part, "}") - id += "-" + part - } - return ToCamelCase(id) -} - -func combineParameters(global, local []*ParameterDescriptor) []*ParameterDescriptor { - // Local parameters override global parameters with the same name and location - seen := make(map[string]bool) - var result []*ParameterDescriptor - - for _, p := range local { - key := p.Location + ":" + p.Name - seen[key] = true - result = append(result, p) - } - - for _, p := range global { - key := p.Location + ":" + p.Name - if !seen[key] { - result = append(result, p) - } - } - - return result -} - -func filterParamsByLocation(params []*ParameterDescriptor, location string) []*ParameterDescriptor { - var result []*ParameterDescriptor - for _, p := range params { - if p.Location == location { - result = append(result, p) - } - } - return result -} - -func sortPathParamsByPath(path string, params []*ParameterDescriptor) ([]*ParameterDescriptor, error) { - // Extract parameter names from path in order - var pathParamNames []string - parts := strings.Split(path, "/") - for _, part := range parts { - if strings.HasPrefix(part, "{") && strings.HasSuffix(part, "}") { - name := strings.TrimPrefix(part, "{") - name = strings.TrimSuffix(name, "}") - pathParamNames = append(pathParamNames, name) - } - } - - // Build a map of params by name - paramMap := make(map[string]*ParameterDescriptor) - for _, p := range params { - paramMap[p.Name] = p - } - - // Sort params according to path order - var result []*ParameterDescriptor - for _, name := range pathParamNames { - if p, ok := paramMap[name]; ok { - result = append(result, p) - } - } - - return result, nil -} - -// GatherWebhookOperations traverses an OpenAPI document and collects operations from webhooks. -func GatherWebhookOperations(doc libopenapi.Document, paramTracker *ParamUsageTracker) ([]*OperationDescriptor, error) { - model, err := doc.BuildV3Model() - if err != nil { - return nil, fmt.Errorf("building v3 model: %w", err) - } - if model == nil { - return nil, fmt.Errorf("failed to build v3 model") - } - - g := &operationGatherer{ - paramTracker: paramTracker, - } - - return g.gatherWebhooks(&model.Model) -} - -// GatherCallbackOperations traverses an OpenAPI document and collects operations from callbacks. -func GatherCallbackOperations(doc libopenapi.Document, paramTracker *ParamUsageTracker) ([]*OperationDescriptor, error) { - model, err := doc.BuildV3Model() - if err != nil { - return nil, fmt.Errorf("building v3 model: %w", err) - } - if model == nil { - return nil, fmt.Errorf("failed to build v3 model") - } - - g := &operationGatherer{ - paramTracker: paramTracker, - } - - return g.gatherCallbacks(&model.Model) -} - -func (g *operationGatherer) gatherWebhooks(doc *v3.Document) ([]*OperationDescriptor, error) { - var operations []*OperationDescriptor - - if doc.Webhooks == nil || doc.Webhooks.Len() == 0 { - return operations, nil - } - - // Collect webhook names in sorted order for deterministic output - var webhookNames []string - for pair := doc.Webhooks.First(); pair != nil; pair = pair.Next() { - webhookNames = append(webhookNames, pair.Key()) - } - sort.Strings(webhookNames) - - for _, webhookName := range webhookNames { - pathItem := doc.Webhooks.GetOrZero(webhookName) - if pathItem == nil { - continue - } - - // Gather path-level parameters - globalParams, err := g.gatherParameters(pathItem.Parameters) - if err != nil { - return nil, fmt.Errorf("error gathering parameters for webhook %s: %w", webhookName, err) - } - - ops := pathItem.GetOperations() - if ops == nil { - continue - } - - var methods []string - for pair := ops.First(); pair != nil; pair = pair.Next() { - methods = append(methods, pair.Key()) - } - sort.Strings(methods) - - for _, method := range methods { - op := ops.GetOrZero(method) - if op == nil { - continue - } - - // For webhooks, Path is empty (no URL path in the spec) - opDesc, err := g.gatherOperation(method, "", op, globalParams) - if err != nil { - return nil, fmt.Errorf("error gathering webhook operation %s %s: %w", method, webhookName, err) - } - - // Override operation ID if not set - use webhook name + method - if op.OperationId == "" { - opDesc.OperationID = ToCamelCase(method + "-" + webhookName) - opDesc.GoOperationID = ToGoIdentifier(opDesc.OperationID) - opDesc.ParamsTypeName = opDesc.GoOperationID + "Params" - } - - opDesc.Source = OperationSourceWebhook - opDesc.WebhookName = webhookName - - operations = append(operations, opDesc) - } - } - - return operations, nil -} - -func (g *operationGatherer) gatherCallbacks(doc *v3.Document) ([]*OperationDescriptor, error) { - var operations []*OperationDescriptor - - if doc.Paths == nil || doc.Paths.PathItems == nil { - return operations, nil - } - - // Iterate all paths in sorted order - var paths []string - for pair := doc.Paths.PathItems.First(); pair != nil; pair = pair.Next() { - paths = append(paths, pair.Key()) - } - sort.Strings(paths) - - for _, pathStr := range paths { - pathItem := doc.Paths.PathItems.GetOrZero(pathStr) - if pathItem == nil { - continue - } - - pathOps := pathItem.GetOperations() - if pathOps == nil { - continue - } - - var methods []string - for pair := pathOps.First(); pair != nil; pair = pair.Next() { - methods = append(methods, pair.Key()) - } - sort.Strings(methods) - - for _, method := range methods { - parentOp := pathOps.GetOrZero(method) - if parentOp == nil || parentOp.Callbacks == nil || parentOp.Callbacks.Len() == 0 { - continue - } - - parentOpID := parentOp.OperationId - if parentOpID == "" { - parentOpID = generateOperationID(method, pathStr) - } - - // Collect callback names in sorted order - var callbackNames []string - for pair := parentOp.Callbacks.First(); pair != nil; pair = pair.Next() { - callbackNames = append(callbackNames, pair.Key()) - } - sort.Strings(callbackNames) - - for _, callbackName := range callbackNames { - callback := parentOp.Callbacks.GetOrZero(callbackName) - if callback == nil || callback.Expression == nil || callback.Expression.Len() == 0 { - continue - } - - // Iterate callback expressions in sorted order - var expressions []string - for pair := callback.Expression.First(); pair != nil; pair = pair.Next() { - expressions = append(expressions, pair.Key()) - } - sort.Strings(expressions) - - for _, expression := range expressions { - cbPathItem := callback.Expression.GetOrZero(expression) - if cbPathItem == nil { - continue - } - - cbOps := cbPathItem.GetOperations() - if cbOps == nil { - continue - } - - var cbMethods []string - for pair := cbOps.First(); pair != nil; pair = pair.Next() { - cbMethods = append(cbMethods, pair.Key()) - } - sort.Strings(cbMethods) - - for _, cbMethod := range cbMethods { - cbOp := cbOps.GetOrZero(cbMethod) - if cbOp == nil { - continue - } - - // URL expression is stored as path but params are not extracted - // (expressions are runtime-evaluated) - opDesc, err := g.gatherOperation(cbMethod, expression, cbOp, nil) - if err != nil { - return nil, fmt.Errorf("error gathering callback operation %s %s %s: %w", cbMethod, callbackName, expression, err) - } - - // Override operation ID if not set - if cbOp.OperationId == "" { - opDesc.OperationID = ToCamelCase(parentOpID + "-" + callbackName) - opDesc.GoOperationID = ToGoIdentifier(opDesc.OperationID) - opDesc.ParamsTypeName = opDesc.GoOperationID + "Params" - } - - // Clear path params since callback URLs are runtime expressions - opDesc.PathParams = nil - - opDesc.Source = OperationSourceCallback - opDesc.CallbackName = callbackName - opDesc.ParentOpID = parentOpID - - operations = append(operations, opDesc) - } - } - } - } - } - - return operations, nil -} - -// schemaProxyToDescriptor converts a schema proxy to a basic descriptor. -// This is a simplified version - for full type resolution, use the schema gatherer. -func schemaProxyToDescriptor(proxy *base.SchemaProxy) *SchemaDescriptor { - if proxy == nil { - return nil - } - - schema := proxy.Schema() - if schema == nil { - return nil - } - - desc := &SchemaDescriptor{ - Schema: schema, - } - - // Capture reference if this is a reference schema - if proxy.IsReference() { - desc.Ref = proxy.GetReference() - } - - return desc -} - -// schemaToGoType converts a schema to a Go type string. -// This is a simplified version for parameter types. -func schemaToGoType(schema *base.Schema) string { - if schema == nil { - return "interface{}" - } - - // Check for array - if schema.Items != nil && schema.Items.A != nil { - itemType := "interface{}" - if itemSchema := schema.Items.A.Schema(); itemSchema != nil { - itemType = schemaToGoType(itemSchema) - } - return "[]" + itemType - } - - // Check explicit type - for _, t := range schema.Type { - switch t { - case "string": - if schema.Format == "date-time" { - return "time.Time" - } - if schema.Format == "date" { - return "Date" - } - if schema.Format == "uuid" { - return "uuid.UUID" - } - return "string" - case "integer": - if schema.Format == "int64" { - return "int64" - } - if schema.Format == "int32" { - return "int32" - } - return "int" - case "number": - if schema.Format == "float" { - return "float32" - } - return "float64" - case "boolean": - return "bool" - case "array": - // Handled above - return "[]interface{}" - case "object": - return "map[string]interface{}" - } - } - - return "interface{}" -} diff --git a/experimental/internal/codegen/identifiers.go b/experimental/internal/codegen/identifiers.go deleted file mode 100644 index 6a8f58f05b..0000000000 --- a/experimental/internal/codegen/identifiers.go +++ /dev/null @@ -1,125 +0,0 @@ -package codegen - -import ( - "strings" - "unicode" -) - -// Go keywords that can't be used as identifiers -var goKeywords = map[string]bool{ - "break": true, - "case": true, - "chan": true, - "const": true, - "continue": true, - "default": true, - "defer": true, - "else": true, - "fallthrough": true, - "for": true, - "func": true, - "go": true, - "goto": true, - "if": true, - "import": true, - "interface": true, - "map": true, - "package": true, - "range": true, - "return": true, - "select": true, - "struct": true, - "switch": true, - "type": true, - "var": true, -} - -// IsGoKeyword returns true if s is a Go keyword. -func IsGoKeyword(s string) bool { - return goKeywords[s] -} - -// ToCamelCase converts a string to CamelCase (PascalCase). -// It treats hyphens, underscores, spaces, and other non-alphanumeric characters as word separators. -// Example: "user-name" -> "UserName", "user_id" -> "UserId" -func ToCamelCase(s string) string { - if s == "" { - return "" - } - - var result strings.Builder - capitalizeNext := true - - for _, r := range s { - if isWordSeparator(r) { - capitalizeNext = true - continue - } - - if !unicode.IsLetter(r) && !unicode.IsDigit(r) { - capitalizeNext = true - continue - } - - if capitalizeNext { - result.WriteRune(unicode.ToUpper(r)) - capitalizeNext = false - } else { - result.WriteRune(r) - } - } - - return result.String() -} - -// LowercaseFirstCharacter lowercases only the first character of a string. -// Example: "UserName" -> "userName" -func LowercaseFirstCharacter(s string) string { - if s == "" { - return "" - } - runes := []rune(s) - runes[0] = unicode.ToLower(runes[0]) - return string(runes) -} - -// UppercaseFirstCharacter uppercases only the first character of a string. -// Example: "userName" -> "UserName" -func UppercaseFirstCharacter(s string) string { - if s == "" { - return "" - } - runes := []rune(s) - runes[0] = unicode.ToUpper(runes[0]) - return string(runes) -} - -// isWordSeparator returns true if the rune is a word separator. -func isWordSeparator(r rune) bool { - return r == '-' || r == '_' || r == ' ' || r == '.' || r == '/' -} - -// ToGoIdentifier converts a string to a valid Go identifier. -// It converts to CamelCase, handles leading digits, and avoids Go keywords. -func ToGoIdentifier(s string) string { - result := ToCamelCase(s) - - // Handle empty result - if result == "" { - return "Empty" - } - - // Handle leading digits - if result[0] >= '0' && result[0] <= '9' { - result = "N" + result - } - - // Handle Go keywords - check both the original input and lowercase result - // "type" -> "Type" but we still want to avoid "Type" being used as-is - // since user might write it as lowercase in code - if IsGoKeyword(s) || IsGoKeyword(strings.ToLower(result)) { - result = result + "_" - } - - return result -} diff --git a/experimental/internal/codegen/identifiers_test.go b/experimental/internal/codegen/identifiers_test.go deleted file mode 100644 index 4d5eca6328..0000000000 --- a/experimental/internal/codegen/identifiers_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package codegen - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestIsGoKeyword(t *testing.T) { - keywords := []string{ - "break", "case", "chan", "const", "continue", - "default", "defer", "else", "fallthrough", "for", - "func", "go", "goto", "if", "import", - "interface", "map", "package", "range", "return", - "select", "struct", "switch", "type", "var", - } - - for _, kw := range keywords { - t.Run(kw, func(t *testing.T) { - assert.True(t, IsGoKeyword(kw), "%s should be a keyword", kw) - }) - } - - nonKeywords := []string{ - "user", "name", "id", "Type", "Interface", "Map", - "string", "int", "bool", "error", // predeclared but not keywords - } - - for _, nkw := range nonKeywords { - t.Run(nkw+"_not_keyword", func(t *testing.T) { - assert.False(t, IsGoKeyword(nkw), "%s should not be a keyword", nkw) - }) - } -} - -func TestToCamelCase(t *testing.T) { - tests := []struct { - input string - expected string - }{ - {"", ""}, - {"user", "User"}, - {"user_name", "UserName"}, - {"user-name", "UserName"}, - {"user.name", "UserName"}, - {"user name", "UserName"}, - {"USER", "USER"}, - {"USER_NAME", "USERNAME"}, - {"123", "123"}, - {"user123", "User123"}, - {"user123name", "User123name"}, - {"get-users-by-id", "GetUsersById"}, - {"__private", "Private"}, - {"a_b_c", "ABC"}, - {"already_CamelCase", "AlreadyCamelCase"}, - {"path/to/resource", "PathToResource"}, - } - - for _, tc := range tests { - t.Run(tc.input, func(t *testing.T) { - result := ToCamelCase(tc.input) - assert.Equal(t, tc.expected, result) - }) - } -} - -func TestLowercaseFirstCharacter(t *testing.T) { - tests := []struct { - input string - expected string - }{ - {"", ""}, - {"User", "user"}, - {"UserName", "userName"}, - {"user", "user"}, - {"ABC", "aBC"}, - {"123", "123"}, - } - - for _, tc := range tests { - t.Run(tc.input, func(t *testing.T) { - result := LowercaseFirstCharacter(tc.input) - assert.Equal(t, tc.expected, result) - }) - } -} - -func TestUppercaseFirstCharacter(t *testing.T) { - tests := []struct { - input string - expected string - }{ - {"", ""}, - {"user", "User"}, - {"userName", "UserName"}, - {"User", "User"}, - {"abc", "Abc"}, - } - - for _, tc := range tests { - t.Run(tc.input, func(t *testing.T) { - result := UppercaseFirstCharacter(tc.input) - assert.Equal(t, tc.expected, result) - }) - } -} - -func TestToGoIdentifier(t *testing.T) { - tests := []struct { - input string - expected string - }{ - {"user", "User"}, - {"user_name", "UserName"}, - {"123abc", "N123abc"}, - {"type", "Type_"}, - {"map", "Map_"}, - {"interface", "Interface_"}, - {"", "Empty"}, - {"get-users", "GetUsers"}, - } - - for _, tc := range tests { - t.Run(tc.input, func(t *testing.T) { - result := ToGoIdentifier(tc.input) - assert.Equal(t, tc.expected, result) - }) - } -} diff --git a/experimental/internal/codegen/initiatorgen.go b/experimental/internal/codegen/initiatorgen.go deleted file mode 100644 index 67a9aa08fa..0000000000 --- a/experimental/internal/codegen/initiatorgen.go +++ /dev/null @@ -1,216 +0,0 @@ -package codegen - -import ( - "bytes" - "fmt" - "strings" - "text/template" - - "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/templates" -) - -// InitiatorTemplateData is passed to initiator templates. -type InitiatorTemplateData struct { - Prefix string // "Webhook" or "Callback" - PrefixLower string // "webhook" or "callback" - Operations []*OperationDescriptor // Operations to generate for -} - -// InitiatorGenerator generates initiator (sender) code from operation descriptors. -// It is parameterized by prefix to support both webhooks and callbacks. -type InitiatorGenerator struct { - tmpl *template.Template - prefix string // "Webhook" or "Callback" - schemaIndex map[string]*SchemaDescriptor - generateSimple bool - modelsPackage *ModelsPackage -} - -// NewInitiatorGenerator creates a new initiator generator. -func NewInitiatorGenerator(prefix string, schemaIndex map[string]*SchemaDescriptor, generateSimple bool, modelsPackage *ModelsPackage) (*InitiatorGenerator, error) { - tmpl := template.New("initiator").Funcs(templates.Funcs()).Funcs(clientFuncs(schemaIndex, modelsPackage)) - - // Parse initiator templates - for _, pt := range templates.InitiatorTemplates { - content, err := templates.TemplateFS.ReadFile("files/" + pt.Template) - if err != nil { - return nil, fmt.Errorf("failed to read initiator template %s: %w", pt.Template, err) - } - _, err = tmpl.New(pt.Name).Parse(string(content)) - if err != nil { - return nil, fmt.Errorf("failed to parse initiator template %s: %w", pt.Template, err) - } - } - - // Parse shared templates (param_types) - for _, st := range templates.SharedServerTemplates { - content, err := templates.TemplateFS.ReadFile("files/" + st.Template) - if err != nil { - return nil, fmt.Errorf("failed to read shared template %s: %w", st.Template, err) - } - _, err = tmpl.New(st.Name).Parse(string(content)) - if err != nil { - return nil, fmt.Errorf("failed to parse shared template %s: %w", st.Template, err) - } - } - - return &InitiatorGenerator{ - tmpl: tmpl, - prefix: prefix, - schemaIndex: schemaIndex, - generateSimple: generateSimple, - modelsPackage: modelsPackage, - }, nil -} - -func (g *InitiatorGenerator) templateData(ops []*OperationDescriptor) InitiatorTemplateData { - return InitiatorTemplateData{ - Prefix: g.prefix, - PrefixLower: strings.ToLower(g.prefix), - Operations: ops, - } -} - -// GenerateBase generates the base initiator types and helpers. -func (g *InitiatorGenerator) GenerateBase(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "initiator_base", g.templateData(ops)); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateInterface generates the InitiatorInterface. -func (g *InitiatorGenerator) GenerateInterface(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "initiator_interface", g.templateData(ops)); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateMethods generates the Initiator methods. -func (g *InitiatorGenerator) GenerateMethods(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "initiator_methods", g.templateData(ops)); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateRequestBuilders generates the request builder functions. -func (g *InitiatorGenerator) GenerateRequestBuilders(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "initiator_request_builders", g.templateData(ops)); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateSimple generates the SimpleInitiator with typed responses. -func (g *InitiatorGenerator) GenerateSimple(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "initiator_simple", g.templateData(ops)); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateParamTypes generates the parameter struct types. -func (g *InitiatorGenerator) GenerateParamTypes(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "param_types", ops); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateRequestBodyTypes generates type aliases for request bodies. -func (g *InitiatorGenerator) GenerateRequestBodyTypes(ops []*OperationDescriptor) string { - var buf bytes.Buffer - pkgPrefix := g.modelsPackage.Prefix() - - for _, op := range ops { - for _, body := range op.Bodies { - if !body.IsJSON { - continue - } - var targetType string - if body.Schema != nil { - if body.Schema.Ref != "" { - if target, ok := g.schemaIndex[body.Schema.Ref]; ok { - targetType = pkgPrefix + target.ShortName - } - } else if body.Schema.ShortName != "" { - targetType = pkgPrefix + body.Schema.ShortName - } - } - if targetType == "" { - targetType = "interface{}" - } - buf.WriteString(fmt.Sprintf("type %s = %s\n\n", body.GoTypeName, targetType)) - } - } - - return buf.String() -} - -// GenerateInitiator generates the complete initiator code. -func (g *InitiatorGenerator) GenerateInitiator(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - - // Generate request body type aliases first - bodyTypes := g.GenerateRequestBodyTypes(ops) - buf.WriteString(bodyTypes) - - // Generate base initiator - base, err := g.GenerateBase(ops) - if err != nil { - return "", fmt.Errorf("generating base initiator: %w", err) - } - buf.WriteString(base) - buf.WriteString("\n") - - // Generate interface - iface, err := g.GenerateInterface(ops) - if err != nil { - return "", fmt.Errorf("generating initiator interface: %w", err) - } - buf.WriteString(iface) - buf.WriteString("\n") - - // Generate param types - paramTypes, err := g.GenerateParamTypes(ops) - if err != nil { - return "", fmt.Errorf("generating param types: %w", err) - } - buf.WriteString(paramTypes) - buf.WriteString("\n") - - // Generate methods - methods, err := g.GenerateMethods(ops) - if err != nil { - return "", fmt.Errorf("generating initiator methods: %w", err) - } - buf.WriteString(methods) - buf.WriteString("\n") - - // Generate request builders - builders, err := g.GenerateRequestBuilders(ops) - if err != nil { - return "", fmt.Errorf("generating request builders: %w", err) - } - buf.WriteString(builders) - buf.WriteString("\n") - - // Generate simple initiator if requested - if g.generateSimple { - simple, err := g.GenerateSimple(ops) - if err != nil { - return "", fmt.Errorf("generating simple initiator: %w", err) - } - buf.WriteString(simple) - } - - return buf.String(), nil -} diff --git a/experimental/internal/codegen/inline.go b/experimental/internal/codegen/inline.go deleted file mode 100644 index 25767cffa8..0000000000 --- a/experimental/internal/codegen/inline.go +++ /dev/null @@ -1,92 +0,0 @@ -package codegen - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "fmt" - "strings" -) - -// generateEmbeddedSpec produces Go code that embeds the raw OpenAPI spec as -// gzip+base64 encoded data, with a public GetSwaggerSpecJSON() function to -// retrieve the decompressed JSON bytes. -func generateEmbeddedSpec(specData []byte) (string, error) { - // Gzip compress - var buf bytes.Buffer - gz, err := gzip.NewWriterLevel(&buf, gzip.BestCompression) - if err != nil { - return "", fmt.Errorf("creating gzip writer: %w", err) - } - if _, err := gz.Write(specData); err != nil { - return "", fmt.Errorf("gzip writing: %w", err) - } - if err := gz.Close(); err != nil { - return "", fmt.Errorf("gzip close: %w", err) - } - - // Base64 encode - encoded := base64.StdEncoding.EncodeToString(buf.Bytes()) - - // Split into 80-char chunks - var chunks []string - for len(encoded) > 0 { - end := 80 - if end > len(encoded) { - end = len(encoded) - } - chunks = append(chunks, encoded[:end]) - encoded = encoded[end:] - } - - // Build Go code - var b strings.Builder - - b.WriteString("// Base64-encoded, gzip-compressed OpenAPI spec.\n") - b.WriteString("var swaggerSpecJSON = []string{\n") - for _, chunk := range chunks { - fmt.Fprintf(&b, "\t%q,\n", chunk) - } - b.WriteString("}\n\n") - - b.WriteString("// decodeSwaggerSpec decodes and decompresses the embedded spec.\n") - b.WriteString("func decodeSwaggerSpec() ([]byte, error) {\n") - b.WriteString("\tjoined := strings.Join(swaggerSpecJSON, \"\")\n") - b.WriteString("\traw, err := base64.StdEncoding.DecodeString(joined)\n") - b.WriteString("\tif err != nil {\n") - b.WriteString("\t\treturn nil, fmt.Errorf(\"decoding base64: %w\", err)\n") - b.WriteString("\t}\n") - b.WriteString("\tr, err := gzip.NewReader(bytes.NewReader(raw))\n") - b.WriteString("\tif err != nil {\n") - b.WriteString("\t\treturn nil, fmt.Errorf(\"creating gzip reader: %w\", err)\n") - b.WriteString("\t}\n") - b.WriteString("\tdefer r.Close()\n") - b.WriteString("\tvar out bytes.Buffer\n") - b.WriteString("\tif _, err := out.ReadFrom(r); err != nil {\n") - b.WriteString("\t\treturn nil, fmt.Errorf(\"decompressing: %w\", err)\n") - b.WriteString("\t}\n") - b.WriteString("\treturn out.Bytes(), nil\n") - b.WriteString("}\n\n") - - b.WriteString("// decodeSwaggerSpecCached returns a closure that caches the decoded spec.\n") - b.WriteString("func decodeSwaggerSpecCached() func() ([]byte, error) {\n") - b.WriteString("\tvar cached []byte\n") - b.WriteString("\tvar cachedErr error\n") - b.WriteString("\tvar once sync.Once\n") - b.WriteString("\treturn func() ([]byte, error) {\n") - b.WriteString("\t\tonce.Do(func() {\n") - b.WriteString("\t\t\tcached, cachedErr = decodeSwaggerSpec()\n") - b.WriteString("\t\t})\n") - b.WriteString("\t\treturn cached, cachedErr\n") - b.WriteString("\t}\n") - b.WriteString("}\n\n") - - b.WriteString("var swaggerSpec = decodeSwaggerSpecCached()\n\n") - - b.WriteString("// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes.\n") - b.WriteString("func GetSwaggerSpecJSON() ([]byte, error) {\n") - b.WriteString("\treturn swaggerSpec()\n") - b.WriteString("}\n") - - return b.String(), nil -} diff --git a/experimental/internal/codegen/inline_test.go b/experimental/internal/codegen/inline_test.go deleted file mode 100644 index ae35938493..0000000000 --- a/experimental/internal/codegen/inline_test.go +++ /dev/null @@ -1,139 +0,0 @@ -package codegen - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "strings" - "testing" - - "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenerateEmbeddedSpec(t *testing.T) { - specData := []byte(`{"openapi":"3.0.0","info":{"title":"Test","version":"1.0"}}`) - - code, err := generateEmbeddedSpec(specData) - require.NoError(t, err) - - // Should contain the chunked base64 variable - assert.Contains(t, code, "var swaggerSpecJSON = []string{") - - // Should contain the decode function - assert.Contains(t, code, "func decodeSwaggerSpec() ([]byte, error)") - - // Should contain the cached decode function - assert.Contains(t, code, "func decodeSwaggerSpecCached() func() ([]byte, error)") - - // Should contain the public API - assert.Contains(t, code, "func GetSwaggerSpecJSON() ([]byte, error)") - - // Should contain the cached var - assert.Contains(t, code, "var swaggerSpec = decodeSwaggerSpecCached()") -} - -func TestGenerateEmbeddedSpecRoundTrip(t *testing.T) { - specData := []byte(`{"openapi":"3.0.0","info":{"title":"Test API","version":"1.0"},"paths":{}}`) - - code, err := generateEmbeddedSpec(specData) - require.NoError(t, err) - - // Extract the base64 chunks from the generated code - var chunks []string - for _, line := range strings.Split(code, "\n") { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, `"`) && strings.HasSuffix(line, `",`) { - // Remove quotes and trailing comma - chunk := line[1 : len(line)-2] - chunks = append(chunks, chunk) - } - } - require.NotEmpty(t, chunks, "should have extracted base64 chunks") - - // Decode base64 - joined := strings.Join(chunks, "") - raw, err := base64.StdEncoding.DecodeString(joined) - require.NoError(t, err) - - // Decompress gzip - r, err := gzip.NewReader(bytes.NewReader(raw)) - require.NoError(t, err) - defer func() { _ = r.Close() }() - - var out bytes.Buffer - _, err = out.ReadFrom(r) - require.NoError(t, err) - - // Should match original spec - assert.Equal(t, specData, out.Bytes()) -} - -func TestGenerateEmbeddedSpecInGenerate(t *testing.T) { - spec := `openapi: "3.0.0" -info: - title: Test API - version: "1.0" -paths: {} -components: - schemas: - Pet: - type: object - properties: - name: - type: string -` - - specBytes := []byte(spec) - - doc, err := libopenapi.NewDocument(specBytes) - require.NoError(t, err) - - cfg := Configuration{ - PackageName: "testpkg", - } - - code, err := Generate(doc, specBytes, cfg) - require.NoError(t, err) - - // Should contain the model type - assert.Contains(t, code, "type Pet struct") - - // Should contain the embedded spec - assert.Contains(t, code, "GetSwaggerSpecJSON") - assert.Contains(t, code, "swaggerSpecJSON") -} - -func TestGenerateWithNilSpecData(t *testing.T) { - spec := `openapi: "3.0.0" -info: - title: Test API - version: "1.0" -paths: {} -components: - schemas: - Pet: - type: object - properties: - name: - type: string -` - - doc, err := libopenapi.NewDocument([]byte(spec)) - require.NoError(t, err) - - cfg := Configuration{ - PackageName: "testpkg", - } - - code, err := Generate(doc, nil, cfg) - require.NoError(t, err) - - // Should contain the model type - assert.Contains(t, code, "type Pet struct") - - // Should NOT contain the embedded spec - assert.NotContains(t, code, "GetSwaggerSpecJSON") - assert.NotContains(t, code, "swaggerSpecJSON") -} diff --git a/experimental/internal/codegen/namemangling.go b/experimental/internal/codegen/namemangling.go deleted file mode 100644 index 9dff27a642..0000000000 --- a/experimental/internal/codegen/namemangling.go +++ /dev/null @@ -1,420 +0,0 @@ -package codegen - -import ( - "strings" - "unicode" -) - -// NameMangling configures how OpenAPI names are converted to valid Go identifiers. -type NameMangling struct { - // CharacterSubstitutions maps characters to their word replacements. - // Used when these characters appear at the start of a name. - // Example: '$' -> "DollarSign", '-' -> "Minus" - CharacterSubstitutions map[string]string `yaml:"character-substitutions,omitempty"` - - // WordSeparators is a string of characters that mark word boundaries. - // When encountered, the next letter is capitalized. - // Example: "-_. " means "foo-bar" becomes "FooBar" - WordSeparators string `yaml:"word-separators,omitempty"` - - // NumericPrefix is prepended when a name starts with a digit. - // Example: "N" means "123foo" becomes "N123foo" - NumericPrefix string `yaml:"numeric-prefix,omitempty"` - - // KeywordPrefix is prepended when a name conflicts with a Go keyword. - // Example: "_" means "type" becomes "_type" - KeywordPrefix string `yaml:"keyword-prefix,omitempty"` - - // Initialisms is a list of words that should be all-uppercase. - // Example: ["ID", "HTTP", "URL"] means "userId" becomes "UserID" - Initialisms []string `yaml:"initialisms,omitempty"` -} - -// DefaultNameMangling returns sensible defaults for name mangling. -func DefaultNameMangling() NameMangling { - return NameMangling{ - CharacterSubstitutions: map[string]string{ - "$": "DollarSign", - "-": "Minus", - "+": "Plus", - "&": "And", - "|": "Or", - "~": "Tilde", - "=": "Equal", - ">": "GreaterThan", - "<": "LessThan", - "#": "Hash", - ".": "Dot", - "*": "Asterisk", - "^": "Caret", - "%": "Percent", - "_": "Underscore", - "@": "At", - "!": "Bang", - "?": "Question", - "/": "Slash", - "\\": "Backslash", - ":": "Colon", - ";": "Semicolon", - "'": "Apos", - "\"": "Quote", - "`": "Backtick", - "(": "LParen", - ")": "RParen", - "[": "LBracket", - "]": "RBracket", - "{": "LBrace", - "}": "RBrace", - }, - WordSeparators: "-#@!$&=.+:;_~ (){}[]|<>?/\\", - NumericPrefix: "N", - KeywordPrefix: "_", - Initialisms: []string{ - "ACL", "API", "ASCII", "CPU", "CSS", "DB", "DNS", "EOF", - "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", - "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", - "TLS", "TTL", "UDP", "UI", "UID", "GID", "URI", "URL", - "UTF8", "UUID", "VM", "XML", "XMPP", "XSRF", "XSS", - "SIP", "RTP", "AMQP", "TS", - }, - } -} - -// Merge returns a new NameMangling with user values overlaid on defaults. -// Non-zero user values override defaults. -func (n NameMangling) Merge(user NameMangling) NameMangling { - result := n - - // Merge character substitutions (user overrides/adds to defaults) - if len(user.CharacterSubstitutions) > 0 { - merged := make(map[string]string, len(n.CharacterSubstitutions)) - for k, v := range n.CharacterSubstitutions { - merged[k] = v - } - for k, v := range user.CharacterSubstitutions { - if v == "" { - // Empty string means "remove this substitution" - delete(merged, k) - } else { - merged[k] = v - } - } - result.CharacterSubstitutions = merged - } - - if user.WordSeparators != "" { - result.WordSeparators = user.WordSeparators - } - if user.NumericPrefix != "" { - result.NumericPrefix = user.NumericPrefix - } - if user.KeywordPrefix != "" { - result.KeywordPrefix = user.KeywordPrefix - } - if len(user.Initialisms) > 0 { - result.Initialisms = user.Initialisms - } - - return result -} - -// NameSubstitutions holds direct name overrides for generated identifiers. -type NameSubstitutions struct { - // TypeNames maps generated type names to user-preferred names. - // Example: {"MyGeneratedType": "MyPreferredName"} - TypeNames map[string]string `yaml:"type-names,omitempty"` - - // PropertyNames maps generated property/field names to user-preferred names. - // Example: {"GeneratedField": "PreferredField"} - PropertyNames map[string]string `yaml:"property-names,omitempty"` -} - -// NameConverter handles converting OpenAPI names to Go identifiers. -type NameConverter struct { - mangling NameMangling - substitutions NameSubstitutions - initialismSet map[string]bool -} - -// NewNameConverter creates a NameConverter with the given configuration. -func NewNameConverter(mangling NameMangling, substitutions NameSubstitutions) *NameConverter { - initialismSet := make(map[string]bool, len(mangling.Initialisms)) - for _, init := range mangling.Initialisms { - initialismSet[strings.ToUpper(init)] = true - } - return &NameConverter{ - mangling: mangling, - substitutions: substitutions, - initialismSet: initialismSet, - } -} - -// ToTypeName converts an OpenAPI schema name to a Go type name. -func (c *NameConverter) ToTypeName(name string) string { - // Check for direct substitution first - if sub, ok := c.substitutions.TypeNames[name]; ok { - return sub - } - return c.toGoIdentifier(name, true) -} - -// ToTypeNamePart converts a name to a type name component that will be joined with others. -// Unlike ToTypeName, it doesn't add a numeric prefix since the result won't be the start of an identifier. -func (c *NameConverter) ToTypeNamePart(name string) string { - // Check for direct substitution first - if sub, ok := c.substitutions.TypeNames[name]; ok { - return sub - } - return c.toGoIdentifierPart(name) -} - -// ToPropertyName converts an OpenAPI property name to a Go field name. -func (c *NameConverter) ToPropertyName(name string) string { - // Check for direct substitution first - if sub, ok := c.substitutions.PropertyNames[name]; ok { - return sub - } - return c.toGoIdentifier(name, true) -} - -// ToVariableName converts an OpenAPI name to a Go variable name (unexported). -func (c *NameConverter) ToVariableName(name string) string { - id := c.toGoIdentifier(name, false) - if id == "" { - return id - } - // Make first letter lowercase - runes := []rune(id) - runes[0] = unicode.ToLower(runes[0]) - return string(runes) -} - -// toGoIdentifier converts a name to a valid Go identifier. -func (c *NameConverter) toGoIdentifier(name string, exported bool) string { - if name == "" { - return "Empty" - } - - // Build the identifier with prefix handling - var result strings.Builder - prefix := c.getPrefix(name) - result.WriteString(prefix) - - // Convert the rest using word boundaries - capitalizeNext := exported || prefix != "" - prevWasDigit := false - for _, r := range name { - if c.isWordSeparator(r) { - capitalizeNext = true - prevWasDigit = false - continue - } - - if !unicode.IsLetter(r) && !unicode.IsDigit(r) { - // Skip invalid characters (already handled by prefix if at start) - capitalizeNext = true - prevWasDigit = false - continue - } - - // Capitalize after digits - if prevWasDigit && unicode.IsLetter(r) { - capitalizeNext = true - } - - if capitalizeNext && unicode.IsLetter(r) { - result.WriteRune(unicode.ToUpper(r)) - capitalizeNext = false - } else { - result.WriteRune(r) - } - - prevWasDigit = unicode.IsDigit(r) - } - - id := result.String() - if id == "" { - return "Empty" - } - - // Apply initialism fixes - id = c.applyInitialisms(id) - - return id -} - -// toGoIdentifierPart converts a name to a Go identifier component (for joining with others). -// It doesn't add a numeric prefix since the result won't necessarily be at the start of an identifier. -func (c *NameConverter) toGoIdentifierPart(name string) string { - if name == "" { - return "" - } - - // Build the identifier without numeric prefix (but still handle special characters at start) - var result strings.Builder - - // Only add prefix for non-digit special characters at the start - firstRune := []rune(name)[0] - if !unicode.IsLetter(firstRune) && !unicode.IsDigit(firstRune) { - firstChar := string(firstRune) - if sub, ok := c.mangling.CharacterSubstitutions[firstChar]; ok { - result.WriteString(sub) - } else { - result.WriteString("X") - } - } - - // Convert the rest using word boundaries (always capitalize since this is a part) - capitalizeNext := true - prevWasDigit := false - for _, r := range name { - if c.isWordSeparator(r) { - capitalizeNext = true - prevWasDigit = false - continue - } - - if !unicode.IsLetter(r) && !unicode.IsDigit(r) { - // Skip invalid characters (already handled by prefix if at start) - capitalizeNext = true - prevWasDigit = false - continue - } - - // Capitalize after digits - if prevWasDigit && unicode.IsLetter(r) { - capitalizeNext = true - } - - if capitalizeNext && unicode.IsLetter(r) { - result.WriteRune(unicode.ToUpper(r)) - capitalizeNext = false - } else { - result.WriteRune(r) - } - - prevWasDigit = unicode.IsDigit(r) - } - - id := result.String() - - // Apply initialism fixes - id = c.applyInitialisms(id) - - return id -} - -// getPrefix returns the prefix needed for names starting with invalid characters. -func (c *NameConverter) getPrefix(name string) string { - if name == "" { - return "" - } - - firstRune := []rune(name)[0] - - // Check if starts with digit - if unicode.IsDigit(firstRune) { - return c.mangling.NumericPrefix - } - - // Check if starts with letter (valid, no prefix needed) - if unicode.IsLetter(firstRune) { - return "" - } - - // Check character substitutions - firstChar := string(firstRune) - if sub, ok := c.mangling.CharacterSubstitutions[firstChar]; ok { - return sub - } - - // Unknown special character, use generic prefix - return "X" -} - -// isWordSeparator returns true if the rune is a word separator. -func (c *NameConverter) isWordSeparator(r rune) bool { - return strings.ContainsRune(c.mangling.WordSeparators, r) -} - -// applyInitialisms uppercases known initialisms in the identifier. -// It detects initialisms at word boundaries in PascalCase identifiers. -func (c *NameConverter) applyInitialisms(name string) string { - if len(name) == 0 { - return name - } - - // Split the identifier into "words" based on case transitions - // e.g., "UserId" -> ["User", "Id"], "HTTPUrl" -> ["HTTP", "Url"] - words := splitPascalCase(name) - - // Check each word against initialisms - for i, word := range words { - upper := strings.ToUpper(word) - if c.initialismSet[upper] { - words[i] = upper - } - } - - return strings.Join(words, "") -} - -// splitPascalCase splits a PascalCase identifier into words. -// e.g., "UserId" -> ["User", "Id"], "HTTPServer" -> ["HTTP", "Server"] -func splitPascalCase(s string) []string { - if len(s) == 0 { - return nil - } - - var words []string - var currentWord strings.Builder - - runes := []rune(s) - for i := 0; i < len(runes); i++ { - r := runes[i] - - if i == 0 { - currentWord.WriteRune(r) - continue - } - - prevUpper := unicode.IsUpper(runes[i-1]) - currUpper := unicode.IsUpper(r) - currDigit := unicode.IsDigit(r) - - // Start new word on: - // 1. Lowercase to uppercase transition (e.g., "userId" -> "user" | "Id") - // 2. Multiple uppercase followed by lowercase (e.g., "HTTPServer" -> "HTTP" | "Server") - if currUpper && !prevUpper { - // Lowercase to uppercase: start new word - words = append(words, currentWord.String()) - currentWord.Reset() - currentWord.WriteRune(r) - } else if currUpper && prevUpper && i+1 < len(runes) && unicode.IsLower(runes[i+1]) { - // Uppercase followed by lowercase, and previous was uppercase - // This is the start of a new word after an acronym - // e.g., in "HTTPServer", 'S' starts a new word - words = append(words, currentWord.String()) - currentWord.Reset() - currentWord.WriteRune(r) - } else if currDigit && !unicode.IsDigit(runes[i-1]) { - // Transition to digit: start new word - words = append(words, currentWord.String()) - currentWord.Reset() - currentWord.WriteRune(r) - } else if !currDigit && unicode.IsDigit(runes[i-1]) { - // Transition from digit: start new word - words = append(words, currentWord.String()) - currentWord.Reset() - currentWord.WriteRune(r) - } else { - currentWord.WriteRune(r) - } - } - - if currentWord.Len() > 0 { - words = append(words, currentWord.String()) - } - - return words -} diff --git a/experimental/internal/codegen/namemangling_test.go b/experimental/internal/codegen/namemangling_test.go deleted file mode 100644 index 6d36cc0749..0000000000 --- a/experimental/internal/codegen/namemangling_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package codegen - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestToTypeName(t *testing.T) { - c := NewNameConverter(DefaultNameMangling(), NameSubstitutions{}) - - tests := []struct { - input string - expected string - }{ - // Basic conversions - {"foo", "Foo"}, - {"fooBar", "FooBar"}, - {"foo_bar", "FooBar"}, - {"foo-bar", "FooBar"}, - {"foo.bar", "FooBar"}, - - // Names starting with numbers - {"123", "N123"}, - {"123foo", "N123Foo"}, - {"1param", "N1Param"}, - - // Names starting with special characters - {"$ref", "DollarSignRef"}, - {"$", "DollarSign"}, - {"-1", "Minus1"}, - {"+1", "Plus1"}, - {"&now", "AndNow"}, - {"#tag", "HashTag"}, - {".hidden", "DotHidden"}, - {"@timestamp", "AtTimestamp"}, - {"_private", "UnderscorePrivate"}, - - // Initialisms - {"userId", "UserID"}, - {"httpUrl", "HTTPURL"}, - {"apiId", "APIID"}, - {"jsonData", "JSONData"}, - {"xmlParser", "XMLParser"}, - {"getHttpResponse", "GetHTTPResponse"}, - - // Go keywords - PascalCase doesn't conflict with lowercase keywords - {"type", "Type"}, - {"interface", "Interface"}, - {"map", "Map"}, - {"chan", "Chan"}, - - // Predeclared identifiers - PascalCase doesn't conflict with lowercase identifiers - {"string", "String"}, - {"int", "Int"}, - {"error", "Error"}, - {"nil", "Nil"}, - - // Edge cases - {"", "Empty"}, - {"a", "A"}, - {"A", "A"}, - {"ABC", "ABC"}, - {"myXMLParser", "MyXMLParser"}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - result := c.ToTypeName(tt.input) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestToPropertyName(t *testing.T) { - c := NewNameConverter(DefaultNameMangling(), NameSubstitutions{}) - - tests := []struct { - input string - expected string - }{ - {"user_id", "UserID"}, - {"created_at", "CreatedAt"}, - {"is_active", "IsActive"}, - {"123field", "N123Field"}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - result := c.ToPropertyName(tt.input) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestToVariableName(t *testing.T) { - c := NewNameConverter(DefaultNameMangling(), NameSubstitutions{}) - - tests := []struct { - input string - expected string - }{ - {"Foo", "foo"}, - {"FooBar", "fooBar"}, - {"user_id", "userID"}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - result := c.ToVariableName(tt.input) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestNameSubstitutions(t *testing.T) { - c := NewNameConverter(DefaultNameMangling(), NameSubstitutions{ - TypeNames: map[string]string{ - "foo": "MyCustomFoo", - }, - PropertyNames: map[string]string{ - "bar": "MyCustomBar", - }, - }) - - assert.Equal(t, "MyCustomFoo", c.ToTypeName("foo")) - assert.Equal(t, "MyCustomBar", c.ToPropertyName("bar")) - - // Non-substituted names still work normally - assert.Equal(t, "Baz", c.ToTypeName("baz")) -} - -func TestCustomNameMangling(t *testing.T) { - // Custom config that changes numeric prefix - mangling := DefaultNameMangling() - mangling.NumericPrefix = "Num" - - c := NewNameConverter(mangling, NameSubstitutions{}) - - assert.Equal(t, "Num123", c.ToTypeName("123")) - assert.Equal(t, "Num1Foo", c.ToTypeName("1foo")) -} - -func TestMergeNameMangling(t *testing.T) { - defaults := DefaultNameMangling() - - // User wants to change just the numeric prefix - user := NameMangling{ - NumericPrefix: "Number", - } - - merged := defaults.Merge(user) - - // User value overrides - assert.Equal(t, "Number", merged.NumericPrefix) - - // Defaults preserved - assert.Equal(t, defaults.KeywordPrefix, merged.KeywordPrefix) - assert.Equal(t, defaults.WordSeparators, merged.WordSeparators) - assert.Equal(t, len(defaults.CharacterSubstitutions), len(merged.CharacterSubstitutions)) -} - -func TestMergeCharacterSubstitutions(t *testing.T) { - defaults := DefaultNameMangling() - - // User wants to override $ and add a new one - user := NameMangling{ - CharacterSubstitutions: map[string]string{ - "$": "Dollar", // Override default "DollarSign" - "€": "Euro", // Add new - }, - } - - merged := defaults.Merge(user) - - assert.Equal(t, "Dollar", merged.CharacterSubstitutions["$"]) - assert.Equal(t, "Euro", merged.CharacterSubstitutions["€"]) - assert.Equal(t, "Minus", merged.CharacterSubstitutions["-"]) // Default preserved -} - -func TestRemoveCharacterSubstitution(t *testing.T) { - defaults := DefaultNameMangling() - - // User wants to remove $ substitution (empty string = remove) - user := NameMangling{ - CharacterSubstitutions: map[string]string{ - "$": "", // Remove - }, - } - - merged := defaults.Merge(user) - - _, exists := merged.CharacterSubstitutions["$"] - assert.False(t, exists) -} diff --git a/experimental/internal/codegen/operation.go b/experimental/internal/codegen/operation.go deleted file mode 100644 index 88cb4fe27a..0000000000 --- a/experimental/internal/codegen/operation.go +++ /dev/null @@ -1,287 +0,0 @@ -package codegen - -import ( - "strings" - - v3 "github.com/pb33f/libopenapi/datamodel/high/v3" -) - -// OperationSource indicates where an operation was defined in the spec. -type OperationSource string - -const ( - OperationSourcePath OperationSource = "path" - OperationSourceWebhook OperationSource = "webhook" - OperationSourceCallback OperationSource = "callback" -) - -// OperationDescriptor describes a single API operation from an OpenAPI spec. -type OperationDescriptor struct { - OperationID string // Normalized operation ID for function names - GoOperationID string // Go-safe identifier (handles leading digits, keywords) - Method string // HTTP method: GET, POST, PUT, DELETE, etc. - Path string // Original path: /users/{id} - Summary string // For generating comments - Description string // Longer description - - // Source indicates where this operation was defined (path, webhook, or callback) - Source OperationSource - WebhookName string // Webhook name (for Source=webhook) - CallbackName string // Callback key (for Source=callback) - ParentOpID string // Parent operation ID (for Source=callback) - - PathParams []*ParameterDescriptor - QueryParams []*ParameterDescriptor - HeaderParams []*ParameterDescriptor - CookieParams []*ParameterDescriptor - - Bodies []*RequestBodyDescriptor - Responses []*ResponseDescriptor - - Security []SecurityRequirement - - // Precomputed for templates - HasBody bool // Has at least one request body - HasParams bool // Has non-path params (needs Params struct) - ParamsTypeName string // "{OperationID}Params" - - // Reference to the underlying spec - Spec *v3.Operation -} - -// Params returns all non-path parameters (query, header, cookie). -// These are bundled into a Params struct. -func (o *OperationDescriptor) Params() []*ParameterDescriptor { - result := make([]*ParameterDescriptor, 0, len(o.QueryParams)+len(o.HeaderParams)+len(o.CookieParams)) - result = append(result, o.QueryParams...) - result = append(result, o.HeaderParams...) - result = append(result, o.CookieParams...) - return result -} - -// AllParams returns all parameters including path params. -func (o *OperationDescriptor) AllParams() []*ParameterDescriptor { - result := make([]*ParameterDescriptor, 0, len(o.PathParams)+len(o.QueryParams)+len(o.HeaderParams)+len(o.CookieParams)) - result = append(result, o.PathParams...) - result = append(result, o.QueryParams...) - result = append(result, o.HeaderParams...) - result = append(result, o.CookieParams...) - return result -} - -// SummaryAsComment returns the summary formatted as a Go comment. -func (o *OperationDescriptor) SummaryAsComment() string { - if o.Summary == "" { - return "" - } - trimmed := strings.TrimSuffix(o.Summary, "\n") - parts := strings.Split(trimmed, "\n") - for i, p := range parts { - parts[i] = "// " + p - } - return strings.Join(parts, "\n") -} - -// DefaultBody returns the default request body (typically application/json), or nil. -func (o *OperationDescriptor) DefaultBody() *RequestBodyDescriptor { - for _, b := range o.Bodies { - if b.IsDefault { - return b - } - } - if len(o.Bodies) > 0 { - return o.Bodies[0] - } - return nil -} - -// ParameterDescriptor describes a parameter in any location. -type ParameterDescriptor struct { - Name string // Original name from spec (e.g., "user_id") - GoName string // Go-safe name for struct fields (e.g., "UserId") - Location string // "path", "query", "header", "cookie" - Required bool - - // Serialization style - Style string // "simple", "form", "label", "matrix", etc. - Explode bool - - // Type information - Schema *SchemaDescriptor - TypeDecl string // Go type declaration (e.g., "string", "[]int", "*MyType") - - // Precomputed function names for templates - StyleFunc string // "StyleSimpleParam", "StyleFormExplodeParam", etc. - BindFunc string // "BindSimpleParam", "BindFormExplodeParam", etc. - - // Encoding modes - IsStyled bool // Uses style/explode serialization (most common) - IsPassThrough bool // No styling, just pass the string through - IsJSON bool // Parameter uses JSON content encoding - - Spec *v3.Parameter -} - -// GoVariableName returns a Go-safe variable name for this parameter. -// Used for local variables in generated code. -func (p *ParameterDescriptor) GoVariableName() string { - name := LowercaseFirstCharacter(p.GoName) - if IsGoKeyword(name) { - name = "p" + p.GoName - } - // Handle leading digits - if len(name) > 0 && name[0] >= '0' && name[0] <= '9' { - name = "n" + name - } - return name -} - -// HasOptionalPointer returns true if this parameter should be a pointer -// (optional parameters that aren't required). -func (p *ParameterDescriptor) HasOptionalPointer() bool { - if p.Required { - return false - } - // Check if schema has skip-optional-pointer extension - if p.Schema != nil && p.Schema.Extensions != nil && - p.Schema.Extensions.SkipOptionalPointer != nil && *p.Schema.Extensions.SkipOptionalPointer { - return false - } - return true -} - -// RequestBodyDescriptor describes a request body for a specific content type. -type RequestBodyDescriptor struct { - ContentType string // "application/json", "multipart/form-data", etc. - Required bool - Schema *SchemaDescriptor - - // Precomputed for templates - NameTag string // "JSON", "Formdata", "Multipart", "Text", etc. - GoTypeName string // "{OperationID}JSONBody", etc. - FuncSuffix string // "", "WithJSONBody", "WithFormBody" (empty for default) - IsDefault bool // Is this the default body type? - IsJSON bool // Is this a JSON content type? - - // Encoding options for form data - Encoding map[string]RequestBodyEncoding -} - -// RequestBodyEncoding describes encoding options for a form field. -type RequestBodyEncoding struct { - ContentType string - Style string - Explode *bool -} - -// ResponseDescriptor describes a response for a status code. -type ResponseDescriptor struct { - StatusCode string // "200", "404", "default", "2XX" - Description string - Contents []*ResponseContentDescriptor - Headers []*ResponseHeaderDescriptor - Ref string // If this is a reference to a named response -} - -// GoName returns a Go-safe name for this response (e.g., "200" -> "200", "default" -> "Default"). -func (r *ResponseDescriptor) GoName() string { - return ToCamelCase(r.StatusCode) -} - -// HasFixedStatusCode returns true if the status code is a specific number (not "default" or "2XX"). -func (r *ResponseDescriptor) HasFixedStatusCode() bool { - if r.StatusCode == "default" { - return false - } - // Check for wildcard patterns like "2XX" - if strings.HasSuffix(strings.ToUpper(r.StatusCode), "XX") { - return false - } - return true -} - -// ResponseContentDescriptor describes response content for a content type. -type ResponseContentDescriptor struct { - ContentType string - Schema *SchemaDescriptor - NameTag string // "JSON", "XML", etc. - IsJSON bool -} - -// ResponseHeaderDescriptor describes a response header. -type ResponseHeaderDescriptor struct { - Name string - GoName string - Required bool - Schema *SchemaDescriptor -} - -// SecurityRequirement describes a security requirement for an operation. -type SecurityRequirement struct { - Name string // Security scheme name - Scopes []string // Required scopes (for OAuth2) -} - -// Helper functions for computing descriptor fields - -// ComputeStyleFunc returns the style function name for a parameter. -func ComputeStyleFunc(style string, explode bool) string { - base := "Style" + ToCamelCase(style) - if explode { - return base + "ExplodeParam" - } - return base + "Param" -} - -// ComputeBindFunc returns the bind function name for a parameter. -func ComputeBindFunc(style string, explode bool) string { - base := "Bind" + ToCamelCase(style) - if explode { - return base + "ExplodeParam" - } - return base + "Param" -} - -// ComputeBodyNameTag returns the name tag for a content type. -func ComputeBodyNameTag(contentType string) string { - switch { - case contentType == "application/json": - return "JSON" - case IsMediaTypeJSON(contentType): - return MediaTypeToCamelCase(contentType) - case strings.HasPrefix(contentType, "multipart/"): - return "Multipart" - case contentType == "application/x-www-form-urlencoded": - return "Formdata" - case contentType == "text/plain": - return "Text" - case strings.HasPrefix(contentType, "application/xml") || strings.HasSuffix(contentType, "+xml"): - return "XML" - default: - return "" - } -} - -// IsMediaTypeJSON returns true if the content type is a JSON media type. -func IsMediaTypeJSON(contentType string) bool { - if contentType == "application/json" { - return true - } - if strings.HasSuffix(contentType, "+json") { - return true - } - if strings.Contains(contentType, "json") { - return true - } - return false -} - -// MediaTypeToCamelCase converts a media type to a CamelCase identifier. -func MediaTypeToCamelCase(mediaType string) string { - // application/vnd.api+json -> ApplicationVndApiJson - mediaType = strings.ReplaceAll(mediaType, "/", " ") - mediaType = strings.ReplaceAll(mediaType, "+", " ") - mediaType = strings.ReplaceAll(mediaType, ".", " ") - mediaType = strings.ReplaceAll(mediaType, "-", " ") - return ToCamelCase(mediaType) -} diff --git a/experimental/internal/codegen/output.go b/experimental/internal/codegen/output.go deleted file mode 100644 index 0793124223..0000000000 --- a/experimental/internal/codegen/output.go +++ /dev/null @@ -1,764 +0,0 @@ -package codegen - -import ( - "bytes" - "fmt" - "sort" - "strings" - - "golang.org/x/tools/imports" -) - -// Output collects generated Go code and formats it. -type Output struct { - packageName string - imports map[string]string // path -> alias - types []string // type definitions in order -} - -// NewOutput creates a new output collector. -func NewOutput(packageName string) *Output { - return &Output{ - packageName: packageName, - imports: make(map[string]string), - } -} - -// AddImport adds an import path with optional alias. -func (o *Output) AddImport(path, alias string) { - if path == "" { - return - } - o.imports[path] = alias -} - -// AddImports adds multiple imports from a map. -func (o *Output) AddImports(imports map[string]string) { - for path, alias := range imports { - o.AddImport(path, alias) - } -} - -// AddType adds a type definition to the output. -func (o *Output) AddType(code string) { - if code != "" { - o.types = append(o.types, code) - } -} - -// String generates the complete Go source file. -func (o *Output) String() string { - var buf bytes.Buffer - - // Generated code header (tells linters to skip this file) - buf.WriteString("// Code generated by oapi-codegen; DO NOT EDIT.\n\n") - - // Package declaration - fmt.Fprintf(&buf, "package %s\n\n", o.packageName) - - // Imports - if len(o.imports) > 0 { - buf.WriteString("import (\n") - paths := make([]string, 0, len(o.imports)) - for path := range o.imports { - paths = append(paths, path) - } - sort.Strings(paths) - - for _, path := range paths { - alias := o.imports[path] - if alias != "" { - fmt.Fprintf(&buf, "\t%s %q\n", alias, path) - } else { - fmt.Fprintf(&buf, "\t%q\n", path) - } - } - buf.WriteString(")\n\n") - } - - // Types - for _, t := range o.types { - buf.WriteString(t) - buf.WriteString("\n\n") - } - - return buf.String() -} - -// Format returns the formatted Go source code with imports organized. -func (o *Output) Format() (string, error) { - src := o.String() - formatted, err := imports.Process("", []byte(src), nil) - if err != nil { - return src, fmt.Errorf("formatting output: %w (source:\n%s)", err, src) - } - return string(formatted), nil -} - -// CodeBuilder helps construct Go code fragments. -type CodeBuilder struct { - buf bytes.Buffer - indent int -} - -// NewCodeBuilder creates a new code builder. -func NewCodeBuilder() *CodeBuilder { - return &CodeBuilder{} -} - -// Indent increases indentation. -func (b *CodeBuilder) Indent() { - b.indent++ -} - -// Dedent decreases indentation. -func (b *CodeBuilder) Dedent() { - if b.indent > 0 { - b.indent-- - } -} - -// Line writes a line with current indentation. -func (b *CodeBuilder) Line(format string, args ...any) { - for i := 0; i < b.indent; i++ { - b.buf.WriteByte('\t') - } - if len(args) > 0 { - fmt.Fprintf(&b.buf, format, args...) - } else { - b.buf.WriteString(format) - } - b.buf.WriteByte('\n') -} - -// BlankLine writes an empty line. -func (b *CodeBuilder) BlankLine() { - b.buf.WriteByte('\n') -} - -// Raw writes raw text without indentation or newline. -func (b *CodeBuilder) Raw(s string) { - b.buf.WriteString(s) -} - -// String returns the built code. -func (b *CodeBuilder) String() string { - return b.buf.String() -} - -// GenerateStruct generates a struct type definition. -func GenerateStruct(name string, fields []StructField, doc string, tagGen *StructTagGenerator) string { - b := NewCodeBuilder() - - // Type documentation - if doc != "" { - for _, line := range strings.Split(doc, "\n") { - b.Line("// %s", line) - } - } - - b.Line("type %s struct {", name) - b.Indent() - - for _, f := range fields { - tag := generateFieldTag(f, tagGen) - if f.Doc != "" { - // Single line comment for field - b.Line("%s %s %s // %s", f.Name, f.Type, tag, f.Doc) - } else { - b.Line("%s %s %s", f.Name, f.Type, tag) - } - } - - b.Dedent() - b.Line("}") - - return b.String() -} - -// generateFieldTag generates the struct tag for a field. -func generateFieldTag(f StructField, tagGen *StructTagGenerator) string { - if tagGen == nil { - if f.JSONIgnore { - return "`json:\"-\"`" - } - return FormatJSONTag(f.JSONName, f.OmitEmpty) - } - info := StructTagInfo{ - FieldName: f.JSONName, - GoFieldName: f.Name, - IsOptional: !f.Required, - IsNullable: f.Nullable, - IsPointer: f.Pointer, - OmitEmpty: f.OmitEmpty, - OmitZero: f.OmitZero, - JSONIgnore: f.JSONIgnore, - } - return tagGen.GenerateTags(info) -} - -// GenerateStructWithAdditionalProps generates a struct with AdditionalProperties field -// and custom marshal/unmarshal methods. -func GenerateStructWithAdditionalProps(name string, fields []StructField, addPropsType string, doc string, tagGen *StructTagGenerator) string { - b := NewCodeBuilder() - - // Type documentation - if doc != "" { - for _, line := range strings.Split(doc, "\n") { - b.Line("// %s", line) - } - } - - b.Line("type %s struct {", name) - b.Indent() - - // Regular fields - for _, f := range fields { - tag := generateFieldTag(f, tagGen) - b.Line("%s %s %s", f.Name, f.Type, tag) - } - - // AdditionalProperties field - b.Line("AdditionalProperties map[string]%s `json:\"-\"`", addPropsType) - - b.Dedent() - b.Line("}") - - return b.String() -} - -// GenerateTypeAlias generates a type alias definition. -func GenerateTypeAlias(name, targetType, doc string) string { - b := NewCodeBuilder() - - if doc != "" { - for _, line := range strings.Split(doc, "\n") { - b.Line("// %s", line) - } - } - - b.Line("type %s = %s", name, targetType) - - return b.String() -} - -// GenerateEnum generates an enum type with const values. -// If customNames is provided and has the same length as values, those names will be used -// as the constant names instead of auto-generated ones. -func GenerateEnum(name, baseType string, values []string, customNames []string, doc string) string { - return GenerateEnumWithConstPrefix(name, name, baseType, values, customNames, doc) -} - -// GenerateEnumWithConstPrefix generates an enum type with const values. -// typeName is used for the type definition, constPrefix is used for constant names. -// This allows the type to be defined with a stable name while constants use a friendly name. -func GenerateEnumWithConstPrefix(typeName, constPrefix, baseType string, values []string, customNames []string, doc string) string { - b := NewCodeBuilder() - - if doc != "" { - for _, line := range strings.Split(doc, "\n") { - b.Line("// %s", line) - } - } - - b.Line("type %s %s", typeName, baseType) - b.BlankLine() - - if len(values) > 0 { - b.Line("const (") - b.Indent() - - // Track used names to handle duplicates - usedNames := make(map[string]int) - - for i, v := range values { - var constName string - - // Use custom name if provided, otherwise auto-generate - if len(customNames) > i && customNames[i] != "" { - constName = constPrefix + "_" + customNames[i] - } else { - constSuffix := sanitizeEnumValue(v, baseType) - constName = constPrefix + "_" + constSuffix - } - - // Handle duplicate names by adding a numeric suffix - if count, exists := usedNames[constName]; exists { - constName = fmt.Sprintf("%s_%d", constName, count) - usedNames[constName] = count + 1 - } else { - usedNames[constName] = 1 - } - - if baseType == "string" { - b.Line("%s %s = %q", constName, typeName, v) - } else { - b.Line("%s %s = %s", constName, typeName, v) - } - } - - b.Dedent() - b.Line(")") - } - - return b.String() -} - -// sanitizeEnumValue converts an enum value to a valid Go identifier suffix. -func sanitizeEnumValue(v string, baseType string) string { - // For integer enums, prefix with N for readability - if baseType == "int" || baseType == "int32" || baseType == "int64" { - // Check if it's a numeric value - if len(v) > 0 { - firstChar := v[0] - if firstChar >= '0' && firstChar <= '9' || firstChar == '-' { - // Negative numbers - if firstChar == '-' { - return "Minus" + v[1:] - } - return "N" + v - } - } - } - - // Replace common special characters - v = strings.ReplaceAll(v, "-", "_") - v = strings.ReplaceAll(v, " ", "_") - v = strings.ReplaceAll(v, ".", "_") - - // Remove any remaining invalid characters - var result strings.Builder - for i, r := range v { - if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r == '_' { - result.WriteRune(r) - } else if r >= '0' && r <= '9' && i > 0 { - result.WriteRune(r) - } - } - - if result.Len() == 0 { - return "Value" - } - return result.String() -} - -// GenerateUnionType generates a union struct for anyOf/oneOf with marshal/unmarshal. -func GenerateUnionType(name string, members []UnionMember, isOneOf bool, doc string) string { - b := NewCodeBuilder() - - if doc != "" { - for _, line := range strings.Split(doc, "\n") { - b.Line("// %s", line) - } - } - - // Generate struct with pointer field for each member - b.Line("type %s struct {", name) - b.Indent() - - for _, m := range members { - b.Line("%s *%s", m.FieldName, m.TypeName) - } - - b.Dedent() - b.Line("}") - - return b.String() -} - -// UnionMember represents a member of a union type (anyOf/oneOf). -type UnionMember struct { - FieldName string // Go field name - TypeName string // Go type name - Index int // Position in anyOf/oneOf array - HasApplyDefaults bool // Whether this type has an ApplyDefaults method -} - -// isPrimitiveType returns true if the type is a Go primitive or collection -// that doesn't have an ApplyDefaults method. -func isPrimitiveType(typeName string) bool { - switch typeName { - case "string", "int", "int8", "int16", "int32", "int64", - "uint", "uint8", "uint16", "uint32", "uint64", - "float32", "float64", "bool", "any": - return true - default: - // Slices and maps don't have ApplyDefaults - if strings.HasPrefix(typeName, "[]") || strings.HasPrefix(typeName, "map[") { - return true - } - return false - } -} - -// GenerateUnionApplyDefaults generates ApplyDefaults for a union type. -// It recurses into non-nil members that have ApplyDefaults. -func GenerateUnionApplyDefaults(name string, members []UnionMember) string { - b := NewCodeBuilder() - - b.Line("// ApplyDefaults sets default values for fields that are nil.") - b.Line("func (u *%s) ApplyDefaults() {", name) - b.Indent() - - for _, m := range members { - // Only recurse into types that have ApplyDefaults - if m.HasApplyDefaults { - b.Line("if u.%s != nil {", m.FieldName) - b.Indent() - b.Line("u.%s.ApplyDefaults()", m.FieldName) - b.Dedent() - b.Line("}") - } - } - - b.Dedent() - b.Line("}") - - return b.String() -} - -// GenerateUnionMarshalOneOf generates MarshalJSON for a oneOf type. -func GenerateUnionMarshalOneOf(name string, members []UnionMember) string { - b := NewCodeBuilder() - - b.Line("func (u %s) MarshalJSON() ([]byte, error) {", name) - b.Indent() - - b.Line("var count int") - b.Line("var data []byte") - b.Line("var err error") - b.BlankLine() - - for _, m := range members { - b.Line("if u.%s != nil {", m.FieldName) - b.Indent() - b.Line("count++") - b.Line("data, err = json.Marshal(u.%s)", m.FieldName) - b.Line("if err != nil {") - b.Indent() - b.Line("return nil, err") - b.Dedent() - b.Line("}") - b.Dedent() - b.Line("}") - } - - b.BlankLine() - b.Line("if count != 1 {") - b.Indent() - b.Line("return nil, fmt.Errorf(\"%s: exactly one member must be set, got %%d\", count)", name) - b.Dedent() - b.Line("}") - b.BlankLine() - b.Line("return data, nil") - - b.Dedent() - b.Line("}") - - return b.String() -} - -// GenerateUnionUnmarshalOneOf generates UnmarshalJSON for a oneOf type. -func GenerateUnionUnmarshalOneOf(name string, members []UnionMember) string { - b := NewCodeBuilder() - - b.Line("func (u *%s) UnmarshalJSON(data []byte) error {", name) - b.Indent() - - b.Line("var successCount int") - b.BlankLine() - - for _, m := range members { - b.Line("var v%d %s", m.Index, m.TypeName) - b.Line("if err := json.Unmarshal(data, &v%d); err == nil {", m.Index) - b.Indent() - b.Line("u.%s = &v%d", m.FieldName, m.Index) - b.Line("successCount++") - b.Dedent() - b.Line("}") - b.BlankLine() - } - - b.Line("if successCount != 1 {") - b.Indent() - b.Line("return fmt.Errorf(\"%s: expected exactly one type to match, got %%d\", successCount)", name) - b.Dedent() - b.Line("}") - b.BlankLine() - b.Line("return nil") - - b.Dedent() - b.Line("}") - - return b.String() -} - -// GenerateUnionMarshalAnyOf generates MarshalJSON for an anyOf type. -func GenerateUnionMarshalAnyOf(name string, members []UnionMember) string { - b := NewCodeBuilder() - - b.Line("func (u %s) MarshalJSON() ([]byte, error) {", name) - b.Indent() - - // Check if any members are objects (need field merging) - hasObjects := false - for _, m := range members { - if !isPrimitiveType(m.TypeName) { - hasObjects = true - break - } - } - - if hasObjects { - // Merge object fields - b.Line("result := make(map[string]any)") - b.BlankLine() - - for _, m := range members { - b.Line("if u.%s != nil {", m.FieldName) - b.Indent() - if isPrimitiveType(m.TypeName) { - // For primitives, we can't merge - just return the value - b.Line("return json.Marshal(u.%s)", m.FieldName) - } else { - b.Line("data, err := json.Marshal(u.%s)", m.FieldName) - b.Line("if err != nil {") - b.Indent() - b.Line("return nil, err") - b.Dedent() - b.Line("}") - b.Line("var m map[string]any") - b.Line("if err := json.Unmarshal(data, &m); err == nil {") - b.Indent() - b.Line("for k, v := range m {") - b.Indent() - b.Line("result[k] = v") - b.Dedent() - b.Line("}") - b.Dedent() - b.Line("}") - } - b.Dedent() - b.Line("}") - } - - b.BlankLine() - b.Line("return json.Marshal(result)") - } else { - // All primitives - marshal the first non-nil one - for _, m := range members { - b.Line("if u.%s != nil {", m.FieldName) - b.Indent() - b.Line("return json.Marshal(u.%s)", m.FieldName) - b.Dedent() - b.Line("}") - } - b.Line("return []byte(\"null\"), nil") - } - - b.Dedent() - b.Line("}") - - return b.String() -} - -// GenerateUnionUnmarshalAnyOf generates UnmarshalJSON for an anyOf type. -func GenerateUnionUnmarshalAnyOf(name string, members []UnionMember) string { - b := NewCodeBuilder() - - b.Line("func (u *%s) UnmarshalJSON(data []byte) error {", name) - b.Indent() - - for _, m := range members { - b.Line("var v%d %s", m.Index, m.TypeName) - b.Line("if err := json.Unmarshal(data, &v%d); err == nil {", m.Index) - b.Indent() - b.Line("u.%s = &v%d", m.FieldName, m.Index) - b.Dedent() - b.Line("}") - b.BlankLine() - } - - b.Line("return nil") - - b.Dedent() - b.Line("}") - - return b.String() -} - -// GenerateMixedPropertiesMarshal generates MarshalJSON for structs with additionalProperties. -func GenerateMixedPropertiesMarshal(name string, fields []StructField) string { - b := NewCodeBuilder() - - b.Line("func (s %s) MarshalJSON() ([]byte, error) {", name) - b.Indent() - - b.Line("result := make(map[string]any)") - b.BlankLine() - - // Copy known fields - for _, f := range fields { - if f.Pointer { - b.Line("if s.%s != nil {", f.Name) - b.Indent() - b.Line("result[%q] = s.%s", f.JSONName, f.Name) - b.Dedent() - b.Line("}") - } else { - b.Line("result[%q] = s.%s", f.JSONName, f.Name) - } - } - - b.BlankLine() - b.Line("// Add additional properties") - b.Line("for k, v := range s.AdditionalProperties {") - b.Indent() - b.Line("result[k] = v") - b.Dedent() - b.Line("}") - b.BlankLine() - b.Line("return json.Marshal(result)") - - b.Dedent() - b.Line("}") - - return b.String() -} - -// GenerateApplyDefaults generates an ApplyDefaults method for a struct. -// It sets default values for fields that are nil and have defaults defined, -// and recursively calls ApplyDefaults on nested struct fields. -// Always generates the method (even if empty) so it can be called uniformly. -func GenerateApplyDefaults(name string, fields []StructField) string { - b := NewCodeBuilder() - - b.Line("// ApplyDefaults sets default values for fields that are nil.") - b.Line("func (s *%s) ApplyDefaults() {", name) - b.Indent() - - for _, f := range fields { - // Apply defaults to nil pointer fields - if f.Default != "" && f.Pointer { - b.Line("if s.%s == nil {", f.Name) - b.Indent() - // Get the base type (without *) - baseType := strings.TrimPrefix(f.Type, "*") - // Check if we need an explicit type conversion for numeric types - // This is needed because Go infers float64 for floating point literals - // and int for integer literals, which may not match the target type - if needsTypeConversion(baseType) { - b.Line("v := %s(%s)", baseType, f.Default) - } else { - b.Line("v := %s", f.Default) - } - b.Line("s.%s = &v", f.Name) - b.Dedent() - b.Line("}") - } - - // Recursively apply defaults to struct fields - if f.IsStruct && f.Pointer { - b.Line("if s.%s != nil {", f.Name) - b.Indent() - b.Line("s.%s.ApplyDefaults()", f.Name) - b.Dedent() - b.Line("}") - } - } - - b.Dedent() - b.Line("}") - - return b.String() -} - -// needsTypeConversion returns true if a numeric type needs an explicit conversion -// from the default Go literal type (int for integers, float64 for floats). -func needsTypeConversion(goType string) bool { - switch goType { - case "int8", "int16", "int32", "int64", - "uint", "uint8", "uint16", "uint32", "uint64", - "float32": - return true - default: - return false - } -} - -// GenerateMixedPropertiesUnmarshal generates UnmarshalJSON for structs with additionalProperties. -func GenerateMixedPropertiesUnmarshal(name string, fields []StructField, addPropsType string) string { - b := NewCodeBuilder() - - b.Line("func (s *%s) UnmarshalJSON(data []byte) error {", name) - b.Indent() - - // Build set of known field names - b.Line("// Known fields") - b.Line("knownFields := map[string]bool{") - b.Indent() - for _, f := range fields { - b.Line("%q: true,", f.JSONName) - } - b.Dedent() - b.Line("}") - b.BlankLine() - - // Unmarshal into a map first - b.Line("var raw map[string]json.RawMessage") - b.Line("if err := json.Unmarshal(data, &raw); err != nil {") - b.Indent() - b.Line("return err") - b.Dedent() - b.Line("}") - b.BlankLine() - - // Unmarshal known fields - for _, f := range fields { - b.Line("if v, ok := raw[%q]; ok {", f.JSONName) - b.Indent() - if f.Pointer { - b.Line("var val %s", strings.TrimPrefix(f.Type, "*")) - b.Line("if err := json.Unmarshal(v, &val); err != nil {") - b.Indent() - b.Line("return err") - b.Dedent() - b.Line("}") - b.Line("s.%s = &val", f.Name) - } else { - b.Line("if err := json.Unmarshal(v, &s.%s); err != nil {", f.Name) - b.Indent() - b.Line("return err") - b.Dedent() - b.Line("}") - } - b.Dedent() - b.Line("}") - } - - b.BlankLine() - b.Line("// Collect additional properties") - b.Line("s.AdditionalProperties = make(map[string]%s)", addPropsType) - b.Line("for k, v := range raw {") - b.Indent() - b.Line("if !knownFields[k] {") - b.Indent() - b.Line("var val %s", addPropsType) - b.Line("if err := json.Unmarshal(v, &val); err != nil {") - b.Indent() - b.Line("return err") - b.Dedent() - b.Line("}") - b.Line("s.AdditionalProperties[k] = val") - b.Dedent() - b.Line("}") - b.Dedent() - b.Line("}") - b.BlankLine() - b.Line("return nil") - - b.Dedent() - b.Line("}") - - return b.String() -} diff --git a/experimental/internal/codegen/paramgen.go b/experimental/internal/codegen/paramgen.go deleted file mode 100644 index 19221ac0c0..0000000000 --- a/experimental/internal/codegen/paramgen.go +++ /dev/null @@ -1,178 +0,0 @@ -package codegen - -import ( - "fmt" - "sort" - - "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/templates" -) - -// ParamUsageTracker tracks which parameter styling and binding functions -// are needed based on the OpenAPI spec being processed. -type ParamUsageTracker struct { - // usedStyles tracks which style/explode combinations are used. - // Keys are formatted as "style_{style}" or "style_{style}_explode" for serialization, - // and "bind_{style}" or "bind_{style}_explode" for binding. - usedStyles map[string]bool -} - -// NewParamUsageTracker creates a new ParamUsageTracker. -func NewParamUsageTracker() *ParamUsageTracker { - return &ParamUsageTracker{ - usedStyles: make(map[string]bool), - } -} - -// RecordStyleParam records that a style/explode combination is used for serialization. -// This is typically called when processing client parameters. -func (t *ParamUsageTracker) RecordStyleParam(style string, explode bool) { - key := templates.ParamStyleKey("style_", style, explode) - t.usedStyles[key] = true -} - -// RecordBindParam records that a style/explode combination is used for binding. -// This is typically called when processing server parameters. -func (t *ParamUsageTracker) RecordBindParam(style string, explode bool) { - key := templates.ParamStyleKey("bind_", style, explode) - t.usedStyles[key] = true -} - -// RecordParam records both style and bind usage for a parameter. -// Use this when generating both client and server code. -func (t *ParamUsageTracker) RecordParam(style string, explode bool) { - t.RecordStyleParam(style, explode) - t.RecordBindParam(style, explode) -} - -// HasAnyUsage returns true if any parameter functions are needed. -func (t *ParamUsageTracker) HasAnyUsage() bool { - return len(t.usedStyles) > 0 -} - -// GetRequiredTemplates returns the list of templates needed based on usage. -// The helpers template is always included first if any functions are needed. -func (t *ParamUsageTracker) GetRequiredTemplates() []templates.ParamTemplate { - if !t.HasAnyUsage() { - return nil - } - - var result []templates.ParamTemplate - - // Always include helpers first - result = append(result, templates.ParamHelpersTemplate) - - // Get all used style keys and sort them for deterministic output - keys := make([]string, 0, len(t.usedStyles)) - for key := range t.usedStyles { - keys = append(keys, key) - } - sort.Strings(keys) - - // Add each required template - for _, key := range keys { - tmpl, ok := templates.ParamTemplates[key] - if !ok { - // This shouldn't happen if keys are properly validated - continue - } - result = append(result, tmpl) - } - - return result -} - -// GetRequiredImports returns all imports needed for the used parameter functions. -// This aggregates imports from the helpers template and all used templates. -func (t *ParamUsageTracker) GetRequiredImports() []templates.Import { - if !t.HasAnyUsage() { - return nil - } - - // Use a map to deduplicate imports - importSet := make(map[string]templates.Import) - - // Add helpers imports - for _, imp := range templates.ParamHelpersTemplate.Imports { - importSet[imp.Path] = imp - } - - // Add imports from each used template - for key := range t.usedStyles { - tmpl, ok := templates.ParamTemplates[key] - if !ok { - continue - } - for _, imp := range tmpl.Imports { - importSet[imp.Path] = imp - } - } - - // Convert to sorted slice - result := make([]templates.Import, 0, len(importSet)) - for _, imp := range importSet { - result = append(result, imp) - } - sort.Slice(result, func(i, j int) bool { - return result[i].Path < result[j].Path - }) - - return result -} - -// GetUsedStyleKeys returns the sorted list of used style keys for debugging. -func (t *ParamUsageTracker) GetUsedStyleKeys() []string { - keys := make([]string, 0, len(t.usedStyles)) - for key := range t.usedStyles { - keys = append(keys, key) - } - sort.Strings(keys) - return keys -} - -// DefaultParamStyle returns the default style for a parameter location. -func DefaultParamStyle(location string) string { - switch location { - case "path", "header": - return "simple" - case "query", "cookie": - return "form" - default: - return "form" - } -} - -// DefaultParamExplode returns the default explode value for a parameter location. -func DefaultParamExplode(location string) bool { - switch location { - case "path", "header": - return false - case "query", "cookie": - return true - default: - return false - } -} - -// ValidateParamStyle validates that a style is supported for a location. -// Returns an error if the combination is invalid. -func ValidateParamStyle(style, location string) error { - validStyles := map[string][]string{ - "path": {"simple", "label", "matrix"}, - "query": {"form", "spaceDelimited", "pipeDelimited", "deepObject"}, - "header": {"simple"}, - "cookie": {"form"}, - } - - allowed, ok := validStyles[location] - if !ok { - return fmt.Errorf("unknown parameter location: %s", location) - } - - for _, s := range allowed { - if s == style { - return nil - } - } - - return fmt.Errorf("style '%s' is not valid for location '%s'; valid styles are: %v", style, location, allowed) -} diff --git a/experimental/internal/codegen/paramgen_test.go b/experimental/internal/codegen/paramgen_test.go deleted file mode 100644 index a80de1e40d..0000000000 --- a/experimental/internal/codegen/paramgen_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package codegen - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestParamUsageTracker(t *testing.T) { - t.Run("empty tracker has no usage", func(t *testing.T) { - tracker := NewParamUsageTracker() - assert.False(t, tracker.HasAnyUsage()) - assert.Empty(t, tracker.GetRequiredTemplates()) - assert.Empty(t, tracker.GetRequiredImports()) - }) - - t.Run("records style param", func(t *testing.T) { - tracker := NewParamUsageTracker() - tracker.RecordStyleParam("simple", false) - - assert.True(t, tracker.HasAnyUsage()) - keys := tracker.GetUsedStyleKeys() - assert.Contains(t, keys, "style_simple") - }) - - t.Run("records style param with explode", func(t *testing.T) { - tracker := NewParamUsageTracker() - tracker.RecordStyleParam("form", true) - - keys := tracker.GetUsedStyleKeys() - assert.Contains(t, keys, "style_form_explode") - }) - - t.Run("records bind param", func(t *testing.T) { - tracker := NewParamUsageTracker() - tracker.RecordBindParam("label", false) - - keys := tracker.GetUsedStyleKeys() - assert.Contains(t, keys, "bind_label") - }) - - t.Run("records both style and bind", func(t *testing.T) { - tracker := NewParamUsageTracker() - tracker.RecordParam("matrix", true) - - keys := tracker.GetUsedStyleKeys() - assert.Contains(t, keys, "style_matrix_explode") - assert.Contains(t, keys, "bind_matrix_explode") - }) - - t.Run("returns helpers template first", func(t *testing.T) { - tracker := NewParamUsageTracker() - tracker.RecordStyleParam("simple", false) - - templates := tracker.GetRequiredTemplates() - require.NotEmpty(t, templates) - assert.Equal(t, "helpers", templates[0].Name) - }) - - t.Run("aggregates imports", func(t *testing.T) { - tracker := NewParamUsageTracker() - tracker.RecordStyleParam("simple", false) - tracker.RecordStyleParam("form", true) - - imports := tracker.GetRequiredImports() - assert.NotEmpty(t, imports) - - // Check that common imports are included - paths := make([]string, len(imports)) - for i, imp := range imports { - paths[i] = imp.Path - } - assert.Contains(t, paths, "reflect") - assert.Contains(t, paths, "strings") - }) -} - -func TestDefaultParamStyle(t *testing.T) { - tests := []struct { - location string - expected string - }{ - {"path", "simple"}, - {"header", "simple"}, - {"query", "form"}, - {"cookie", "form"}, - {"unknown", "form"}, - } - - for _, tc := range tests { - t.Run(tc.location, func(t *testing.T) { - assert.Equal(t, tc.expected, DefaultParamStyle(tc.location)) - }) - } -} - -func TestDefaultParamExplode(t *testing.T) { - tests := []struct { - location string - expected bool - }{ - {"path", false}, - {"header", false}, - {"query", true}, - {"cookie", true}, - {"unknown", false}, - } - - for _, tc := range tests { - t.Run(tc.location, func(t *testing.T) { - assert.Equal(t, tc.expected, DefaultParamExplode(tc.location)) - }) - } -} - -func TestValidateParamStyle(t *testing.T) { - validCases := []struct { - style string - location string - }{ - {"simple", "path"}, - {"label", "path"}, - {"matrix", "path"}, - {"form", "query"}, - {"spaceDelimited", "query"}, - {"pipeDelimited", "query"}, - {"deepObject", "query"}, - {"simple", "header"}, - {"form", "cookie"}, - } - - for _, tc := range validCases { - t.Run(tc.style+"_in_"+tc.location, func(t *testing.T) { - err := ValidateParamStyle(tc.style, tc.location) - assert.NoError(t, err) - }) - } - - invalidCases := []struct { - style string - location string - }{ - {"deepObject", "path"}, - {"matrix", "query"}, - {"label", "header"}, - {"simple", "cookie"}, - } - - for _, tc := range invalidCases { - t.Run(tc.style+"_in_"+tc.location+"_invalid", func(t *testing.T) { - err := ValidateParamStyle(tc.style, tc.location) - assert.Error(t, err) - }) - } - - t.Run("unknown location", func(t *testing.T) { - err := ValidateParamStyle("simple", "body") - assert.Error(t, err) - }) -} diff --git a/experimental/internal/codegen/receivergen.go b/experimental/internal/codegen/receivergen.go deleted file mode 100644 index e57fef3929..0000000000 --- a/experimental/internal/codegen/receivergen.go +++ /dev/null @@ -1,131 +0,0 @@ -package codegen - -import ( - "bytes" - "fmt" - "strings" - "text/template" - - "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/templates" -) - -// ReceiverTemplateData is passed to receiver templates. -type ReceiverTemplateData struct { - Prefix string // "Webhook" or "Callback" - PrefixLower string // "webhook" or "callback" - Operations []*OperationDescriptor // Operations to generate for -} - -// ReceiverGenerator generates receiver code from operation descriptors. -// It is parameterized by prefix to support both webhooks and callbacks. -type ReceiverGenerator struct { - tmpl *template.Template - prefix string // "Webhook" or "Callback" - serverType string -} - -// NewReceiverGenerator creates a new receiver generator for the specified server type. -func NewReceiverGenerator(prefix string, serverType string) (*ReceiverGenerator, error) { - if serverType == "" { - return nil, fmt.Errorf("%s receiver requires a server type to be set", prefix) - } - - tmpl := template.New("receiver").Funcs(templates.Funcs()) - - // Get receiver templates for the specified server type - receiverTemplates, err := getReceiverTemplates(serverType) - if err != nil { - return nil, err - } - - // Parse receiver-specific templates - for _, ct := range receiverTemplates { - content, err := templates.TemplateFS.ReadFile("files/" + ct.Template) - if err != nil { - return nil, fmt.Errorf("failed to read receiver template %s: %w", ct.Template, err) - } - _, err = tmpl.New(ct.Name).Parse(string(content)) - if err != nil { - return nil, fmt.Errorf("failed to parse receiver template %s: %w", ct.Template, err) - } - } - - // Parse shared templates (errors, param_types) - for _, st := range templates.SharedServerTemplates { - content, err := templates.TemplateFS.ReadFile("files/" + st.Template) - if err != nil { - return nil, fmt.Errorf("failed to read shared template %s: %w", st.Template, err) - } - _, err = tmpl.New(st.Name).Parse(string(content)) - if err != nil { - return nil, fmt.Errorf("failed to parse shared template %s: %w", st.Template, err) - } - } - - return &ReceiverGenerator{ - tmpl: tmpl, - prefix: prefix, - serverType: serverType, - }, nil -} - -// getReceiverTemplates returns the receiver templates for the specified server type. -func getReceiverTemplates(serverType string) (map[string]templates.ReceiverTemplate, error) { - switch serverType { - case ServerTypeStdHTTP: - return templates.StdHTTPReceiverTemplates, nil - case ServerTypeChi: - return templates.ChiReceiverTemplates, nil - case ServerTypeEcho: - return templates.EchoReceiverTemplates, nil - case ServerTypeEchoV4: - return templates.EchoV4ReceiverTemplates, nil - case ServerTypeGin: - return templates.GinReceiverTemplates, nil - case ServerTypeGorilla: - return templates.GorillaReceiverTemplates, nil - case ServerTypeFiber: - return templates.FiberReceiverTemplates, nil - case ServerTypeIris: - return templates.IrisReceiverTemplates, nil - default: - return nil, fmt.Errorf("unsupported server type for receiver: %q", serverType) - } -} - -func (g *ReceiverGenerator) templateData(ops []*OperationDescriptor) ReceiverTemplateData { - return ReceiverTemplateData{ - Prefix: g.prefix, - PrefixLower: strings.ToLower(g.prefix), - Operations: ops, - } -} - -// GenerateReceiver generates the receiver interface and handler functions. -func (g *ReceiverGenerator) GenerateReceiver(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - - if err := g.tmpl.ExecuteTemplate(&buf, "receiver", g.templateData(ops)); err != nil { - return "", fmt.Errorf("generating receiver code: %w", err) - } - - return buf.String(), nil -} - -// GenerateParamTypes generates the parameter struct types. -func (g *ReceiverGenerator) GenerateParamTypes(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "param_types", ops); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateErrors generates error types (shared with server). -func (g *ReceiverGenerator) GenerateErrors() (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "errors", nil); err != nil { - return "", err - } - return buf.String(), nil -} diff --git a/experimental/internal/codegen/schema.go b/experimental/internal/codegen/schema.go deleted file mode 100644 index bb0770bb65..0000000000 --- a/experimental/internal/codegen/schema.go +++ /dev/null @@ -1,117 +0,0 @@ -package codegen - -import ( - "strings" - - "github.com/pb33f/libopenapi/datamodel/high/base" -) - -// SchemaPath represents the location of a schema in the OpenAPI document. -// Used for deriving type names and disambiguating collisions. -// Example: ["components", "schemas", "Pet", "properties", "address"] -type SchemaPath []string - -// String returns the path as a JSON pointer-style string. -func (p SchemaPath) String() string { - return "#/" + strings.Join(p, "/") -} - -// Append returns a new SchemaPath with the given elements appended. -// This creates a fresh slice to avoid aliasing issues with append. -func (p SchemaPath) Append(elements ...string) SchemaPath { - result := make(SchemaPath, len(p)+len(elements)) - copy(result, p) - copy(result[len(p):], elements) - return result -} - -// ContainsProperties returns true if this path contains "properties" anywhere. -// This indicates it's an inline property schema rather than a component schema. -func (p SchemaPath) ContainsProperties() bool { - for _, element := range p { - if element == "properties" { - return true - } - } - return false -} - -// SchemaDescriptor represents a schema found during the first pass through the spec. -type SchemaDescriptor struct { - // Path is where this schema appears in the document - Path SchemaPath - - // Ref is the $ref string if this is a reference (e.g., "#/components/schemas/Pet") - // Empty if this is an inline schema definition - Ref string - - // Schema is the underlying schema from libopenapi - // nil for unresolved external references - Schema *base.Schema - - // Parent points to the containing schema (nil for top-level schemas) - Parent *SchemaDescriptor - - // StableName is the deterministic Go type name derived from the full path. - // This name is stable across spec changes and should be used for type definitions. - // Example: #/components/schemas/Cat -> CatSchemaComponent - StableName string - - // ShortName is a friendly alias that may change due to deduplication. - // Generated as a type alias pointing to StableName. - ShortName string - - // OperationID is the operationId from the path operation, if this schema - // comes from a path's request body or response. Used for friendlier naming. - OperationID string - - // ContentType is the media type (e.g., "application/json") if this schema - // comes from a request body or response content. Used for naming. - ContentType string - - // Extensions holds parsed x- extension values for this schema. - // These control code generation behavior (type overrides, field names, etc.) - Extensions *Extensions - - // Recursive structure: - Properties map[string]*SchemaDescriptor - Items *SchemaDescriptor - AllOf []*SchemaDescriptor - AnyOf []*SchemaDescriptor - OneOf []*SchemaDescriptor - AdditionalProps *SchemaDescriptor -} - -// IsReference returns true if this schema is a $ref to another schema -func (d *SchemaDescriptor) IsReference() bool { - return d.Ref != "" -} - -// IsExternalReference returns true if this is a reference to an external file. -// External refs have the format: file.yaml#/path/to/schema -func (d *SchemaDescriptor) IsExternalReference() bool { - if d.Ref == "" { - return false - } - // External refs contain # but don't start with it - return !strings.HasPrefix(d.Ref, "#") && strings.Contains(d.Ref, "#") -} - -// ParseExternalRef splits an external reference into its file path and internal path. -// For "common/api.yaml#/components/schemas/Pet", returns ("common/api.yaml", "#/components/schemas/Pet"). -// Returns empty strings if not an external ref. -func (d *SchemaDescriptor) ParseExternalRef() (filePath, internalPath string) { - if !d.IsExternalReference() { - return "", "" - } - parts := strings.SplitN(d.Ref, "#", 2) - if len(parts) != 2 { - return "", "" - } - return parts[0], "#" + parts[1] -} - -// IsComponentSchema returns true if this schema is defined in #/components/schemas -func (d *SchemaDescriptor) IsComponentSchema() bool { - return len(d.Path) >= 2 && d.Path[0] == "components" && d.Path[1] == "schemas" -} diff --git a/experimental/internal/codegen/schemanames.go b/experimental/internal/codegen/schemanames.go deleted file mode 100644 index c41e8b6cef..0000000000 --- a/experimental/internal/codegen/schemanames.go +++ /dev/null @@ -1,812 +0,0 @@ -package codegen - -import ( - "fmt" - "strings" -) - -// SchemaContext identifies what kind of schema this is based on its location. -type SchemaContext int - -const ( - ContextUnknown SchemaContext = iota - ContextComponentSchema - ContextParameter - ContextRequestBody - ContextResponse - ContextHeader - ContextCallback - ContextWebhook - ContextProperty - ContextItems - ContextAllOf - ContextAnyOf - ContextOneOf - ContextAdditionalProperties -) - -// ComputeSchemaNames assigns StableName and ShortName to each schema descriptor. -// StableName is deterministic from the path; ShortName is a friendly alias. -// If a schema has a TypeNameOverride extension, that takes precedence over computed names. -func ComputeSchemaNames(schemas []*SchemaDescriptor, converter *NameConverter, contentTypeNamer *ContentTypeShortNamer) { - // First: compute stable names from full paths - for _, s := range schemas { - // Check for TypeNameOverride extension - if s.Extensions != nil && s.Extensions.TypeNameOverride != "" { - s.StableName = s.Extensions.TypeNameOverride - } else { - s.StableName = computeStableName(s.Path, converter) - } - } - - // Second: generate candidate short names - candidates := make(map[*SchemaDescriptor]string) - for _, s := range schemas { - // TypeNameOverride also applies to short names - if s.Extensions != nil && s.Extensions.TypeNameOverride != "" { - candidates[s] = s.Extensions.TypeNameOverride - } else { - candidates[s] = generateCandidateName(s, converter, contentTypeNamer) - } - } - - // Third: detect collisions and resolve them for short names - resolveCollisions(schemas, candidates, converter) - - // Assign final short names - for _, s := range schemas { - s.ShortName = candidates[s] - } -} - -// computeStableName generates a deterministic type name from the full path. -// The format is: {meaningful_names}{reversed_context_suffix} -// Example: #/components/schemas/Cat -> CatSchemaComponent -func computeStableName(path SchemaPath, converter *NameConverter) string { - if len(path) == 0 { - return "Schema" - } - - // Separate path into name parts and context parts - var nameParts []string - var contextParts []string - - for i := 0; i < len(path); i++ { - part := path[i] - // Strip leading slash from API paths (e.g., "/pets" -> "pets") - part = strings.TrimPrefix(part, "/") - if part == "" { - continue - } - if isContextKeyword(part) { - contextParts = append(contextParts, part) - } else { - nameParts = append(nameParts, part) - } - } - - // Build the name: names first, then reversed context as suffix - var result strings.Builder - - // Add name parts - // First part uses ToTypeName (adds numeric prefix if needed) - // Subsequent parts use ToTypeNamePart (no numeric prefix since they're not at the start) - for i, name := range nameParts { - if i == 0 { - result.WriteString(converter.ToTypeName(name)) - } else { - result.WriteString(converter.ToTypeNamePart(name)) - } - } - - // Add reversed context as suffix (singularized) - for i := len(contextParts) - 1; i >= 0; i-- { - suffix := contextToSuffix(contextParts[i]) - result.WriteString(suffix) - } - - name := result.String() - if name == "" { - return "Schema" - } - return name -} - -// isContextKeyword returns true if the path segment is a structural keyword -// rather than a user-defined name. -func isContextKeyword(s string) bool { - switch s { - case "components", "schemas", "parameters", "responses", "requestBodies", - "headers", "callbacks", "paths", "webhooks", - "properties", "items", "additionalProperties", - "allOf", "anyOf", "oneOf", "not", - "prefixItems", "contains", "if", "then", "else", - "dependentSchemas", "patternProperties", "propertyNames", - "unevaluatedItems", "unevaluatedProperties", - "content", "schema", "requestBody": - return true - default: - return false - } -} - -// contextToSuffix converts a context keyword to its singular suffix form. -func contextToSuffix(context string) string { - switch context { - case "components": - return "Component" - case "schemas": - return "Schema" - case "parameters": - return "Parameter" - case "responses": - return "Response" - case "requestBodies", "requestBody": - return "Request" - case "headers": - return "Header" - case "callbacks": - return "Callback" - case "paths": - return "Path" - case "webhooks": - return "Webhook" - case "properties": - return "Property" - case "items": - return "Item" - case "additionalProperties": - return "Value" - case "allOf": - return "AllOf" - case "anyOf": - return "AnyOf" - case "oneOf": - return "OneOf" - case "not": - return "Not" - case "prefixItems": - return "PrefixItem" - case "contains": - return "Contains" - case "if": - return "If" - case "then": - return "Then" - case "else": - return "Else" - case "content": - return "Content" - case "schema": - return "" // Skip redundant "Schema" suffix from content/schema - default: - return "" - } -} - -// generateCandidateName creates a candidate short name based on the schema's path. -func generateCandidateName(s *SchemaDescriptor, converter *NameConverter, contentTypeNamer *ContentTypeShortNamer) string { - path := s.Path - if len(path) == 0 { - return "Schema" - } - - ctx, parts := parsePathContext(path) - - switch ctx { - case ContextComponentSchema: - // #/components/schemas/Cat -> "Cat" - // #/components/schemas/Cat/properties/name -> "CatName" - return buildComponentSchemaName(parts, converter) - - case ContextParameter: - // Always suffix with Parameter - return buildParameterName(parts, converter) - - case ContextRequestBody: - // Use operationId if available for nicer names, but only for the direct request body schema - // (not nested items, properties, etc.) - if s.OperationID != "" && isDirectBodySchema(path) { - return buildOperationRequestName(s.OperationID, s.ContentType, converter, contentTypeNamer) - } - return buildRequestBodyName(parts, converter) - - case ContextResponse: - // Use operationId if available for nicer names, but only for the direct response schema - // (not nested items, properties, etc.) - if s.OperationID != "" && isDirectBodySchema(path) { - // Extract status code from path - statusCode := extractStatusCode(parts) - return buildOperationResponseName(s.OperationID, statusCode, s.ContentType, converter, contentTypeNamer) - } - return buildResponseName(parts, converter) - - case ContextHeader: - return buildHeaderName(parts, converter) - - case ContextCallback: - return buildCallbackName(parts, converter) - - case ContextWebhook: - return buildWebhookName(parts, converter) - - default: - // Fallback: join all meaningful parts - return buildFallbackName(path, converter) - } -} - -// parsePathContext determines the schema context and extracts relevant path parts. -func parsePathContext(path SchemaPath) (SchemaContext, []string) { - if len(path) == 0 { - return ContextUnknown, nil - } - - switch path[0] { - case "components": - if len(path) >= 3 && path[1] == "schemas" { - return ContextComponentSchema, path[2:] - } - if len(path) >= 3 && path[1] == "parameters" { - return ContextParameter, path[2:] - } - if len(path) >= 3 && path[1] == "requestBodies" { - return ContextRequestBody, path[2:] - } - if len(path) >= 3 && path[1] == "responses" { - return ContextResponse, path[2:] - } - if len(path) >= 3 && path[1] == "headers" { - return ContextHeader, path[2:] - } - if len(path) >= 3 && path[1] == "callbacks" { - return ContextCallback, path[2:] - } - - case "paths": - // paths/{path}/{method}/... - if len(path) >= 3 { - remaining := path[3:] // skip paths, {path}, {method} - return detectPathsContext(remaining), path[1:] // include path and method - } - - case "webhooks": - return ContextWebhook, path[1:] - } - - return ContextUnknown, path -} - -// detectPathsContext determines context from within a path item. -func detectPathsContext(remaining SchemaPath) SchemaContext { - if len(remaining) == 0 { - return ContextUnknown - } - - switch remaining[0] { - case "parameters": - return ContextParameter - case "requestBody": - return ContextRequestBody - case "responses": - return ContextResponse - case "callbacks": - return ContextCallback - } - - return ContextUnknown -} - -// buildComponentSchemaName builds a name for a component schema. -// e.g., ["Cat"] -> "Cat", ["Cat", "properties", "name"] -> "CatName" -func buildComponentSchemaName(parts []string, converter *NameConverter) string { - if len(parts) == 0 { - return "Schema" - } - - var nameParts []string - nameParts = append(nameParts, parts[0]) // schema name - - // Track trailing structural elements (only add suffix if they're at the end) - trailingSuffix := "" - - // Process nested parts - for i := 1; i < len(parts); i++ { - part := parts[i] - switch part { - case "properties": - // Skip, but next part (property name) will be added - // Clear trailing suffix since we're going deeper - trailingSuffix = "" - continue - case "items": - // Accumulate Item suffix (for nested arrays) - trailingSuffix += "Item" - continue - case "additionalProperties": - // Set Value suffix - trailingSuffix = "Value" - continue - case "allOf", "anyOf", "oneOf": - // Include the composition type and index - trailingSuffix = "" // Clear since we're adding meaningful content - if i+1 < len(parts) { - nameParts = append(nameParts, part+parts[i+1]) - i++ // Skip the index - } else { - nameParts = append(nameParts, part) - } - case "not", "prefixItems", "contains", "if", "then", "else": - // Include these structural keywords - trailingSuffix = "" - nameParts = append(nameParts, part) - default: - // Include meaningful parts (property names, indices) - // Clear trailing suffix since we have a meaningful name part - trailingSuffix = "" - nameParts = append(nameParts, part) - } - } - - name := converter.ToTypeName(strings.Join(nameParts, "_")) - - // Add trailing structural suffix if still present - if trailingSuffix != "" { - name += trailingSuffix - } - - return name -} - -// buildParameterName builds a name for a parameter schema. -func buildParameterName(parts []string, converter *NameConverter) string { - // parts could be: - // - from components/parameters: [paramName, "schema"] - // - from paths: [path, method, "parameters", index, "schema"] - - var baseName string - if len(parts) >= 2 && parts[0] != "" { - // Try to extract operation-style name - baseName = buildOperationName(parts, converter) - } - if baseName == "" && len(parts) > 0 { - baseName = converter.ToTypeName(parts[0]) - } - if baseName == "" { - baseName = "Param" - } - - // Always add Parameter suffix - if !strings.HasSuffix(baseName, "Parameter") { - baseName += "Parameter" - } - return baseName -} - -// buildRequestBodyName builds a name for a request body schema. -func buildRequestBodyName(parts []string, converter *NameConverter) string { - var baseName string - if len(parts) >= 2 { - baseName = buildOperationName(parts, converter) - } - if baseName == "" && len(parts) > 0 { - baseName = converter.ToTypeName(parts[0]) - } - if baseName == "" { - baseName = "Request" - } - - // Always add Request suffix - if !strings.HasSuffix(baseName, "Request") { - baseName += "Request" - } - return baseName -} - -// buildResponseName builds a name for a response schema. -func buildResponseName(parts []string, converter *NameConverter) string { - var baseName string - var statusCode string - - if len(parts) >= 4 { - // paths: [path, method, "responses", code, ...] - baseName = buildOperationName(parts[:2], converter) - // Find status code - for i, p := range parts { - if p == "responses" && i+1 < len(parts) { - statusCode = parts[i+1] - break - } - } - } - if baseName == "" && len(parts) > 0 { - baseName = converter.ToTypeName(parts[0]) - } - if baseName == "" { - baseName = "Response" - } - - // Add status code if present - if statusCode != "" && statusCode != "default" { - baseName += statusCode - } - - // Always add Response suffix - if !strings.HasSuffix(baseName, "Response") { - baseName += "Response" - } - return baseName -} - -// buildHeaderName builds a name for a header schema. -func buildHeaderName(parts []string, converter *NameConverter) string { - if len(parts) == 0 { - return "Header" - } - baseName := converter.ToTypeName(parts[0]) - if !strings.HasSuffix(baseName, "Header") { - baseName += "Header" - } - return baseName -} - -// buildCallbackName builds a name for a callback schema. -func buildCallbackName(parts []string, converter *NameConverter) string { - if len(parts) == 0 { - return "Callback" - } - return converter.ToTypeName(parts[0]) + "Callback" -} - -// buildWebhookName builds a name for a webhook schema. -func buildWebhookName(parts []string, converter *NameConverter) string { - if len(parts) == 0 { - return "Webhook" - } - return converter.ToTypeName(parts[0]) + "Webhook" -} - -// buildOperationName builds a name from path and method. -// e.g., ["/pets", "get"] -> "GetPets" -func buildOperationName(parts []string, converter *NameConverter) string { - if len(parts) < 2 { - return "" - } - - pathStr := parts[0] - method := parts[1] - - // Convert method to title case - methodName := converter.ToTypeName(method) - - // Convert path to name parts - // /pets/{petId}/toys -> PetsPetIdToys - pathName := pathToName(pathStr, converter) - - return methodName + pathName -} - -// pathToName converts an API path to a name component. -// e.g., "/pets/{petId}" -> "PetsPetId" -func pathToName(path string, converter *NameConverter) string { - // Remove leading slash - path = strings.TrimPrefix(path, "/") - - // Split by slash - segments := strings.Split(path, "/") - - var parts []string - for _, seg := range segments { - if seg == "" { - continue - } - // Remove braces from path parameters - seg = strings.TrimPrefix(seg, "{") - seg = strings.TrimSuffix(seg, "}") - parts = append(parts, seg) - } - - return converter.ToTypeName(strings.Join(parts, "_")) -} - -// buildFallbackName creates a name from the full path as a last resort. -func buildFallbackName(path SchemaPath, converter *NameConverter) string { - var parts []string - for _, p := range path { - // Skip common structural elements - switch p { - case "components", "schemas", "paths", "properties", - "items", "schema", "content", "application/json": - continue - default: - parts = append(parts, p) - } - } - - if len(parts) == 0 { - return "Schema" - } - - return converter.ToTypeName(strings.Join(parts, "_")) -} - -// schemaContextSuffix maps a SchemaContext to a disambiguation suffix. -func schemaContextSuffix(ctx SchemaContext) string { - switch ctx { - case ContextComponentSchema: - return "Schema" - case ContextParameter: - return "Parameter" - case ContextRequestBody: - return "Request" - case ContextResponse: - return "Response" - case ContextHeader: - return "Header" - case ContextCallback: - return "Callback" - case ContextWebhook: - return "Webhook" - default: - return "" - } -} - -// resolveCollisions detects name collisions and makes them unique. -// Reference schemas are excluded from collision detection because they don't -// generate types — their names are only used for type resolution lookups. -// -// Resolution proceeds in phases: -// 1. Context suffix: append a suffix derived from the schema's location -// (e.g. "Request", "Response"). If exactly one collider lives under -// components/schemas it keeps the bare name. -// 2. Existing disambiguateName logic (content type, status code, composition). -// 3. Numeric fallback as a last resort. -func resolveCollisions(schemas []*SchemaDescriptor, candidates map[*SchemaDescriptor]string, converter *NameConverter) { - // Filter out reference schemas — they don't generate types so their - // short names can safely shadow non-ref names without causing a collision. - var nonRefSchemas []*SchemaDescriptor - for _, s := range schemas { - if s.Ref == "" { - nonRefSchemas = append(nonRefSchemas, s) - } - } - - maxIterations := 10 // Prevent infinite loops - - for iteration := range maxIterations { - // Group non-ref schemas by candidate name - byName := make(map[string][]*SchemaDescriptor) - for _, s := range nonRefSchemas { - name := candidates[s] - byName[name] = append(byName[name], s) - } - - // Check if there are any collisions - hasCollisions := false - for _, group := range byName { - if len(group) > 1 { - hasCollisions = true - break - } - } - - if !hasCollisions { - return // All names are unique - } - - // Resolve collisions - for _, group := range byName { - if len(group) <= 1 { - continue // No collision - } - - // On last iteration, just add numeric suffixes - if iteration == maxIterations-1 { - for i, s := range group { - candidates[s] = fmt.Sprintf("%s%d", candidates[s], i+1) - } - continue - } - - // First iteration: try context suffix disambiguation - if iteration == 0 { - resolveWithContextSuffix(group, candidates) - continue - } - - // Subsequent iterations: existing disambiguateName logic - for i, s := range group { - newName := disambiguateName(s, candidates[s], i, converter) - candidates[s] = newName - } - } - } -} - -// resolveWithContextSuffix attempts to disambiguate colliding schemas by -// appending a suffix derived from their path context (e.g. "Request", -// "Response"). If exactly one member is a component schema, it keeps the -// bare name and only the others are suffixed. -func resolveWithContextSuffix(group []*SchemaDescriptor, candidates map[*SchemaDescriptor]string) { - // Count how many are from components/schemas - var componentSchemaCount int - for _, s := range group { - ctx, _ := parsePathContext(s.Path) - if ctx == ContextComponentSchema { - componentSchemaCount++ - } - } - - // If exactly one is from components/schemas, it is "privileged" and keeps - // the bare name. - privileged := componentSchemaCount == 1 - - for _, s := range group { - ctx, _ := parsePathContext(s.Path) - - // Privileged component schema keeps the bare name - if privileged && ctx == ContextComponentSchema { - continue - } - - suffix := schemaContextSuffix(ctx) - if suffix != "" { - name := candidates[s] - if !strings.HasSuffix(name, suffix) { - candidates[s] = name + suffix - } - } - // If suffix is empty (unknown context), leave unchanged for later - // iterations to handle via disambiguateName. - } -} - -// disambiguateName adds more context to make a name unique. -func disambiguateName(s *SchemaDescriptor, currentName string, index int, converter *NameConverter) string { - path := s.Path - - // Try to add more path context based on what's in the path - // but not already in the name - - // Check for content type differentiation - for i, part := range path { - if part == "content" && i+1 < len(path) { - contentType := path[i+1] - var suffix string - switch { - case strings.Contains(contentType, "json"): - suffix = "JSON" - case strings.Contains(contentType, "xml"): - suffix = "XML" - case strings.Contains(contentType, "form"): - suffix = "Form" - case strings.Contains(contentType, "text"): - suffix = "Text" - case strings.Contains(contentType, "binary"): - suffix = "Binary" - default: - suffix = converter.ToTypeName(strings.ReplaceAll(contentType, "/", "_")) - } - // Use Contains since the suffix might be embedded before "Response" or "Request" - if !strings.Contains(currentName, suffix) { - return currentName + suffix - } - } - } - - // Check for status code differentiation (for responses) - for i, part := range path { - if part == "responses" && i+1 < len(path) { - code := path[i+1] - if !strings.Contains(currentName, code) { - return currentName + code - } - } - } - - // Check for parameter index differentiation - for i, part := range path { - if part == "parameters" && i+1 < len(path) { - idx := path[i+1] - if !strings.HasSuffix(currentName, idx) { - return currentName + idx - } - } - } - - // Check for composition type differentiation - for i := len(path) - 1; i >= 0; i-- { - part := path[i] - switch part { - case "allOf", "anyOf", "oneOf": - suffix := converter.ToTypeName(part) - if i+1 < len(path) { - suffix += path[i+1] // Add index - } - if !strings.Contains(currentName, suffix) { - return currentName + suffix - } - } - } - - // Last resort: use numeric suffix - return fmt.Sprintf("%s%d", currentName, index+1) -} - -// buildOperationRequestName builds a name for a request body using the operationId. -// e.g., operationId="addPet" -> "AddPetJSONRequest" -func buildOperationRequestName(operationID, contentType string, converter *NameConverter, contentTypeNamer *ContentTypeShortNamer) string { - baseName := converter.ToTypeName(operationID) - - // Add content type short name if available - if contentType != "" && contentTypeNamer != nil { - baseName += contentTypeNamer.ShortName(contentType) - } - - return baseName + "Request" -} - -// buildOperationResponseName builds a name for a response using the operationId. -// e.g., operationId="findPets", statusCode="200" -> "FindPetsJSONResponse" -// e.g., operationId="findPets", statusCode="404" -> "FindPets404JSONResponse" -// e.g., operationId="findPets", statusCode="default" -> "FindPetsDefaultJSONResponse" -func buildOperationResponseName(operationID, statusCode, contentType string, converter *NameConverter, contentTypeNamer *ContentTypeShortNamer) string { - baseName := converter.ToTypeName(operationID) - - // Add status code, skipping only for 200 (the common success case) - if statusCode != "" && statusCode != "200" { - if statusCode == "default" { - baseName += "Default" - } else { - baseName += statusCode - } - } - - // Add content type short name if available - if contentType != "" && contentTypeNamer != nil { - baseName += contentTypeNamer.ShortName(contentType) - } - - return baseName + "Response" -} - -// extractStatusCode extracts the HTTP status code from path parts. -// Looks for "responses" followed by the status code. -func extractStatusCode(parts []string) string { - for i, p := range parts { - if p == "responses" && i+1 < len(parts) { - return parts[i+1] - } - } - return "" -} - -// isDirectBodySchema returns true if the schema path represents the direct -// schema of a request body or response (i.e., content/{type}/schema), -// not a nested schema (items, properties, allOf members, etc.). -func isDirectBodySchema(path SchemaPath) bool { - // Find the position of "schema" after "content" - schemaIdx := -1 - for i := len(path) - 1; i >= 0; i-- { - if path[i] == "schema" { - schemaIdx = i - break - } - } - if schemaIdx == -1 { - return false - } - - // Check that "schema" is directly after a content type (content/{type}/schema) - // and there are no structural elements after it - if schemaIdx < 2 { - return false - } - if path[schemaIdx-2] != "content" { - return false - } - - // If schema is at the end of the path, it's a direct body schema - return schemaIdx == len(path)-1 -} diff --git a/experimental/internal/codegen/servergen.go b/experimental/internal/codegen/servergen.go deleted file mode 100644 index 86177c4167..0000000000 --- a/experimental/internal/codegen/servergen.go +++ /dev/null @@ -1,180 +0,0 @@ -package codegen - -import ( - "bytes" - "fmt" - "text/template" - - "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/templates" -) - -// ServerGenerator generates server code from operation descriptors. -type ServerGenerator struct { - tmpl *template.Template - serverType string -} - -// NewServerGenerator creates a new server generator for the specified server type. -func NewServerGenerator(serverType string) (*ServerGenerator, error) { - if serverType == "" { - // No server generation requested - return &ServerGenerator{serverType: ""}, nil - } - - tmpl := template.New("server").Funcs(templates.Funcs()) - - // Get templates for the specified server type - serverTemplates, err := getServerTemplates(serverType) - if err != nil { - return nil, err - } - - // Parse server-specific templates - for _, st := range serverTemplates { - content, err := templates.TemplateFS.ReadFile("files/" + st.Template) - if err != nil { - return nil, fmt.Errorf("failed to read template %s: %w", st.Template, err) - } - _, err = tmpl.New(st.Name).Parse(string(content)) - if err != nil { - return nil, fmt.Errorf("failed to parse template %s: %w", st.Template, err) - } - } - - // Parse shared templates - for _, st := range templates.SharedServerTemplates { - content, err := templates.TemplateFS.ReadFile("files/" + st.Template) - if err != nil { - return nil, fmt.Errorf("failed to read shared template %s: %w", st.Template, err) - } - _, err = tmpl.New(st.Name).Parse(string(content)) - if err != nil { - return nil, fmt.Errorf("failed to parse shared template %s: %w", st.Template, err) - } - } - - return &ServerGenerator{tmpl: tmpl, serverType: serverType}, nil -} - -// getServerTemplates returns the templates for the specified server type. -func getServerTemplates(serverType string) (map[string]templates.ServerTemplate, error) { - switch serverType { - case ServerTypeStdHTTP: - return templates.StdHTTPServerTemplates, nil - case ServerTypeChi: - return templates.ChiServerTemplates, nil - case ServerTypeEcho: - return templates.EchoServerTemplates, nil - case ServerTypeEchoV4: - return templates.EchoV4ServerTemplates, nil - case ServerTypeGin: - return templates.GinServerTemplates, nil - case ServerTypeGorilla: - return templates.GorillaServerTemplates, nil - case ServerTypeFiber: - return templates.FiberServerTemplates, nil - case ServerTypeIris: - return templates.IrisServerTemplates, nil - default: - return nil, fmt.Errorf("unsupported server type: %q (supported: %q, %q, %q, %q, %q, %q, %q, %q)", - serverType, - ServerTypeStdHTTP, ServerTypeChi, ServerTypeEcho, ServerTypeEchoV4, ServerTypeGin, - ServerTypeGorilla, ServerTypeFiber, ServerTypeIris) - } -} - -// GenerateInterface generates the ServerInterface. -func (g *ServerGenerator) GenerateInterface(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "interface", ops); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateHandler generates the HTTP handler and routing code. -func (g *ServerGenerator) GenerateHandler(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "handler", ops); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateWrapper generates the ServerInterfaceWrapper. -func (g *ServerGenerator) GenerateWrapper(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "wrapper", ops); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateErrors generates the error types. -func (g *ServerGenerator) GenerateErrors() (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "errors", nil); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateParamTypes generates the parameter struct types. -func (g *ServerGenerator) GenerateParamTypes(ops []*OperationDescriptor) (string, error) { - var buf bytes.Buffer - if err := g.tmpl.ExecuteTemplate(&buf, "param_types", ops); err != nil { - return "", err - } - return buf.String(), nil -} - -// GenerateServer generates all server code components. -// Returns empty string if no server type was configured. -func (g *ServerGenerator) GenerateServer(ops []*OperationDescriptor) (string, error) { - if g.serverType == "" || g.tmpl == nil { - return "", nil - } - - var buf bytes.Buffer - - // Generate interface - iface, err := g.GenerateInterface(ops) - if err != nil { - return "", err - } - buf.WriteString(iface) - buf.WriteString("\n") - - // Generate param types - paramTypes, err := g.GenerateParamTypes(ops) - if err != nil { - return "", err - } - buf.WriteString(paramTypes) - buf.WriteString("\n") - - // Generate wrapper - wrapper, err := g.GenerateWrapper(ops) - if err != nil { - return "", err - } - buf.WriteString(wrapper) - buf.WriteString("\n") - - // Generate handler - handler, err := g.GenerateHandler(ops) - if err != nil { - return "", err - } - buf.WriteString(handler) - buf.WriteString("\n") - - // Generate errors - errors, err := g.GenerateErrors() - if err != nil { - return "", err - } - buf.WriteString(errors) - - return buf.String(), nil -} diff --git a/experimental/internal/codegen/skip_external_ref_test.go b/experimental/internal/codegen/skip_external_ref_test.go deleted file mode 100644 index 6983852a73..0000000000 --- a/experimental/internal/codegen/skip_external_ref_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package codegen - -import ( - "os" - "strings" - "testing" - - "github.com/pb33f/libopenapi" - "github.com/pb33f/libopenapi/datamodel" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestSkipExternalRefResolution verifies that we can parse a spec containing -// external $ref references without resolving them, and still generate correct -// code using import mappings. This uses libopenapi's SkipExternalRefResolution -// flag (added in v0.33.4, pb33f/libopenapi#519). -func TestSkipExternalRefResolution(t *testing.T) { - specData, err := os.ReadFile("test/external_ref/spec.yaml") - require.NoError(t, err) - - // Parse WITHOUT BasePath or AllowFileReferences — the external spec files - // won't be read. Instead, we rely on SkipExternalRefResolution to leave - // external $refs unresolved while still building an iterable model. - docConfig := datamodel.NewDocumentConfiguration() - docConfig.SkipExternalRefResolution = true - - doc, err := libopenapi.NewDocumentWithConfiguration(specData, docConfig) - require.NoError(t, err) - - cfg := Configuration{ - PackageName: "externalref", - ImportMapping: map[string]string{ - "./packagea/spec.yaml": "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packagea", - "./packageb/spec.yaml": "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packageb", - }, - } - - code, err := Generate(doc, nil, cfg) - require.NoError(t, err) - - // The generated code should contain the Container struct with external type references - assert.Contains(t, code, "type Container struct") - assert.Contains(t, code, "ObjectA") - assert.Contains(t, code, "ObjectB") - - // Should reference the external packages via hashed aliases - assert.Contains(t, code, "ext_95d82e90") - assert.Contains(t, code, "ext_a5fddf6c") - - // Should contain the import declarations - assert.Contains(t, code, `"github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packagea"`) - assert.Contains(t, code, `"github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packageb"`) - - // Should NOT contain "any" as a fallback type for the external refs - // (which would indicate the refs weren't properly detected) - lines := strings.Split(code, "\n") - for _, line := range lines { - if strings.Contains(line, "ObjectA") && strings.Contains(line, "any") { - t.Errorf("ObjectA resolved to 'any' instead of external type: %s", line) - } - if strings.Contains(line, "ObjectB") && strings.Contains(line, "any") { - t.Errorf("ObjectB resolved to 'any' instead of external type: %s", line) - } - } - - t.Logf("Generated code:\n%s", code) -} diff --git a/experimental/internal/codegen/structtags.go b/experimental/internal/codegen/structtags.go deleted file mode 100644 index 36279956b7..0000000000 --- a/experimental/internal/codegen/structtags.go +++ /dev/null @@ -1,172 +0,0 @@ -package codegen - -import ( - "bytes" - "sort" - "strings" - "text/template" -) - -// StructTagInfo contains the data available to struct tag templates. -type StructTagInfo struct { - // FieldName is the JSON/YAML field name (from the OpenAPI property name) - FieldName string - // GoFieldName is the Go struct field name - GoFieldName string - // IsOptional is true if the field is optional (not required) - IsOptional bool - // IsNullable is true if the field can be null - IsNullable bool - // IsPointer is true if the Go type is a pointer - IsPointer bool - // OmitEmpty is true if the omitempty tag option should be used - // (derived from IsOptional but can be overridden via extensions) - OmitEmpty bool - // OmitZero is true if the omitzero tag option should be used (Go 1.24+) - OmitZero bool - // JSONIgnore is true if the field should be excluded from JSON (json:"-") - JSONIgnore bool -} - -// StructTagTemplate defines a single struct tag with a name and template. -type StructTagTemplate struct { - // Name is the tag name (e.g., "json", "yaml", "form") - Name string `yaml:"name"` - // Template is a Go text/template that produces the tag value. - // Available fields: .FieldName, .GoFieldName, .IsOptional, .IsNullable, .IsPointer - // Example: `{{ .FieldName }}{{if .IsOptional}},omitempty{{end}}` - Template string `yaml:"template"` -} - -// StructTagsConfig configures struct tag generation. -type StructTagsConfig struct { - // Tags is the list of tags to generate for struct fields. - // Order is preserved in the generated output. - Tags []StructTagTemplate `yaml:"tags,omitempty"` -} - -// DefaultStructTagsConfig returns the default struct tag configuration. -// By default, json and form tags are generated. -func DefaultStructTagsConfig() StructTagsConfig { - return StructTagsConfig{ - Tags: []StructTagTemplate{ - { - Name: "json", - Template: `{{if .JSONIgnore}}-{{else}}{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}{{if .OmitZero}},omitzero{{end}}{{end}}`, - }, - { - Name: "form", - Template: `{{if .JSONIgnore}}-{{else}}{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}{{end}}`, - }, - }, - } -} - -// Merge merges user config on top of this config. -// If user specifies any tags, they completely replace the defaults. -func (c StructTagsConfig) Merge(other StructTagsConfig) StructTagsConfig { - if len(other.Tags) > 0 { - return other - } - return c -} - -// StructTagGenerator generates struct tags from templates. -type StructTagGenerator struct { - templates []*tagTemplate -} - -type tagTemplate struct { - name string - tmpl *template.Template -} - -// NewStructTagGenerator creates a generator from the configuration. -// Invalid templates are silently skipped. -func NewStructTagGenerator(config StructTagsConfig) *StructTagGenerator { - g := &StructTagGenerator{ - templates: make([]*tagTemplate, 0, len(config.Tags)), - } - - for _, tag := range config.Tags { - tmpl, err := template.New(tag.Name).Parse(tag.Template) - if err != nil { - // Skip invalid templates - continue - } - g.templates = append(g.templates, &tagTemplate{ - name: tag.Name, - tmpl: tmpl, - }) - } - - return g -} - -// GenerateTags generates the complete struct tag string for a field. -// Returns a string like `json:"name,omitempty" yaml:"name,omitempty"`. -func (g *StructTagGenerator) GenerateTags(info StructTagInfo) string { - if len(g.templates) == 0 { - return "" - } - - var tags []string - for _, t := range g.templates { - var buf bytes.Buffer - if err := t.tmpl.Execute(&buf, info); err != nil { - // Skip tags that fail to render - continue - } - value := buf.String() - if value != "" { - tags = append(tags, t.name+`:`+`"`+value+`"`) - } - } - - if len(tags) == 0 { - return "" - } - - return "`" + strings.Join(tags, " ") + "`" -} - -// GenerateTagsMap generates tags as a map for cases where we need to add extra tags. -// Returns a map of tag name -> tag value (without quotes). -func (g *StructTagGenerator) GenerateTagsMap(info StructTagInfo) map[string]string { - result := make(map[string]string) - - for _, t := range g.templates { - var buf bytes.Buffer - if err := t.tmpl.Execute(&buf, info); err != nil { - continue - } - value := buf.String() - if value != "" { - result[t.name] = value - } - } - - return result -} - -// FormatTagsMap formats a tag map into a struct tag string. -// Tags are sorted alphabetically by name for deterministic output. -func FormatTagsMap(tags map[string]string) string { - if len(tags) == 0 { - return "" - } - - // Sort tag names for deterministic output - names := make([]string, 0, len(tags)) - for name := range tags { - names = append(names, name) - } - sort.Strings(names) - - var parts []string - for _, name := range names { - parts = append(parts, name+`:`+`"`+tags[name]+`"`) - } - - return "`" + strings.Join(parts, " ") + "`" -} diff --git a/experimental/internal/codegen/templates/embed.go b/experimental/internal/codegen/templates/embed.go deleted file mode 100644 index ed91bb95e7..0000000000 --- a/experimental/internal/codegen/templates/embed.go +++ /dev/null @@ -1,9 +0,0 @@ -package templates - -import "embed" - -// TemplateFS contains all embedded template files. -// The files/* pattern recursively includes all files in subdirectories. -// -//go:embed files/* -var TemplateFS embed.FS diff --git a/experimental/internal/codegen/templates/files/client/base.go.tmpl b/experimental/internal/codegen/templates/files/client/base.go.tmpl deleted file mode 100644 index 96d32997f4..0000000000 --- a/experimental/internal/codegen/templates/files/client/base.go.tmpl +++ /dev/null @@ -1,95 +0,0 @@ -{{/* Base client template - returns raw *http.Response */}} - -// RequestEditorFn is the function signature for the RequestEditor callback function. -type RequestEditorFn func(ctx context.Context, req *http.Request) error - -// HttpRequestDoer performs HTTP requests. -// The standard http.Client implements this interface. -type HttpRequestDoer interface { - Do(req *http.Request) (*http.Response, error) -} - -// Client which conforms to the OpenAPI3 specification for this service. -type Client struct { - // The endpoint of the server conforming to this interface, with scheme, - // https://api.deepmap.com for example. This can contain a path relative - // to the server, such as https://api.deepmap.com/dev-test, and all the - // paths in the swagger spec will be appended to the server. - Server string - - // 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 -} - -// ClientOption allows setting custom parameters during construction. -type ClientOption func(*Client) error - -// NewClient creates a new Client with reasonable defaults. -func NewClient(server string, opts ...ClientOption) (*Client, error) { - client := Client{ - Server: server, - } - for _, o := range opts { - if err := o(&client); err != nil { - return nil, err - } - } - // Ensure the server URL always has a trailing slash - if !strings.HasSuffix(client.Server, "/") { - client.Server += "/" - } - // Create httpClient if not already present - if client.Client == nil { - client.Client = &http.Client{} - } - return &client, nil -} - -// WithHTTPClient allows overriding the default Doer, which is -// automatically created using http.Client. This is useful for tests. -func WithHTTPClient(doer HttpRequestDoer) ClientOption { - return func(c *Client) error { - c.Client = doer - return nil - } -} - -// WithRequestEditorFn allows setting up a callback function, which will be -// called right before sending the request. This can be used to mutate the request. -func WithRequestEditorFn(fn RequestEditorFn) ClientOption { - return func(c *Client) error { - c.RequestEditors = append(c.RequestEditors, fn) - return nil - } -} - -// WithBaseURL overrides the baseURL. -func WithBaseURL(baseURL string) ClientOption { - return func(c *Client) error { - newBaseURL, err := url.Parse(baseURL) - if err != nil { - return err - } - c.Server = newBaseURL.String() - return nil - } -} - -func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { - for _, r := range c.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 -} diff --git a/experimental/internal/codegen/templates/files/client/interface.go.tmpl b/experimental/internal/codegen/templates/files/client/interface.go.tmpl deleted file mode 100644 index 458e4867dc..0000000000 --- a/experimental/internal/codegen/templates/files/client/interface.go.tmpl +++ /dev/null @@ -1,17 +0,0 @@ -{{/* Client interface template */}} - -// ClientInterface is the interface specification for the client. -type ClientInterface interface { -{{- range . }} -{{- $opid := .GoOperationID }} -{{- $hasParams := .HasParams }} -{{- $pathParams := .PathParams }} - // {{ $opid }}{{ if .HasBody }}WithBody{{ end }} makes a {{ .Method }} request to {{ .Path }} - {{ $opid }}{{ if .HasBody }}WithBody{{ end }}(ctx context.Context{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ .ParamsTypeName }}{{ end }}{{ if .HasBody }}, contentType string, body io.Reader{{ end }}, reqEditors ...RequestEditorFn) (*http.Response, error) -{{- range .Bodies }} -{{- if .IsJSON }} - {{ $opid }}{{ .FuncSuffix }}(ctx context.Context{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ $.ParamsTypeName }}{{ end }}, body {{ .GoTypeName }}, reqEditors ...RequestEditorFn) (*http.Response, error) -{{- end }} -{{- end }} -{{- end }} -} diff --git a/experimental/internal/codegen/templates/files/client/methods.go.tmpl b/experimental/internal/codegen/templates/files/client/methods.go.tmpl deleted file mode 100644 index a7a6dfd7f1..0000000000 --- a/experimental/internal/codegen/templates/files/client/methods.go.tmpl +++ /dev/null @@ -1,40 +0,0 @@ -{{/* Client methods template - implements ClientInterface */}} - -{{- range . }} -{{- $op := . }} -{{- $opid := .GoOperationID }} -{{- $hasParams := .HasParams }} -{{- $pathParams := .PathParams }} -{{- $paramsTypeName := .ParamsTypeName }} - -// {{ $opid }}{{ if .HasBody }}WithBody{{ end }} makes a {{ .Method }} request to {{ .Path }} -{{ if .Summary }}// {{ .Summary }}{{ end }} -func (c *Client) {{ $opid }}{{ if .HasBody }}WithBody{{ end }}(ctx context.Context{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ 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{{ range $pathParams }}, {{ .GoVariableName }}{{ end }}{{ 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) -} -{{- range .Bodies }} -{{- if .IsJSON }} - -// {{ $opid }}{{ .FuncSuffix }} makes a {{ $op.Method }} request to {{ $op.Path }} with JSON body -func (c *Client) {{ $opid }}{{ .FuncSuffix }}(ctx context.Context{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}, body {{ .GoTypeName }}, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := New{{ $opid }}Request{{ .FuncSuffix }}(c.Server{{ range $pathParams }}, {{ .GoVariableName }}{{ end }}{{ if $hasParams }}, params{{ end }}, body) - 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) -} -{{- end }} -{{- end }} -{{- end }} diff --git a/experimental/internal/codegen/templates/files/client/request_builders.go.tmpl b/experimental/internal/codegen/templates/files/client/request_builders.go.tmpl deleted file mode 100644 index 6461c14de9..0000000000 --- a/experimental/internal/codegen/templates/files/client/request_builders.go.tmpl +++ /dev/null @@ -1,177 +0,0 @@ -{{/* Request builder functions template */}} - -{{- range . }} -{{- $op := . }} -{{- $opid := .GoOperationID }} -{{- $hasParams := .HasParams }} -{{- $pathParams := .PathParams }} -{{- $paramsTypeName := .ParamsTypeName }} -{{- $queryParams := .QueryParams }} -{{- $headerParams := .HeaderParams }} -{{- $cookieParams := .CookieParams }} - -{{- /* Generate typed body request builders for JSON bodies */ -}} -{{- range .Bodies }} -{{- if .IsJSON }} - -// New{{ $opid }}Request{{ .FuncSuffix }} creates a {{ $op.Method }} request for {{ $op.Path }} with {{ .ContentType }} body -func New{{ $opid }}Request{{ .FuncSuffix }}(server string{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}, body {{ .GoTypeName }}) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return New{{ $opid }}RequestWithBody(server{{ range $pathParams }}, {{ .GoVariableName }}{{ end }}{{ if $hasParams }}, params{{ end }}, "{{ .ContentType }}", bodyReader) -} -{{- end }} -{{- end }} - -// New{{ $opid }}Request{{ if .HasBody }}WithBody{{ end }} creates a {{ .Method }} request for {{ .Path }}{{ if .HasBody }} with any body{{ end }} -func New{{ $opid }}Request{{ if .HasBody }}WithBody{{ end }}(server string{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}{{ if .HasBody }}, contentType string, body io.Reader{{ end }}) (*http.Request, error) { - var err error -{{- range $idx, $param := $pathParams }} - - var pathParam{{ $idx }} string - {{- if .IsPassThrough }} - pathParam{{ $idx }} = {{ .GoVariableName }} - {{- else if .IsJSON }} - var pathParamBuf{{ $idx }} []byte - pathParamBuf{{ $idx }}, err = json.Marshal({{ .GoVariableName }}) - if err != nil { - return nil, err - } - pathParam{{ $idx }} = string(pathParamBuf{{ $idx }}) - {{- else if .IsStyled }} - pathParam{{ $idx }}, err = {{ .StyleFunc }}("{{ .Name }}", ParamLocationPath, {{ .GoVariableName }}) - if err != nil { - return nil, err - } - {{- end }} -{{- end }} - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("{{ pathFmt .Path }}"{{ range $idx, $_ := $pathParams }}, pathParam{{ $idx }}{{ end }}) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } -{{- if $queryParams }} - - if params != nil { - queryValues := queryURL.Query() -{{- range $idx, $param := $queryParams }} - {{- if .HasOptionalPointer }} - if params.{{ .GoName }} != nil { - {{- end }} - {{- if .IsPassThrough }} - queryValues.Add("{{ .Name }}", {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) - {{- else if .IsJSON }} - if queryParamBuf, err := json.Marshal({{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}); err != nil { - return nil, err - } else { - queryValues.Add("{{ .Name }}", string(queryParamBuf)) - } - {{- else if .IsStyled }} - if queryFrag, err := {{ .StyleFunc }}("{{ .Name }}", ParamLocationQuery, {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { - return nil, err - } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } - } - } - {{- end }} - {{- if .HasOptionalPointer }} - } - {{- end }} -{{- end }} - queryURL.RawQuery = queryValues.Encode() - } -{{- end }} - - req, err := http.NewRequest("{{ .Method }}", queryURL.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 $idx, $param := $headerParams }} - {{- if .HasOptionalPointer }} - if params.{{ .GoName }} != nil { - {{- end }} - var headerParam{{ $idx }} string - {{- if .IsPassThrough }} - headerParam{{ $idx }} = {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }} - {{- else if .IsJSON }} - var headerParamBuf{{ $idx }} []byte - headerParamBuf{{ $idx }}, err = json.Marshal({{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) - if err != nil { - return nil, err - } - headerParam{{ $idx }} = string(headerParamBuf{{ $idx }}) - {{- else if .IsStyled }} - headerParam{{ $idx }}, err = {{ .StyleFunc }}("{{ .Name }}", ParamLocationHeader, {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) - if err != nil { - return nil, err - } - {{- end }} - req.Header.Set("{{ .Name }}", headerParam{{ $idx }}) - {{- if .HasOptionalPointer }} - } - {{- end }} -{{- end }} - } -{{- end }} -{{- if $cookieParams }} - - if params != nil { -{{- range $idx, $param := $cookieParams }} - {{- if .HasOptionalPointer }} - if params.{{ .GoName }} != nil { - {{- end }} - var cookieParam{{ $idx }} string - {{- if .IsPassThrough }} - cookieParam{{ $idx }} = {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }} - {{- else if .IsJSON }} - var cookieParamBuf{{ $idx }} []byte - cookieParamBuf{{ $idx }}, err = json.Marshal({{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) - if err != nil { - return nil, err - } - cookieParam{{ $idx }} = url.QueryEscape(string(cookieParamBuf{{ $idx }})) - {{- else if .IsStyled }} - cookieParam{{ $idx }}, err = StyleSimpleParam("{{ .Name }}", ParamLocationCookie, {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) - if err != nil { - return nil, err - } - {{- end }} - cookie{{ $idx }} := &http.Cookie{ - Name: "{{ .Name }}", - Value: cookieParam{{ $idx }}, - } - req.AddCookie(cookie{{ $idx }}) - {{- if .HasOptionalPointer }} - } - {{- end }} -{{- end }} - } -{{- end }} - - return req, nil -} -{{- end }} diff --git a/experimental/internal/codegen/templates/files/client/simple.go.tmpl b/experimental/internal/codegen/templates/files/client/simple.go.tmpl deleted file mode 100644 index 988cf73182..0000000000 --- a/experimental/internal/codegen/templates/files/client/simple.go.tmpl +++ /dev/null @@ -1,121 +0,0 @@ -{{/* SimpleClient template - wraps Client with typed responses for simple operations */}} - -// ClientHttpError represents an HTTP error response from the server. -// The type parameter E is the type of the parsed error body. -type ClientHttpError[E any] struct { - StatusCode int - Body E - RawBody []byte -} - -func (e *ClientHttpError[E]) Error() string { - return fmt.Sprintf("HTTP %d", e.StatusCode) -} - -// SimpleClient wraps Client with typed responses for operations that have -// unambiguous response types. Methods return the success type directly, -// and HTTP errors are returned as *ClientHttpError[E] where E is the error type. -type SimpleClient struct { - *Client -} - -// NewSimpleClient creates a new SimpleClient which wraps a Client. -func NewSimpleClient(server string, opts ...ClientOption) (*SimpleClient, error) { - client, err := NewClient(server, opts...) - if err != nil { - return nil, err - } - return &SimpleClient{Client: client}, nil -} - -{{- range . }} -{{- $op := . }} -{{- $opid := .GoOperationID }} -{{- $hasParams := .HasParams }} -{{- $pathParams := .PathParams }} -{{- $paramsTypeName := .ParamsTypeName }} - -{{- /* Determine if this operation is "simple" - single success content type, single JSON success response */}} -{{- $simpleOp := isSimpleOperation . }} -{{- if $simpleOp }} -{{- $successResponse := simpleOperationSuccessResponse . }} -{{- $successContent := index $successResponse.Contents 0 }} -{{- $successType := goTypeForContent $successContent }} -{{- $errorResponse := errorResponseForOperation . }} - -// {{ $opid }} makes a {{ .Method }} request to {{ .Path }} and returns the parsed response. -{{ if .Summary }}// {{ .Summary }}{{ end }} -{{- if $errorResponse }} -{{- $errorContent := index $errorResponse.Contents 0 }} -{{- $errorType := goTypeForContent $errorContent }} -// On success, returns the response body. On HTTP error, returns *ClientHttpError[{{ $errorType }}]. -func (c *SimpleClient) {{ $opid }}(ctx context.Context{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}{{ if .HasBody }}, body {{ (index .Bodies 0).GoTypeName }}{{ end }}, reqEditors ...RequestEditorFn) ({{ $successType }}, error) { - var result {{ $successType }} -{{- if .HasBody }} -{{- $defaultBody := index .Bodies 0 }} - resp, err := c.Client.{{ $opid }}{{ $defaultBody.FuncSuffix }}(ctx{{ range $pathParams }}, {{ .GoVariableName }}{{ end }}{{ if $hasParams }}, params{{ end }}, body, reqEditors...) -{{- else }} - resp, err := c.Client.{{ $opid }}(ctx{{ range $pathParams }}, {{ .GoVariableName }}{{ end }}{{ if $hasParams }}, params{{ end }}, reqEditors...) -{{- end }} - if err != nil { - return result, err - } - defer resp.Body.Close() - - rawBody, err := io.ReadAll(resp.Body) - if err != nil { - return result, err - } - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - if err := json.Unmarshal(rawBody, &result); err != nil { - return result, err - } - return result, nil - } - - // Parse error response - var errBody {{ $errorType }} - _ = json.Unmarshal(rawBody, &errBody) // Best effort parse - return result, &ClientHttpError[{{ $errorType }}]{ - StatusCode: resp.StatusCode, - Body: errBody, - RawBody: rawBody, - } -} -{{- else }} -// On success, returns the response body. On HTTP error, returns *ClientHttpError[struct{}]. -func (c *SimpleClient) {{ $opid }}(ctx context.Context{{ range $pathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}{{ if .HasBody }}, body {{ (index .Bodies 0).GoTypeName }}{{ end }}, reqEditors ...RequestEditorFn) ({{ $successType }}, error) { - var result {{ $successType }} -{{- if .HasBody }} -{{- $defaultBody := index .Bodies 0 }} - resp, err := c.Client.{{ $opid }}{{ $defaultBody.FuncSuffix }}(ctx{{ range $pathParams }}, {{ .GoVariableName }}{{ end }}{{ if $hasParams }}, params{{ end }}, body, reqEditors...) -{{- else }} - resp, err := c.Client.{{ $opid }}(ctx{{ range $pathParams }}, {{ .GoVariableName }}{{ end }}{{ if $hasParams }}, params{{ end }}, reqEditors...) -{{- end }} - if err != nil { - return result, err - } - defer resp.Body.Close() - - rawBody, err := io.ReadAll(resp.Body) - if err != nil { - return result, err - } - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - if err := json.Unmarshal(rawBody, &result); err != nil { - return result, err - } - return result, nil - } - - // No typed error response defined - return result, &ClientHttpError[struct{}]{ - StatusCode: resp.StatusCode, - RawBody: rawBody, - } -} -{{- end }} -{{- end }} -{{- end }} diff --git a/experimental/internal/codegen/templates/files/initiator/base.go.tmpl b/experimental/internal/codegen/templates/files/initiator/base.go.tmpl deleted file mode 100644 index 99e885136d..0000000000 --- a/experimental/internal/codegen/templates/files/initiator/base.go.tmpl +++ /dev/null @@ -1,73 +0,0 @@ -{{/* Initiator base template - framework-agnostic HTTP client for webhooks/callbacks */}} -{{/* Input: InitiatorTemplateData */}} - -// RequestEditorFn is the function signature for the RequestEditor callback function. -// It may already be defined if client code is also generated; this is a compatible redeclaration. -type RequestEditorFn func(ctx context.Context, req *http.Request) error - -// HttpRequestDoer performs HTTP requests. -// The standard http.Client implements this interface. -type HttpRequestDoer interface { - Do(req *http.Request) (*http.Response, error) -} - -// {{ .Prefix }}Initiator sends {{ .PrefixLower }} requests to target URLs. -// Unlike Client, it has no stored base URL — the full target URL is provided per-call. -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 request. This can be used to mutate the request. -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) applyEditors(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 -} diff --git a/experimental/internal/codegen/templates/files/initiator/interface.go.tmpl b/experimental/internal/codegen/templates/files/initiator/interface.go.tmpl deleted file mode 100644 index a6ad431733..0000000000 --- a/experimental/internal/codegen/templates/files/initiator/interface.go.tmpl +++ /dev/null @@ -1,17 +0,0 @@ -{{/* Initiator interface template */}} -{{/* Input: InitiatorTemplateData */}} - -// {{ .Prefix }}InitiatorInterface is the interface specification for the {{ .PrefixLower }} initiator. -type {{ .Prefix }}InitiatorInterface interface { -{{- range .Operations }} -{{- $opid := .GoOperationID }} -{{- $hasParams := .HasParams }} - // {{ $opid }}{{ if .HasBody }}WithBody{{ end }} sends a {{ .Method }} {{ $.PrefixLower }} request - {{ $opid }}{{ if .HasBody }}WithBody{{ end }}(ctx context.Context, targetURL string{{ if $hasParams }}, params *{{ .ParamsTypeName }}{{ end }}{{ if .HasBody }}, contentType string, body io.Reader{{ end }}, reqEditors ...RequestEditorFn) (*http.Response, error) -{{- range .Bodies }} -{{- if .IsJSON }} - {{ $opid }}{{ .FuncSuffix }}(ctx context.Context, targetURL string{{ if $hasParams }}, params *{{ $.ParamsTypeName }}{{ end }}, body {{ .GoTypeName }}, reqEditors ...RequestEditorFn) (*http.Response, error) -{{- end }} -{{- end }} -{{- end }} -} diff --git a/experimental/internal/codegen/templates/files/initiator/methods.go.tmpl b/experimental/internal/codegen/templates/files/initiator/methods.go.tmpl deleted file mode 100644 index ff079042ed..0000000000 --- a/experimental/internal/codegen/templates/files/initiator/methods.go.tmpl +++ /dev/null @@ -1,41 +0,0 @@ -{{/* Initiator methods template - implements InitiatorInterface */}} -{{/* Input: InitiatorTemplateData */}} - -{{- range .Operations }} -{{- $op := . }} -{{- $opid := .GoOperationID }} -{{- $hasParams := .HasParams }} -{{- $paramsTypeName := .ParamsTypeName }} -{{- $prefix := $.Prefix }} - -// {{ $opid }}{{ if .HasBody }}WithBody{{ end }} sends a {{ .Method }} {{ $.PrefixLower }} request -{{ if .Summary }}// {{ .Summary }}{{ end }} -func (p *{{ $prefix }}Initiator) {{ $opid }}{{ if .HasBody }}WithBody{{ end }}(ctx context.Context, targetURL string{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}{{ if .HasBody }}, contentType string, body io.Reader{{ end }}, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := New{{ $opid }}{{ $.Prefix }}Request{{ 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.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return p.Client.Do(req) -} -{{- range .Bodies }} -{{- if .IsJSON }} - -// {{ $opid }}{{ .FuncSuffix }} sends a {{ $op.Method }} {{ $.PrefixLower }} request with JSON body -func (p *{{ $prefix }}Initiator) {{ $opid }}{{ .FuncSuffix }}(ctx context.Context, targetURL string{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}, body {{ .GoTypeName }}, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := New{{ $opid }}{{ $prefix }}Request{{ .FuncSuffix }}(targetURL{{ if $hasParams }}, params{{ end }}, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := p.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return p.Client.Do(req) -} -{{- end }} -{{- end }} -{{- end }} diff --git a/experimental/internal/codegen/templates/files/initiator/request_builders.go.tmpl b/experimental/internal/codegen/templates/files/initiator/request_builders.go.tmpl deleted file mode 100644 index d651403492..0000000000 --- a/experimental/internal/codegen/templates/files/initiator/request_builders.go.tmpl +++ /dev/null @@ -1,149 +0,0 @@ -{{/* Initiator request builder functions template */}} -{{/* Input: InitiatorTemplateData */}} - -{{- range .Operations }} -{{- $op := . }} -{{- $opid := .GoOperationID }} -{{- $hasParams := .HasParams }} -{{- $paramsTypeName := .ParamsTypeName }} -{{- $queryParams := .QueryParams }} -{{- $headerParams := .HeaderParams }} -{{- $cookieParams := .CookieParams }} -{{- $prefix := $.Prefix }} - -{{- /* Generate typed body request builders for JSON bodies */ -}} -{{- range .Bodies }} -{{- if .IsJSON }} - -// New{{ $opid }}{{ $prefix }}Request{{ .FuncSuffix }} creates a {{ $op.Method }} request for the {{ $.PrefixLower }} with {{ .ContentType }} body -func New{{ $opid }}{{ $prefix }}Request{{ .FuncSuffix }}(targetURL string{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}, body {{ .GoTypeName }}) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return New{{ $opid }}{{ $prefix }}RequestWithBody(targetURL{{ if $hasParams }}, params{{ end }}, "{{ .ContentType }}", bodyReader) -} -{{- end }} -{{- end }} - -// New{{ $opid }}{{ $prefix }}Request{{ if .HasBody }}WithBody{{ end }} creates a {{ .Method }} request for the {{ $.PrefixLower }}{{ if .HasBody }} with any body{{ end }} -func New{{ $opid }}{{ $prefix }}Request{{ if .HasBody }}WithBody{{ end }}(targetURL string{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}{{ if .HasBody }}, contentType string, body io.Reader{{ end }}) (*http.Request, error) { - var err error - - parsedURL, err := url.Parse(targetURL) - if err != nil { - return nil, err - } -{{- if $queryParams }} - - if params != nil { - queryValues := parsedURL.Query() -{{- range $idx, $param := $queryParams }} - {{- if .HasOptionalPointer }} - if params.{{ .GoName }} != nil { - {{- end }} - {{- if .IsPassThrough }} - queryValues.Add("{{ .Name }}", {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) - {{- else if .IsJSON }} - if queryParamBuf, err := json.Marshal({{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}); err != nil { - return nil, err - } else { - queryValues.Add("{{ .Name }}", string(queryParamBuf)) - } - {{- else if .IsStyled }} - if queryFrag, err := {{ .StyleFunc }}("{{ .Name }}", ParamLocationQuery, {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { - return nil, err - } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } - } - } - {{- end }} - {{- if .HasOptionalPointer }} - } - {{- end }} -{{- end }} - parsedURL.RawQuery = queryValues.Encode() - } -{{- end }} - - req, err := http.NewRequest("{{ .Method }}", parsedURL.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 $idx, $param := $headerParams }} - {{- if .HasOptionalPointer }} - if params.{{ .GoName }} != nil { - {{- end }} - var headerParam{{ $idx }} string - {{- if .IsPassThrough }} - headerParam{{ $idx }} = {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }} - {{- else if .IsJSON }} - var headerParamBuf{{ $idx }} []byte - headerParamBuf{{ $idx }}, err = json.Marshal({{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) - if err != nil { - return nil, err - } - headerParam{{ $idx }} = string(headerParamBuf{{ $idx }}) - {{- else if .IsStyled }} - headerParam{{ $idx }}, err = {{ .StyleFunc }}("{{ .Name }}", ParamLocationHeader, {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) - if err != nil { - return nil, err - } - {{- end }} - req.Header.Set("{{ .Name }}", headerParam{{ $idx }}) - {{- if .HasOptionalPointer }} - } - {{- end }} -{{- end }} - } -{{- end }} -{{- if $cookieParams }} - - if params != nil { -{{- range $idx, $param := $cookieParams }} - {{- if .HasOptionalPointer }} - if params.{{ .GoName }} != nil { - {{- end }} - var cookieParam{{ $idx }} string - {{- if .IsPassThrough }} - cookieParam{{ $idx }} = {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }} - {{- else if .IsJSON }} - var cookieParamBuf{{ $idx }} []byte - cookieParamBuf{{ $idx }}, err = json.Marshal({{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) - if err != nil { - return nil, err - } - cookieParam{{ $idx }} = url.QueryEscape(string(cookieParamBuf{{ $idx }})) - {{- else if .IsStyled }} - cookieParam{{ $idx }}, err = StyleSimpleParam("{{ .Name }}", ParamLocationCookie, {{ if .HasOptionalPointer }}*{{ end }}params.{{ .GoName }}) - if err != nil { - return nil, err - } - {{- end }} - cookie{{ $idx }} := &http.Cookie{ - Name: "{{ .Name }}", - Value: cookieParam{{ $idx }}, - } - req.AddCookie(cookie{{ $idx }}) - {{- if .HasOptionalPointer }} - } - {{- end }} -{{- end }} - } -{{- end }} - - return req, nil -} -{{- end }} diff --git a/experimental/internal/codegen/templates/files/initiator/simple.go.tmpl b/experimental/internal/codegen/templates/files/initiator/simple.go.tmpl deleted file mode 100644 index 008d4095ec..0000000000 --- a/experimental/internal/codegen/templates/files/initiator/simple.go.tmpl +++ /dev/null @@ -1,121 +0,0 @@ -{{/* Simple initiator template - wraps Initiator with typed responses for simple operations */}} -{{/* Input: InitiatorTemplateData */}} - -// {{ .Prefix }}HttpError represents an HTTP error response. -// The type parameter E is the type of the parsed error body. -type {{ .Prefix }}HttpError[E any] struct { - StatusCode int - Body E - RawBody []byte -} - -func (e *{{ .Prefix }}HttpError[E]) Error() string { - return fmt.Sprintf("HTTP %d", e.StatusCode) -} - -// Simple{{ .Prefix }}Initiator wraps {{ .Prefix }}Initiator with typed responses for operations that have -// unambiguous response types. Methods return the success type directly, -// and HTTP errors are returned as *{{ .Prefix }}HttpError[E] where E is the error type. -type Simple{{ .Prefix }}Initiator struct { - *{{ .Prefix }}Initiator -} - -// NewSimple{{ .Prefix }}Initiator creates a new Simple{{ .Prefix }}Initiator which wraps a {{ .Prefix }}Initiator. -func NewSimple{{ .Prefix }}Initiator(opts ...{{ .Prefix }}InitiatorOption) (*Simple{{ .Prefix }}Initiator, error) { - initiator, err := New{{ .Prefix }}Initiator(opts...) - if err != nil { - return nil, err - } - return &Simple{{ .Prefix }}Initiator{ {{ .Prefix }}Initiator: initiator}, nil -} - -{{- range .Operations }} -{{- $op := . }} -{{- $opid := .GoOperationID }} -{{- $hasParams := .HasParams }} -{{- $paramsTypeName := .ParamsTypeName }} - -{{- /* Determine if this operation is "simple" - single success content type, single JSON success response */}} -{{- $simpleOp := isSimpleOperation . }} -{{- if $simpleOp }} -{{- $successResponse := simpleOperationSuccessResponse . }} -{{- $successContent := index $successResponse.Contents 0 }} -{{- $successType := goTypeForContent $successContent }} -{{- $errorResponse := errorResponseForOperation . }} - -// {{ $opid }} sends a {{ .Method }} {{ $.PrefixLower }} request and returns the parsed response. -{{ if .Summary }}// {{ .Summary }}{{ end }} -{{- if $errorResponse }} -{{- $errorContent := index $errorResponse.Contents 0 }} -{{- $errorType := goTypeForContent $errorContent }} -// On success, returns the response body. On HTTP error, returns *{{ $.Prefix }}HttpError[{{ $errorType }}]. -func (p *Simple{{ $.Prefix }}Initiator) {{ $opid }}(ctx context.Context, targetURL string{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}{{ if .HasBody }}, body {{ (index .Bodies 0).GoTypeName }}{{ end }}, reqEditors ...RequestEditorFn) ({{ $successType }}, error) { - var result {{ $successType }} -{{- if .HasBody }} -{{- $defaultBody := index .Bodies 0 }} - resp, err := p.{{ $.Prefix }}Initiator.{{ $opid }}{{ $defaultBody.FuncSuffix }}(ctx, targetURL{{ if $hasParams }}, params{{ end }}, body, reqEditors...) -{{- else }} - resp, err := p.{{ $.Prefix }}Initiator.{{ $opid }}(ctx, targetURL{{ if $hasParams }}, params{{ end }}, reqEditors...) -{{- end }} - if err != nil { - return result, err - } - defer resp.Body.Close() - - rawBody, err := io.ReadAll(resp.Body) - if err != nil { - return result, err - } - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - if err := json.Unmarshal(rawBody, &result); err != nil { - return result, err - } - return result, nil - } - - // Parse error response - var errBody {{ $errorType }} - _ = json.Unmarshal(rawBody, &errBody) // Best effort parse - return result, &{{ $.Prefix }}HttpError[{{ $errorType }}]{ - StatusCode: resp.StatusCode, - Body: errBody, - RawBody: rawBody, - } -} -{{- else }} -// On success, returns the response body. On HTTP error, returns *{{ $.Prefix }}HttpError[struct{}]. -func (p *Simple{{ $.Prefix }}Initiator) {{ $opid }}(ctx context.Context, targetURL string{{ if $hasParams }}, params *{{ $paramsTypeName }}{{ end }}{{ if .HasBody }}, body {{ (index .Bodies 0).GoTypeName }}{{ end }}, reqEditors ...RequestEditorFn) ({{ $successType }}, error) { - var result {{ $successType }} -{{- if .HasBody }} -{{- $defaultBody := index .Bodies 0 }} - resp, err := p.{{ $.Prefix }}Initiator.{{ $opid }}{{ $defaultBody.FuncSuffix }}(ctx, targetURL{{ if $hasParams }}, params{{ end }}, body, reqEditors...) -{{- else }} - resp, err := p.{{ $.Prefix }}Initiator.{{ $opid }}(ctx, targetURL{{ if $hasParams }}, params{{ end }}, reqEditors...) -{{- end }} - if err != nil { - return result, err - } - defer resp.Body.Close() - - rawBody, err := io.ReadAll(resp.Body) - if err != nil { - return result, err - } - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - if err := json.Unmarshal(rawBody, &result); err != nil { - return result, err - } - return result, nil - } - - // No typed error response defined - return result, &{{ $.Prefix }}HttpError[struct{}]{ - StatusCode: resp.StatusCode, - RawBody: rawBody, - } -} -{{- end }} -{{- end }} -{{- end }} diff --git a/experimental/internal/codegen/templates/files/params/bind_deep_object.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_deep_object.go.tmpl deleted file mode 100644 index 403eb5c13d..0000000000 --- a/experimental/internal/codegen/templates/files/params/bind_deep_object.go.tmpl +++ /dev/null @@ -1,271 +0,0 @@ -{{/* BindDeepObjectParam - deepObject style (always exploded) */}} - -// BindDeepObjectParam binds a deepObject-style parameter to a destination. -// DeepObject style is only valid for query parameters and must be exploded. -// Objects: ?paramName[key1]=value1¶mName[key2]=value2 -> struct{Key1, Key2} -// Nested: ?paramName[outer][inner]=value -> struct{Outer: {Inner: value}} -func BindDeepObjectParam(paramName string, queryParams url.Values, dest interface{}) error { - return UnmarshalDeepObject(dest, paramName, queryParams) -} - -// UnmarshalDeepObject unmarshals deepObject-style query parameters to a destination. -func UnmarshalDeepObject(dst interface{}, paramName string, params url.Values) error { - // Find all params that look like "paramName[..." - var fieldNames []string - var fieldValues []string - searchStr := paramName + "[" - - for pName, pValues := range params { - if strings.HasPrefix(pName, searchStr) { - // Trim the parameter name prefix - pName = pName[len(paramName):] - fieldNames = append(fieldNames, pName) - if len(pValues) != 1 { - return fmt.Errorf("%s has multiple values", pName) - } - fieldValues = append(fieldValues, pValues[0]) - } - } - - // Reconstruct subscript paths - paths := make([][]string, len(fieldNames)) - for i, path := range fieldNames { - path = strings.TrimLeft(path, "[") - path = strings.TrimRight(path, "]") - paths[i] = strings.Split(path, "][") - } - - fieldPaths := makeFieldOrValue(paths, fieldValues) - return assignPathValues(dst, fieldPaths) -} - -type fieldOrValue struct { - fields map[string]fieldOrValue - value string -} - -func (f *fieldOrValue) appendPathValue(path []string, value string) { - fieldName := path[0] - if len(path) == 1 { - f.fields[fieldName] = fieldOrValue{value: value} - return - } - - pv, found := f.fields[fieldName] - if !found { - pv = fieldOrValue{ - fields: make(map[string]fieldOrValue), - } - f.fields[fieldName] = pv - } - pv.appendPathValue(path[1:], value) -} - -func makeFieldOrValue(paths [][]string, values []string) fieldOrValue { - f := fieldOrValue{ - fields: make(map[string]fieldOrValue), - } - for i := range paths { - f.appendPathValue(paths[i], values[i]) - } - return f -} - -func getFieldName(f reflect.StructField) string { - n := f.Name - tag, found := f.Tag.Lookup("json") - if found { - parts := strings.Split(tag, ",") - if parts[0] != "" { - n = parts[0] - } - } - return n -} - -func fieldIndicesByJsonTag(i interface{}) (map[string]int, error) { - t := reflect.TypeOf(i) - if t.Kind() != reflect.Struct { - return nil, errors.New("expected a struct as input") - } - - n := t.NumField() - fieldMap := make(map[string]int) - for i := 0; i < n; i++ { - field := t.Field(i) - fieldName := getFieldName(field) - fieldMap[fieldName] = i - } - return fieldMap, nil -} - -func assignPathValues(dst interface{}, pathValues fieldOrValue) error { - v := reflect.ValueOf(dst) - iv := reflect.Indirect(v) - it := iv.Type() - - switch it.Kind() { - case reflect.Map: - dstMap := reflect.MakeMap(iv.Type()) - for key, value := range pathValues.fields { - dstKey := reflect.ValueOf(key) - dstVal := reflect.New(iv.Type().Elem()) - err := assignPathValues(dstVal.Interface(), value) - if err != nil { - return fmt.Errorf("error binding map: %w", err) - } - dstMap.SetMapIndex(dstKey, dstVal.Elem()) - } - iv.Set(dstMap) - return nil - - case reflect.Slice: - sliceLength := len(pathValues.fields) - dstSlice := reflect.MakeSlice(it, sliceLength, sliceLength) - err := assignDeepObjectSlice(dstSlice, pathValues) - if err != nil { - return fmt.Errorf("error assigning slice: %w", err) - } - iv.Set(dstSlice) - return nil - - case reflect.Struct: - // Check for Binder interface - if dst, isBinder := v.Interface().(Binder); isBinder { - return dst.Bind(pathValues.value) - } - - // Handle Date type - if it.ConvertibleTo(reflect.TypeOf(Date{})) { - var date Date - var err error - date.Time, err = time.Parse(DateFormat, pathValues.value) - if err != nil { - return fmt.Errorf("invalid date format: %w", err) - } - dst := iv - if it != reflect.TypeOf(Date{}) { - ivPtr := iv.Addr() - aPtr := ivPtr.Convert(reflect.TypeOf(&Date{})) - dst = reflect.Indirect(aPtr) - } - dst.Set(reflect.ValueOf(date)) - return nil - } - - // Handle time.Time type - if it.ConvertibleTo(reflect.TypeOf(time.Time{})) { - tm, err := time.Parse(time.RFC3339Nano, pathValues.value) - if err != nil { - tm, err = time.Parse(DateFormat, pathValues.value) - if err != nil { - return fmt.Errorf("error parsing '%s' as RFC3339 or date: %w", pathValues.value, err) - } - } - dst := iv - if it != reflect.TypeOf(time.Time{}) { - ivPtr := iv.Addr() - aPtr := ivPtr.Convert(reflect.TypeOf(&time.Time{})) - dst = reflect.Indirect(aPtr) - } - dst.Set(reflect.ValueOf(tm)) - return nil - } - - // Regular struct - fieldMap, err := fieldIndicesByJsonTag(iv.Interface()) - if err != nil { - return fmt.Errorf("failed enumerating fields: %w", err) - } - for _, fieldName := range sortedFieldOrValueKeys(pathValues.fields) { - fieldValue := pathValues.fields[fieldName] - fieldIndex, found := fieldMap[fieldName] - if !found { - return fmt.Errorf("field [%s] is not present in destination object", fieldName) - } - field := iv.Field(fieldIndex) - err = assignPathValues(field.Addr().Interface(), fieldValue) - if err != nil { - return fmt.Errorf("error assigning field [%s]: %w", fieldName, err) - } - } - return nil - - case reflect.Ptr: - dstVal := reflect.New(it.Elem()) - dstPtr := dstVal.Interface() - err := assignPathValues(dstPtr, pathValues) - iv.Set(dstVal) - return err - - case reflect.Bool: - val, err := strconv.ParseBool(pathValues.value) - if err != nil { - return fmt.Errorf("expected a valid bool, got %s", pathValues.value) - } - iv.SetBool(val) - return nil - - case reflect.Float32: - val, err := strconv.ParseFloat(pathValues.value, 32) - if err != nil { - return fmt.Errorf("expected a valid float, got %s", pathValues.value) - } - iv.SetFloat(val) - return nil - - case reflect.Float64: - val, err := strconv.ParseFloat(pathValues.value, 64) - if err != nil { - return fmt.Errorf("expected a valid float, got %s", pathValues.value) - } - iv.SetFloat(val) - return nil - - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - val, err := strconv.ParseInt(pathValues.value, 10, 64) - if err != nil { - return fmt.Errorf("expected a valid int, got %s", pathValues.value) - } - iv.SetInt(val) - return nil - - case reflect.String: - iv.SetString(pathValues.value) - return nil - - default: - return errors.New("unhandled type: " + it.String()) - } -} - -func assignDeepObjectSlice(dst reflect.Value, pathValues fieldOrValue) error { - nValues := len(pathValues.fields) - values := make([]string, nValues) - for i := 0; i < nValues; i++ { - indexStr := strconv.Itoa(i) - fv, found := pathValues.fields[indexStr] - if !found { - return errors.New("array deepObjects must have consecutive indices") - } - values[i] = fv.value - } - - for i := 0; i < nValues; i++ { - dstElem := dst.Index(i).Addr() - err := assignPathValues(dstElem.Interface(), fieldOrValue{value: values[i]}) - if err != nil { - return fmt.Errorf("error binding array: %w", err) - } - } - return nil -} - -func sortedFieldOrValueKeys(m map[string]fieldOrValue) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} diff --git a/experimental/internal/codegen/templates/files/params/bind_form.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_form.go.tmpl deleted file mode 100644 index 6b6b6d63de..0000000000 --- a/experimental/internal/codegen/templates/files/params/bind_form.go.tmpl +++ /dev/null @@ -1,38 +0,0 @@ -{{/* BindFormParam - form style without explode */}} - -// BindFormParam binds a form-style parameter without explode to a destination. -// Form style is the default for query and cookie parameters. -// This function handles a single query parameter value (not url.Values). -// Arrays: a,b,c -> []string{"a", "b", "c"} -// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} -func BindFormParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} diff --git a/experimental/internal/codegen/templates/files/params/bind_form_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_form_explode.go.tmpl deleted file mode 100644 index ec0f66d692..0000000000 --- a/experimental/internal/codegen/templates/files/params/bind_form_explode.go.tmpl +++ /dev/null @@ -1,145 +0,0 @@ -{{/* BindFormExplodeParam - form style with explode */}} - -// BindFormExplodeParam binds a form-style parameter with explode to a destination. -// Form style is the default for query and cookie parameters. -// This handles the exploded case where arrays come as multiple query params. -// Arrays: ?param=a¶m=b -> []string{"a", "b"} (values passed as slice) -// Objects: ?key1=value1&key2=value2 -> struct{Key1, Key2} (queryParams passed) -func BindFormExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { - dv := reflect.Indirect(reflect.ValueOf(dest)) - v := dv - var output interface{} - - if required { - output = dest - } else { - // For optional parameters, allocate if nil - if v.IsNil() { - t := v.Type() - newValue := reflect.New(t.Elem()) - output = newValue.Interface() - } else { - output = v.Interface() - } - v = reflect.Indirect(reflect.ValueOf(output)) - } - - t := v.Type() - k := t.Kind() - - values, found := queryParams[paramName] - - switch k { - case reflect.Slice: - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := bindSplitPartsToDestinationArray(values, output) - if err != nil { - return err - } - case reflect.Struct: - // For exploded objects, fields are spread across query params - fieldsPresent, err := bindParamsToExplodedObject(paramName, queryParams, output) - if err != nil { - return err - } - if !fieldsPresent { - return nil - } - default: - // Primitive - if len(values) == 0 { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - if len(values) != 1 { - return fmt.Errorf("multiple values for single value parameter '%s'", paramName) - } - if !found { - if required { - return fmt.Errorf("query parameter '%s' is required", paramName) - } - return nil - } - err := BindStringToObject(values[0], output) - if err != nil { - return err - } - } - - if !required { - dv.Set(reflect.ValueOf(output)) - } - return nil -} - -// bindParamsToExplodedObject binds query params to struct fields for exploded objects. -func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) { - binder, v, t := indirectBinder(dest) - if binder != nil { - _, found := values[paramName] - if !found { - return false, nil - } - return true, BindStringToObject(values.Get(paramName), dest) - } - if t.Kind() != reflect.Struct { - return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName) - } - - fieldsPresent := false - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - if !v.Field(i).CanSet() { - continue - } - - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - - fieldVal, found := values[fieldName] - if found { - if len(fieldVal) != 1 { - return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName) - } - err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface()) - if err != nil { - return false, fmt.Errorf("could not bind query arg '%s': %w", paramName, err) - } - fieldsPresent = true - } - } - return fieldsPresent, nil -} - -// indirectBinder checks if dest implements Binder and returns reflect values. -func indirectBinder(dest interface{}) (interface{}, reflect.Value, reflect.Type) { - v := reflect.ValueOf(dest) - if v.Type().NumMethod() > 0 && v.CanInterface() { - if u, ok := v.Interface().(Binder); ok { - return u, reflect.Value{}, nil - } - } - v = reflect.Indirect(v) - t := v.Type() - // Handle special types like time.Time and Date - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - return dest, reflect.Value{}, nil - } - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - return dest, reflect.Value{}, nil - } - return nil, v, t -} diff --git a/experimental/internal/codegen/templates/files/params/bind_label.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_label.go.tmpl deleted file mode 100644 index a9bff4ab18..0000000000 --- a/experimental/internal/codegen/templates/files/params/bind_label.go.tmpl +++ /dev/null @@ -1,46 +0,0 @@ -{{/* BindLabelParam - label style without explode */}} - -// BindLabelParam binds a label-style parameter without explode to a destination. -// Label style values are prefixed with a dot. -// Primitives: .value -> "value" -// Arrays: .a,b,c -> []string{"a", "b", "c"} -// Objects: .key1,value1,key2,value2 -> struct{Key1, Key2} -func BindLabelParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Label style requires leading dot - if value[0] != '.' { - return fmt.Errorf("invalid format for label parameter '%s', should start with '.'", paramName) - } - - // Strip the leading dot and split on comma - stripped := value[1:] - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(stripped)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - parts := strings.Split(stripped, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(stripped, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(stripped, dest) - } -} diff --git a/experimental/internal/codegen/templates/files/params/bind_label_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_label_explode.go.tmpl deleted file mode 100644 index 766d29e3ea..0000000000 --- a/experimental/internal/codegen/templates/files/params/bind_label_explode.go.tmpl +++ /dev/null @@ -1,51 +0,0 @@ -{{/* BindLabelExplodeParam - label style with explode */}} - -// BindLabelExplodeParam binds a label-style parameter with explode to a destination. -// Label style values are prefixed with a dot. -// Primitives: .value -> "value" -// Arrays: .a.b.c -> []string{"a", "b", "c"} -// Objects: .key1=value1.key2=value2 -> struct{Key1, Key2} -func BindLabelExplodeParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Label style requires leading dot - if value[0] != '.' { - return fmt.Errorf("invalid format for label parameter '%s', should start with '.'", paramName) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value[1:])) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // Split on dot (skip first empty part) - parts := strings.Split(value, ".") - if parts[0] != "" { - return fmt.Errorf("invalid format for label parameter '%s', should start with '.'", paramName) - } - return bindSplitPartsToDestinationStruct(paramName, parts[1:], true, dest) - case reflect.Slice: - // Split on dot (skip first empty part) - parts := strings.Split(value, ".") - if parts[0] != "" { - return fmt.Errorf("invalid format for label parameter '%s', should start with '.'", paramName) - } - return bindSplitPartsToDestinationArray(parts[1:], dest) - default: - return BindStringToObject(value[1:], dest) - } -} diff --git a/experimental/internal/codegen/templates/files/params/bind_matrix.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_matrix.go.tmpl deleted file mode 100644 index 0702dfa755..0000000000 --- a/experimental/internal/codegen/templates/files/params/bind_matrix.go.tmpl +++ /dev/null @@ -1,47 +0,0 @@ -{{/* BindMatrixParam - matrix style without explode */}} - -// BindMatrixParam binds a matrix-style parameter without explode to a destination. -// Matrix style values are prefixed with ;paramName=. -// Primitives: ;paramName=value -> "value" -// Arrays: ;paramName=a,b,c -> []string{"a", "b", "c"} -// Objects: ;paramName=key1,value1,key2,value2 -> struct{Key1, Key2} -func BindMatrixParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Matrix style requires ;paramName= prefix - prefix := ";" + paramName + "=" - if !strings.HasPrefix(value, prefix) { - return fmt.Errorf("expected parameter '%s' to start with %s", paramName, prefix) - } - - // Strip the prefix - stripped := strings.TrimPrefix(value, prefix) - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(stripped)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - parts := strings.Split(stripped, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(stripped, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(stripped, dest) - } -} diff --git a/experimental/internal/codegen/templates/files/params/bind_matrix_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_matrix_explode.go.tmpl deleted file mode 100644 index 0b2f7ffd9a..0000000000 --- a/experimental/internal/codegen/templates/files/params/bind_matrix_explode.go.tmpl +++ /dev/null @@ -1,65 +0,0 @@ -{{/* BindMatrixExplodeParam - matrix style with explode */}} - -// BindMatrixExplodeParam binds a matrix-style parameter with explode to a destination. -// Matrix style values are prefixed with semicolons. -// Primitives: ;paramName=value -> "value" -// Arrays: ;paramName=a;paramName=b;paramName=c -> []string{"a", "b", "c"} -// Objects: ;key1=value1;key2=value2 -> struct{Key1, Key2} -func BindMatrixExplodeParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Break up on semicolon - parts := strings.Split(value, ";") - // First part should be empty since we start with ; - if parts[0] != "" { - return fmt.Errorf("invalid format for matrix parameter '%s', should start with ';'", paramName) - } - parts = parts[1:] - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - // For primitives, should be ;paramName=value - if len(parts) == 1 { - kv := strings.SplitN(parts[0], "=", 2) - if len(kv) == 2 && kv[0] == paramName { - return tu.UnmarshalText([]byte(kv[1])) - } - } - return fmt.Errorf("invalid format for matrix parameter '%s'", paramName) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // For objects, we have key1=value1, key2=value2 - return bindSplitPartsToDestinationStruct(paramName, parts, true, dest) - case reflect.Slice: - // For arrays, strip paramName= prefix from each part - prefix := paramName + "=" - values := make([]string, len(parts)) - for i, part := range parts { - values[i] = strings.TrimPrefix(part, prefix) - } - return bindSplitPartsToDestinationArray(values, dest) - default: - // Primitive: ;paramName=value - if len(parts) == 1 { - kv := strings.SplitN(parts[0], "=", 2) - if len(kv) == 2 && kv[0] == paramName { - return BindStringToObject(kv[1], dest) - } - } - return fmt.Errorf("invalid format for matrix parameter '%s'", paramName) - } -} diff --git a/experimental/internal/codegen/templates/files/params/bind_pipe_delimited.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_pipe_delimited.go.tmpl deleted file mode 100644 index 1adfd9ef3e..0000000000 --- a/experimental/internal/codegen/templates/files/params/bind_pipe_delimited.go.tmpl +++ /dev/null @@ -1,33 +0,0 @@ -{{/* BindPipeDelimitedParam - pipeDelimited style without explode */}} - -// BindPipeDelimitedParam binds a pipeDelimited-style parameter without explode. -// Pipe-delimited style uses pipe as the separator. -// Arrays: a|b|c -> []string{"a", "b", "c"} -func BindPipeDelimitedParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Slice: - parts := strings.Split(value, "|") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} diff --git a/experimental/internal/codegen/templates/files/params/bind_pipe_delimited_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_pipe_delimited_explode.go.tmpl deleted file mode 100644 index 9af6d4a5f3..0000000000 --- a/experimental/internal/codegen/templates/files/params/bind_pipe_delimited_explode.go.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -{{/* BindPipeDelimitedExplodeParam - pipeDelimited style with explode */}} - -// BindPipeDelimitedExplodeParam binds a pipeDelimited-style parameter with explode. -// When exploded, arrays come as multiple query params (same as form explode). -// Arrays: ?param=a¶m=b -> []string{"a", "b"} -func BindPipeDelimitedExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { - // Exploded pipe-delimited is same as exploded form - return BindFormExplodeParam(paramName, required, queryParams, dest) -} diff --git a/experimental/internal/codegen/templates/files/params/bind_simple.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_simple.go.tmpl deleted file mode 100644 index 3b48a81c15..0000000000 --- a/experimental/internal/codegen/templates/files/params/bind_simple.go.tmpl +++ /dev/null @@ -1,38 +0,0 @@ -{{/* BindSimpleParam - simple style without explode */}} - -// BindSimpleParam binds a simple-style parameter without explode to a destination. -// Simple style is the default for path and header parameters. -// Arrays: a,b,c -> []string{"a", "b", "c"} -// Objects: key1,value1,key2,value2 -> struct{Key1, Key2} -func BindSimpleParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // Split on comma and bind as key,value pairs - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, false, dest) - case reflect.Slice: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} diff --git a/experimental/internal/codegen/templates/files/params/bind_simple_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_simple_explode.go.tmpl deleted file mode 100644 index 3d9468f26a..0000000000 --- a/experimental/internal/codegen/templates/files/params/bind_simple_explode.go.tmpl +++ /dev/null @@ -1,38 +0,0 @@ -{{/* BindSimpleExplodeParam - simple style with explode */}} - -// BindSimpleExplodeParam binds a simple-style parameter with explode to a destination. -// Simple style is the default for path and header parameters. -// Arrays: a,b,c -> []string{"a", "b", "c"} (same as non-explode) -// Objects: key1=value1,key2=value2 -> struct{Key1, Key2} -func BindSimpleExplodeParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Struct: - // Split on comma and bind as key=value pairs - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationStruct(paramName, parts, true, dest) - case reflect.Slice: - parts := strings.Split(value, ",") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} diff --git a/experimental/internal/codegen/templates/files/params/bind_space_delimited.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_space_delimited.go.tmpl deleted file mode 100644 index b7b83685df..0000000000 --- a/experimental/internal/codegen/templates/files/params/bind_space_delimited.go.tmpl +++ /dev/null @@ -1,33 +0,0 @@ -{{/* BindSpaceDelimitedParam - spaceDelimited style without explode */}} - -// BindSpaceDelimitedParam binds a spaceDelimited-style parameter without explode. -// Space-delimited style uses space as the separator. -// Arrays: a b c -> []string{"a", "b", "c"} -func BindSpaceDelimitedParam(paramName string, paramLocation ParamLocation, value string, dest interface{}) error { - if value == "" { - return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName) - } - - // Unescape based on location - var err error - value, err = unescapeParameterString(value, paramLocation) - if err != nil { - return fmt.Errorf("error unescaping parameter '%s': %w", paramName, err) - } - - // Check for TextUnmarshaler - if tu, ok := dest.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(value)) - } - - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - switch t.Kind() { - case reflect.Slice: - parts := strings.Split(value, " ") - return bindSplitPartsToDestinationArray(parts, dest) - default: - return BindStringToObject(value, dest) - } -} diff --git a/experimental/internal/codegen/templates/files/params/bind_space_delimited_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/bind_space_delimited_explode.go.tmpl deleted file mode 100644 index f79f097eff..0000000000 --- a/experimental/internal/codegen/templates/files/params/bind_space_delimited_explode.go.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -{{/* BindSpaceDelimitedExplodeParam - spaceDelimited style with explode */}} - -// BindSpaceDelimitedExplodeParam binds a spaceDelimited-style parameter with explode. -// When exploded, arrays come as multiple query params (same as form explode). -// Arrays: ?param=a¶m=b -> []string{"a", "b"} -func BindSpaceDelimitedExplodeParam(paramName string, required bool, queryParams url.Values, dest interface{}) error { - // Exploded space-delimited is same as exploded form - return BindFormExplodeParam(paramName, required, queryParams, dest) -} diff --git a/experimental/internal/codegen/templates/files/params/helpers.go.tmpl b/experimental/internal/codegen/templates/files/params/helpers.go.tmpl deleted file mode 100644 index b3f650f211..0000000000 --- a/experimental/internal/codegen/templates/files/params/helpers.go.tmpl +++ /dev/null @@ -1,260 +0,0 @@ -{{/* Parameter styling and binding helper functions - included when any param function is used */}} - -// ParamLocation indicates where a parameter is located in an HTTP request. -type ParamLocation int - -const ( - ParamLocationUndefined ParamLocation = iota - ParamLocationQuery - ParamLocationPath - ParamLocationHeader - ParamLocationCookie -) - -// Binder is an interface for types that can bind themselves from a string value. -type Binder interface { - Bind(value string) error -} - -// DateFormat is the format used for date (without time) parameters. -const DateFormat = "2006-01-02" - -// Date represents a date (without time) for OpenAPI date format. -type Date struct { - time.Time -} - -// UnmarshalText implements encoding.TextUnmarshaler for Date. -func (d *Date) UnmarshalText(data []byte) error { - t, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = t - return nil -} - -// MarshalText implements encoding.TextMarshaler for Date. -func (d Date) MarshalText() ([]byte, error) { - return []byte(d.Format(DateFormat)), nil -} - -// Format returns the date formatted according to layout. -func (d Date) Format(layout string) string { - return d.Time.Format(layout) -} - -// primitiveToString converts a primitive value to a string representation. -// It handles basic Go types, time.Time, types.Date, and types that implement -// json.Marshaler or fmt.Stringer. -func primitiveToString(value interface{}) (string, error) { - // Check for known types first (time, date, uuid) - if res, ok := marshalKnownTypes(value); ok { - return res, nil - } - - // Dereference pointers for optional values - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - kind := t.Kind() - - switch kind { - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: - return strconv.FormatInt(v.Int(), 10), nil - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: - return strconv.FormatUint(v.Uint(), 10), nil - case reflect.Float64: - return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil - case reflect.Float32: - return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil - case reflect.Bool: - if v.Bool() { - return "true", nil - } - return "false", nil - case reflect.String: - return v.String(), nil - case reflect.Struct: - // Check if it's a UUID - if u, ok := value.(uuid.UUID); ok { - return u.String(), nil - } - // Check if it implements json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - var i2 interface{} - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to decode JSON: %w", err) - } - return primitiveToString(i2) - } - fallthrough - default: - if s, ok := value.(fmt.Stringer); ok { - return s.String(), nil - } - return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String()) - } -} - -// marshalKnownTypes checks for special types (time.Time, Date, UUID) and marshals them. -func marshalKnownTypes(value interface{}) (string, bool) { - v := reflect.Indirect(reflect.ValueOf(value)) - t := v.Type() - - if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { - tt := v.Convert(reflect.TypeOf(time.Time{})) - timeVal := tt.Interface().(time.Time) - return timeVal.Format(time.RFC3339Nano), true - } - - if t.ConvertibleTo(reflect.TypeOf(Date{})) { - d := v.Convert(reflect.TypeOf(Date{})) - dateVal := d.Interface().(Date) - return dateVal.Format(DateFormat), true - } - - if t.ConvertibleTo(reflect.TypeOf(uuid.UUID{})) { - u := v.Convert(reflect.TypeOf(uuid.UUID{})) - uuidVal := u.Interface().(uuid.UUID) - return uuidVal.String(), true - } - - return "", false -} - -// escapeParameterString escapes a parameter value based on its location. -// Query and path parameters need URL escaping; headers and cookies do not. -func escapeParameterString(value string, paramLocation ParamLocation) string { - switch paramLocation { - case ParamLocationQuery: - return url.QueryEscape(value) - case ParamLocationPath: - return url.PathEscape(value) - default: - return value - } -} - -// unescapeParameterString unescapes a parameter value based on its location. -func unescapeParameterString(value string, paramLocation ParamLocation) (string, error) { - switch paramLocation { - case ParamLocationQuery, ParamLocationUndefined: - return url.QueryUnescape(value) - case ParamLocationPath: - return url.PathUnescape(value) - default: - return value, nil - } -} - -// sortedKeys returns the keys of a map in sorted order. -func sortedKeys(m map[string]string) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -// BindStringToObject binds a string value to a destination object. -// It handles primitives, encoding.TextUnmarshaler, and the Binder interface. -func BindStringToObject(src string, dst interface{}) error { - // Check for TextUnmarshaler - if tu, ok := dst.(encoding.TextUnmarshaler); ok { - return tu.UnmarshalText([]byte(src)) - } - - // Check for Binder interface - if b, ok := dst.(Binder); ok { - return b.Bind(src) - } - - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr { - return fmt.Errorf("dst must be a pointer, got %T", dst) - } - v = v.Elem() - - switch v.Kind() { - case reflect.String: - v.SetString(src) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - i, err := strconv.ParseInt(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - v.SetInt(i) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - u, err := strconv.ParseUint(src, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse uint: %w", err) - } - v.SetUint(u) - case reflect.Float32, reflect.Float64: - f, err := strconv.ParseFloat(src, 64) - if err != nil { - return fmt.Errorf("failed to parse float: %w", err) - } - v.SetFloat(f) - case reflect.Bool: - b, err := strconv.ParseBool(src) - if err != nil { - return fmt.Errorf("failed to parse bool: %w", err) - } - v.SetBool(b) - default: - // Try JSON unmarshal as a fallback - return json.Unmarshal([]byte(src), dst) - } - return nil -} - -// bindSplitPartsToDestinationArray binds a slice of string parts to a destination slice. -func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error { - v := reflect.Indirect(reflect.ValueOf(dest)) - t := v.Type() - - newArray := reflect.MakeSlice(t, len(parts), len(parts)) - for i, p := range parts { - err := BindStringToObject(p, newArray.Index(i).Addr().Interface()) - if err != nil { - return fmt.Errorf("error setting array element: %w", err) - } - } - v.Set(newArray) - return nil -} - -// bindSplitPartsToDestinationStruct binds string parts to a destination struct via JSON. -func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error { - var fields []string - if explode { - fields = make([]string, len(parts)) - for i, property := range parts { - propertyParts := strings.Split(property, "=") - if len(propertyParts) != 2 { - return fmt.Errorf("parameter '%s' has invalid exploded format", paramName) - } - fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\"" - } - } else { - if len(parts)%2 != 0 { - return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName) - } - fields = make([]string, len(parts)/2) - for i := 0; i < len(parts); i += 2 { - key := parts[i] - value := parts[i+1] - fields[i/2] = "\"" + key + "\":\"" + value + "\"" - } - } - jsonParam := "{" + strings.Join(fields, ",") + "}" - return json.Unmarshal([]byte(jsonParam), dest) -} diff --git a/experimental/internal/codegen/templates/files/params/style_deep_object.go.tmpl b/experimental/internal/codegen/templates/files/params/style_deep_object.go.tmpl deleted file mode 100644 index 753ff4433a..0000000000 --- a/experimental/internal/codegen/templates/files/params/style_deep_object.go.tmpl +++ /dev/null @@ -1,74 +0,0 @@ -{{/* StyleDeepObjectParam - deepObject style (always exploded) */}} - -// StyleDeepObjectParam serializes a value using deepObject style. -// DeepObject style is only valid for query parameters with object values and must be exploded. -// Objects: paramName[key1]=value1¶mName[key2]=value2 -// Nested: paramName[outer][inner]=value -func StyleDeepObjectParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // deepObject always requires explode=true - return MarshalDeepObject(value, paramName) -} - -// MarshalDeepObject marshals an object to deepObject style query parameters. -func MarshalDeepObject(i interface{}, paramName string) (string, error) { - // Marshal to JSON first to handle all field annotations - buf, err := json.Marshal(i) - if err != nil { - return "", fmt.Errorf("failed to marshal input to JSON: %w", err) - } - var i2 interface{} - err = json.Unmarshal(buf, &i2) - if err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - fields, err := marshalDeepObjectRecursive(i2, nil) - if err != nil { - return "", fmt.Errorf("error traversing JSON structure: %w", err) - } - - // Prefix the param name to each subscripted field - for idx := range fields { - fields[idx] = paramName + fields[idx] - } - return strings.Join(fields, "&"), nil -} - -func marshalDeepObjectRecursive(in interface{}, path []string) ([]string, error) { - var result []string - - switch t := in.(type) { - case []interface{}: - // Arrays use numerical subscripts [0], [1], etc. - for i, iface := range t { - newPath := append(path, strconv.Itoa(i)) - fields, err := marshalDeepObjectRecursive(iface, newPath) - if err != nil { - return nil, fmt.Errorf("error traversing array: %w", err) - } - result = append(result, fields...) - } - case map[string]interface{}: - // Maps use key subscripts [key1], [key2], etc. - keys := make([]string, 0, len(t)) - for k := range t { - keys = append(keys, k) - } - sort.Strings(keys) - - for _, k := range keys { - newPath := append(path, k) - fields, err := marshalDeepObjectRecursive(t[k], newPath) - if err != nil { - return nil, fmt.Errorf("error traversing map: %w", err) - } - result = append(result, fields...) - } - default: - // Concrete value: turn path into [a][b][c] format - prefix := "[" + strings.Join(path, "][") + "]" - result = []string{ - prefix + fmt.Sprintf("=%v", t), - } - } - return result, nil -} diff --git a/experimental/internal/codegen/templates/files/params/style_form.go.tmpl b/experimental/internal/codegen/templates/files/params/style_form.go.tmpl deleted file mode 100644 index 91df97e6e0..0000000000 --- a/experimental/internal/codegen/templates/files/params/style_form.go.tmpl +++ /dev/null @@ -1,132 +0,0 @@ -{{/* StyleFormParam - form style without explode */}} - -// StyleFormParam serializes a value using form style (RFC 6570) without exploding. -// Form style is the default for query and cookie parameters. -// Primitives: paramName=value -// Arrays: paramName=a,b,c -// Objects: paramName=key1,value1,key2,value2 -func StyleFormParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleFormSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleFormStruct(paramName, paramLocation, value) - case reflect.Map: - return styleFormMap(paramName, paramLocation, value) - default: - return styleFormPrimitive(paramName, paramLocation, value) - } -} - -func styleFormPrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleFormSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Form without explode: paramName=a,b,c - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, ","), nil -} - -func styleFormStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleFormParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Form style without explode: paramName=key1,value1,key2,value2 - prefix := fmt.Sprintf("%s=", paramName) - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return prefix + strings.Join(parts, ","), nil -} - -func styleFormMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Form style without explode: paramName=key1,value1,key2,value2 - prefix := fmt.Sprintf("%s=", paramName) - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return prefix + strings.Join(parts, ","), nil -} diff --git a/experimental/internal/codegen/templates/files/params/style_form_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/style_form_explode.go.tmpl deleted file mode 100644 index a39c67c21b..0000000000 --- a/experimental/internal/codegen/templates/files/params/style_form_explode.go.tmpl +++ /dev/null @@ -1,130 +0,0 @@ -{{/* StyleFormExplodeParam - form style with explode */}} - -// StyleFormExplodeParam serializes a value using form style (RFC 6570) with exploding. -// Form style is the default for query and cookie parameters. -// Primitives: paramName=value -// Arrays: paramName=a¶mName=b¶mName=c -// Objects: key1=value1&key2=value2 -func StyleFormExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleFormExplodeSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleFormExplodeStruct(paramName, paramLocation, value) - case reflect.Map: - return styleFormExplodeMap(paramName, paramLocation, value) - default: - return styleFormExplodePrimitive(paramName, paramLocation, value) - } -} - -func styleFormExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleFormExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Form with explode: paramName=a¶mName=b¶mName=c - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, "&"+prefix), nil -} - -func styleFormExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleFormExplodeParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} - -func styleFormExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Form style with explode: key1=value1&key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, "&"), nil -} diff --git a/experimental/internal/codegen/templates/files/params/style_label.go.tmpl b/experimental/internal/codegen/templates/files/params/style_label.go.tmpl deleted file mode 100644 index def8c74ee0..0000000000 --- a/experimental/internal/codegen/templates/files/params/style_label.go.tmpl +++ /dev/null @@ -1,129 +0,0 @@ -{{/* StyleLabelParam - label style without explode */}} - -// StyleLabelParam serializes a value using label style (RFC 6570) without exploding. -// Label style prefixes values with a dot. -// Primitives: .value -// Arrays: .a,b,c -// Objects: .key1,value1,key2,value2 -func StyleLabelParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return "." + escapeParameterString(string(b), paramLocation), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleLabelSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleLabelStruct(paramName, paramLocation, value) - case reflect.Map: - return styleLabelMap(paramName, paramLocation, value) - default: - return styleLabelPrimitive(paramLocation, value) - } -} - -func styleLabelPrimitive(paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return "." + escapeParameterString(strVal, paramLocation), nil -} - -func styleLabelSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Label without explode: .a,b,c - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return "." + strings.Join(parts, ","), nil -} - -func styleLabelStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return "." + escapeParameterString(timeVal, paramLocation), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleLabelParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Label style without explode: .key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return "." + strings.Join(parts, ","), nil -} - -func styleLabelMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Label style without explode: .key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return "." + strings.Join(parts, ","), nil -} diff --git a/experimental/internal/codegen/templates/files/params/style_label_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/style_label_explode.go.tmpl deleted file mode 100644 index c770480811..0000000000 --- a/experimental/internal/codegen/templates/files/params/style_label_explode.go.tmpl +++ /dev/null @@ -1,129 +0,0 @@ -{{/* StyleLabelExplodeParam - label style with explode */}} - -// StyleLabelExplodeParam serializes a value using label style (RFC 6570) with exploding. -// Label style prefixes values with a dot. -// Primitives: .value -// Arrays: .a.b.c -// Objects: .key1=value1.key2=value2 -func StyleLabelExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return "." + escapeParameterString(string(b), paramLocation), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleLabelExplodeSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleLabelExplodeStruct(paramName, paramLocation, value) - case reflect.Map: - return styleLabelExplodeMap(paramName, paramLocation, value) - default: - return styleLabelExplodePrimitive(paramLocation, value) - } -} - -func styleLabelExplodePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return "." + escapeParameterString(strVal, paramLocation), nil -} - -func styleLabelExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Label with explode: .a.b.c - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return "." + strings.Join(parts, "."), nil -} - -func styleLabelExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return "." + escapeParameterString(timeVal, paramLocation), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleLabelExplodeParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Label style with explode: .key1=value1.key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return "." + strings.Join(parts, "."), nil -} - -func styleLabelExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Label style with explode: .key1=value1.key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return "." + strings.Join(parts, "."), nil -} diff --git a/experimental/internal/codegen/templates/files/params/style_matrix.go.tmpl b/experimental/internal/codegen/templates/files/params/style_matrix.go.tmpl deleted file mode 100644 index 49c001f9dc..0000000000 --- a/experimental/internal/codegen/templates/files/params/style_matrix.go.tmpl +++ /dev/null @@ -1,132 +0,0 @@ -{{/* StyleMatrixParam - matrix style without explode */}} - -// StyleMatrixParam serializes a value using matrix style (RFC 6570) without exploding. -// Matrix style prefixes values with ;paramName=. -// Primitives: ;paramName=value -// Arrays: ;paramName=a,b,c -// Objects: ;paramName=key1,value1,key2,value2 -func StyleMatrixParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf(";%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleMatrixSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleMatrixStruct(paramName, paramLocation, value) - case reflect.Map: - return styleMatrixMap(paramName, paramLocation, value) - default: - return styleMatrixPrimitive(paramName, paramLocation, value) - } -} - -func styleMatrixPrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf(";%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleMatrixSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Matrix without explode: ;paramName=a,b,c - prefix := fmt.Sprintf(";%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, ","), nil -} - -func styleMatrixStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return fmt.Sprintf(";%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleMatrixParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Matrix style without explode: ;paramName=key1,value1,key2,value2 - prefix := fmt.Sprintf(";%s=", paramName) - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return prefix + strings.Join(parts, ","), nil -} - -func styleMatrixMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Matrix style without explode: ;paramName=key1,value1,key2,value2 - prefix := fmt.Sprintf(";%s=", paramName) - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return prefix + strings.Join(parts, ","), nil -} diff --git a/experimental/internal/codegen/templates/files/params/style_matrix_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/style_matrix_explode.go.tmpl deleted file mode 100644 index 63790426f4..0000000000 --- a/experimental/internal/codegen/templates/files/params/style_matrix_explode.go.tmpl +++ /dev/null @@ -1,130 +0,0 @@ -{{/* StyleMatrixExplodeParam - matrix style with explode */}} - -// StyleMatrixExplodeParam serializes a value using matrix style (RFC 6570) with exploding. -// Matrix style prefixes values with ;paramName=. -// Primitives: ;paramName=value -// Arrays: ;paramName=a;paramName=b;paramName=c -// Objects: ;key1=value1;key2=value2 -func StyleMatrixExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf(";%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleMatrixExplodeSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleMatrixExplodeStruct(paramName, paramLocation, value) - case reflect.Map: - return styleMatrixExplodeMap(paramName, paramLocation, value) - default: - return styleMatrixExplodePrimitive(paramName, paramLocation, value) - } -} - -func styleMatrixExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf(";%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleMatrixExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Matrix with explode: ;paramName=a;paramName=b;paramName=c - prefix := fmt.Sprintf(";%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, prefix), nil -} - -func styleMatrixExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return fmt.Sprintf(";%s=%s", paramName, escapeParameterString(timeVal, paramLocation)), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleMatrixExplodeParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Matrix style with explode: ;key1=value1;key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return ";" + strings.Join(parts, ";"), nil -} - -func styleMatrixExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Matrix style with explode: ;key1=value1;key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return ";" + strings.Join(parts, ";"), nil -} diff --git a/experimental/internal/codegen/templates/files/params/style_pipe_delimited.go.tmpl b/experimental/internal/codegen/templates/files/params/style_pipe_delimited.go.tmpl deleted file mode 100644 index de7022ecda..0000000000 --- a/experimental/internal/codegen/templates/files/params/style_pipe_delimited.go.tmpl +++ /dev/null @@ -1,66 +0,0 @@ -{{/* StylePipeDelimitedParam - pipeDelimited style without explode */}} - -// StylePipeDelimitedParam serializes a value using pipeDelimited style without exploding. -// Pipe-delimited style is used for query parameters with array values. -// Arrays: paramName=a|b|c -// Note: Only valid for arrays; objects should use other styles. -func StylePipeDelimitedParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return stylePipeDelimitedSlice(paramName, paramLocation, sliceVal) - default: - // For primitives, fall back to form style - return stylePipeDelimitedPrimitive(paramName, paramLocation, value) - } -} - -func stylePipeDelimitedPrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func stylePipeDelimitedSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Pipe-delimited without explode: paramName=a|b|c - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, "|"), nil -} diff --git a/experimental/internal/codegen/templates/files/params/style_pipe_delimited_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/style_pipe_delimited_explode.go.tmpl deleted file mode 100644 index fa156dea7e..0000000000 --- a/experimental/internal/codegen/templates/files/params/style_pipe_delimited_explode.go.tmpl +++ /dev/null @@ -1,66 +0,0 @@ -{{/* StylePipeDelimitedExplodeParam - pipeDelimited style with explode */}} - -// StylePipeDelimitedExplodeParam serializes a value using pipeDelimited style with exploding. -// Pipe-delimited style is used for query parameters with array values. -// Arrays: paramName=a¶mName=b¶mName=c (same as form explode) -// Note: Only valid for arrays; objects should use other styles. -func StylePipeDelimitedExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return stylePipeDelimitedExplodeSlice(paramName, paramLocation, sliceVal) - default: - // For primitives, fall back to form style - return stylePipeDelimitedExplodePrimitive(paramName, paramLocation, value) - } -} - -func stylePipeDelimitedExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func stylePipeDelimitedExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Pipe-delimited with explode: paramName=a¶mName=b¶mName=c - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, "&"+prefix), nil -} diff --git a/experimental/internal/codegen/templates/files/params/style_simple.go.tmpl b/experimental/internal/codegen/templates/files/params/style_simple.go.tmpl deleted file mode 100644 index 7476b2d4a4..0000000000 --- a/experimental/internal/codegen/templates/files/params/style_simple.go.tmpl +++ /dev/null @@ -1,158 +0,0 @@ -{{/* StyleSimpleParam - simple style without explode */}} - -// StyleSimpleParam serializes a value using simple style (RFC 6570) without exploding. -// Simple style is the default for path and header parameters. -// Arrays are comma-separated: a,b,c -// Objects are key,value pairs: key1,value1,key2,value2 -func StyleSimpleParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return escapeParameterString(string(b), paramLocation), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleSimpleSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleSimpleStruct(paramName, paramLocation, value) - case reflect.Map: - return styleSimpleMap(paramName, paramLocation, value) - default: - return styleSimplePrimitive(paramLocation, value) - } -} - -func styleSimplePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return escapeParameterString(strVal, paramLocation), nil -} - -func styleSimpleSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return escapeParameterString(timeVal, paramLocation), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleSimpleParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Simple style without explode: key1,value1,key2,value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k, v) - } - return strings.Join(parts, ","), nil -} - -// structToFieldDict converts a struct to a map of field names to string values. -func structToFieldDict(value interface{}) (map[string]string, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - fieldDict := make(map[string]string) - - for i := 0; i < t.NumField(); i++ { - fieldT := t.Field(i) - tag := fieldT.Tag.Get("json") - fieldName := fieldT.Name - if tag != "" { - tagParts := strings.Split(tag, ",") - if tagParts[0] != "" { - fieldName = tagParts[0] - } - } - f := v.Field(i) - - // Skip nil optional fields - if f.Type().Kind() == reflect.Ptr && f.IsNil() { - continue - } - str, err := primitiveToString(f.Interface()) - if err != nil { - return nil, fmt.Errorf("error formatting field '%s': %w", fieldName, err) - } - fieldDict[fieldName] = str - } - return fieldDict, nil -} diff --git a/experimental/internal/codegen/templates/files/params/style_simple_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/style_simple_explode.go.tmpl deleted file mode 100644 index 38dc6d3983..0000000000 --- a/experimental/internal/codegen/templates/files/params/style_simple_explode.go.tmpl +++ /dev/null @@ -1,128 +0,0 @@ -{{/* StyleSimpleExplodeParam - simple style with explode */}} - -// StyleSimpleExplodeParam serializes a value using simple style (RFC 6570) with exploding. -// Simple style is the default for path and header parameters. -// Arrays are comma-separated: a,b,c (same as non-explode) -// Objects are key=value pairs: key1=value1,key2=value2 -func StyleSimpleExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return escapeParameterString(string(b), paramLocation), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleSimpleExplodeSlice(paramName, paramLocation, sliceVal) - case reflect.Struct: - return styleSimpleExplodeStruct(paramName, paramLocation, value) - case reflect.Map: - return styleSimpleExplodeMap(paramName, paramLocation, value) - default: - return styleSimpleExplodePrimitive(paramLocation, value) - } -} - -func styleSimpleExplodePrimitive(paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return escapeParameterString(strVal, paramLocation), nil -} - -func styleSimpleExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Exploded simple array is same as non-exploded: comma-separated - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleExplodeStruct(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - // Check for known types first - if timeVal, ok := marshalKnownTypes(value); ok { - return escapeParameterString(timeVal, paramLocation), nil - } - - // Check for json.Marshaler - if m, ok := value.(json.Marshaler); ok { - buf, err := m.MarshalJSON() - if err != nil { - return "", fmt.Errorf("failed to marshal to JSON: %w", err) - } - var i2 interface{} - e := json.NewDecoder(bytes.NewReader(buf)) - e.UseNumber() - if err = e.Decode(&i2); err != nil { - return "", fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return StyleSimpleExplodeParam(paramName, paramLocation, i2) - } - - // Build field dictionary - fieldDict, err := structToFieldDict(value) - if err != nil { - return "", err - } - - // Simple style with explode: key1=value1,key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, ","), nil -} - -func styleSimpleExplodeMap(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - dict, ok := value.(map[string]interface{}) - if !ok { - return "", errors.New("map not of type map[string]interface{}") - } - - fieldDict := make(map[string]string) - for fieldName, val := range dict { - str, err := primitiveToString(val) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - fieldDict[fieldName] = str - } - - // Simple style with explode: key1=value1,key2=value2 - var parts []string - for _, k := range sortedKeys(fieldDict) { - v := escapeParameterString(fieldDict[k], paramLocation) - parts = append(parts, k+"="+v) - } - return strings.Join(parts, ","), nil -} diff --git a/experimental/internal/codegen/templates/files/params/style_space_delimited.go.tmpl b/experimental/internal/codegen/templates/files/params/style_space_delimited.go.tmpl deleted file mode 100644 index 7256e3a43c..0000000000 --- a/experimental/internal/codegen/templates/files/params/style_space_delimited.go.tmpl +++ /dev/null @@ -1,66 +0,0 @@ -{{/* StyleSpaceDelimitedParam - spaceDelimited style without explode */}} - -// StyleSpaceDelimitedParam serializes a value using spaceDelimited style without exploding. -// Space-delimited style is used for query parameters with array values. -// Arrays: paramName=a b c (space-separated) -// Note: Only valid for arrays; objects should use other styles. -func StyleSpaceDelimitedParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleSpaceDelimitedSlice(paramName, paramLocation, sliceVal) - default: - // For primitives, fall back to form style - return styleSpaceDelimitedPrimitive(paramName, paramLocation, value) - } -} - -func styleSpaceDelimitedPrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleSpaceDelimitedSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Space-delimited without explode: paramName=a b c (space becomes %20 when encoded) - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, " "), nil -} diff --git a/experimental/internal/codegen/templates/files/params/style_space_delimited_explode.go.tmpl b/experimental/internal/codegen/templates/files/params/style_space_delimited_explode.go.tmpl deleted file mode 100644 index e32813ba91..0000000000 --- a/experimental/internal/codegen/templates/files/params/style_space_delimited_explode.go.tmpl +++ /dev/null @@ -1,66 +0,0 @@ -{{/* StyleSpaceDelimitedExplodeParam - spaceDelimited style with explode */}} - -// StyleSpaceDelimitedExplodeParam serializes a value using spaceDelimited style with exploding. -// Space-delimited style is used for query parameters with array values. -// Arrays: paramName=a¶mName=b¶mName=c (same as form explode) -// Note: Only valid for arrays; objects should use other styles. -func StyleSpaceDelimitedExplodeParam(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - t := reflect.TypeOf(value) - v := reflect.ValueOf(value) - - // Dereference pointers - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return "", fmt.Errorf("value is a nil pointer") - } - v = reflect.Indirect(v) - t = v.Type() - } - - // Check for TextMarshaler (but not time.Time or Date) - if tu, ok := value.(encoding.TextMarshaler); ok { - innerT := reflect.Indirect(reflect.ValueOf(value)).Type() - if !innerT.ConvertibleTo(reflect.TypeOf(time.Time{})) && !innerT.ConvertibleTo(reflect.TypeOf(Date{})) { - b, err := tu.MarshalText() - if err != nil { - return "", fmt.Errorf("error marshaling '%s' as text: %w", value, err) - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(string(b), paramLocation)), nil - } - } - - switch t.Kind() { - case reflect.Slice: - n := v.Len() - sliceVal := make([]interface{}, n) - for i := 0; i < n; i++ { - sliceVal[i] = v.Index(i).Interface() - } - return styleSpaceDelimitedExplodeSlice(paramName, paramLocation, sliceVal) - default: - // For primitives, fall back to form style - return styleSpaceDelimitedExplodePrimitive(paramName, paramLocation, value) - } -} - -func styleSpaceDelimitedExplodePrimitive(paramName string, paramLocation ParamLocation, value interface{}) (string, error) { - strVal, err := primitiveToString(value) - if err != nil { - return "", err - } - return fmt.Sprintf("%s=%s", paramName, escapeParameterString(strVal, paramLocation)), nil -} - -func styleSpaceDelimitedExplodeSlice(paramName string, paramLocation ParamLocation, values []interface{}) (string, error) { - // Space-delimited with explode: paramName=a¶mName=b¶mName=c - prefix := fmt.Sprintf("%s=", paramName) - parts := make([]string, len(values)) - for i, v := range values { - part, err := primitiveToString(v) - if err != nil { - return "", fmt.Errorf("error formatting '%s': %w", paramName, err) - } - parts[i] = escapeParameterString(part, paramLocation) - } - return prefix + strings.Join(parts, "&"+prefix), nil -} diff --git a/experimental/internal/codegen/templates/files/server/chi/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/chi/handler.go.tmpl deleted file mode 100644 index ccd9653b40..0000000000 --- a/experimental/internal/codegen/templates/files/server/chi/handler.go.tmpl +++ /dev/null @@ -1,59 +0,0 @@ -{{- /* - This template generates the HTTP handler and routing for Chi servers. - Input: []OperationDescriptor -*/ -}} - -// Handler creates http.Handler with routing matching OpenAPI spec. -func Handler(si ServerInterface) http.Handler { - return HandlerWithOptions(si, ChiServerOptions{}) -} - -// ChiServerOptions configures the Chi server. -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, - }) -} - -// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. -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+"{{ pathToChiPattern .Path }}", wrapper.{{ .GoOperationID }}) - }) -{{- end }} - return r -} diff --git a/experimental/internal/codegen/templates/files/server/chi/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/chi/interface.go.tmpl deleted file mode 100644 index 010ed1b157..0000000000 --- a/experimental/internal/codegen/templates/files/server/chi/interface.go.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -{{- /* - This template generates the ServerInterface for Chi servers. - Input: []OperationDescriptor -*/ -}} - -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{- range . }} -{{ .SummaryAsComment }} - // ({{ .Method }} {{ .Path }}) - {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) -{{- end }} -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. -type Unimplemented struct{} - -{{- range . }} -{{ .SummaryAsComment }} -// ({{ .Method }} {{ .Path }}) -func (_ Unimplemented) {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) { - w.WriteHeader(http.StatusNotImplemented) -} -{{- end }} diff --git a/experimental/internal/codegen/templates/files/server/chi/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/chi/receiver.go.tmpl deleted file mode 100644 index dce41f1d14..0000000000 --- a/experimental/internal/codegen/templates/files/server/chi/receiver.go.tmpl +++ /dev/null @@ -1,95 +0,0 @@ -{{- /* - This template generates the receiver interface and handler functions for Chi. - Input: ReceiverTemplateData -*/ -}} - -// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. -type {{ .Prefix }}ReceiverInterface interface { -{{- range .Operations }} -{{ .SummaryAsComment }} - // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. - Handle{{ .GoOperationID }}{{ $.Prefix }}(w http.ResponseWriter, r *http.Request{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) -{{- end }} -} - -// {{ .Prefix }}ReceiverMiddlewareFunc is a middleware function for {{ $.PrefixLower }} receiver handlers. -type {{ .Prefix }}ReceiverMiddlewareFunc func(http.Handler) http.Handler - -{{ range .Operations }} -// {{ .GoOperationID }}{{ $.Prefix }}Handler returns an http.Handler for the {{ .GoOperationID }} {{ $.PrefixLower }}. -// The caller is responsible for registering this handler at the appropriate path. -func {{ .GoOperationID }}{{ $.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) - } - } - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -{{- if .HasParams }} - var err error - _ = err - - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} -{{- if or .Required .IsPassThrough .IsJSON }} - if paramValue := r.URL.Query().Get("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - errHandler(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - errHandler(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) - return - }{{ end }} -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, r.URL.Query(), ¶ms.{{ .GoName }}) - if err != nil { - errHandler(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{ end }} -{{ range .HeaderParams }} - if valueList, found := r.Header[http.CanonicalHeaderKey("{{ .Name }}")]; found { - var {{ .GoVariableName }} {{ .TypeDecl }} - n := len(valueList) - if n != 1 { - errHandler(w, r, &TooManyValuesForParamError{ParamName: "{{ .Name }}", Count: n}) - return - } -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) - if err != nil { - errHandler(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - errHandler(w, r, &RequiredHeaderError{ParamName: "{{ .Name }}", Err: fmt.Errorf("header parameter {{ .Name }} is required, but not found")}) - return - }{{ end }} -{{ end }} -{{- end }} - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - si.Handle{{ .GoOperationID }}{{ $.Prefix }}(w, r{{ if .HasParams }}, params{{ end }}) - })) - - for _, middleware := range middlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) - }) -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/chi/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/chi/wrapper.go.tmpl deleted file mode 100644 index 91d346b682..0000000000 --- a/experimental/internal/codegen/templates/files/server/chi/wrapper.go.tmpl +++ /dev/null @@ -1,166 +0,0 @@ -{{- /* - This template generates the ServerInterfaceWrapper that extracts parameters - from HTTP requests and calls the ServerInterface methods for Chi. - Input: []OperationDescriptor -*/ -}} - -// ServerInterfaceWrapper converts HTTP requests to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -// MiddlewareFunc is a middleware function type. -type MiddlewareFunc func(http.Handler) http.Handler - -{{ range . }} -// {{ .GoOperationID }} operation middleware -func (siw *ServerInterfaceWrapper) {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request) { -{{- if or .PathParams .HasParams }} - var err error -{{- end }} -{{ range .PathParams }} - // ------------- Path parameter "{{ .Name }}" ------------- - var {{ .GoVariableName }} {{ .TypeDecl }} -{{ if .IsPassThrough }} - {{ .GoVariableName }} = chi.URLParam(r, "{{ .Name }}") -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(chi.URLParam(r, "{{ .Name }}")), &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, chi.URLParam(r, "{{ .Name }}"), &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{ end }} -{{- if .Security }} - ctx := r.Context() -{{- range .Security }} - ctx = context.WithValue(ctx, {{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) -{{- end }} - r = r.WithContext(ctx) -{{- end }} -{{ if .HasParams }} - // Parameter object where we will unmarshal all parameters from the context - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- -{{- if or .Required .IsPassThrough .IsJSON }} - if paramValue := r.URL.Query().Get("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) - return - }{{ end }} -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, r.URL.Query(), ¶ms.{{ .GoName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{ end }} -{{ if .HeaderParams }} - headers := r.Header -{{ range .HeaderParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found { - var {{ .GoVariableName }} {{ .TypeDecl }} - n := len(valueList) - if n != 1 { - siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "{{ .Name }}", Count: n}) - return - } -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - err := fmt.Errorf("Header parameter {{ .Name }} is required, but not found") - siw.ErrorHandlerFunc(w, r, &RequiredHeaderError{ParamName: "{{ .Name }}", Err: err}) - return - }{{ end }} -{{ end }} -{{ end }} -{{ range .CookieParams }} - { - var cookie *http.Cookie - if cookie, err = r.Cookie("{{ .Name }}"); err == nil { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie.Value -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - decoded, err := url.QueryUnescape(cookie.Value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnescapedCookieParamError{ParamName: "{{ .Name }}", Err: err}) - return - } - err = json.Unmarshal([]byte(decoded), &value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} -{{- if .IsStyled }} - var value {{ .TypeDecl }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie.Value, &value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) - return - }{{ end }} - } -{{ end }} -{{ end }} - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.{{ .GoOperationID }}(w, r{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/echo-v4/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/echo-v4/handler.go.tmpl deleted file mode 100644 index fcbb54b60d..0000000000 --- a/experimental/internal/codegen/templates/files/server/echo-v4/handler.go.tmpl +++ /dev/null @@ -1,34 +0,0 @@ -{{- /* - This template generates the HTTP handler and routing for Echo servers. - Input: []OperationDescriptor -*/ -}} - -// EchoRouter is an interface for echo.Echo and echo.Group. -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 -} - -// RegisterHandlers adds each server route to the EchoRouter. -func RegisterHandlers(router EchoRouter, si ServerInterface) { - RegisterHandlersWithBaseURL(router, si, "") -} - -// RegisterHandlersWithBaseURL adds each server route to the EchoRouter with a base URL prefix. -func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { -{{ if . }} - wrapper := ServerInterfaceWrapper{ - Handler: si, - } -{{ end }} -{{- range . }} - router.{{ .Method }}(baseURL+"{{ pathToEchoPattern .Path }}", wrapper.{{ .GoOperationID }}) -{{- end }} -} diff --git a/experimental/internal/codegen/templates/files/server/echo-v4/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/echo-v4/interface.go.tmpl deleted file mode 100644 index 22cb0ebe68..0000000000 --- a/experimental/internal/codegen/templates/files/server/echo-v4/interface.go.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -{{- /* - This template generates the ServerInterface for Echo servers. - Input: []OperationDescriptor -*/ -}} - -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{- range . }} -{{ .SummaryAsComment }} - // ({{ .Method }} {{ .Path }}) - {{ .GoOperationID }}(ctx echo.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error -{{- end }} -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. -type Unimplemented struct{} - -{{- range . }} -{{ .SummaryAsComment }} -// ({{ .Method }} {{ .Path }}) -func (_ Unimplemented) {{ .GoOperationID }}(ctx echo.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error { - return ctx.NoContent(http.StatusNotImplemented) -} -{{- end }} diff --git a/experimental/internal/codegen/templates/files/server/echo-v4/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/echo-v4/receiver.go.tmpl deleted file mode 100644 index 0fa1b97708..0000000000 --- a/experimental/internal/codegen/templates/files/server/echo-v4/receiver.go.tmpl +++ /dev/null @@ -1,72 +0,0 @@ -{{- /* - This template generates the receiver interface and handler functions for Echo v4. - Input: ReceiverTemplateData -*/ -}} - -// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. -type {{ .Prefix }}ReceiverInterface interface { -{{- range .Operations }} -{{ .SummaryAsComment }} - // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. - Handle{{ .GoOperationID }}{{ $.Prefix }}(ctx echo.Context{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error -{{- end }} -} - -{{ range .Operations }} -// {{ .GoOperationID }}{{ $.Prefix }}Handler returns an echo.HandlerFunc for the {{ .GoOperationID }} {{ $.PrefixLower }}. -// The caller is responsible for registering this handler at the appropriate path. -func {{ .GoOperationID }}{{ $.Prefix }}Handler(si {{ $.Prefix }}ReceiverInterface) echo.HandlerFunc { - return func(ctx echo.Context) error { -{{- if .HasParams }} - var err error - _ = err - - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} -{{- if or .Required .IsPassThrough .IsJSON }} - if paramValue := ctx.QueryParam("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query parameter {{ .Name }} is required")) - }{{ end }} -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, ctx.QueryParams(), ¶ms.{{ .GoName }}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- end }} -{{ end }} -{{ range .HeaderParams }} - if valueList := ctx.Request().Header[http.CanonicalHeaderKey("{{ .Name }}")]; len(valueList) > 0 { - var {{ .GoVariableName }} {{ .TypeDecl }} - n := len(valueList) - if n != 1 { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for {{ .Name }}, got %d", n)) - } -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter {{ .Name }} is required")) - }{{ end }} -{{ end }} -{{- end }} - return si.Handle{{ .GoOperationID }}{{ $.Prefix }}(ctx{{ if .HasParams }}, params{{ end }}) - } -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/echo-v4/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/echo-v4/wrapper.go.tmpl deleted file mode 100644 index 6847388a7e..0000000000 --- a/experimental/internal/codegen/templates/files/server/echo-v4/wrapper.go.tmpl +++ /dev/null @@ -1,134 +0,0 @@ -{{- /* - This template generates the ServerInterfaceWrapper that extracts parameters - from HTTP requests and calls the ServerInterface methods for Echo. - Input: []OperationDescriptor -*/ -}} - -// ServerInterfaceWrapper converts echo contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface -} - -{{ range . }} -// {{ .GoOperationID }} converts echo context to params. -func (w *ServerInterfaceWrapper) {{ .GoOperationID }}(ctx echo.Context) error { - var err error - -{{ range .PathParams }} - // ------------- Path parameter "{{ .Name }}" ------------- - var {{ .GoVariableName }} {{ .TypeDecl }} -{{ if .IsPassThrough }} - {{ .GoVariableName }} = ctx.Param("{{ .Name }}") -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(ctx.Param("{{ .Name }}")), &{{ .GoVariableName }}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, ctx.Param("{{ .Name }}"), &{{ .GoVariableName }}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- end }} -{{ end }} -{{- if .Security }} -{{- range .Security }} - ctx.Set({{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) -{{- end }} -{{- end }} -{{ if .HasParams }} - // Parameter object where we will unmarshal all parameters from the context - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, ctx.QueryParams(), ¶ms.{{ .GoName }}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- else }} - if paramValue := ctx.QueryParam("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{ .Name }} is required, but not found")) - }{{ end }} -{{- end }} -{{ end }} -{{ if .HeaderParams }} - headers := ctx.Request().Header -{{ range .HeaderParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found { - var {{ .GoVariableName }} {{ .TypeDecl }} - n := len(valueList) - if n != 1 { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for {{ .Name }}, got %d", n)) - } -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter {{ .Name }} is required, but not found")) - }{{ end }} -{{ end }} -{{ end }} -{{ range .CookieParams }} - if cookie, err := ctx.Cookie("{{ .Name }}"); err == nil { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie.Value -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - decoded, err := url.QueryUnescape(cookie.Value) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unescaping cookie parameter '%s'", "{{ .Name }}")) - } - err = json.Unmarshal([]byte(decoded), &value) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} -{{- if .IsStyled }} - var value {{ .TypeDecl }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie.Value, &value) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{ .Name }} is required, but not found")) - }{{ end }} -{{ end }} -{{ end }} - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.{{ .GoOperationID }}(ctx{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) - return err -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/echo/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/echo/handler.go.tmpl deleted file mode 100644 index 0e6d2dbd01..0000000000 --- a/experimental/internal/codegen/templates/files/server/echo/handler.go.tmpl +++ /dev/null @@ -1,35 +0,0 @@ -{{- /* - This template generates the HTTP handler and routing for Echo v5 servers. - Input: []OperationDescriptor -*/ -}} - -// EchoRouter is an interface for echo.Echo and echo.Group. -type EchoRouter interface { - Add(method string, path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo - 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 -} - -// RegisterHandlers adds each server route to the EchoRouter. -func RegisterHandlers(router EchoRouter, si ServerInterface) { - RegisterHandlersWithBaseURL(router, si, "") -} - -// RegisterHandlersWithBaseURL adds each server route to the EchoRouter with a base URL prefix. -func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { -{{ if . }} - wrapper := ServerInterfaceWrapper{ - Handler: si, - } -{{ end }} -{{- range . }} - router.{{ .Method }}(baseURL+"{{ pathToEchoPattern .Path }}", wrapper.{{ .GoOperationID }}) -{{- end }} -} diff --git a/experimental/internal/codegen/templates/files/server/echo/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/echo/interface.go.tmpl deleted file mode 100644 index e1c4ac779a..0000000000 --- a/experimental/internal/codegen/templates/files/server/echo/interface.go.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -{{- /* - This template generates the ServerInterface for Echo v5 servers. - Input: []OperationDescriptor -*/ -}} - -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{- range . }} -{{ .SummaryAsComment }} - // ({{ .Method }} {{ .Path }}) - {{ .GoOperationID }}(ctx *echo.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error -{{- end }} -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. -type Unimplemented struct{} - -{{- range . }} -{{ .SummaryAsComment }} -// ({{ .Method }} {{ .Path }}) -func (_ Unimplemented) {{ .GoOperationID }}(ctx *echo.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error { - return ctx.NoContent(http.StatusNotImplemented) -} -{{- end }} diff --git a/experimental/internal/codegen/templates/files/server/echo/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/echo/receiver.go.tmpl deleted file mode 100644 index 8a336c5e23..0000000000 --- a/experimental/internal/codegen/templates/files/server/echo/receiver.go.tmpl +++ /dev/null @@ -1,72 +0,0 @@ -{{- /* - This template generates the receiver interface and handler functions for Echo v5. - Input: ReceiverTemplateData -*/ -}} - -// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. -type {{ .Prefix }}ReceiverInterface interface { -{{- range .Operations }} -{{ .SummaryAsComment }} - // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. - Handle{{ .GoOperationID }}{{ $.Prefix }}(ctx *echo.Context{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error -{{- end }} -} - -{{ range .Operations }} -// {{ .GoOperationID }}{{ $.Prefix }}Handler returns an echo.HandlerFunc for the {{ .GoOperationID }} {{ $.PrefixLower }}. -// The caller is responsible for registering this handler at the appropriate path. -func {{ .GoOperationID }}{{ $.Prefix }}Handler(si {{ $.Prefix }}ReceiverInterface) echo.HandlerFunc { - return func(ctx *echo.Context) error { -{{- if .HasParams }} - var err error - _ = err - - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} -{{- if or .Required .IsPassThrough .IsJSON }} - if paramValue := ctx.QueryParam("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query parameter {{ .Name }} is required")) - }{{ end }} -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, ctx.QueryParams(), ¶ms.{{ .GoName }}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- end }} -{{ end }} -{{ range .HeaderParams }} - if valueList := ctx.Request().Header[http.CanonicalHeaderKey("{{ .Name }}")]; len(valueList) > 0 { - var {{ .GoVariableName }} {{ .TypeDecl }} - n := len(valueList) - if n != 1 { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for {{ .Name }}, got %d", n)) - } -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter {{ .Name }} is required")) - }{{ end }} -{{ end }} -{{- end }} - return si.Handle{{ .GoOperationID }}{{ $.Prefix }}(ctx{{ if .HasParams }}, params{{ end }}) - } -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/echo/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/echo/wrapper.go.tmpl deleted file mode 100644 index ae92e02879..0000000000 --- a/experimental/internal/codegen/templates/files/server/echo/wrapper.go.tmpl +++ /dev/null @@ -1,134 +0,0 @@ -{{- /* - This template generates the ServerInterfaceWrapper that extracts parameters - from HTTP requests and calls the ServerInterface methods for Echo v5. - Input: []OperationDescriptor -*/ -}} - -// ServerInterfaceWrapper converts echo contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface -} - -{{ range . }} -// {{ .GoOperationID }} converts echo context to params. -func (w *ServerInterfaceWrapper) {{ .GoOperationID }}(ctx *echo.Context) error { - var err error - -{{ range .PathParams }} - // ------------- Path parameter "{{ .Name }}" ------------- - var {{ .GoVariableName }} {{ .TypeDecl }} -{{ if .IsPassThrough }} - {{ .GoVariableName }} = ctx.Param("{{ .Name }}") -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(ctx.Param("{{ .Name }}")), &{{ .GoVariableName }}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, ctx.Param("{{ .Name }}"), &{{ .GoVariableName }}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- end }} -{{ end }} -{{- if .Security }} -{{- range .Security }} - ctx.Set({{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) -{{- end }} -{{- end }} -{{ if .HasParams }} - // Parameter object where we will unmarshal all parameters from the context - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, ctx.QueryParams(), ¶ms.{{ .GoName }}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- else }} - if paramValue := ctx.QueryParam("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{ .Name }} is required, but not found")) - }{{ end }} -{{- end }} -{{ end }} -{{ if .HeaderParams }} - headers := ctx.Request().Header -{{ range .HeaderParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found { - var {{ .GoVariableName }} {{ .TypeDecl }} - n := len(valueList) - if n != 1 { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for {{ .Name }}, got %d", n)) - } -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter {{ .Name }} is required, but not found")) - }{{ end }} -{{ end }} -{{ end }} -{{ range .CookieParams }} - if cookie, err := ctx.Cookie("{{ .Name }}"); err == nil { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie.Value -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - decoded, err := url.QueryUnescape(cookie.Value) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unescaping cookie parameter '%s'", "{{ .Name }}")) - } - err = json.Unmarshal([]byte(decoded), &value) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} -{{- if .IsStyled }} - var value {{ .TypeDecl }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie.Value, &value) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{ .Name }} is required, but not found")) - }{{ end }} -{{ end }} -{{ end }} - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.{{ .GoOperationID }}(ctx{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) - return err -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/errors.go.tmpl b/experimental/internal/codegen/templates/files/server/errors.go.tmpl deleted file mode 100644 index c4727ef976..0000000000 --- a/experimental/internal/codegen/templates/files/server/errors.go.tmpl +++ /dev/null @@ -1,79 +0,0 @@ -{{- /* - This template generates error types for server parameter handling. - These are shared across all router implementations. -*/ -}} - -// UnescapedCookieParamError is returned when a cookie parameter cannot be unescaped. -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 -} - -// UnmarshalingParamError is returned when a parameter cannot be unmarshaled. -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 -} - -// RequiredParamError is returned when a required parameter is missing. -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -// RequiredHeaderError is returned when a required header is missing. -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 -} - -// InvalidParamFormatError is returned when a parameter has an invalid format. -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 -} - -// TooManyValuesForParamError is returned when a parameter has too many values. -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/experimental/internal/codegen/templates/files/server/fiber/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/fiber/handler.go.tmpl deleted file mode 100644 index b6e59cfb75..0000000000 --- a/experimental/internal/codegen/templates/files/server/fiber/handler.go.tmpl +++ /dev/null @@ -1,31 +0,0 @@ -{{- /* - This template generates the HTTP handler and routing for Fiber servers. - Input: []OperationDescriptor -*/ -}} - -// FiberServerOptions provides options for the Fiber server. -type FiberServerOptions struct { - BaseURL string - Middlewares []fiber.Handler -} - -// 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, - } - - for _, m := range options.Middlewares { - router.Use(m) - } -{{ end }} -{{- range . }} - router.{{ .Method | lower | title }}(options.BaseURL+"{{ pathToFiberPattern .Path }}", wrapper.{{ .GoOperationID }}) -{{- end }} -} diff --git a/experimental/internal/codegen/templates/files/server/fiber/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/fiber/interface.go.tmpl deleted file mode 100644 index dddefc81c6..0000000000 --- a/experimental/internal/codegen/templates/files/server/fiber/interface.go.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -{{- /* - This template generates the ServerInterface for Fiber servers. - Input: []OperationDescriptor -*/ -}} - -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{- range . }} -{{ .SummaryAsComment }} - // ({{ .Method }} {{ .Path }}) - {{ .GoOperationID }}(c fiber.Ctx{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error -{{- end }} -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. -type Unimplemented struct{} - -{{- range . }} -{{ .SummaryAsComment }} -// ({{ .Method }} {{ .Path }}) -func (_ Unimplemented) {{ .GoOperationID }}(c fiber.Ctx{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error { - return c.SendStatus(fiber.StatusNotImplemented) -} -{{- end }} diff --git a/experimental/internal/codegen/templates/files/server/fiber/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/fiber/receiver.go.tmpl deleted file mode 100644 index 5775f45ed1..0000000000 --- a/experimental/internal/codegen/templates/files/server/fiber/receiver.go.tmpl +++ /dev/null @@ -1,71 +0,0 @@ -{{- /* - This template generates the receiver interface and handler functions for Fiber. - Input: ReceiverTemplateData -*/ -}} - -// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. -type {{ .Prefix }}ReceiverInterface interface { -{{- range .Operations }} -{{ .SummaryAsComment }} - // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. - Handle{{ .GoOperationID }}{{ $.Prefix }}(c fiber.Ctx{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) error -{{- end }} -} - -{{ range .Operations }} -// {{ .GoOperationID }}{{ $.Prefix }}Handler returns a fiber.Handler for the {{ .GoOperationID }} {{ $.PrefixLower }}. -// The caller is responsible for registering this handler at the appropriate path. -func {{ .GoOperationID }}{{ $.Prefix }}Handler(si {{ $.Prefix }}ReceiverInterface) fiber.Handler { - return func(c fiber.Ctx) error { -{{- if .HasParams }} - var err error - _ = err - - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} -{{- if or .Required .IsPassThrough .IsJSON }} - if paramValue := c.Query("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - return fiber.NewError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - return fiber.NewError(http.StatusBadRequest, "Query parameter {{ .Name }} is required") - }{{ end }} -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, c.Queries(), ¶ms.{{ .GoName }}) - if err != nil { - return fiber.NewError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- end }} -{{ end }} -{{ range .HeaderParams }} - { - headerValue := c.Get("{{ .Name }}") - if headerValue != "" { - var {{ .GoVariableName }} {{ .TypeDecl }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, headerValue, &{{ .GoVariableName }}) - if err != nil { - return fiber.NewError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - return fiber.NewError(http.StatusBadRequest, "Header parameter {{ .Name }} is required") - }{{ end }} - } -{{ end }} -{{- end }} - return si.Handle{{ .GoOperationID }}{{ $.Prefix }}(c{{ if .HasParams }}, params{{ end }}) - } -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/fiber/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/fiber/wrapper.go.tmpl deleted file mode 100644 index 7abbd2da7a..0000000000 --- a/experimental/internal/codegen/templates/files/server/fiber/wrapper.go.tmpl +++ /dev/null @@ -1,137 +0,0 @@ -{{- /* - This template generates the ServerInterfaceWrapper that extracts parameters - from HTTP requests and calls the ServerInterface methods for Fiber. - Input: []OperationDescriptor -*/ -}} - -// ServerInterfaceWrapper converts contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface -} - -{{ range . }} -// {{ .GoOperationID }} operation middleware -func (siw *ServerInterfaceWrapper) {{ .GoOperationID }}(c fiber.Ctx) error { -{{- if or .PathParams .HasParams }} - var err error -{{- end }} -{{ range .PathParams }} - // ------------- Path parameter "{{ .Name }}" ------------- - var {{ .GoVariableName }} {{ .TypeDecl }} -{{ if .IsPassThrough }} - {{ .GoVariableName }} = c.Params("{{ .Name }}") -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(c.Params("{{ .Name }}")), &{{ .GoVariableName }}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON: %s", "{{ .Name }}", err)) - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, c.Params("{{ .Name }}"), &{{ .GoVariableName }}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- end }} -{{ end }} -{{- if .Security }} -{{- range .Security }} - c.Locals({{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) -{{- end }} -{{- end }} -{{ if .HasParams }} - // Parameter object where we will unmarshal all parameters from the context - var params {{ .ParamsTypeName }} -{{ if .QueryParams }} - var query url.Values - query, err = url.ParseQuery(string(c.Request().URI().QueryString())) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for query string: %s", err)) - } -{{ end }} -{{ range .QueryParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, query, ¶ms.{{ .GoName }}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- else if or .Required .IsPassThrough .IsJSON }} - if paramValue := c.Query("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON: %s", "{{ .Name }}", err)) - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - return fiber.NewError(fiber.StatusBadRequest, "Query argument {{ .Name }} is required, but not found") - }{{ end }} -{{- end }} -{{ end }} -{{ if .HeaderParams }} - headers := c.GetReqHeaders() -{{ range .HeaderParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found && len(valueList) > 0 { - var {{ .GoVariableName }} {{ .TypeDecl }} - value := valueList[0] -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(value), &{{ .GoVariableName }}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON: %s", "{{ .Name }}", err)) - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, value, &{{ .GoVariableName }}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - return fiber.NewError(fiber.StatusBadRequest, "Header parameter {{ .Name }} is required, but not found") - }{{ end }} -{{ end }} -{{ end }} -{{ range .CookieParams }} - if cookie := c.Cookies("{{ .Name }}"); cookie != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - decoded, err := url.QueryUnescape(cookie) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error unescaping cookie parameter '%s': %s", "{{ .Name }}", err)) - } - err = json.Unmarshal([]byte(decoded), &value) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error unmarshaling parameter '%s' as JSON: %s", "{{ .Name }}", err)) - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} -{{- if .IsStyled }} - var value {{ .TypeDecl }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie, &value) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - return fiber.NewError(fiber.StatusBadRequest, "Query argument {{ .Name }} is required, but not found") - }{{ end }} -{{ end }} -{{ end }} - return siw.Handler.{{ .GoOperationID }}(c{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/gin/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/gin/handler.go.tmpl deleted file mode 100644 index 3ba848a34a..0000000000 --- a/experimental/internal/codegen/templates/files/server/gin/handler.go.tmpl +++ /dev/null @@ -1,37 +0,0 @@ -{{- /* - This template generates the HTTP handler and routing for Gin servers. - Input: []OperationDescriptor -*/ -}} - -// GinServerOptions provides options for the Gin server. -type GinServerOptions struct { - BaseURL string - Middlewares []MiddlewareFunc - ErrorHandler func(*gin.Context, error, int) -} - -// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. -func RegisterHandlers(router gin.IRouter, si ServerInterface) { - RegisterHandlersWithOptions(router, si, GinServerOptions{}) -} - -// RegisterHandlersWithOptions creates http.Handler with additional options. -func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options GinServerOptions) { -{{ if . }} - errorHandler := options.ErrorHandler - if errorHandler == nil { - errorHandler = func(c *gin.Context, err error, statusCode int) { - c.JSON(statusCode, gin.H{"msg": err.Error()}) - } - } - - wrapper := ServerInterfaceWrapper{ - Handler: si, - HandlerMiddlewares: options.Middlewares, - ErrorHandler: errorHandler, - } -{{ end }} -{{- range . }} - router.{{ .Method }}(options.BaseURL+"{{ pathToGinPattern .Path }}", wrapper.{{ .GoOperationID }}) -{{- end }} -} diff --git a/experimental/internal/codegen/templates/files/server/gin/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/gin/interface.go.tmpl deleted file mode 100644 index a2b22df6d6..0000000000 --- a/experimental/internal/codegen/templates/files/server/gin/interface.go.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -{{- /* - This template generates the ServerInterface for Gin servers. - Input: []OperationDescriptor -*/ -}} - -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{- range . }} -{{ .SummaryAsComment }} - // ({{ .Method }} {{ .Path }}) - {{ .GoOperationID }}(c *gin.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) -{{- end }} -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. -type Unimplemented struct{} - -{{- range . }} -{{ .SummaryAsComment }} -// ({{ .Method }} {{ .Path }}) -func (_ Unimplemented) {{ .GoOperationID }}(c *gin.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) { - c.Status(http.StatusNotImplemented) -} -{{- end }} diff --git a/experimental/internal/codegen/templates/files/server/gin/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/gin/receiver.go.tmpl deleted file mode 100644 index 1b150abc7b..0000000000 --- a/experimental/internal/codegen/templates/files/server/gin/receiver.go.tmpl +++ /dev/null @@ -1,78 +0,0 @@ -{{- /* - This template generates the receiver interface and handler functions for Gin. - Input: ReceiverTemplateData -*/ -}} - -// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. -type {{ .Prefix }}ReceiverInterface interface { -{{- range .Operations }} -{{ .SummaryAsComment }} - // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. - Handle{{ .GoOperationID }}{{ $.Prefix }}(c *gin.Context{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) -{{- end }} -} - -{{ range .Operations }} -// {{ .GoOperationID }}{{ $.Prefix }}Handler returns a gin.HandlerFunc for the {{ .GoOperationID }} {{ $.PrefixLower }}. -// The caller is responsible for registering this handler at the appropriate path. -func {{ .GoOperationID }}{{ $.Prefix }}Handler(si {{ $.Prefix }}ReceiverInterface) gin.HandlerFunc { - return func(c *gin.Context) { -{{- if .HasParams }} - var err error - _ = err - - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} -{{- if or .Required .IsPassThrough .IsJSON }} - if paramValue := c.Query("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)}) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - c.JSON(http.StatusBadRequest, gin.H{"error": "Query parameter {{ .Name }} is required"}) - return - }{{ end }} -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, c.Request.URL.Query(), ¶ms.{{ .GoName }}) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)}) - return - } -{{- end }} -{{ end }} -{{ range .HeaderParams }} - if valueList := c.Request.Header[http.CanonicalHeaderKey("{{ .Name }}")]; len(valueList) > 0 { - var {{ .GoVariableName }} {{ .TypeDecl }} - n := len(valueList) - if n != 1 { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Expected one value for {{ .Name }}, got %d", n)}) - return - } -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)}) - return - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - c.JSON(http.StatusBadRequest, gin.H{"error": "Header parameter {{ .Name }} is required"}) - return - }{{ end }} -{{ end }} -{{- end }} - si.Handle{{ .GoOperationID }}{{ $.Prefix }}(c{{ if .HasParams }}, params{{ end }}) - } -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/gin/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/gin/wrapper.go.tmpl deleted file mode 100644 index 1fb5792ee5..0000000000 --- a/experimental/internal/codegen/templates/files/server/gin/wrapper.go.tmpl +++ /dev/null @@ -1,161 +0,0 @@ -{{- /* - This template generates the ServerInterfaceWrapper that extracts parameters - from HTTP requests and calls the ServerInterface methods for Gin. - Input: []OperationDescriptor -*/ -}} - -// ServerInterfaceWrapper converts contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandler func(*gin.Context, error, int) -} - -// MiddlewareFunc is a middleware function type. -type MiddlewareFunc func(c *gin.Context) - -{{ range . }} -// {{ .GoOperationID }} operation middleware -func (siw *ServerInterfaceWrapper) {{ .GoOperationID }}(c *gin.Context) { -{{- if or .PathParams .HasParams }} - var err error -{{- end }} -{{ range .PathParams }} - // ------------- Path parameter "{{ .Name }}" ------------- - var {{ .GoVariableName }} {{ .TypeDecl }} -{{ if .IsPassThrough }} - {{ .GoVariableName }} = c.Param("{{ .Name }}") -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(c.Param("{{ .Name }}")), &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Error unmarshaling parameter '{{ .Name }}' as JSON"), http.StatusBadRequest) - return - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, c.Param("{{ .Name }}"), &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter {{ .Name }}: %w", err), http.StatusBadRequest) - return - } -{{- end }} -{{ end }} -{{- if .Security }} -{{- range .Security }} - c.Set({{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) -{{- end }} -{{- end }} -{{ if .HasParams }} - // Parameter object where we will unmarshal all parameters from the context - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, c.Request.URL.Query(), ¶ms.{{ .GoName }}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter {{ .Name }}: %w", err), http.StatusBadRequest) - return - } -{{- else if or .Required .IsPassThrough .IsJSON }} - if paramValue := c.Query("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Error unmarshaling parameter '{{ .Name }}' as JSON: %w", err), http.StatusBadRequest) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - siw.ErrorHandler(c, fmt.Errorf("Query argument {{ .Name }} is required, but not found"), http.StatusBadRequest) - return - }{{ end }} -{{- end }} -{{ end }} -{{ if .HeaderParams }} - headers := c.Request.Header -{{ range .HeaderParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found { - var {{ .GoVariableName }} {{ .TypeDecl }} - n := len(valueList) - if n != 1 { - siw.ErrorHandler(c, fmt.Errorf("Expected one value for {{ .Name }}, got %d", n), http.StatusBadRequest) - return - } -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Error unmarshaling parameter '{{ .Name }}' as JSON"), http.StatusBadRequest) - return - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter {{ .Name }}: %w", err), http.StatusBadRequest) - return - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - siw.ErrorHandler(c, fmt.Errorf("Header parameter {{ .Name }} is required, but not found"), http.StatusBadRequest) - return - }{{ end }} -{{ end }} -{{ end }} -{{ range .CookieParams }} - { - var cookie string - if cookie, err = c.Cookie("{{ .Name }}"); err == nil { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - decoded, err := url.QueryUnescape(cookie) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Error unescaping cookie parameter '{{ .Name }}'"), http.StatusBadRequest) - return - } - err = json.Unmarshal([]byte(decoded), &value) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Error unmarshaling parameter '{{ .Name }}' as JSON"), http.StatusBadRequest) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} -{{- if .IsStyled }} - var value {{ .TypeDecl }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie, &value) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter {{ .Name }}: %w", err), http.StatusBadRequest) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - siw.ErrorHandler(c, fmt.Errorf("Query argument {{ .Name }} is required, but not found"), http.StatusBadRequest) - return - }{{ end }} - } -{{ end }} -{{ end }} - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.{{ .GoOperationID }}(c{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/gorilla/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/gorilla/handler.go.tmpl deleted file mode 100644 index 9d7bcf2121..0000000000 --- a/experimental/internal/codegen/templates/files/server/gorilla/handler.go.tmpl +++ /dev/null @@ -1,57 +0,0 @@ -{{- /* - This template generates the HTTP handler and routing for Gorilla servers. - Input: []OperationDescriptor -*/ -}} - -// Handler creates http.Handler with routing matching OpenAPI spec. -func Handler(si ServerInterface) http.Handler { - return HandlerWithOptions(si, GorillaServerOptions{}) -} - -// GorillaServerOptions configures the Gorilla server. -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, - }) -} - -// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. -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+"{{ pathToGorillaPattern .Path }}", wrapper.{{ .GoOperationID }}).Methods("{{ .Method }}") -{{- end }} - return r -} diff --git a/experimental/internal/codegen/templates/files/server/gorilla/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/gorilla/interface.go.tmpl deleted file mode 100644 index 7a9855b515..0000000000 --- a/experimental/internal/codegen/templates/files/server/gorilla/interface.go.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -{{- /* - This template generates the ServerInterface for Gorilla servers. - Input: []OperationDescriptor -*/ -}} - -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{- range . }} -{{ .SummaryAsComment }} - // ({{ .Method }} {{ .Path }}) - {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) -{{- end }} -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. -type Unimplemented struct{} - -{{- range . }} -{{ .SummaryAsComment }} -// ({{ .Method }} {{ .Path }}) -func (_ Unimplemented) {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) { - w.WriteHeader(http.StatusNotImplemented) -} -{{- end }} diff --git a/experimental/internal/codegen/templates/files/server/gorilla/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/gorilla/receiver.go.tmpl deleted file mode 100644 index b0a24f0fdc..0000000000 --- a/experimental/internal/codegen/templates/files/server/gorilla/receiver.go.tmpl +++ /dev/null @@ -1,95 +0,0 @@ -{{- /* - This template generates the receiver interface and handler functions for Gorilla. - Input: ReceiverTemplateData -*/ -}} - -// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. -type {{ .Prefix }}ReceiverInterface interface { -{{- range .Operations }} -{{ .SummaryAsComment }} - // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. - Handle{{ .GoOperationID }}{{ $.Prefix }}(w http.ResponseWriter, r *http.Request{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) -{{- end }} -} - -// {{ .Prefix }}ReceiverMiddlewareFunc is a middleware function for {{ $.PrefixLower }} receiver handlers. -type {{ .Prefix }}ReceiverMiddlewareFunc func(http.Handler) http.Handler - -{{ range .Operations }} -// {{ .GoOperationID }}{{ $.Prefix }}Handler returns an http.Handler for the {{ .GoOperationID }} {{ $.PrefixLower }}. -// The caller is responsible for registering this handler at the appropriate path. -func {{ .GoOperationID }}{{ $.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) - } - } - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -{{- if .HasParams }} - var err error - _ = err - - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} -{{- if or .Required .IsPassThrough .IsJSON }} - if paramValue := r.URL.Query().Get("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - errHandler(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - errHandler(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) - return - }{{ end }} -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, r.URL.Query(), ¶ms.{{ .GoName }}) - if err != nil { - errHandler(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{ end }} -{{ range .HeaderParams }} - if valueList, found := r.Header[http.CanonicalHeaderKey("{{ .Name }}")]; found { - var {{ .GoVariableName }} {{ .TypeDecl }} - n := len(valueList) - if n != 1 { - errHandler(w, r, &TooManyValuesForParamError{ParamName: "{{ .Name }}", Count: n}) - return - } -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) - if err != nil { - errHandler(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - errHandler(w, r, &RequiredHeaderError{ParamName: "{{ .Name }}", Err: fmt.Errorf("header parameter {{ .Name }} is required, but not found")}) - return - }{{ end }} -{{ end }} -{{- end }} - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - si.Handle{{ .GoOperationID }}{{ $.Prefix }}(w, r{{ if .HasParams }}, params{{ end }}) - })) - - for _, middleware := range middlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) - }) -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/gorilla/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/gorilla/wrapper.go.tmpl deleted file mode 100644 index 395fea4d7b..0000000000 --- a/experimental/internal/codegen/templates/files/server/gorilla/wrapper.go.tmpl +++ /dev/null @@ -1,169 +0,0 @@ -{{- /* - This template generates the ServerInterfaceWrapper that extracts parameters - from HTTP requests and calls the ServerInterface methods for Gorilla. - Input: []OperationDescriptor -*/ -}} - -// ServerInterfaceWrapper converts HTTP requests to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -// MiddlewareFunc is a middleware function type. -type MiddlewareFunc func(http.Handler) http.Handler - -{{ range . }} -// {{ .GoOperationID }} operation middleware -func (siw *ServerInterfaceWrapper) {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request) { -{{- if or .PathParams .HasParams }} - var err error -{{- end }} -{{ if .PathParams }} - pathParams := mux.Vars(r) -{{ end }} -{{ range .PathParams }} - // ------------- Path parameter "{{ .Name }}" ------------- - var {{ .GoVariableName }} {{ .TypeDecl }} -{{ if .IsPassThrough }} - {{ .GoVariableName }} = pathParams["{{ .Name }}"] -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(pathParams["{{ .Name }}"]), &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, pathParams["{{ .Name }}"], &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{ end }} -{{- if .Security }} - ctx := r.Context() -{{- range .Security }} - ctx = context.WithValue(ctx, {{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) -{{- end }} - r = r.WithContext(ctx) -{{- end }} -{{ if .HasParams }} - // Parameter object where we will unmarshal all parameters from the context - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- -{{- if or .Required .IsPassThrough .IsJSON }} - if paramValue := r.URL.Query().Get("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) - return - }{{ end }} -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, r.URL.Query(), ¶ms.{{ .GoName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{ end }} -{{ if .HeaderParams }} - headers := r.Header -{{ range .HeaderParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found { - var {{ .GoVariableName }} {{ .TypeDecl }} - n := len(valueList) - if n != 1 { - siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "{{ .Name }}", Count: n}) - return - } -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - err := fmt.Errorf("Header parameter {{ .Name }} is required, but not found") - siw.ErrorHandlerFunc(w, r, &RequiredHeaderError{ParamName: "{{ .Name }}", Err: err}) - return - }{{ end }} -{{ end }} -{{ end }} -{{ range .CookieParams }} - { - var cookie *http.Cookie - if cookie, err = r.Cookie("{{ .Name }}"); err == nil { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie.Value -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - decoded, err := url.QueryUnescape(cookie.Value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnescapedCookieParamError{ParamName: "{{ .Name }}", Err: err}) - return - } - err = json.Unmarshal([]byte(decoded), &value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} -{{- if .IsStyled }} - var value {{ .TypeDecl }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie.Value, &value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) - return - }{{ end }} - } -{{ end }} -{{ end }} - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.{{ .GoOperationID }}(w, r{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/iris/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/iris/handler.go.tmpl deleted file mode 100644 index 2fa5813b49..0000000000 --- a/experimental/internal/codegen/templates/files/server/iris/handler.go.tmpl +++ /dev/null @@ -1,28 +0,0 @@ -{{- /* - This template generates the HTTP handler and routing for Iris servers. - Input: []OperationDescriptor -*/ -}} - -// IrisServerOptions is the option for iris server. -type IrisServerOptions struct { - BaseURL string - Middlewares []iris.Handler -} - -// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. -func RegisterHandlers(router *iris.Application, si ServerInterface) { - RegisterHandlersWithOptions(router, si, IrisServerOptions{}) -} - -// RegisterHandlersWithOptions creates http.Handler with additional options. -func RegisterHandlersWithOptions(router *iris.Application, si ServerInterface, options IrisServerOptions) { -{{ if . }} - wrapper := ServerInterfaceWrapper{ - Handler: si, - } -{{ end }} -{{- range . }} - router.{{ .Method | lower | title }}(options.BaseURL+"{{ pathToIrisPattern .Path }}", wrapper.{{ .GoOperationID }}) -{{- end }} - router.Build() -} diff --git a/experimental/internal/codegen/templates/files/server/iris/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/iris/interface.go.tmpl deleted file mode 100644 index 5b80fa550c..0000000000 --- a/experimental/internal/codegen/templates/files/server/iris/interface.go.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -{{- /* - This template generates the ServerInterface for Iris servers. - Input: []OperationDescriptor -*/ -}} - -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{- range . }} -{{ .SummaryAsComment }} - // ({{ .Method }} {{ .Path }}) - {{ .GoOperationID }}(ctx iris.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) -{{- end }} -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. -type Unimplemented struct{} - -{{- range . }} -{{ .SummaryAsComment }} -// ({{ .Method }} {{ .Path }}) -func (_ Unimplemented) {{ .GoOperationID }}(ctx iris.Context{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) { - ctx.StatusCode(http.StatusNotImplemented) -} -{{- end }} diff --git a/experimental/internal/codegen/templates/files/server/iris/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/iris/receiver.go.tmpl deleted file mode 100644 index 050076b4a4..0000000000 --- a/experimental/internal/codegen/templates/files/server/iris/receiver.go.tmpl +++ /dev/null @@ -1,84 +0,0 @@ -{{- /* - This template generates the receiver interface and handler functions for Iris. - Input: ReceiverTemplateData -*/ -}} - -// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. -type {{ .Prefix }}ReceiverInterface interface { -{{- range .Operations }} -{{ .SummaryAsComment }} - // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. - Handle{{ .GoOperationID }}{{ $.Prefix }}(ctx iris.Context{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) -{{- end }} -} - -{{ range .Operations }} -// {{ .GoOperationID }}{{ $.Prefix }}Handler returns an iris.Handler for the {{ .GoOperationID }} {{ $.PrefixLower }}. -// The caller is responsible for registering this handler at the appropriate path. -func {{ .GoOperationID }}{{ $.Prefix }}Handler(si {{ $.Prefix }}ReceiverInterface) iris.Handler { - return func(ctx iris.Context) { -{{- if .HasParams }} - var err error - _ = err - - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} -{{- if or .Required .IsPassThrough .IsJSON }} - if paramValue := ctx.URLParam("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - _, _ = ctx.WriteString(fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - ctx.StatusCode(http.StatusBadRequest) - _, _ = ctx.WriteString("Query parameter {{ .Name }} is required") - return - }{{ end }} -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, ctx.Request().URL.Query(), ¶ms.{{ .GoName }}) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - _, _ = ctx.WriteString(fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - return - } -{{- end }} -{{ end }} -{{ range .HeaderParams }} - if valueList := ctx.Request().Header[http.CanonicalHeaderKey("{{ .Name }}")]; len(valueList) > 0 { - var {{ .GoVariableName }} {{ .TypeDecl }} - n := len(valueList) - if n != 1 { - ctx.StatusCode(http.StatusBadRequest) - _, _ = ctx.WriteString(fmt.Sprintf("Expected one value for {{ .Name }}, got %d", n)) - return - } -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - _, _ = ctx.WriteString(fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - return - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - ctx.StatusCode(http.StatusBadRequest) - _, _ = ctx.WriteString("Header parameter {{ .Name }} is required") - return - }{{ end }} -{{ end }} -{{- end }} - si.Handle{{ .GoOperationID }}{{ $.Prefix }}(ctx{{ if .HasParams }}, params{{ end }}) - } -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/iris/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/iris/wrapper.go.tmpl deleted file mode 100644 index 8c44f3dd10..0000000000 --- a/experimental/internal/codegen/templates/files/server/iris/wrapper.go.tmpl +++ /dev/null @@ -1,160 +0,0 @@ -{{- /* - This template generates the ServerInterfaceWrapper that extracts parameters - from HTTP requests and calls the ServerInterface methods for Iris. - Input: []OperationDescriptor -*/ -}} - -// ServerInterfaceWrapper converts iris contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface -} - -{{ range . }} -// {{ .GoOperationID }} converts iris context to params. -func (w *ServerInterfaceWrapper) {{ .GoOperationID }}(ctx iris.Context) { -{{- if or .PathParams .HasParams }} - var err error -{{- end }} -{{ range .PathParams }} - // ------------- Path parameter "{{ .Name }}" ------------- - var {{ .GoVariableName }} {{ .TypeDecl }} -{{ if .IsPassThrough }} - {{ .GoVariableName }} = ctx.Params().Get("{{ .Name }}") -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(ctx.Params().Get("{{ .Name }}")), &{{ .GoVariableName }}) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString(fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) - return - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, ctx.Params().Get("{{ .Name }}"), &{{ .GoVariableName }}) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString(fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - return - } -{{- end }} -{{ end }} -{{- if .Security }} -{{- range .Security }} - ctx.Values().Set({{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) -{{- end }} -{{- end }} -{{ if .HasParams }} - // Parameter object where we will unmarshal all parameters from the context - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, ctx.Request().URL.Query(), ¶ms.{{ .GoName }}) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString(fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - return - } -{{- else }} - if paramValue := ctx.URLParam("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString(fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString("Query argument {{ .Name }} is required, but not found") - return - }{{ end }} -{{- end }} -{{ end }} -{{ if .HeaderParams }} - headers := ctx.Request().Header -{{ range .HeaderParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found { - var {{ .GoVariableName }} {{ .TypeDecl }} - n := len(valueList) - if n != 1 { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString(fmt.Sprintf("Expected one value for {{ .Name }}, got %d", n)) - return - } -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString(fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) - return - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString(fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - return - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString("Header {{ .Name }} is required, but not found") - return - }{{ end }} -{{ end }} -{{ end }} -{{ range .CookieParams }} - if cookie := ctx.GetCookie("{{ .Name }}"); cookie != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - decoded, err := url.QueryUnescape(cookie) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString(fmt.Sprintf("Error unescaping cookie parameter '%s'", "{{ .Name }}")) - return - } - err = json.Unmarshal([]byte(decoded), &value) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString(fmt.Sprintf("Error unmarshaling parameter '%s' as JSON", "{{ .Name }}")) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} -{{- if .IsStyled }} - var value {{ .TypeDecl }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie, &value) - if err != nil { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString(fmt.Sprintf("Invalid format for parameter {{ .Name }}: %s", err)) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - ctx.StatusCode(http.StatusBadRequest) - ctx.WriteString("Cookie {{ .Name }} is required, but not found") - return - }{{ end }} -{{ end }} -{{ end }} - // Invoke the callback with all the unmarshaled arguments - w.Handler.{{ .GoOperationID }}(ctx{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/param_types.go.tmpl b/experimental/internal/codegen/templates/files/server/param_types.go.tmpl deleted file mode 100644 index 7b0eb914de..0000000000 --- a/experimental/internal/codegen/templates/files/server/param_types.go.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -{{- /* - This template generates parameter struct types for server operations. - Input: []OperationDescriptor -*/ -}} - -{{ range . }} -{{- if .HasParams }} -// {{ .ParamsTypeName }} defines parameters for {{ .GoOperationID }}. -type {{ .ParamsTypeName }} struct { -{{- range .QueryParams }} - // {{ .Name }} {{ if .Required }}(required){{ else }}(optional){{ end }} - {{ .GoName }} {{ if .HasOptionalPointer }}*{{ end }}{{ .TypeDecl }} `form:"{{ .Name }}" json:"{{ .Name }}"` -{{- end }} -{{- range .HeaderParams }} - // {{ .Name }} (header{{ if .Required }}, required{{ end }}) - {{ .GoName }} {{ if .HasOptionalPointer }}*{{ end }}{{ .TypeDecl }} -{{- end }} -{{- range .CookieParams }} - // {{ .Name }} (cookie{{ if .Required }}, required{{ end }}) - {{ .GoName }} {{ if .HasOptionalPointer }}*{{ end }}{{ .TypeDecl }} -{{- end }} -} -{{ end }} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/stdhttp/handler.go.tmpl b/experimental/internal/codegen/templates/files/server/stdhttp/handler.go.tmpl deleted file mode 100644 index 47d50ec8bc..0000000000 --- a/experimental/internal/codegen/templates/files/server/stdhttp/handler.go.tmpl +++ /dev/null @@ -1,63 +0,0 @@ -{{- /* - This template generates the HTTP handler and routing for StdHTTP servers. - Input: []OperationDescriptor -*/ -}} - -// 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)) - ServeHTTP(w http.ResponseWriter, r *http.Request) -} - -// StdHTTPServerOptions configures the StdHTTP server. -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, - }) -} - -// HandlerFromMuxWithBaseURL creates http.Handler with routing and a base URL. -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 }} "+options.BaseURL+"{{ pathToStdHTTPPattern .Path }}", wrapper.{{ .GoOperationID }}) -{{- end }} - return m -} diff --git a/experimental/internal/codegen/templates/files/server/stdhttp/interface.go.tmpl b/experimental/internal/codegen/templates/files/server/stdhttp/interface.go.tmpl deleted file mode 100644 index 6dc320f8f7..0000000000 --- a/experimental/internal/codegen/templates/files/server/stdhttp/interface.go.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -{{- /* - This template generates the ServerInterface for StdHTTP servers. - Input: []OperationDescriptor -*/ -}} - -// ServerInterface represents all server handlers. -type ServerInterface interface { -{{- range . }} -{{ .SummaryAsComment }} - // ({{ .Method }} {{ .Path }}) - {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request{{ range .PathParams }}, {{ .GoVariableName }} {{ .TypeDecl }}{{ end }}{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) -{{- end }} -} diff --git a/experimental/internal/codegen/templates/files/server/stdhttp/receiver.go.tmpl b/experimental/internal/codegen/templates/files/server/stdhttp/receiver.go.tmpl deleted file mode 100644 index 5f6d1fb5d0..0000000000 --- a/experimental/internal/codegen/templates/files/server/stdhttp/receiver.go.tmpl +++ /dev/null @@ -1,108 +0,0 @@ -{{- /* - This template generates the receiver interface and handler functions for StdHTTP. - Receiver handlers receive webhook/callback requests — the caller registers them at chosen paths. - Input: ReceiverTemplateData -*/ -}} - -// {{ .Prefix }}ReceiverInterface represents handlers for receiving {{ .PrefixLower }} requests. -type {{ .Prefix }}ReceiverInterface interface { -{{- range .Operations }} -{{ .SummaryAsComment }} - // Handle{{ .GoOperationID }}{{ $.Prefix }} handles the {{ .Method }} {{ $.PrefixLower }} request. - Handle{{ .GoOperationID }}{{ $.Prefix }}(w http.ResponseWriter, r *http.Request{{ if .HasParams }}, params {{ .ParamsTypeName }}{{ end }}) -{{- end }} -} - -// {{ .Prefix }}ReceiverMiddlewareFunc is a middleware function for {{ $.PrefixLower }} receiver handlers. -type {{ .Prefix }}ReceiverMiddlewareFunc func(http.Handler) http.Handler - -{{ range .Operations }} -// {{ .GoOperationID }}{{ $.Prefix }}Handler returns an http.Handler for the {{ .GoOperationID }} {{ $.PrefixLower }}. -// The caller is responsible for registering this handler at the appropriate path. -func {{ .GoOperationID }}{{ $.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) - } - } - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -{{- if .HasParams }} - var err error - _ = err - - // Parameter object where we will unmarshal all parameters from the context - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- -{{- if or .Required .IsPassThrough .IsJSON }} - if paramValue := r.URL.Query().Get("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - errHandler(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - errHandler(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) - return - }{{ end }} -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, r.URL.Query(), ¶ms.{{ .GoName }}) - if err != nil { - errHandler(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{ end }} -{{ range .HeaderParams }} - if valueList, found := r.Header[http.CanonicalHeaderKey("{{ .Name }}")]; found { - var {{ .GoVariableName }} {{ .TypeDecl }} - n := len(valueList) - if n != 1 { - errHandler(w, r, &TooManyValuesForParamError{ParamName: "{{ .Name }}", Count: n}) - return - } -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) - if err != nil { - errHandler(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) - if err != nil { - errHandler(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - errHandler(w, r, &RequiredHeaderError{ParamName: "{{ .Name }}", Err: fmt.Errorf("header parameter {{ .Name }} is required, but not found")}) - return - }{{ end }} -{{ end }} -{{- end }} - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - si.Handle{{ .GoOperationID }}{{ $.Prefix }}(w, r{{ if .HasParams }}, params{{ end }}) - })) - - for _, middleware := range middlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) - }) -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/server/stdhttp/wrapper.go.tmpl b/experimental/internal/codegen/templates/files/server/stdhttp/wrapper.go.tmpl deleted file mode 100644 index 6c74e9c980..0000000000 --- a/experimental/internal/codegen/templates/files/server/stdhttp/wrapper.go.tmpl +++ /dev/null @@ -1,166 +0,0 @@ -{{- /* - This template generates the ServerInterfaceWrapper that extracts parameters - from HTTP requests and calls the ServerInterface methods. - Input: []OperationDescriptor -*/ -}} - -// ServerInterfaceWrapper converts HTTP requests to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -// MiddlewareFunc is a middleware function type. -type MiddlewareFunc func(http.Handler) http.Handler - -{{ range . }} -// {{ .GoOperationID }} operation middleware -func (siw *ServerInterfaceWrapper) {{ .GoOperationID }}(w http.ResponseWriter, r *http.Request) { -{{- if or .PathParams .HasParams }} - var err error -{{- end }} -{{ range .PathParams }} - // ------------- Path parameter "{{ .Name }}" ------------- - var {{ .GoVariableName }} {{ .TypeDecl }} -{{ if .IsPassThrough }} - {{ .GoVariableName }} = r.PathValue("{{ .Name }}") -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(r.PathValue("{{ .Name }}")), &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationPath, r.PathValue("{{ .Name }}"), &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{ end }} -{{- if .Security }} - ctx := r.Context() -{{- range .Security }} - ctx = context.WithValue(ctx, {{ .Name | toGoIdentifier }}Scopes, []string{ {{- range $i, $s := .Scopes }}{{ if $i }}, {{ end }}"{{ $s }}"{{ end -}} }) -{{- end }} - r = r.WithContext(ctx) -{{- end }} -{{ if .HasParams }} - // Parameter object where we will unmarshal all parameters from the context - var params {{ .ParamsTypeName }} -{{ range .QueryParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} query parameter "{{ .Name }}" ------------- -{{- if or .Required .IsPassThrough .IsJSON }} - if paramValue := r.URL.Query().Get("{{ .Name }}"); paramValue != "" { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}paramValue -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - err = json.Unmarshal([]byte(paramValue), &value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) - return - }{{ end }} -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", {{ .Required }}, r.URL.Query(), ¶ms.{{ .GoName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{ end }} -{{ if .HeaderParams }} - headers := r.Header -{{ range .HeaderParams }} - // ------------- {{ if .Required }}Required{{ else }}Optional{{ end }} header parameter "{{ .Name }}" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("{{ .Name }}")]; found { - var {{ .GoVariableName }} {{ .TypeDecl }} - n := len(valueList) - if n != 1 { - siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "{{ .Name }}", Count: n}) - return - } -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}valueList[0] -{{- end }} -{{- if .IsJSON }} - err = json.Unmarshal([]byte(valueList[0]), &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} -{{- if .IsStyled }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationHeader, valueList[0], &{{ .GoVariableName }}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } -{{- end }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}{{ .GoVariableName }} - }{{ if .Required }} else { - err := fmt.Errorf("Header parameter {{ .Name }} is required, but not found") - siw.ErrorHandlerFunc(w, r, &RequiredHeaderError{ParamName: "{{ .Name }}", Err: err}) - return - }{{ end }} -{{ end }} -{{ end }} -{{ range .CookieParams }} - { - var cookie *http.Cookie - if cookie, err = r.Cookie("{{ .Name }}"); err == nil { -{{- if .IsPassThrough }} - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}cookie.Value -{{- end }} -{{- if .IsJSON }} - var value {{ .TypeDecl }} - decoded, err := url.QueryUnescape(cookie.Value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnescapedCookieParamError{ParamName: "{{ .Name }}", Err: err}) - return - } - err = json.Unmarshal([]byte(decoded), &value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{ .Name }}", Err: err}) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} -{{- if .IsStyled }} - var value {{ .TypeDecl }} - err = {{ .BindFunc }}("{{ .Name }}", ParamLocationCookie, cookie.Value, &value) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{ .Name }}", Err: err}) - return - } - params.{{ .GoName }} = {{ if .HasOptionalPointer }}&{{ end }}value -{{- end }} - }{{ if .Required }} else { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{ .Name }}"}) - return - }{{ end }} - } -{{ end }} -{{ end }} - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.{{ .GoOperationID }}(w, r{{ range .PathParams }}, {{ .GoVariableName }}{{ end }}{{ if .HasParams }}, params{{ end }}) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} -{{ end }} diff --git a/experimental/internal/codegen/templates/files/types/date.tmpl b/experimental/internal/codegen/templates/files/types/date.tmpl deleted file mode 100644 index 1698b09320..0000000000 --- a/experimental/internal/codegen/templates/files/types/date.tmpl +++ /dev/null @@ -1,38 +0,0 @@ -{{/* Date type for OpenAPI format: date */}} - -const DateFormat = "2006-01-02" - -type Date struct { - time.Time -} - -func (d Date) MarshalJSON() ([]byte, error) { - return json.Marshal(d.Format(DateFormat)) -} - -func (d *Date) UnmarshalJSON(data []byte) error { - var dateStr string - err := json.Unmarshal(data, &dateStr) - if err != nil { - return err - } - parsed, err := time.Parse(DateFormat, dateStr) - if err != nil { - return err - } - d.Time = parsed - return nil -} - -func (d Date) String() string { - return d.Format(DateFormat) -} - -func (d *Date) UnmarshalText(data []byte) error { - parsed, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = parsed - return nil -} diff --git a/experimental/internal/codegen/templates/files/types/email.tmpl b/experimental/internal/codegen/templates/files/types/email.tmpl deleted file mode 100644 index 1f1e5d100a..0000000000 --- a/experimental/internal/codegen/templates/files/types/email.tmpl +++ /dev/null @@ -1,43 +0,0 @@ -{{/* Email type for OpenAPI format: email */}} - -const ( - emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" -) - -var ( - emailRegex = regexp.MustCompile(emailRegexString) -) - -// ErrValidationEmail is the sentinel error returned when an email fails validation -var ErrValidationEmail = errors.New("email: failed to pass regex validation") - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. -type Email string - -func (e Email) MarshalJSON() ([]byte, error) { - if !emailRegex.MatchString(string(e)) { - return nil, ErrValidationEmail - } - - return json.Marshal(string(e)) -} - -func (e *Email) UnmarshalJSON(data []byte) error { - if e == nil { - return nil - } - - var s string - if err := json.Unmarshal(data, &s); err != nil { - return err - } - - *e = Email(s) - if !emailRegex.MatchString(s) { - return ErrValidationEmail - } - - return nil -} diff --git a/experimental/internal/codegen/templates/files/types/file.tmpl b/experimental/internal/codegen/templates/files/types/file.tmpl deleted file mode 100644 index 3b853101bb..0000000000 --- a/experimental/internal/codegen/templates/files/types/file.tmpl +++ /dev/null @@ -1,64 +0,0 @@ -{{/* File type for OpenAPI format: binary */}} - -type File struct { - multipart *multipart.FileHeader - data []byte - filename string -} - -func (file *File) InitFromMultipart(header *multipart.FileHeader) { - file.multipart = header - file.data = nil - file.filename = "" -} - -func (file *File) InitFromBytes(data []byte, filename string) { - file.data = data - file.filename = filename - file.multipart = nil -} - -func (file File) MarshalJSON() ([]byte, error) { - b, err := file.Bytes() - if err != nil { - return nil, err - } - return json.Marshal(b) -} - -func (file *File) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, &file.data) -} - -func (file File) Bytes() ([]byte, error) { - if file.multipart != nil { - f, err := file.multipart.Open() - if err != nil { - return nil, err - } - defer func() { _ = f.Close() }() - return io.ReadAll(f) - } - return file.data, nil -} - -func (file File) Reader() (io.ReadCloser, error) { - if file.multipart != nil { - return file.multipart.Open() - } - return io.NopCloser(bytes.NewReader(file.data)), nil -} - -func (file File) Filename() string { - if file.multipart != nil { - return file.multipart.Filename - } - return file.filename -} - -func (file File) FileSize() int64 { - if file.multipart != nil { - return file.multipart.Size - } - return int64(len(file.data)) -} diff --git a/experimental/internal/codegen/templates/files/types/nullable.tmpl b/experimental/internal/codegen/templates/files/types/nullable.tmpl deleted file mode 100644 index cf75b0910c..0000000000 --- a/experimental/internal/codegen/templates/files/types/nullable.tmpl +++ /dev/null @@ -1,104 +0,0 @@ -{{/* Nullable type for OpenAPI nullable fields - implements three-state semantics: unspecified, null, or value */}} - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value -type Nullable[T any] map[bool]T - -// NewNullableWithValue creates a Nullable with the given value. -func NewNullableWithValue[T any](value T) Nullable[T] { - return Nullable[T]{true: value} -} - -// NewNullNullable creates a Nullable that is explicitly null. -func NewNullNullable[T any]() Nullable[T] { - return Nullable[T]{false: *new(T)} -} - -// Get returns the value if set, or an error if null or unspecified. -func (n Nullable[T]) Get() (T, error) { - if v, ok := n[true]; ok { - return v, nil - } - var zero T - if n.IsNull() { - return zero, ErrNullableIsNull - } - return zero, ErrNullableNotSpecified -} - -// MustGet returns the value or panics if null or unspecified. -func (n Nullable[T]) MustGet() T { - v, err := n.Get() - if err != nil { - panic(err) - } - return v -} - -// Set assigns a value. -func (n *Nullable[T]) Set(value T) { - *n = Nullable[T]{true: value} -} - -// SetNull marks the field as explicitly null. -func (n *Nullable[T]) SetNull() { - *n = Nullable[T]{false: *new(T)} -} - -// SetUnspecified clears the field (as if it was never set). -func (n *Nullable[T]) SetUnspecified() { - *n = nil -} - -// IsNull returns true if the field is explicitly null. -func (n Nullable[T]) IsNull() bool { - if n == nil { - return false - } - _, ok := n[false] - return ok -} - -// IsSpecified returns true if the field was provided (either null or a value). -func (n Nullable[T]) IsSpecified() bool { - return len(n) > 0 -} - -// MarshalJSON implements json.Marshaler. -func (n Nullable[T]) MarshalJSON() ([]byte, error) { - if n.IsNull() { - return []byte("null"), nil - } - if v, ok := n[true]; ok { - return json.Marshal(v) - } - // Unspecified - this shouldn't be called if omitempty is used correctly - return []byte("null"), nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (n *Nullable[T]) UnmarshalJSON(data []byte) error { - if string(data) == "null" { - n.SetNull() - return nil - } - var v T - if err := json.Unmarshal(data, &v); err != nil { - return err - } - n.Set(v) - return nil -} - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. -var ErrNullableIsNull = errors.New("nullable value is null") - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. -var ErrNullableNotSpecified = errors.New("nullable value is not specified") diff --git a/experimental/internal/codegen/templates/files/types/uuid.tmpl b/experimental/internal/codegen/templates/files/types/uuid.tmpl deleted file mode 100644 index f136f6a150..0000000000 --- a/experimental/internal/codegen/templates/files/types/uuid.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -{{/* UUID type for OpenAPI format: uuid */}} - -type UUID = uuid.UUID diff --git a/experimental/internal/codegen/templates/funcs.go b/experimental/internal/codegen/templates/funcs.go deleted file mode 100644 index b790f1702e..0000000000 --- a/experimental/internal/codegen/templates/funcs.go +++ /dev/null @@ -1,151 +0,0 @@ -package templates - -import ( - "regexp" - "strings" - "text/template" - - "golang.org/x/text/cases" - "golang.org/x/text/language" -) - -var titleCaser = cases.Title(language.English) - -// pathParamRE matches OpenAPI path parameters including styled variants. -// Matches: {param}, {param*}, {.param}, {.param*}, {;param}, {;param*}, {?param}, {?param*} -var pathParamRE = regexp.MustCompile(`{[.;?]?([^{}*]+)\*?}`) - -// Funcs returns the template function map for server templates. -func Funcs() template.FuncMap { - return template.FuncMap{ - "pathToStdHTTPPattern": PathToStdHTTPPattern, - "pathToChiPattern": PathToChiPattern, - "pathToEchoPattern": PathToEchoPattern, - "pathToGinPattern": PathToGinPattern, - "pathToGorillaPattern": PathToGorillaPattern, - "pathToFiberPattern": PathToFiberPattern, - "pathToIrisPattern": PathToIrisPattern, - "toGoIdentifier": ToGoIdentifier, - "lower": strings.ToLower, - "title": titleCaser.String, - } -} - -// PathToStdHTTPPattern converts an OpenAPI path template to a Go 1.22+ std http pattern. -// OpenAPI: /users/{user_id}/posts/{post_id} -// StdHTTP: /users/{user_id}/posts/{post_id} -// Special case: "/" becomes "/{$}" to match only the root path. -func PathToStdHTTPPattern(path string) string { - // https://pkg.go.dev/net/http#hdr-Patterns-ServeMux - // The special wildcard {$} matches only the end of the URL. - if path == "/" { - return "/{$}" - } - return pathParamRE.ReplaceAllString(path, "{$1}") -} - -// PathToChiPattern converts an OpenAPI path template to a Chi-compatible pattern. -// OpenAPI: /users/{user_id}/posts/{post_id} -// Chi: /users/{user_id}/posts/{post_id} -func PathToChiPattern(path string) string { - return pathParamRE.ReplaceAllString(path, "{$1}") -} - -// PathToEchoPattern converts an OpenAPI path template to an Echo-compatible pattern. -// OpenAPI: /users/{user_id}/posts/{post_id} -// Echo: /users/:user_id/posts/:post_id -func PathToEchoPattern(path string) string { - return pathParamRE.ReplaceAllString(path, ":$1") -} - -// PathToGinPattern converts an OpenAPI path template to a Gin-compatible pattern. -// OpenAPI: /users/{user_id}/posts/{post_id} -// Gin: /users/:user_id/posts/:post_id -func PathToGinPattern(path string) string { - return pathParamRE.ReplaceAllString(path, ":$1") -} - -// PathToGorillaPattern converts an OpenAPI path template to a Gorilla Mux-compatible pattern. -// OpenAPI: /users/{user_id}/posts/{post_id} -// Gorilla: /users/{user_id}/posts/{post_id} -func PathToGorillaPattern(path string) string { - return pathParamRE.ReplaceAllString(path, "{$1}") -} - -// PathToFiberPattern converts an OpenAPI path template to a Fiber-compatible pattern. -// OpenAPI: /users/{user_id}/posts/{post_id} -// Fiber: /users/:user_id/posts/:post_id -func PathToFiberPattern(path string) string { - return pathParamRE.ReplaceAllString(path, ":$1") -} - -// PathToIrisPattern converts an OpenAPI path template to an Iris-compatible pattern. -// OpenAPI: /users/{user_id}/posts/{post_id} -// Iris: /users/:user_id/posts/:post_id -func PathToIrisPattern(path string) string { - return pathParamRE.ReplaceAllString(path, ":$1") -} - -// ToGoIdentifier converts a string to a valid Go identifier. -// This is a simple version for template usage. -func ToGoIdentifier(s string) string { - if s == "" { - return "Empty" - } - - // Replace non-alphanumeric characters with underscores - result := make([]byte, 0, len(s)) - capitalizeNext := true - - for i := 0; i < len(s); i++ { - c := s[i] - if c >= 'a' && c <= 'z' { - if capitalizeNext { - result = append(result, c-32) // uppercase - capitalizeNext = false - } else { - result = append(result, c) - } - } else if c >= 'A' && c <= 'Z' { - result = append(result, c) - capitalizeNext = false - } else if c >= '0' && c <= '9' { - result = append(result, c) - capitalizeNext = false - } else { - // Word separator - capitalizeNext = true - } - } - - if len(result) == 0 { - return "Empty" - } - - // Handle leading digit - if result[0] >= '0' && result[0] <= '9' { - result = append([]byte("N"), result...) - } - - str := string(result) - - // Handle Go keywords - lower := strings.ToLower(str) - if isGoKeyword(lower) { - str = str + "_" - } - - return str -} - -// isGoKeyword returns true if s is a Go keyword. -func isGoKeyword(s string) bool { - keywords := map[string]bool{ - "break": true, "case": true, "chan": true, "const": true, "continue": true, - "default": true, "defer": true, "else": true, "fallthrough": true, "for": true, - "func": true, "go": true, "goto": true, "if": true, "import": true, - "interface": true, "map": true, "package": true, "range": true, "return": true, - "select": true, "struct": true, "switch": true, "type": true, "var": true, - } - return keywords[s] -} diff --git a/experimental/internal/codegen/templates/registry.go b/experimental/internal/codegen/templates/registry.go deleted file mode 100644 index 94ae240769..0000000000 --- a/experimental/internal/codegen/templates/registry.go +++ /dev/null @@ -1,909 +0,0 @@ -package templates - -// Import represents a Go import with optional alias. -type Import struct { - Path string - Alias string // empty if no alias -} - -// TypeTemplate defines a template for a custom type along with its required imports. -type TypeTemplate struct { - Name string // Type name (e.g., "Email", "Date") - Imports []Import // Required imports for this type - Template string // Template name in embedded FS (e.g., "types/email.tmpl") -} - -// TypeTemplates maps type names to their template definitions. -var TypeTemplates = map[string]TypeTemplate{ - "Email": { - Name: "Email", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "errors"}, - {Path: "regexp"}, - }, - Template: "types/email.tmpl", - }, - "Date": { - Name: "Date", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "time"}, - }, - Template: "types/date.tmpl", - }, - "UUID": { - Name: "UUID", - Imports: []Import{ - {Path: "github.com/google/uuid"}, - }, - Template: "types/uuid.tmpl", - }, - "File": { - Name: "File", - Imports: []Import{ - {Path: "bytes"}, - {Path: "encoding/json"}, - {Path: "io"}, - {Path: "mime/multipart"}, - }, - Template: "types/file.tmpl", - }, - "Nullable": { - Name: "Nullable", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "errors"}, - }, - Template: "types/nullable.tmpl", - }, -} - -// ParamTemplate defines a template for a parameter styling/binding function. -type ParamTemplate struct { - Name string // Function name (e.g., "StyleSimpleParam") - Imports []Import // Required imports for this function - Template string // Template name in embedded FS (e.g., "params/style_simple.go.tmpl") -} - -// ParamHelpersTemplate is the template for shared helper functions. -// This is included whenever any param function is used. -var ParamHelpersTemplate = ParamTemplate{ - Name: "helpers", - Imports: []Import{ - {Path: "bytes"}, - {Path: "encoding"}, - {Path: "encoding/json"}, - {Path: "errors"}, - {Path: "fmt"}, - {Path: "net/url"}, - {Path: "reflect"}, - {Path: "sort"}, - {Path: "strconv"}, - {Path: "strings"}, - {Path: "time"}, - {Path: "github.com/google/uuid"}, - }, - Template: "params/helpers.go.tmpl", -} - -// ParamTemplates maps style/explode combinations to their template definitions. -// Keys follow the pattern: "style_{style}" or "style_{style}_explode" for styling, -// and "bind_{style}" or "bind_{style}_explode" for binding. -var ParamTemplates = map[string]ParamTemplate{ - // Style templates (serialization) - "style_simple": { - Name: "StyleSimpleParam", - Imports: []Import{ - {Path: "bytes"}, - {Path: "encoding"}, - {Path: "encoding/json"}, - {Path: "errors"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - {Path: "time"}, - }, - Template: "params/style_simple.go.tmpl", - }, - "style_simple_explode": { - Name: "StyleSimpleExplodeParam", - Imports: []Import{ - {Path: "bytes"}, - {Path: "encoding"}, - {Path: "encoding/json"}, - {Path: "errors"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - {Path: "time"}, - }, - Template: "params/style_simple_explode.go.tmpl", - }, - "style_label": { - Name: "StyleLabelParam", - Imports: []Import{ - {Path: "bytes"}, - {Path: "encoding"}, - {Path: "encoding/json"}, - {Path: "errors"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - {Path: "time"}, - }, - Template: "params/style_label.go.tmpl", - }, - "style_label_explode": { - Name: "StyleLabelExplodeParam", - Imports: []Import{ - {Path: "bytes"}, - {Path: "encoding"}, - {Path: "encoding/json"}, - {Path: "errors"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - {Path: "time"}, - }, - Template: "params/style_label_explode.go.tmpl", - }, - "style_matrix": { - Name: "StyleMatrixParam", - Imports: []Import{ - {Path: "bytes"}, - {Path: "encoding"}, - {Path: "encoding/json"}, - {Path: "errors"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - {Path: "time"}, - }, - Template: "params/style_matrix.go.tmpl", - }, - "style_matrix_explode": { - Name: "StyleMatrixExplodeParam", - Imports: []Import{ - {Path: "bytes"}, - {Path: "encoding"}, - {Path: "encoding/json"}, - {Path: "errors"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - {Path: "time"}, - }, - Template: "params/style_matrix_explode.go.tmpl", - }, - "style_form": { - Name: "StyleFormParam", - Imports: []Import{ - {Path: "bytes"}, - {Path: "encoding"}, - {Path: "encoding/json"}, - {Path: "errors"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - {Path: "time"}, - }, - Template: "params/style_form.go.tmpl", - }, - "style_form_explode": { - Name: "StyleFormExplodeParam", - Imports: []Import{ - {Path: "bytes"}, - {Path: "encoding"}, - {Path: "encoding/json"}, - {Path: "errors"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - {Path: "time"}, - }, - Template: "params/style_form_explode.go.tmpl", - }, - "style_spaceDelimited": { - Name: "StyleSpaceDelimitedParam", - Imports: []Import{ - {Path: "encoding"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - {Path: "time"}, - }, - Template: "params/style_space_delimited.go.tmpl", - }, - "style_spaceDelimited_explode": { - Name: "StyleSpaceDelimitedExplodeParam", - Imports: []Import{ - {Path: "encoding"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - {Path: "time"}, - }, - Template: "params/style_space_delimited_explode.go.tmpl", - }, - "style_pipeDelimited": { - Name: "StylePipeDelimitedParam", - Imports: []Import{ - {Path: "encoding"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - {Path: "time"}, - }, - Template: "params/style_pipe_delimited.go.tmpl", - }, - "style_pipeDelimited_explode": { - Name: "StylePipeDelimitedExplodeParam", - Imports: []Import{ - {Path: "encoding"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - {Path: "time"}, - }, - Template: "params/style_pipe_delimited_explode.go.tmpl", - }, - "style_deepObject": { - Name: "StyleDeepObjectParam", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "sort"}, - {Path: "strconv"}, - {Path: "strings"}, - }, - Template: "params/style_deep_object.go.tmpl", - }, - - // Bind templates (deserialization) - "bind_simple": { - Name: "BindSimpleParam", - Imports: []Import{ - {Path: "encoding"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - }, - Template: "params/bind_simple.go.tmpl", - }, - "bind_simple_explode": { - Name: "BindSimpleExplodeParam", - Imports: []Import{ - {Path: "encoding"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - }, - Template: "params/bind_simple_explode.go.tmpl", - }, - "bind_label": { - Name: "BindLabelParam", - Imports: []Import{ - {Path: "encoding"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - }, - Template: "params/bind_label.go.tmpl", - }, - "bind_label_explode": { - Name: "BindLabelExplodeParam", - Imports: []Import{ - {Path: "encoding"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - }, - Template: "params/bind_label_explode.go.tmpl", - }, - "bind_matrix": { - Name: "BindMatrixParam", - Imports: []Import{ - {Path: "encoding"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - }, - Template: "params/bind_matrix.go.tmpl", - }, - "bind_matrix_explode": { - Name: "BindMatrixExplodeParam", - Imports: []Import{ - {Path: "encoding"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - }, - Template: "params/bind_matrix_explode.go.tmpl", - }, - "bind_form": { - Name: "BindFormParam", - Imports: []Import{ - {Path: "encoding"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - }, - Template: "params/bind_form.go.tmpl", - }, - "bind_form_explode": { - Name: "BindFormExplodeParam", - Imports: []Import{ - {Path: "fmt"}, - {Path: "net/url"}, - {Path: "reflect"}, - {Path: "strings"}, - {Path: "time"}, - }, - Template: "params/bind_form_explode.go.tmpl", - }, - "bind_spaceDelimited": { - Name: "BindSpaceDelimitedParam", - Imports: []Import{ - {Path: "encoding"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - }, - Template: "params/bind_space_delimited.go.tmpl", - }, - "bind_spaceDelimited_explode": { - Name: "BindSpaceDelimitedExplodeParam", - Imports: []Import{ - {Path: "net/url"}, - }, - Template: "params/bind_space_delimited_explode.go.tmpl", - }, - "bind_pipeDelimited": { - Name: "BindPipeDelimitedParam", - Imports: []Import{ - {Path: "encoding"}, - {Path: "fmt"}, - {Path: "reflect"}, - {Path: "strings"}, - }, - Template: "params/bind_pipe_delimited.go.tmpl", - }, - "bind_pipeDelimited_explode": { - Name: "BindPipeDelimitedExplodeParam", - Imports: []Import{ - {Path: "net/url"}, - }, - Template: "params/bind_pipe_delimited_explode.go.tmpl", - }, - "bind_deepObject": { - Name: "BindDeepObjectParam", - Imports: []Import{ - {Path: "errors"}, - {Path: "fmt"}, - {Path: "net/url"}, - {Path: "reflect"}, - {Path: "sort"}, - {Path: "strconv"}, - {Path: "strings"}, - {Path: "time"}, - }, - Template: "params/bind_deep_object.go.tmpl", - }, -} - -// ParamStyleKey returns the registry key for a style/explode combination. -// The prefix should be "style_" for serialization or "bind_" for binding. -func ParamStyleKey(prefix, style string, explode bool) string { - key := prefix + style - if explode { - key += "_explode" - } - return key -} - -// ServerTemplate defines a template for server generation. -type ServerTemplate struct { - Name string // Template name (e.g., "interface", "handler") - Imports []Import // Required imports for this template - Template string // Template path in embedded FS -} - -// ReceiverTemplate defines a template for receiver (webhook/callback) generation. -type ReceiverTemplate struct { - Name string // Template name (e.g., "receiver") - Imports []Import // Required imports for this template - Template string // Template path in embedded FS -} - -// StdHTTPReceiverTemplates contains receiver templates for StdHTTP servers. -var StdHTTPReceiverTemplates = map[string]ReceiverTemplate{ - "receiver": { - Name: "receiver", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - }, - Template: "server/stdhttp/receiver.go.tmpl", - }, -} - -// ChiReceiverTemplates contains receiver templates for Chi servers. -var ChiReceiverTemplates = map[string]ReceiverTemplate{ - "receiver": { - Name: "receiver", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - }, - Template: "server/chi/receiver.go.tmpl", - }, -} - -// EchoReceiverTemplates contains receiver templates for Echo v5 servers. -var EchoReceiverTemplates = map[string]ReceiverTemplate{ - "receiver": { - Name: "receiver", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - {Path: "github.com/labstack/echo/v5"}, - }, - Template: "server/echo/receiver.go.tmpl", - }, -} - -// EchoV4ReceiverTemplates contains receiver templates for Echo v4 servers. -var EchoV4ReceiverTemplates = map[string]ReceiverTemplate{ - "receiver": { - Name: "receiver", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - {Path: "github.com/labstack/echo/v4"}, - }, - Template: "server/echo-v4/receiver.go.tmpl", - }, -} - -// GinReceiverTemplates contains receiver templates for Gin servers. -var GinReceiverTemplates = map[string]ReceiverTemplate{ - "receiver": { - Name: "receiver", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - {Path: "github.com/gin-gonic/gin"}, - }, - Template: "server/gin/receiver.go.tmpl", - }, -} - -// GorillaReceiverTemplates contains receiver templates for Gorilla servers. -var GorillaReceiverTemplates = map[string]ReceiverTemplate{ - "receiver": { - Name: "receiver", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - }, - Template: "server/gorilla/receiver.go.tmpl", - }, -} - -// FiberReceiverTemplates contains receiver templates for Fiber servers. -var FiberReceiverTemplates = map[string]ReceiverTemplate{ - "receiver": { - Name: "receiver", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - {Path: "github.com/gofiber/fiber/v3"}, - }, - Template: "server/fiber/receiver.go.tmpl", - }, -} - -// IrisReceiverTemplates contains receiver templates for Iris servers. -var IrisReceiverTemplates = map[string]ReceiverTemplate{ - "receiver": { - Name: "receiver", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - {Path: "github.com/kataras/iris/v12"}, - }, - Template: "server/iris/receiver.go.tmpl", - }, -} - -// StdHTTPServerTemplates contains templates for StdHTTP server generation. -var StdHTTPServerTemplates = map[string]ServerTemplate{ - "interface": { - Name: "interface", - Imports: []Import{ - {Path: "net/http"}, - }, - Template: "server/stdhttp/interface.go.tmpl", - }, - "handler": { - Name: "handler", - Imports: []Import{ - {Path: "net/http"}, - }, - Template: "server/stdhttp/handler.go.tmpl", - }, - "wrapper": { - Name: "wrapper", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - }, - Template: "server/stdhttp/wrapper.go.tmpl", - }, -} - -// ChiServerTemplates contains templates for Chi server generation. -var ChiServerTemplates = map[string]ServerTemplate{ - "interface": { - Name: "interface", - Imports: []Import{ - {Path: "net/http"}, - }, - Template: "server/chi/interface.go.tmpl", - }, - "handler": { - Name: "handler", - Imports: []Import{ - {Path: "net/http"}, - {Path: "github.com/go-chi/chi/v5"}, - }, - Template: "server/chi/handler.go.tmpl", - }, - "wrapper": { - Name: "wrapper", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - {Path: "github.com/go-chi/chi/v5"}, - }, - Template: "server/chi/wrapper.go.tmpl", - }, -} - -// EchoServerTemplates contains templates for Echo v5 server generation. -var EchoServerTemplates = map[string]ServerTemplate{ - "interface": { - Name: "interface", - Imports: []Import{ - {Path: "net/http"}, - {Path: "github.com/labstack/echo/v5"}, - }, - Template: "server/echo/interface.go.tmpl", - }, - "handler": { - Name: "handler", - Imports: []Import{ - {Path: "github.com/labstack/echo/v5"}, - }, - Template: "server/echo/handler.go.tmpl", - }, - "wrapper": { - Name: "wrapper", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - {Path: "github.com/labstack/echo/v5"}, - }, - Template: "server/echo/wrapper.go.tmpl", - }, -} - -// EchoV4ServerTemplates contains templates for Echo v4 server generation. -var EchoV4ServerTemplates = map[string]ServerTemplate{ - "interface": { - Name: "interface", - Imports: []Import{ - {Path: "net/http"}, - {Path: "github.com/labstack/echo/v4"}, - }, - Template: "server/echo-v4/interface.go.tmpl", - }, - "handler": { - Name: "handler", - Imports: []Import{ - {Path: "github.com/labstack/echo/v4"}, - }, - Template: "server/echo-v4/handler.go.tmpl", - }, - "wrapper": { - Name: "wrapper", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - {Path: "github.com/labstack/echo/v4"}, - }, - Template: "server/echo-v4/wrapper.go.tmpl", - }, -} - -// GinServerTemplates contains templates for Gin server generation. -var GinServerTemplates = map[string]ServerTemplate{ - "interface": { - Name: "interface", - Imports: []Import{ - {Path: "net/http"}, - {Path: "github.com/gin-gonic/gin"}, - }, - Template: "server/gin/interface.go.tmpl", - }, - "handler": { - Name: "handler", - Imports: []Import{ - {Path: "github.com/gin-gonic/gin"}, - }, - Template: "server/gin/handler.go.tmpl", - }, - "wrapper": { - Name: "wrapper", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - {Path: "github.com/gin-gonic/gin"}, - }, - Template: "server/gin/wrapper.go.tmpl", - }, -} - -// GorillaServerTemplates contains templates for Gorilla server generation. -var GorillaServerTemplates = map[string]ServerTemplate{ - "interface": { - Name: "interface", - Imports: []Import{ - {Path: "net/http"}, - }, - Template: "server/gorilla/interface.go.tmpl", - }, - "handler": { - Name: "handler", - Imports: []Import{ - {Path: "net/http"}, - {Path: "github.com/gorilla/mux"}, - }, - Template: "server/gorilla/handler.go.tmpl", - }, - "wrapper": { - Name: "wrapper", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - {Path: "github.com/gorilla/mux"}, - }, - Template: "server/gorilla/wrapper.go.tmpl", - }, -} - -// FiberServerTemplates contains templates for Fiber server generation. -var FiberServerTemplates = map[string]ServerTemplate{ - "interface": { - Name: "interface", - Imports: []Import{ - {Path: "github.com/gofiber/fiber/v3"}, - }, - Template: "server/fiber/interface.go.tmpl", - }, - "handler": { - Name: "handler", - Imports: []Import{ - {Path: "github.com/gofiber/fiber/v3"}, - }, - Template: "server/fiber/handler.go.tmpl", - }, - "wrapper": { - Name: "wrapper", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - {Path: "github.com/gofiber/fiber/v3"}, - }, - Template: "server/fiber/wrapper.go.tmpl", - }, -} - -// IrisServerTemplates contains templates for Iris server generation. -var IrisServerTemplates = map[string]ServerTemplate{ - "interface": { - Name: "interface", - Imports: []Import{ - {Path: "net/http"}, - {Path: "github.com/kataras/iris/v12"}, - }, - Template: "server/iris/interface.go.tmpl", - }, - "handler": { - Name: "handler", - Imports: []Import{ - {Path: "github.com/kataras/iris/v12"}, - }, - Template: "server/iris/handler.go.tmpl", - }, - "wrapper": { - Name: "wrapper", - Imports: []Import{ - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "net/http"}, - {Path: "net/url"}, - {Path: "github.com/kataras/iris/v12"}, - }, - Template: "server/iris/wrapper.go.tmpl", - }, -} - -// SharedServerTemplates contains templates shared across all server implementations. -var SharedServerTemplates = map[string]ServerTemplate{ - "errors": { - Name: "errors", - Imports: []Import{ - {Path: "fmt"}, - }, - Template: "server/errors.go.tmpl", - }, - "param_types": { - Name: "param_types", - Imports: []Import{}, - Template: "server/param_types.go.tmpl", - }, -} - -// InitiatorTemplate defines a template for initiator (webhook/callback sender) generation. -type InitiatorTemplate struct { - Name string // Template name (e.g., "initiator_base", "initiator_interface") - Imports []Import // Required imports for this template - Template string // Template path in embedded FS -} - -// InitiatorTemplates contains templates for initiator generation. -// These are shared between webhook and callback initiators (parameterized by prefix). -var InitiatorTemplates = map[string]InitiatorTemplate{ - "initiator_base": { - Name: "initiator_base", - Imports: []Import{ - {Path: "context"}, - {Path: "net/http"}, - }, - Template: "initiator/base.go.tmpl", - }, - "initiator_interface": { - Name: "initiator_interface", - Imports: []Import{ - {Path: "context"}, - {Path: "io"}, - {Path: "net/http"}, - }, - Template: "initiator/interface.go.tmpl", - }, - "initiator_methods": { - Name: "initiator_methods", - Imports: []Import{ - {Path: "context"}, - {Path: "io"}, - {Path: "net/http"}, - }, - Template: "initiator/methods.go.tmpl", - }, - "initiator_request_builders": { - Name: "initiator_request_builders", - Imports: []Import{ - {Path: "bytes"}, - {Path: "encoding/json"}, - {Path: "io"}, - {Path: "net/http"}, - {Path: "net/url"}, - }, - Template: "initiator/request_builders.go.tmpl", - }, - "initiator_simple": { - Name: "initiator_simple", - Imports: []Import{ - {Path: "context"}, - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "io"}, - {Path: "net/http"}, - }, - Template: "initiator/simple.go.tmpl", - }, -} - -// ClientTemplate defines a template for client generation. -type ClientTemplate struct { - Name string // Template name (e.g., "base", "interface") - Imports []Import // Required imports for this template - Template string // Template path in embedded FS -} - -// ClientTemplates contains templates for client generation. -var ClientTemplates = map[string]ClientTemplate{ - "base": { - Name: "base", - Imports: []Import{ - {Path: "context"}, - {Path: "net/http"}, - {Path: "net/url"}, - {Path: "strings"}, - }, - Template: "client/base.go.tmpl", - }, - "interface": { - Name: "interface", - Imports: []Import{ - {Path: "context"}, - {Path: "io"}, - {Path: "net/http"}, - }, - Template: "client/interface.go.tmpl", - }, - "methods": { - Name: "methods", - Imports: []Import{ - {Path: "context"}, - {Path: "io"}, - {Path: "net/http"}, - }, - Template: "client/methods.go.tmpl", - }, - "request_builders": { - Name: "request_builders", - Imports: []Import{ - {Path: "bytes"}, - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "io"}, - {Path: "net/http"}, - {Path: "net/url"}, - }, - Template: "client/request_builders.go.tmpl", - }, - "simple": { - Name: "simple", - Imports: []Import{ - {Path: "context"}, - {Path: "encoding/json"}, - {Path: "fmt"}, - {Path: "io"}, - {Path: "net/http"}, - }, - Template: "client/simple.go.tmpl", - }, -} diff --git a/experimental/internal/codegen/templates/test/types/date.gen.go b/experimental/internal/codegen/templates/test/types/date.gen.go deleted file mode 100644 index 37c30d3b98..0000000000 --- a/experimental/internal/codegen/templates/test/types/date.gen.go +++ /dev/null @@ -1,46 +0,0 @@ -// Code generated by gentypes. DO NOT EDIT. - -package types - -import ( - "encoding/json" - "time" -) - - -const DateFormat = "2006-01-02" - -type Date struct { - time.Time -} - -func (d Date) MarshalJSON() ([]byte, error) { - return json.Marshal(d.Format(DateFormat)) -} - -func (d *Date) UnmarshalJSON(data []byte) error { - var dateStr string - err := json.Unmarshal(data, &dateStr) - if err != nil { - return err - } - parsed, err := time.Parse(DateFormat, dateStr) - if err != nil { - return err - } - d.Time = parsed - return nil -} - -func (d Date) String() string { - return d.Format(DateFormat) -} - -func (d *Date) UnmarshalText(data []byte) error { - parsed, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = parsed - return nil -} diff --git a/experimental/internal/codegen/templates/test/types/date_test.go b/experimental/internal/codegen/templates/test/types/date_test.go deleted file mode 100644 index 211776522f..0000000000 --- a/experimental/internal/codegen/templates/test/types/date_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package types - -import ( - "encoding/json" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestDate_MarshalJSON(t *testing.T) { - testDate := time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC) - b := struct { - DateField Date `json:"date"` - }{ - DateField: Date{testDate}, - } - jsonBytes, err := json.Marshal(b) - assert.NoError(t, err) - assert.JSONEq(t, `{"date":"2019-04-01"}`, string(jsonBytes)) -} - -func TestDate_UnmarshalJSON(t *testing.T) { - testDate := time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC) - jsonStr := `{"date":"2019-04-01"}` - b := struct { - DateField Date `json:"date"` - }{} - err := json.Unmarshal([]byte(jsonStr), &b) - assert.NoError(t, err) - assert.Equal(t, testDate, b.DateField.Time) -} - -func TestDate_Stringer(t *testing.T) { - t.Run("nil date", func(t *testing.T) { - var d *Date - assert.Equal(t, "", fmt.Sprintf("%v", d)) - }) - - t.Run("ptr date", func(t *testing.T) { - d := &Date{ - Time: time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC), - } - assert.Equal(t, "2019-04-01", fmt.Sprintf("%v", d)) - }) - - t.Run("value date", func(t *testing.T) { - d := Date{ - Time: time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC), - } - assert.Equal(t, "2019-04-01", fmt.Sprintf("%v", d)) - }) -} - -func TestDate_UnmarshalText(t *testing.T) { - testDate := time.Date(2022, 6, 14, 0, 0, 0, 0, time.UTC) - value := []byte("2022-06-14") - - date := Date{} - err := date.UnmarshalText(value) - - assert.NoError(t, err) - assert.Equal(t, testDate, date.Time) -} diff --git a/experimental/internal/codegen/templates/test/types/email.gen.go b/experimental/internal/codegen/templates/test/types/email.gen.go deleted file mode 100644 index 6db0d58270..0000000000 --- a/experimental/internal/codegen/templates/test/types/email.gen.go +++ /dev/null @@ -1,52 +0,0 @@ -// Code generated by gentypes. DO NOT EDIT. - -package types - -import ( - "encoding/json" - "errors" - "regexp" -) - - -const ( - emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" -) - -var ( - emailRegex = regexp.MustCompile(emailRegexString) -) - -// ErrValidationEmail is the sentinel error returned when an email fails validation -var ErrValidationEmail = errors.New("email: failed to pass regex validation") - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. -type Email string - -func (e Email) MarshalJSON() ([]byte, error) { - if !emailRegex.MatchString(string(e)) { - return nil, ErrValidationEmail - } - - return json.Marshal(string(e)) -} - -func (e *Email) UnmarshalJSON(data []byte) error { - if e == nil { - return nil - } - - var s string - if err := json.Unmarshal(data, &s); err != nil { - return err - } - - *e = Email(s) - if !emailRegex.MatchString(s) { - return ErrValidationEmail - } - - return nil -} diff --git a/experimental/internal/codegen/templates/test/types/email_test.go b/experimental/internal/codegen/templates/test/types/email_test.go deleted file mode 100644 index 736056b172..0000000000 --- a/experimental/internal/codegen/templates/test/types/email_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package types - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestEmail_MarshalJSON_Validation(t *testing.T) { - type requiredEmail struct { - EmailField Email `json:"email"` - } - - testCases := map[string]struct { - email Email - expectedJSON []byte - expectedError error - }{ - "it should succeed marshalling a valid email and return valid JSON populated with the email": { - email: Email("validemail@openapicodegen.com"), - expectedJSON: []byte(`{"email":"validemail@openapicodegen.com"}`), - expectedError: nil, - }, - "it should fail marshalling an invalid email and return a validation error": { - email: Email("invalidemail"), - expectedJSON: nil, - expectedError: ErrValidationEmail, - }, - "it should fail marshalling an empty email and return a validation error": { - email: Email(""), - expectedJSON: nil, - expectedError: ErrValidationEmail, - }, - } - - for name, tc := range testCases { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - jsonBytes, err := json.Marshal(requiredEmail{EmailField: tc.email}) - - if tc.expectedError != nil { - assert.ErrorIs(t, err, tc.expectedError) - } else { - assert.JSONEq(t, string(tc.expectedJSON), string(jsonBytes)) - } - }) - } -} - -func TestEmail_UnmarshalJSON_RequiredEmail_Validation(t *testing.T) { - type requiredEmail struct { - EmailField Email `json:"email"` - } - - requiredEmailTestCases := map[string]struct { - jsonStr string - expectedEmail Email - expectedError error - }{ - "it should succeed validating a valid email during the unmarshal process": { - jsonStr: `{"email":"gaben@valvesoftware.com"}`, - expectedError: nil, - expectedEmail: func() Email { - e := Email("gaben@valvesoftware.com") - return e - }(), - }, - "it should fail validating an invalid email": { - jsonStr: `{"email":"not-an-email"}`, - expectedError: ErrValidationEmail, - expectedEmail: func() Email { - e := Email("not-an-email") - return e - }(), - }, - "it should fail validating an empty email": { - jsonStr: `{"email":""}`, - expectedEmail: func() Email { - e := Email("") - return e - }(), - expectedError: ErrValidationEmail, - }, - "it should fail validating a null email": { - jsonStr: `{"email":null}`, - expectedEmail: func() Email { - e := Email("") - return e - }(), - expectedError: ErrValidationEmail, - }, - } - - for name, tc := range requiredEmailTestCases { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - b := requiredEmail{} - err := json.Unmarshal([]byte(tc.jsonStr), &b) - assert.Equal(t, tc.expectedEmail, b.EmailField) - assert.ErrorIs(t, err, tc.expectedError) - }) - } - -} - -func TestEmail_UnmarshalJSON_NullableEmail_Validation(t *testing.T) { - - type nullableEmail struct { - EmailField *Email `json:"email,omitempty"` - } - - nullableEmailTestCases := map[string]struct { - body nullableEmail - jsonStr string - expectedEmail *Email - expectedError error - }{ - "it should succeed validating a valid email during the unmarshal process": { - body: nullableEmail{}, - jsonStr: `{"email":"gaben@valvesoftware.com"}`, - expectedError: nil, - expectedEmail: func() *Email { - e := Email("gaben@valvesoftware.com") - return &e - }(), - }, - "it should fail validating an invalid email": { - body: nullableEmail{}, - jsonStr: `{"email":"not-an-email"}`, - expectedError: ErrValidationEmail, - expectedEmail: func() *Email { - e := Email("not-an-email") - return &e - }(), - }, - "it should fail validating an empty email": { - body: nullableEmail{}, - jsonStr: `{"email":""}`, - expectedError: ErrValidationEmail, - expectedEmail: func() *Email { - e := Email("") - return &e - }(), - }, - "it should succeed validating a null email": { - body: nullableEmail{}, - jsonStr: `{"email":null}`, - expectedEmail: nil, - expectedError: nil, - }, - "it should succeed validating a missing email": { - body: nullableEmail{}, - jsonStr: `{}`, - expectedEmail: nil, - expectedError: nil, - }, - } - - for name, tc := range nullableEmailTestCases { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - err := json.Unmarshal([]byte(tc.jsonStr), &tc.body) - assert.Equal(t, tc.expectedEmail, tc.body.EmailField) - if tc.expectedError != nil { - assert.ErrorIs(t, err, tc.expectedError) - } - }) - } -} diff --git a/experimental/internal/codegen/templates/test/types/file.gen.go b/experimental/internal/codegen/templates/test/types/file.gen.go deleted file mode 100644 index 391c94a9da..0000000000 --- a/experimental/internal/codegen/templates/test/types/file.gen.go +++ /dev/null @@ -1,74 +0,0 @@ -// Code generated by gentypes. DO NOT EDIT. - -package types - -import ( - "bytes" - "encoding/json" - "io" - "mime/multipart" -) - - -type File struct { - multipart *multipart.FileHeader - data []byte - filename string -} - -func (file *File) InitFromMultipart(header *multipart.FileHeader) { - file.multipart = header - file.data = nil - file.filename = "" -} - -func (file *File) InitFromBytes(data []byte, filename string) { - file.data = data - file.filename = filename - file.multipart = nil -} - -func (file File) MarshalJSON() ([]byte, error) { - b, err := file.Bytes() - if err != nil { - return nil, err - } - return json.Marshal(b) -} - -func (file *File) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, &file.data) -} - -func (file File) Bytes() ([]byte, error) { - if file.multipart != nil { - f, err := file.multipart.Open() - if err != nil { - return nil, err - } - defer func() { _ = f.Close() }() - return io.ReadAll(f) - } - return file.data, nil -} - -func (file File) Reader() (io.ReadCloser, error) { - if file.multipart != nil { - return file.multipart.Open() - } - return io.NopCloser(bytes.NewReader(file.data)), nil -} - -func (file File) Filename() string { - if file.multipart != nil { - return file.multipart.Filename - } - return file.filename -} - -func (file File) FileSize() int64 { - if file.multipart != nil { - return file.multipart.Size - } - return int64(len(file.data)) -} diff --git a/experimental/internal/codegen/templates/test/types/file_test.go b/experimental/internal/codegen/templates/test/types/file_test.go deleted file mode 100644 index fb4ce98ae1..0000000000 --- a/experimental/internal/codegen/templates/test/types/file_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package types - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var _ json.Marshaler = (*File)(nil) -var _ json.Unmarshaler = (*File)(nil) - -func TestFileJSON(t *testing.T) { - type Object struct { - BinaryField File `json:"binary_field"` - } - - // Check whether we encode JSON properly. - var o Object - o.BinaryField.InitFromBytes([]byte("hello"), "") - buf, err := json.Marshal(o) - require.NoError(t, err) - t.Log(string(buf)) - - // Decode the same object back into File, ensure result is correct. - var o2 Object - err = json.Unmarshal(buf, &o2) - require.NoError(t, err) - o2Bytes, err := o2.BinaryField.Bytes() - require.NoError(t, err) - assert.Equal(t, []byte("hello"), o2Bytes) - - // Ensure it also works via pointer. - type Object2 struct { - BinaryFieldPtr *File `json:"binary_field"` - } - - var o3 Object2 - var f File - f.InitFromBytes([]byte("hello"), "") - o3.BinaryFieldPtr = &f - buf, err = json.Marshal(o) - require.NoError(t, err) - t.Log(string(buf)) - - var o4 Object2 - err = json.Unmarshal(buf, &o4) - require.NoError(t, err) - o4Bytes, err := o4.BinaryFieldPtr.Bytes() - require.NoError(t, err) - assert.Equal(t, []byte("hello"), o4Bytes) - -} diff --git a/experimental/internal/codegen/templates/test/types/generate.go b/experimental/internal/codegen/templates/test/types/generate.go deleted file mode 100644 index bab32084c7..0000000000 --- a/experimental/internal/codegen/templates/test/types/generate.go +++ /dev/null @@ -1,3 +0,0 @@ -package types - -//go:generate go run ./gentypes -package types -output . Email Date UUID File diff --git a/experimental/internal/codegen/templates/test/types/gentypes/main.go b/experimental/internal/codegen/templates/test/types/gentypes/main.go deleted file mode 100644 index c22eee2751..0000000000 --- a/experimental/internal/codegen/templates/test/types/gentypes/main.go +++ /dev/null @@ -1,109 +0,0 @@ -// gentypes generates Go type files from templates. -// Usage: gentypes -package -output [types...] -// -// Example: -// -// //go:generate gentypes -package types -output . Email Date UUID File -package main - -import ( - "bytes" - "flag" - "fmt" - "os" - "path/filepath" - "strings" - "text/template" - - "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/templates" -) - -func main() { - packageName := flag.String("package", "", "Go package name for generated files") - outputDir := flag.String("output", ".", "output directory for generated files") - flag.Parse() - - if *packageName == "" { - fmt.Fprintln(os.Stderr, "error: -package is required") - os.Exit(1) - } - - typeNames := flag.Args() - if len(typeNames) == 0 { - fmt.Fprintln(os.Stderr, "error: at least one type name required") - fmt.Fprintln(os.Stderr, "available types: Email, Date, UUID, File") - os.Exit(1) - } - - for _, typeName := range typeNames { - if err := generateType(*packageName, *outputDir, typeName); err != nil { - fmt.Fprintf(os.Stderr, "error generating %s: %v\n", typeName, err) - os.Exit(1) - } - } -} - -func generateType(packageName, outputDir, typeName string) error { - tt, ok := templates.TypeTemplates[typeName] - if !ok { - return fmt.Errorf("unknown type: %s", typeName) - } - - // Read template content - tmplPath := "files/" + tt.Template - tmplContent, err := templates.TemplateFS.ReadFile(tmplPath) - if err != nil { - return fmt.Errorf("reading template %s: %w", tmplPath, err) - } - - // Parse and execute template (in case it has any directives) - tmpl, err := template.New(typeName).Parse(string(tmplContent)) - if err != nil { - return fmt.Errorf("parsing template: %w", err) - } - - var body bytes.Buffer - if err := tmpl.Execute(&body, nil); err != nil { - return fmt.Errorf("executing template: %w", err) - } - - // Build the output file - var out bytes.Buffer - - // Package declaration - fmt.Fprintf(&out, "// Code generated by gentypes. DO NOT EDIT.\n\n") - fmt.Fprintf(&out, "package %s\n", packageName) - - // Imports - if len(tt.Imports) > 0 { - out.WriteString("\nimport (\n") - for _, imp := range tt.Imports { - if imp.Alias != "" { - fmt.Fprintf(&out, "\t%s %q\n", imp.Alias, imp.Path) - } else { - fmt.Fprintf(&out, "\t%q\n", imp.Path) - } - } - out.WriteString(")\n") - } - - // Template body (skip the leading template comment if present) - bodyStr := body.String() - if strings.HasPrefix(bodyStr, "{{/*") { - // Skip the comment line - if idx := strings.Index(bodyStr, "*/}}"); idx != -1 { - bodyStr = bodyStr[idx+4:] - } - } - out.WriteString(bodyStr) - - // Write output file - filename := strings.ToLower(typeName) + ".gen.go" - outputPath := filepath.Join(outputDir, filename) - if err := os.WriteFile(outputPath, out.Bytes(), 0644); err != nil { - return fmt.Errorf("writing %s: %w", outputPath, err) - } - - fmt.Printf("generated %s\n", outputPath) - return nil -} diff --git a/experimental/internal/codegen/templates/test/types/uuid.gen.go b/experimental/internal/codegen/templates/test/types/uuid.gen.go deleted file mode 100644 index f1c45b3c36..0000000000 --- a/experimental/internal/codegen/templates/test/types/uuid.gen.go +++ /dev/null @@ -1,10 +0,0 @@ -// Code generated by gentypes. DO NOT EDIT. - -package types - -import ( - "github.com/google/uuid" -) - - -type UUID = uuid.UUID diff --git a/experimental/internal/codegen/templates/test/types/uuid_test.go b/experimental/internal/codegen/templates/test/types/uuid_test.go deleted file mode 100644 index bb62040b6c..0000000000 --- a/experimental/internal/codegen/templates/test/types/uuid_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package types - -import ( - "encoding/json" - "testing" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" -) - -func TestUUID_MarshalJSON_Zero(t *testing.T) { - var testUUID UUID - b := struct { - UUIDField UUID `json:"uuid"` - }{ - UUIDField: testUUID, - } - marshaled, err := json.Marshal(b) - assert.NoError(t, err) - assert.JSONEq(t, `{"uuid":"00000000-0000-0000-0000-000000000000"}`, string(marshaled)) -} - -func TestUUID_MarshalJSON_Pass(t *testing.T) { - testUUID := uuid.MustParse("9cb14230-b640-11ec-b909-0242ac120002") - b := struct { - UUIDField UUID `json:"uuid"` - }{ - UUIDField: testUUID, - } - jsonBytes, err := json.Marshal(b) - assert.NoError(t, err) - assert.JSONEq(t, `{"uuid":"9cb14230-b640-11ec-b909-0242ac120002"}`, string(jsonBytes)) -} - -func TestUUID_UnmarshalJSON_Fail(t *testing.T) { - jsonStr := `{"uuid":"this-is-not-a-uuid"}` - b := struct { - UUIDField UUID `json:"uuid"` - }{} - err := json.Unmarshal([]byte(jsonStr), &b) - assert.Error(t, err) -} - -func TestUUID_UnmarshalJSON_Pass(t *testing.T) { - testUUID := uuid.MustParse("9cb14230-b640-11ec-b909-0242ac120002") - jsonStr := `{"uuid":"9cb14230-b640-11ec-b909-0242ac120002"}` - b := struct { - UUIDField UUID `json:"uuid"` - }{} - err := json.Unmarshal([]byte(jsonStr), &b) - assert.NoError(t, err) - assert.Equal(t, testUUID, b.UUIDField) -} diff --git a/experimental/internal/codegen/test/comprehensive/doc.go b/experimental/internal/codegen/test/comprehensive/doc.go deleted file mode 100644 index 658e592917..0000000000 --- a/experimental/internal/codegen/test/comprehensive/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -package comprehensive - -//go:generate go run ../../../../cmd/oapi-codegen -package output -output output/comprehensive.gen.go ../files/comprehensive.yaml diff --git a/experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go b/experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go deleted file mode 100644 index 633aa2dbe7..0000000000 --- a/experimental/internal/codegen/test/comprehensive/output/comprehensive.gen.go +++ /dev/null @@ -1,2098 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "mime/multipart" - "regexp" - "strings" - "sync" - "time" - - "github.com/google/uuid" -) - -// #/components/schemas/AllTypesRequired -type AllTypesRequired struct { - IntField int `json:"intField" form:"intField"` - Int32Field int32 `json:"int32Field" form:"int32Field"` - Int64Field int64 `json:"int64Field" form:"int64Field"` - FloatField float32 `json:"floatField" form:"floatField"` - DoubleField float64 `json:"doubleField" form:"doubleField"` - NumberField float32 `json:"numberField" form:"numberField"` - StringField string `json:"stringField" form:"stringField"` - BoolField bool `json:"boolField" form:"boolField"` - DateField Date `json:"dateField" form:"dateField"` - DateTimeField time.Time `json:"dateTimeField" form:"dateTimeField"` - UUIDField UUID `json:"uuidField" form:"uuidField"` - EmailField Email `json:"emailField" form:"emailField"` - URIField string `json:"uriField" form:"uriField"` - HostnameField string `json:"hostnameField" form:"hostnameField"` - Ipv4Field string `json:"ipv4Field" form:"ipv4Field"` - Ipv6Field string `json:"ipv6Field" form:"ipv6Field"` - ByteField []byte `json:"byteField" form:"byteField"` - BinaryField File `json:"binaryField" form:"binaryField"` - PasswordField string `json:"passwordField" form:"passwordField"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AllTypesRequired) ApplyDefaults() { -} - -// #/components/schemas/AllTypesOptional -type AllTypesOptional struct { - IntField *int `json:"intField,omitempty" form:"intField,omitempty"` - Int32Field *int32 `json:"int32Field,omitempty" form:"int32Field,omitempty"` - Int64Field *int64 `json:"int64Field,omitempty" form:"int64Field,omitempty"` - FloatField *float32 `json:"floatField,omitempty" form:"floatField,omitempty"` - DoubleField *float64 `json:"doubleField,omitempty" form:"doubleField,omitempty"` - NumberField *float32 `json:"numberField,omitempty" form:"numberField,omitempty"` - StringField *string `json:"stringField,omitempty" form:"stringField,omitempty"` - BoolField *bool `json:"boolField,omitempty" form:"boolField,omitempty"` - DateField *Date `json:"dateField,omitempty" form:"dateField,omitempty"` - DateTimeField *time.Time `json:"dateTimeField,omitempty" form:"dateTimeField,omitempty"` - UUIDField *UUID `json:"uuidField,omitempty" form:"uuidField,omitempty"` - EmailField *Email `json:"emailField,omitempty" form:"emailField,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AllTypesOptional) ApplyDefaults() { -} - -// #/components/schemas/NullableRequired -type NullableRequired struct { - NullableString Nullable[string] `json:"nullableString" form:"nullableString"` - NullableInt Nullable[int] `json:"nullableInt" form:"nullableInt"` - NullableObject Nullable[NullableRequiredNullableObject] `json:"nullableObject" form:"nullableObject"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *NullableRequired) ApplyDefaults() { -} - -// #/components/schemas/NullableRequired/properties/nullableObject -type NullableRequiredNullableObject struct { - Name *string `json:"name,omitempty" form:"name,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *NullableRequiredNullableObject) ApplyDefaults() { -} - -// #/components/schemas/NullableOptional -type NullableOptional struct { - NullableString Nullable[string] `json:"nullableString,omitempty" form:"nullableString,omitempty"` - NullableInt Nullable[int] `json:"nullableInt,omitempty" form:"nullableInt,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *NullableOptional) ApplyDefaults() { -} - -// #/components/schemas/ArrayTypes -type ArrayTypes struct { - StringArray []string `json:"stringArray,omitempty" form:"stringArray,omitempty"` - IntArray []int `json:"intArray,omitempty" form:"intArray,omitempty"` - ObjectArray []SimpleObject `json:"objectArray,omitempty" form:"objectArray,omitempty"` - InlineObjectArray []ArrayTypesInlineObjectArrayItem `json:"inlineObjectArray,omitempty" form:"inlineObjectArray,omitempty"` - NestedArray [][]string `json:"nestedArray,omitempty" form:"nestedArray,omitempty"` - NullableArray []string `json:"nullableArray,omitempty" form:"nullableArray,omitempty"` - ArrayWithConstraints []string `json:"arrayWithConstraints,omitempty" form:"arrayWithConstraints,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ArrayTypes) ApplyDefaults() { -} - -// #/components/schemas/ArrayTypes/properties/objectArray -type ArrayTypesObjectArray = []SimpleObject - -// #/components/schemas/ArrayTypes/properties/inlineObjectArray -type ArrayTypesInlineObjectArray = []ArrayTypesInlineObjectArrayItem - -// #/components/schemas/ArrayTypes/properties/inlineObjectArray/items -type ArrayTypesInlineObjectArrayItem struct { - ID *int `json:"id,omitempty" form:"id,omitempty"` - Name *string `json:"name,omitempty" form:"name,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ArrayTypesInlineObjectArrayItem) ApplyDefaults() { -} - -// #/components/schemas/SimpleObject -type SimpleObject struct { - ID int `json:"id" form:"id"` - Name *string `json:"name,omitempty" form:"name,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *SimpleObject) ApplyDefaults() { -} - -// #/components/schemas/NestedObject -type NestedObject struct { - Outer *NestedObjectOuter `json:"outer,omitempty" form:"outer,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *NestedObject) ApplyDefaults() { - if s.Outer != nil { - s.Outer.ApplyDefaults() - } -} - -// #/components/schemas/NestedObject/properties/outer -type NestedObjectOuter struct { - Inner *NestedObjectOuterInner `json:"inner,omitempty" form:"inner,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *NestedObjectOuter) ApplyDefaults() { - if s.Inner != nil { - s.Inner.ApplyDefaults() - } -} - -// #/components/schemas/NestedObject/properties/outer/properties/inner -type NestedObjectOuterInner struct { - Value *string `json:"value,omitempty" form:"value,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *NestedObjectOuterInner) ApplyDefaults() { -} - -// #/components/schemas/AdditionalPropsAny -type AdditionalPropsAny = map[string]any - -// #/components/schemas/AdditionalPropsNone -type AdditionalPropsNone struct { - Known *string `json:"known,omitempty" form:"known,omitempty"` - AdditionalProperties map[string]any `json:"-"` -} - -func (s AdditionalPropsNone) MarshalJSON() ([]byte, error) { - result := make(map[string]any) - - if s.Known != nil { - result["known"] = s.Known - } - - // Add additional properties - for k, v := range s.AdditionalProperties { - result[k] = v - } - - return json.Marshal(result) -} - -func (s *AdditionalPropsNone) UnmarshalJSON(data []byte) error { - // Known fields - knownFields := map[string]bool{ - "known": true, - } - - var raw map[string]json.RawMessage - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - - if v, ok := raw["known"]; ok { - var val string - if err := json.Unmarshal(v, &val); err != nil { - return err - } - s.Known = &val - } - - // Collect additional properties - s.AdditionalProperties = make(map[string]any) - for k, v := range raw { - if !knownFields[k] { - var val any - if err := json.Unmarshal(v, &val); err != nil { - return err - } - s.AdditionalProperties[k] = val - } - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AdditionalPropsNone) ApplyDefaults() { -} - -// #/components/schemas/AdditionalPropsTyped -type AdditionalPropsTyped = map[string]int - -// #/components/schemas/AdditionalPropsObject -type AdditionalPropsObject = map[string]any - -// #/components/schemas/AdditionalPropsWithProps -type AdditionalPropsWithProps struct { - ID *int `json:"id,omitempty" form:"id,omitempty"` - AdditionalProperties map[string]string `json:"-"` -} - -func (s AdditionalPropsWithProps) MarshalJSON() ([]byte, error) { - result := make(map[string]any) - - if s.ID != nil { - result["id"] = s.ID - } - - // Add additional properties - for k, v := range s.AdditionalProperties { - result[k] = v - } - - return json.Marshal(result) -} - -func (s *AdditionalPropsWithProps) UnmarshalJSON(data []byte) error { - // Known fields - knownFields := map[string]bool{ - "id": true, - } - - var raw map[string]json.RawMessage - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - - if v, ok := raw["id"]; ok { - var val int - if err := json.Unmarshal(v, &val); err != nil { - return err - } - s.ID = &val - } - - // Collect additional properties - s.AdditionalProperties = make(map[string]string) - for k, v := range raw { - if !knownFields[k] { - var val string - if err := json.Unmarshal(v, &val); err != nil { - return err - } - s.AdditionalProperties[k] = val - } - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AdditionalPropsWithProps) ApplyDefaults() { -} - -// #/components/schemas/StringEnum -type StringEnum string - -const ( - StringEnum_value1 StringEnum = "value1" - StringEnum_value2 StringEnum = "value2" - StringEnum_value3 StringEnum = "value3" -) - -// #/components/schemas/IntegerEnum -type IntegerEnum int - -const ( - IntegerEnum_N1 IntegerEnum = 1 - IntegerEnum_N2 IntegerEnum = 2 - IntegerEnum_N3 IntegerEnum = 3 -) - -// #/components/schemas/ObjectWithEnum -type ObjectWithEnum struct { - Status *string `json:"status,omitempty" form:"status,omitempty"` - Priority *int `json:"priority,omitempty" form:"priority,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ObjectWithEnum) ApplyDefaults() { -} - -// #/components/schemas/ObjectWithEnum/properties/status -type ObjectWithEnumStatus string - -const ( - ObjectWithEnumStatus_pending ObjectWithEnumStatus = "pending" - ObjectWithEnumStatus_active ObjectWithEnumStatus = "active" - ObjectWithEnumStatus_completed ObjectWithEnumStatus = "completed" -) - -// #/components/schemas/ObjectWithEnum/properties/priority -type ObjectWithEnumPriority int - -const ( - ObjectWithEnumPriority_N1 ObjectWithEnumPriority = 1 - ObjectWithEnumPriority_N2 ObjectWithEnumPriority = 2 - ObjectWithEnumPriority_N3 ObjectWithEnumPriority = 3 -) - -// #/components/schemas/InlineEnumInProperty -type InlineEnumInProperty struct { - InlineStatus *string `json:"inlineStatus,omitempty" form:"inlineStatus,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *InlineEnumInProperty) ApplyDefaults() { -} - -// #/components/schemas/InlineEnumInProperty/properties/inlineStatus -type InlineEnumInPropertyInlineStatus string - -const ( - InlineEnumInPropertyInlineStatus_on InlineEnumInPropertyInlineStatus = "on" - InlineEnumInPropertyInlineStatus_off InlineEnumInPropertyInlineStatus = "off" -) - -// #/components/schemas/BaseProperties -type BaseProperties struct { - ID *int `json:"id,omitempty" form:"id,omitempty"` - CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *BaseProperties) ApplyDefaults() { -} - -// #/components/schemas/ExtendedObject -type ExtendedObject struct { - ID *int `json:"id,omitempty" form:"id,omitempty"` - CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` - Name string `json:"name" form:"name"` - Description *string `json:"description,omitempty" form:"description,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ExtendedObject) ApplyDefaults() { -} - -// #/components/schemas/DeepInheritance -type DeepInheritance struct { - ID *int `json:"id,omitempty" form:"id,omitempty"` - CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` - Name string `json:"name" form:"name"` - Description *string `json:"description,omitempty" form:"description,omitempty"` - Extra *string `json:"extra,omitempty" form:"extra,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *DeepInheritance) ApplyDefaults() { -} - -// #/components/schemas/AllOfMultipleRefs -type AllOfMultipleRefs struct { - ID int `json:"id" form:"id"` - CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` - Name *string `json:"name,omitempty" form:"name,omitempty"` - Merged *bool `json:"merged,omitempty" form:"merged,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AllOfMultipleRefs) ApplyDefaults() { -} - -// #/components/schemas/AllOfInlineOnly -type AllOfInlineOnly struct { - First *string `json:"first,omitempty" form:"first,omitempty"` - Second *int `json:"second,omitempty" form:"second,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AllOfInlineOnly) ApplyDefaults() { -} - -// #/components/schemas/AnyOfPrimitives -type AnyOfPrimitives struct { - String0 *string - Int1 *int -} - -func (u AnyOfPrimitives) MarshalJSON() ([]byte, error) { - if u.String0 != nil { - return json.Marshal(u.String0) - } - if u.Int1 != nil { - return json.Marshal(u.Int1) - } - return []byte("null"), nil -} - -func (u *AnyOfPrimitives) UnmarshalJSON(data []byte) error { - var v0 string - if err := json.Unmarshal(data, &v0); err == nil { - u.String0 = &v0 - } - - var v1 int - if err := json.Unmarshal(data, &v1); err == nil { - u.Int1 = &v1 - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *AnyOfPrimitives) ApplyDefaults() { -} - -// #/components/schemas/AnyOfObjects -type AnyOfObjects struct { - SimpleObject *SimpleObject - BaseProperties *BaseProperties -} - -func (u AnyOfObjects) MarshalJSON() ([]byte, error) { - result := make(map[string]any) - - if u.SimpleObject != nil { - data, err := json.Marshal(u.SimpleObject) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err == nil { - for k, v := range m { - result[k] = v - } - } - } - if u.BaseProperties != nil { - data, err := json.Marshal(u.BaseProperties) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err == nil { - for k, v := range m { - result[k] = v - } - } - } - - return json.Marshal(result) -} - -func (u *AnyOfObjects) UnmarshalJSON(data []byte) error { - var v0 SimpleObject - if err := json.Unmarshal(data, &v0); err == nil { - u.SimpleObject = &v0 - } - - var v1 BaseProperties - if err := json.Unmarshal(data, &v1); err == nil { - u.BaseProperties = &v1 - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *AnyOfObjects) ApplyDefaults() { - if u.SimpleObject != nil { - u.SimpleObject.ApplyDefaults() - } - if u.BaseProperties != nil { - u.BaseProperties.ApplyDefaults() - } -} - -// #/components/schemas/AnyOfMixed -type AnyOfMixed struct { - String0 *string - SimpleObject *SimpleObject - AnyOfMixedAnyOf2 *AnyOfMixedAnyOf2 -} - -func (u AnyOfMixed) MarshalJSON() ([]byte, error) { - result := make(map[string]any) - - if u.String0 != nil { - return json.Marshal(u.String0) - } - if u.SimpleObject != nil { - data, err := json.Marshal(u.SimpleObject) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err == nil { - for k, v := range m { - result[k] = v - } - } - } - if u.AnyOfMixedAnyOf2 != nil { - data, err := json.Marshal(u.AnyOfMixedAnyOf2) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err == nil { - for k, v := range m { - result[k] = v - } - } - } - - return json.Marshal(result) -} - -func (u *AnyOfMixed) UnmarshalJSON(data []byte) error { - var v0 string - if err := json.Unmarshal(data, &v0); err == nil { - u.String0 = &v0 - } - - var v1 SimpleObject - if err := json.Unmarshal(data, &v1); err == nil { - u.SimpleObject = &v1 - } - - var v2 AnyOfMixedAnyOf2 - if err := json.Unmarshal(data, &v2); err == nil { - u.AnyOfMixedAnyOf2 = &v2 - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *AnyOfMixed) ApplyDefaults() { - if u.SimpleObject != nil { - u.SimpleObject.ApplyDefaults() - } - if u.AnyOfMixedAnyOf2 != nil { - u.AnyOfMixedAnyOf2.ApplyDefaults() - } -} - -// #/components/schemas/AnyOfMixed/anyOf/2 -type AnyOfMixedAnyOf2 struct { - Inline *bool `json:"inline,omitempty" form:"inline,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AnyOfMixedAnyOf2) ApplyDefaults() { -} - -// #/components/schemas/AnyOfNullable -type AnyOfNullable struct { - String0 *string - Any1 *any -} - -func (u AnyOfNullable) MarshalJSON() ([]byte, error) { - if u.String0 != nil { - return json.Marshal(u.String0) - } - if u.Any1 != nil { - return json.Marshal(u.Any1) - } - return []byte("null"), nil -} - -func (u *AnyOfNullable) UnmarshalJSON(data []byte) error { - var v0 string - if err := json.Unmarshal(data, &v0); err == nil { - u.String0 = &v0 - } - - var v1 any - if err := json.Unmarshal(data, &v1); err == nil { - u.Any1 = &v1 - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *AnyOfNullable) ApplyDefaults() { -} - -// #/components/schemas/ObjectWithAnyOfProperty -type ObjectWithAnyOfProperty struct { - Value *ObjectWithAnyOfPropertyValue `json:"value,omitempty" form:"value,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ObjectWithAnyOfProperty) ApplyDefaults() { -} - -// #/components/schemas/ObjectWithAnyOfProperty/properties/value -type ObjectWithAnyOfPropertyValue struct { - String0 *string - Int1 *int - Bool2 *bool -} - -func (u ObjectWithAnyOfPropertyValue) MarshalJSON() ([]byte, error) { - if u.String0 != nil { - return json.Marshal(u.String0) - } - if u.Int1 != nil { - return json.Marshal(u.Int1) - } - if u.Bool2 != nil { - return json.Marshal(u.Bool2) - } - return []byte("null"), nil -} - -func (u *ObjectWithAnyOfPropertyValue) UnmarshalJSON(data []byte) error { - var v0 string - if err := json.Unmarshal(data, &v0); err == nil { - u.String0 = &v0 - } - - var v1 int - if err := json.Unmarshal(data, &v1); err == nil { - u.Int1 = &v1 - } - - var v2 bool - if err := json.Unmarshal(data, &v2); err == nil { - u.Bool2 = &v2 - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *ObjectWithAnyOfPropertyValue) ApplyDefaults() { -} - -// #/components/schemas/ArrayOfAnyOf -type ArrayOfAnyOf = []ArrayOfAnyOfItem - -// #/components/schemas/ArrayOfAnyOf/items -type ArrayOfAnyOfItem struct { - SimpleObject *SimpleObject - BaseProperties *BaseProperties -} - -func (u ArrayOfAnyOfItem) MarshalJSON() ([]byte, error) { - result := make(map[string]any) - - if u.SimpleObject != nil { - data, err := json.Marshal(u.SimpleObject) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err == nil { - for k, v := range m { - result[k] = v - } - } - } - if u.BaseProperties != nil { - data, err := json.Marshal(u.BaseProperties) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err == nil { - for k, v := range m { - result[k] = v - } - } - } - - return json.Marshal(result) -} - -func (u *ArrayOfAnyOfItem) UnmarshalJSON(data []byte) error { - var v0 SimpleObject - if err := json.Unmarshal(data, &v0); err == nil { - u.SimpleObject = &v0 - } - - var v1 BaseProperties - if err := json.Unmarshal(data, &v1); err == nil { - u.BaseProperties = &v1 - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *ArrayOfAnyOfItem) ApplyDefaults() { - if u.SimpleObject != nil { - u.SimpleObject.ApplyDefaults() - } - if u.BaseProperties != nil { - u.BaseProperties.ApplyDefaults() - } -} - -// #/components/schemas/OneOfSimple -type OneOfSimple struct { - SimpleObject *SimpleObject - BaseProperties *BaseProperties -} - -func (u OneOfSimple) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.SimpleObject != nil { - count++ - data, err = json.Marshal(u.SimpleObject) - if err != nil { - return nil, err - } - } - if u.BaseProperties != nil { - count++ - data, err = json.Marshal(u.BaseProperties) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("OneOfSimple: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *OneOfSimple) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 SimpleObject - if err := json.Unmarshal(data, &v0); err == nil { - u.SimpleObject = &v0 - successCount++ - } - - var v1 BaseProperties - if err := json.Unmarshal(data, &v1); err == nil { - u.BaseProperties = &v1 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("OneOfSimple: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *OneOfSimple) ApplyDefaults() { - if u.SimpleObject != nil { - u.SimpleObject.ApplyDefaults() - } - if u.BaseProperties != nil { - u.BaseProperties.ApplyDefaults() - } -} - -// #/components/schemas/OneOfWithDiscriminator -type OneOfWithDiscriminator struct { - Cat *Cat - Dog *Dog -} - -func (u OneOfWithDiscriminator) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.Cat != nil { - count++ - data, err = json.Marshal(u.Cat) - if err != nil { - return nil, err - } - } - if u.Dog != nil { - count++ - data, err = json.Marshal(u.Dog) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("OneOfWithDiscriminator: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *OneOfWithDiscriminator) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 Cat - if err := json.Unmarshal(data, &v0); err == nil { - u.Cat = &v0 - successCount++ - } - - var v1 Dog - if err := json.Unmarshal(data, &v1); err == nil { - u.Dog = &v1 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("OneOfWithDiscriminator: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *OneOfWithDiscriminator) ApplyDefaults() { - if u.Cat != nil { - u.Cat.ApplyDefaults() - } - if u.Dog != nil { - u.Dog.ApplyDefaults() - } -} - -// #/components/schemas/OneOfWithDiscriminatorMapping -type OneOfWithDiscriminatorMapping struct { - Cat *Cat - Dog *Dog -} - -func (u OneOfWithDiscriminatorMapping) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.Cat != nil { - count++ - data, err = json.Marshal(u.Cat) - if err != nil { - return nil, err - } - } - if u.Dog != nil { - count++ - data, err = json.Marshal(u.Dog) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("OneOfWithDiscriminatorMapping: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *OneOfWithDiscriminatorMapping) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 Cat - if err := json.Unmarshal(data, &v0); err == nil { - u.Cat = &v0 - successCount++ - } - - var v1 Dog - if err := json.Unmarshal(data, &v1); err == nil { - u.Dog = &v1 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("OneOfWithDiscriminatorMapping: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *OneOfWithDiscriminatorMapping) ApplyDefaults() { - if u.Cat != nil { - u.Cat.ApplyDefaults() - } - if u.Dog != nil { - u.Dog.ApplyDefaults() - } -} - -// #/components/schemas/Cat -type Cat struct { - PetType string `json:"petType" form:"petType"` - Meow string `json:"meow" form:"meow"` - WhiskerLength *float32 `json:"whiskerLength,omitempty" form:"whiskerLength,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Cat) ApplyDefaults() { -} - -// #/components/schemas/Dog -type Dog struct { - PetType string `json:"petType" form:"petType"` - Bark string `json:"bark" form:"bark"` - TailLength *float32 `json:"tailLength,omitempty" form:"tailLength,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Dog) ApplyDefaults() { -} - -// #/components/schemas/OneOfInline -type OneOfInline struct { - OneOfInlineOneOf0 *OneOfInlineOneOf0 - OneOfInlineOneOf1 *OneOfInlineOneOf1 -} - -func (u OneOfInline) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.OneOfInlineOneOf0 != nil { - count++ - data, err = json.Marshal(u.OneOfInlineOneOf0) - if err != nil { - return nil, err - } - } - if u.OneOfInlineOneOf1 != nil { - count++ - data, err = json.Marshal(u.OneOfInlineOneOf1) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("OneOfInline: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *OneOfInline) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 OneOfInlineOneOf0 - if err := json.Unmarshal(data, &v0); err == nil { - u.OneOfInlineOneOf0 = &v0 - successCount++ - } - - var v1 OneOfInlineOneOf1 - if err := json.Unmarshal(data, &v1); err == nil { - u.OneOfInlineOneOf1 = &v1 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("OneOfInline: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *OneOfInline) ApplyDefaults() { - if u.OneOfInlineOneOf0 != nil { - u.OneOfInlineOneOf0.ApplyDefaults() - } - if u.OneOfInlineOneOf1 != nil { - u.OneOfInlineOneOf1.ApplyDefaults() - } -} - -// #/components/schemas/OneOfInline/oneOf/0 -type OneOfInlineOneOf0 struct { - OptionA *string `json:"optionA,omitempty" form:"optionA,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *OneOfInlineOneOf0) ApplyDefaults() { -} - -// #/components/schemas/OneOfInline/oneOf/1 -type OneOfInlineOneOf1 struct { - OptionB *int `json:"optionB,omitempty" form:"optionB,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *OneOfInlineOneOf1) ApplyDefaults() { -} - -// #/components/schemas/OneOfPrimitives -type OneOfPrimitives struct { - String0 *string - Float321 *float32 - Bool2 *bool -} - -func (u OneOfPrimitives) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.String0 != nil { - count++ - data, err = json.Marshal(u.String0) - if err != nil { - return nil, err - } - } - if u.Float321 != nil { - count++ - data, err = json.Marshal(u.Float321) - if err != nil { - return nil, err - } - } - if u.Bool2 != nil { - count++ - data, err = json.Marshal(u.Bool2) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("OneOfPrimitives: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *OneOfPrimitives) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 string - if err := json.Unmarshal(data, &v0); err == nil { - u.String0 = &v0 - successCount++ - } - - var v1 float32 - if err := json.Unmarshal(data, &v1); err == nil { - u.Float321 = &v1 - successCount++ - } - - var v2 bool - if err := json.Unmarshal(data, &v2); err == nil { - u.Bool2 = &v2 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("OneOfPrimitives: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *OneOfPrimitives) ApplyDefaults() { -} - -// #/components/schemas/ObjectWithOneOfProperty -type ObjectWithOneOfProperty struct { - ID *int `json:"id,omitempty" form:"id,omitempty"` - Variant *ObjectWithOneOfPropertyVariant `json:"variant,omitempty" form:"variant,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ObjectWithOneOfProperty) ApplyDefaults() { -} - -// #/components/schemas/ObjectWithOneOfProperty/properties/variant -type ObjectWithOneOfPropertyVariant struct { - Cat *Cat - Dog *Dog -} - -func (u ObjectWithOneOfPropertyVariant) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.Cat != nil { - count++ - data, err = json.Marshal(u.Cat) - if err != nil { - return nil, err - } - } - if u.Dog != nil { - count++ - data, err = json.Marshal(u.Dog) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("ObjectWithOneOfPropertyVariant: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *ObjectWithOneOfPropertyVariant) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 Cat - if err := json.Unmarshal(data, &v0); err == nil { - u.Cat = &v0 - successCount++ - } - - var v1 Dog - if err := json.Unmarshal(data, &v1); err == nil { - u.Dog = &v1 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("ObjectWithOneOfPropertyVariant: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *ObjectWithOneOfPropertyVariant) ApplyDefaults() { - if u.Cat != nil { - u.Cat.ApplyDefaults() - } - if u.Dog != nil { - u.Dog.ApplyDefaults() - } -} - -// #/components/schemas/AllOfWithOneOf -type AllOfWithOneOf struct { - ID *int `json:"id,omitempty" form:"id,omitempty"` - CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` - AllOfWithOneOfAllOf1 *AllOfWithOneOfAllOf1 `json:"-"` -} - -func (s AllOfWithOneOf) MarshalJSON() ([]byte, error) { - result := make(map[string]any) - - if s.ID != nil { - result["id"] = s.ID - } - if s.CreatedAt != nil { - result["createdAt"] = s.CreatedAt - } - - if s.AllOfWithOneOfAllOf1 != nil { - unionData, err := json.Marshal(s.AllOfWithOneOfAllOf1) - if err != nil { - return nil, err - } - var unionMap map[string]any - if err := json.Unmarshal(unionData, &unionMap); err == nil { - for k, v := range unionMap { - result[k] = v - } - } - } - - return json.Marshal(result) -} - -func (s *AllOfWithOneOf) UnmarshalJSON(data []byte) error { - var raw map[string]json.RawMessage - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - - if v, ok := raw["id"]; ok { - var val int - if err := json.Unmarshal(v, &val); err != nil { - return err - } - s.ID = &val - } - if v, ok := raw["createdAt"]; ok { - var val time.Time - if err := json.Unmarshal(v, &val); err != nil { - return err - } - s.CreatedAt = &val - } - - var AllOfWithOneOfAllOf1Val AllOfWithOneOfAllOf1 - if err := json.Unmarshal(data, &AllOfWithOneOfAllOf1Val); err != nil { - return err - } - s.AllOfWithOneOfAllOf1 = &AllOfWithOneOfAllOf1Val - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AllOfWithOneOf) ApplyDefaults() { -} - -// #/components/schemas/AllOfWithOneOf/allOf/1 -type AllOfWithOneOfAllOf1 struct { - Cat *Cat - Dog *Dog -} - -func (u AllOfWithOneOfAllOf1) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.Cat != nil { - count++ - data, err = json.Marshal(u.Cat) - if err != nil { - return nil, err - } - } - if u.Dog != nil { - count++ - data, err = json.Marshal(u.Dog) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("AllOfWithOneOfAllOf1: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *AllOfWithOneOfAllOf1) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 Cat - if err := json.Unmarshal(data, &v0); err == nil { - u.Cat = &v0 - successCount++ - } - - var v1 Dog - if err := json.Unmarshal(data, &v1); err == nil { - u.Dog = &v1 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("AllOfWithOneOfAllOf1: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *AllOfWithOneOfAllOf1) ApplyDefaults() { - if u.Cat != nil { - u.Cat.ApplyDefaults() - } - if u.Dog != nil { - u.Dog.ApplyDefaults() - } -} - -// #/components/schemas/OneOfWithAllOf -type OneOfWithAllOf struct { - OneOfWithAllOfOneOf0 *OneOfWithAllOfOneOf0 - OneOfWithAllOfOneOf1 *OneOfWithAllOfOneOf1 -} - -func (u OneOfWithAllOf) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.OneOfWithAllOfOneOf0 != nil { - count++ - data, err = json.Marshal(u.OneOfWithAllOfOneOf0) - if err != nil { - return nil, err - } - } - if u.OneOfWithAllOfOneOf1 != nil { - count++ - data, err = json.Marshal(u.OneOfWithAllOfOneOf1) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("OneOfWithAllOf: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *OneOfWithAllOf) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 OneOfWithAllOfOneOf0 - if err := json.Unmarshal(data, &v0); err == nil { - u.OneOfWithAllOfOneOf0 = &v0 - successCount++ - } - - var v1 OneOfWithAllOfOneOf1 - if err := json.Unmarshal(data, &v1); err == nil { - u.OneOfWithAllOfOneOf1 = &v1 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("OneOfWithAllOf: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *OneOfWithAllOf) ApplyDefaults() { - if u.OneOfWithAllOfOneOf0 != nil { - u.OneOfWithAllOfOneOf0.ApplyDefaults() - } - if u.OneOfWithAllOfOneOf1 != nil { - u.OneOfWithAllOfOneOf1.ApplyDefaults() - } -} - -// #/components/schemas/OneOfWithAllOf/oneOf/0 -type OneOfWithAllOfOneOf0 struct { - ID *int `json:"id,omitempty" form:"id,omitempty"` - CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` - Variant *string `json:"variant,omitempty" form:"variant,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *OneOfWithAllOfOneOf0) ApplyDefaults() { -} - -// #/components/schemas/OneOfWithAllOf/oneOf/1 -type OneOfWithAllOfOneOf1 struct { - ID *int `json:"id,omitempty" form:"id,omitempty"` - CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` - Variant *string `json:"variant,omitempty" form:"variant,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *OneOfWithAllOfOneOf1) ApplyDefaults() { -} - -// #/components/schemas/TreeNode -type TreeNode struct { - Value *string `json:"value,omitempty" form:"value,omitempty"` - Children []TreeNode `json:"children,omitempty" form:"children,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *TreeNode) ApplyDefaults() { -} - -// #/components/schemas/TreeNode/properties/children -type TreeNodeChildren = []TreeNode - -// #/components/schemas/LinkedListNode -type LinkedListNode struct { - Value *int `json:"value,omitempty" form:"value,omitempty"` - Next *LinkedListNode `json:"next,omitempty" form:"next,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *LinkedListNode) ApplyDefaults() { - if s.Next != nil { - s.Next.ApplyDefaults() - } -} - -// #/components/schemas/RecursiveOneOf -type RecursiveOneOf struct { - String0 *string - RecursiveOneOfOneOf1 *RecursiveOneOfOneOf1 -} - -func (u RecursiveOneOf) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.String0 != nil { - count++ - data, err = json.Marshal(u.String0) - if err != nil { - return nil, err - } - } - if u.RecursiveOneOfOneOf1 != nil { - count++ - data, err = json.Marshal(u.RecursiveOneOfOneOf1) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("RecursiveOneOf: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *RecursiveOneOf) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 string - if err := json.Unmarshal(data, &v0); err == nil { - u.String0 = &v0 - successCount++ - } - - var v1 RecursiveOneOfOneOf1 - if err := json.Unmarshal(data, &v1); err == nil { - u.RecursiveOneOfOneOf1 = &v1 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("RecursiveOneOf: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *RecursiveOneOf) ApplyDefaults() { - if u.RecursiveOneOfOneOf1 != nil { - u.RecursiveOneOfOneOf1.ApplyDefaults() - } -} - -// #/components/schemas/RecursiveOneOf/oneOf/1 -type RecursiveOneOfOneOf1 struct { - Nested *RecursiveOneOf `json:"nested,omitempty" form:"nested,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *RecursiveOneOfOneOf1) ApplyDefaults() { - if s.Nested != nil { - s.Nested.ApplyDefaults() - } -} - -// #/components/schemas/ReadWriteOnly -type ReadWriteOnly struct { - ID int `json:"id" form:"id"` - Password string `json:"password" form:"password"` - Name *string `json:"name,omitempty" form:"name,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ReadWriteOnly) ApplyDefaults() { -} - -// #/components/schemas/WithDefaults -type WithDefaults struct { - StringWithDefault *string `json:"stringWithDefault,omitempty" form:"stringWithDefault,omitempty"` - IntWithDefault *int `json:"intWithDefault,omitempty" form:"intWithDefault,omitempty"` - BoolWithDefault *bool `json:"boolWithDefault,omitempty" form:"boolWithDefault,omitempty"` - ArrayWithDefault []string `json:"arrayWithDefault,omitempty" form:"arrayWithDefault,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *WithDefaults) ApplyDefaults() { - if s.StringWithDefault == nil { - v := "default_value" - s.StringWithDefault = &v - } - if s.IntWithDefault == nil { - v := 42 - s.IntWithDefault = &v - } - if s.BoolWithDefault == nil { - v := true - s.BoolWithDefault = &v - } -} - -// #/components/schemas/WithConst -type WithConst struct { - Version *string `json:"version,omitempty" form:"version,omitempty"` - Type *string `json:"type,omitempty" form:"type,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *WithConst) ApplyDefaults() { -} - -// #/components/schemas/WithConstraints -type WithConstraints struct { - BoundedInt *int `json:"boundedInt,omitempty" form:"boundedInt,omitempty"` - ExclusiveBoundedInt *int `json:"exclusiveBoundedInt,omitempty" form:"exclusiveBoundedInt,omitempty"` - MultipleOf *int `json:"multipleOf,omitempty" form:"multipleOf,omitempty"` - BoundedString *string `json:"boundedString,omitempty" form:"boundedString,omitempty"` - PatternString *string `json:"patternString,omitempty" form:"patternString,omitempty"` - BoundedArray []string `json:"boundedArray,omitempty" form:"boundedArray,omitempty"` - UniqueArray []string `json:"uniqueArray,omitempty" form:"uniqueArray,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *WithConstraints) ApplyDefaults() { -} - -// #/components/schemas/TypeArray31 -type TypeArray31 struct { - Name *string `json:"name,omitempty" form:"name,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *TypeArray31) ApplyDefaults() { -} - -// #/components/schemas/ExplicitAny -type ExplicitAny = Nullable[string] - -// #/components/schemas/ComplexNested -type ComplexNested struct { - Metadata map[string]any `json:"metadata,omitempty" form:"metadata,omitempty"` - Items []ComplexNestedItemItem `json:"items,omitempty" form:"items,omitempty"` - Config *ComplexNestedConfig `json:"config,omitempty" form:"config,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ComplexNested) ApplyDefaults() { -} - -// #/components/schemas/ComplexNested/properties/metadata -type ComplexNestedMetadata = map[string]any - -// #/components/schemas/ComplexNested/properties/metadata/additionalProperties -type ComplexNestedMetadataValue struct { - String0 *string - Int1 *int - LBracketString2 *[]string -} - -func (u ComplexNestedMetadataValue) MarshalJSON() ([]byte, error) { - if u.String0 != nil { - return json.Marshal(u.String0) - } - if u.Int1 != nil { - return json.Marshal(u.Int1) - } - if u.LBracketString2 != nil { - return json.Marshal(u.LBracketString2) - } - return []byte("null"), nil -} - -func (u *ComplexNestedMetadataValue) UnmarshalJSON(data []byte) error { - var v0 string - if err := json.Unmarshal(data, &v0); err == nil { - u.String0 = &v0 - } - - var v1 int - if err := json.Unmarshal(data, &v1); err == nil { - u.Int1 = &v1 - } - - var v2 []string - if err := json.Unmarshal(data, &v2); err == nil { - u.LBracketString2 = &v2 - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *ComplexNestedMetadataValue) ApplyDefaults() { -} - -// #/components/schemas/ComplexNested/properties/items -type ComplexNestedItem = []ComplexNestedItemItem - -// #/components/schemas/ComplexNested/properties/items/items -type ComplexNestedItemItem struct { - ID *int `json:"id,omitempty" form:"id,omitempty"` - CreatedAt *time.Time `json:"createdAt,omitempty" form:"createdAt,omitempty"` - Tags []string `json:"tags,omitempty" form:"tags,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ComplexNestedItemItem) ApplyDefaults() { -} - -// #/components/schemas/ComplexNested/properties/config -type ComplexNestedConfig struct { - ComplexNestedConfigOneOf0 *ComplexNestedConfigOneOf0 - ComplexNestedConfigOneOf1 *ComplexNestedConfigOneOf1 -} - -func (u ComplexNestedConfig) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.ComplexNestedConfigOneOf0 != nil { - count++ - data, err = json.Marshal(u.ComplexNestedConfigOneOf0) - if err != nil { - return nil, err - } - } - if u.ComplexNestedConfigOneOf1 != nil { - count++ - data, err = json.Marshal(u.ComplexNestedConfigOneOf1) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("ComplexNestedConfig: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *ComplexNestedConfig) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 ComplexNestedConfigOneOf0 - if err := json.Unmarshal(data, &v0); err == nil { - u.ComplexNestedConfigOneOf0 = &v0 - successCount++ - } - - var v1 ComplexNestedConfigOneOf1 - if err := json.Unmarshal(data, &v1); err == nil { - u.ComplexNestedConfigOneOf1 = &v1 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("ComplexNestedConfig: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *ComplexNestedConfig) ApplyDefaults() { - if u.ComplexNestedConfigOneOf0 != nil { - u.ComplexNestedConfigOneOf0.ApplyDefaults() - } - if u.ComplexNestedConfigOneOf1 != nil { - u.ComplexNestedConfigOneOf1.ApplyDefaults() - } -} - -// #/components/schemas/ComplexNested/properties/config/oneOf/0 -type ComplexNestedConfigOneOf0 struct { - Mode *string `json:"mode,omitempty" form:"mode,omitempty"` - Value *string `json:"value,omitempty" form:"value,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ComplexNestedConfigOneOf0) ApplyDefaults() { -} - -// #/components/schemas/ComplexNested/properties/config/oneOf/1 -type ComplexNestedConfigOneOf1 struct { - Mode *string `json:"mode,omitempty" form:"mode,omitempty"` - Options map[string]string `json:"options,omitempty" form:"options,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ComplexNestedConfigOneOf1) ApplyDefaults() { -} - -// #/components/schemas/ComplexNested/properties/config/oneOf/1/properties/options -type ComplexNestedConfigOneOf1Options = map[string]string - -// #/components/schemas/StringMap -type StringMap = map[string]string - -// #/components/schemas/ObjectMap -type ObjectMap = map[string]any - -// #/components/schemas/NestedMap -type NestedMap = map[string]map[string]string - -// #/components/schemas/NestedMap/additionalProperties -type NestedMapValue = map[string]string - -// #/paths//inline-response/get/responses/200/content/application/json/schema -type GetInlineResponseJSONResponse struct { - ID int `json:"id" form:"id"` - Name string `json:"name" form:"name"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *GetInlineResponseJSONResponse) ApplyDefaults() { -} - -const DateFormat = "2006-01-02" - -type Date struct { - time.Time -} - -func (d Date) MarshalJSON() ([]byte, error) { - return json.Marshal(d.Format(DateFormat)) -} - -func (d *Date) UnmarshalJSON(data []byte) error { - var dateStr string - err := json.Unmarshal(data, &dateStr) - if err != nil { - return err - } - parsed, err := time.Parse(DateFormat, dateStr) - if err != nil { - return err - } - d.Time = parsed - return nil -} - -func (d Date) String() string { - return d.Format(DateFormat) -} - -func (d *Date) UnmarshalText(data []byte) error { - parsed, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = parsed - return nil -} - -const ( - emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" -) - -var ( - emailRegex = regexp.MustCompile(emailRegexString) -) - -// ErrValidationEmail is the sentinel error returned when an email fails validation -var ErrValidationEmail = errors.New("email: failed to pass regex validation") - -// Email represents an email address. -// It is a string type that must pass regex validation before being marshalled -// to JSON or unmarshalled from JSON. -type Email string - -func (e Email) MarshalJSON() ([]byte, error) { - if !emailRegex.MatchString(string(e)) { - return nil, ErrValidationEmail - } - - return json.Marshal(string(e)) -} - -func (e *Email) UnmarshalJSON(data []byte) error { - if e == nil { - return nil - } - - var s string - if err := json.Unmarshal(data, &s); err != nil { - return err - } - - *e = Email(s) - if !emailRegex.MatchString(s) { - return ErrValidationEmail - } - - return nil -} - -type File struct { - multipart *multipart.FileHeader - data []byte - filename string -} - -func (file *File) InitFromMultipart(header *multipart.FileHeader) { - file.multipart = header - file.data = nil - file.filename = "" -} - -func (file *File) InitFromBytes(data []byte, filename string) { - file.data = data - file.filename = filename - file.multipart = nil -} - -func (file File) MarshalJSON() ([]byte, error) { - b, err := file.Bytes() - if err != nil { - return nil, err - } - return json.Marshal(b) -} - -func (file *File) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, &file.data) -} - -func (file File) Bytes() ([]byte, error) { - if file.multipart != nil { - f, err := file.multipart.Open() - if err != nil { - return nil, err - } - defer func() { _ = f.Close() }() - return io.ReadAll(f) - } - return file.data, nil -} - -func (file File) Reader() (io.ReadCloser, error) { - if file.multipart != nil { - return file.multipart.Open() - } - return io.NopCloser(bytes.NewReader(file.data)), nil -} - -func (file File) Filename() string { - if file.multipart != nil { - return file.multipart.Filename - } - return file.filename -} - -func (file File) FileSize() int64 { - if file.multipart != nil { - return file.multipart.Size - } - return int64(len(file.data)) -} - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value -type Nullable[T any] map[bool]T - -// NewNullableWithValue creates a Nullable with the given value. -func NewNullableWithValue[T any](value T) Nullable[T] { - return Nullable[T]{true: value} -} - -// NewNullNullable creates a Nullable that is explicitly null. -func NewNullNullable[T any]() Nullable[T] { - return Nullable[T]{false: *new(T)} -} - -// Get returns the value if set, or an error if null or unspecified. -func (n Nullable[T]) Get() (T, error) { - if v, ok := n[true]; ok { - return v, nil - } - var zero T - if n.IsNull() { - return zero, ErrNullableIsNull - } - return zero, ErrNullableNotSpecified -} - -// MustGet returns the value or panics if null or unspecified. -func (n Nullable[T]) MustGet() T { - v, err := n.Get() - if err != nil { - panic(err) - } - return v -} - -// Set assigns a value. -func (n *Nullable[T]) Set(value T) { - *n = Nullable[T]{true: value} -} - -// SetNull marks the field as explicitly null. -func (n *Nullable[T]) SetNull() { - *n = Nullable[T]{false: *new(T)} -} - -// SetUnspecified clears the field (as if it was never set). -func (n *Nullable[T]) SetUnspecified() { - *n = nil -} - -// IsNull returns true if the field is explicitly null. -func (n Nullable[T]) IsNull() bool { - if n == nil { - return false - } - _, ok := n[false] - return ok -} - -// IsSpecified returns true if the field was provided (either null or a value). -func (n Nullable[T]) IsSpecified() bool { - return len(n) > 0 -} - -// MarshalJSON implements json.Marshaler. -func (n Nullable[T]) MarshalJSON() ([]byte, error) { - if n.IsNull() { - return []byte("null"), nil - } - if v, ok := n[true]; ok { - return json.Marshal(v) - } - // Unspecified - this shouldn't be called if omitempty is used correctly - return []byte("null"), nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (n *Nullable[T]) UnmarshalJSON(data []byte) error { - if string(data) == "null" { - n.SetNull() - return nil - } - var v T - if err := json.Unmarshal(data, &v); err != nil { - return err - } - n.Set(v) - return nil -} - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. -var ErrNullableIsNull = errors.New("nullable value is null") - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. -var ErrNullableNotSpecified = errors.New("nullable value is not specified") - -type UUID = uuid.UUID - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/+wcXW8iOfKdX2Gx+3Zi8jGz84B0D4QwEnsEEJCdi1Z7J4cuEu90u3vcJgm32v9+st10", - "tz/6y2RGs6PNEynb9eVyVblcECdAcUKGqP/2zcWb836P0F087CHECQ9hiMZxlDB4BJqSJ0AbSDlabx8h", - "wj2EnoClJKZD1L94cy7WIhRAumUk4RIsZqcIhyFK5RKUYM6B0RTtYoZinJDBNg7gAWivl2D+mAq6ZymJ", - "khDER4QegKsPCMUJMCzwToOhgK/ltGyQQZrENIX0OBuh/uX5eb/412BNrc7XlaZtY8qB8vJKhHCShGQr", - "yZ/9nsZUH0WZfCYUoR8Z7Iao/8PZNo6SmALl6Zmam54pFhb3v8OW93tCckJDQmFwZKpRBVM5f6XL0FkV", - "CguKJSOI0C+uFH5IYJjRswYZfN4TBoG9DKEBIoETTHEE1kDChL44KWui+CNOCkfmCOXwAMwxQ5CqW5ly", - "RuiD3M4EMxwBB5ae/SHMeyn+/1MtLsaOyJQUQ5TPzIkQqqA9S0eIs30huK1vjaUCvItZhPkQ7feZPmts", - "7CPJ2JHMZpNs7gv+P++BHdYmSSGDHOnV24eTYw31lHJvvOa2aohHjOGDN2psrEaIcIgs06uV7xFwAEzf", - "fMWHGumuu87OYPGvb8MVChOHlA/u4+CQnZg4dRupGFip6VdxcOgVZySDDJsOjkPOeindMtZJOArDzSGB", - "dJVx0P+rb1C0D7kI3yUGanfoRswfq+l1W/QV9kIXxk3nZfD8/DwQjnKwZyFQkacEX4qw1GWCGT+TBAPM", - "cRtSNXG0LvbtSOiMYBXBwgwa94RidrAmRMCxzfj35Zg0meCFnyUhJh3TnmN+UBATMzN6atEP6J/t/7IV", - "y9X0ZrqZ/jJBm7vlZI0GaBSG2Z6lHljlEtNpHWVyGJ6dtg1EtP1AIAx00NtLB/D9OxO4C2NsLQ/i/X0I", - "JpTuo3tgJlQp2oTex3FoYcUcXLANiSy4SJlMGESYWEj3jJigxzjlItBb8idP7xyw9xbvB5tPdRpNaILT", - "9DlmGqcul3DcITtnNDOlYuOa5xa+Qq4q48j2uSOO9+9yeGEWNg5lBw4UclEOLxlRBxxqVT5QsrlGJCVL", - "bEzOc/u0Z4ohwLSQ42i2HTJ+sUZDkNt4RyQDTkpXrvxU+Fw+xF9xhDpgkIsKHrID14UFRnKodjg74Diu", - "K6z8eJo7IBFrygjeeyB4X9jQobthiDUFgsKpdEGhJwaaD+qA5rhOD0ELmQbgsCYE/e3g/nZw36OD884J", - "57ez2ehqlqWEvkngfB+G+D4EzySQZsvXVskjGyjXcwrooulk63gtpfb0AqHjVjNAfYGjb2Gc6hcLFzpX", - "ebASnxKlCaXjGmdhrL7XuaqSZj2y2EpPZ/rtqtz7iIxWq9Gd99GQFUMZnjpqUulGLrfdgVlJdNQRnX6D", - "UH4qRlPJShB/rN2v1uoJZHEi3crKSHVdxPUeUPcW4H4HcG4MhZRDcKo0pxSYj+fMzYNxpmxCDkfU1igl", - "so+EP45jmnKGSVbwqJctInQqCaCLMhS/HKHnHVjxdg6Lq58n4423dygbeNfKSe2lvUV2aVqnIxZIo2zk", - "zsVAvOfAbOzWcas6bIRSff0JxcwnHO6bq5n+8eH6erqZLuajGVquFsvJajP1z6RGQUBU4F2yOElH9FCj", - "eKxNzsRXzxYuZPOYQmdsOxzmr7wuJX+i8TOtNyODDREMg6589Ny27MLfaK/1BDo9cVjE5VOo+ND1Etp4", - "YNuo5VRTnsxvb/x9mSQ+oftIl13z9lAaFl5Mns0LE3BpAt4qElOlEZuGriqTSBl/GXWGVe2n2DkbcYs0", - "DfN92uLCpjOV1WCBBnYCjLecPIEBFNYYAofiupgwEjPCD23qCC7iF8b/l8b/uc5FtiU0M6WZ5R06V1gE", - "ivUpmoqpCdjtTnDZs9niAxqgKX0ERjimWzgbi+OeyiPmewKucAr24Xw1D4DQlgEWaSL3KlTIwckLBxqY", - "UR2H4WJXPjJ1XlAXs19aVRGg3f06VkuO98W1PFB+EGy87F4DJCUb8NKGrtAW2qgSE144w808jwRz8qU8", - "CWEFu/SV97D7fayzpBGwB7t7Sy/wFbIqB7Sg4aFS0o70d4SlvGXXTUfUKWxjWiGalrR4ua35nXRbt5TE", - "FMmigneeSQ+L3ZKRiIhYU9iQANuardCKnoWJpco0qtH5GFcXMy44uSEvhYm1Fesr2L6KhW1sX/B8rMb5", - "7U+58lUkOdnWe0Vy6yplMFTDlMtwXGO6EsSlf7EblanY5QDjdm/xNPDq3/CwPK8KwnwiD/U1EYErIlRE", - "eHXEvU/3gsJity71SCMUC9BXP4qSEWF0Jeli5sXTGLdm5Tp+OE4NXHRz2z7MVTstcOFM61i+wUlSKmh/", - "a5wX5S+NTZkziiSwBWMIBfFDLV9y6hh3LVSZPA5QBPFzjZPJFjTmuAJN46TnR5J+AjYD+sAfK18pVTYY", - "P5ws2j1mn15BNIGmcRLHJGwjl7ToqRZ3LPvtGMVimWCPvkwOpZBftUiipGR2FlMhXQVfxkO1Mw4V0TMj", - "6XcPbnHFe8KMYP2pyxCno8Pp5HRqHE+D2/QJfePFzdV0PrkWH5aLtayintCHqNz2oqytV7oT/SV2IA9c", - "o7LQ1lkwVOKvltqz3VyTt8y8ZY/xNqapiGi4/x0KdX9CIrmajG9X67y91/cgbRjAPA7g1JuBU+LtIwkD", - "BvTVX4qPXGf6mxH6CYIZSfnriWK9n8GLttV17On8ZEyuYLtnKXkCzWV1C16tg6p6YO7yTQSdvZMMc3R9", - "9nE13UzQYj678zXMFeDgIyNcKwV1fSwtdz6fGKkFJRxIZvSvCh3xtyjSPufy6Cia32Z9NuJ68mF0O9us", - "0WguYu58vfHdC3k/gh3eh9yvr6WEoIWagmwm6mef/ivPaLklhNditPcuR/nuUus7rEVjdh+W0Gjbl3c0", - "VGLybeApUfz1t2IvZOdEVzeXfTO7Wf3H6JR/g9vdGNKwekdeIOifki3O15vVaDr3b7eoaDJpqbD7eE8D", - "CFyNaA4Diwgl0T4aonO9PUUBL84LMLxsw71wtFedCOTLblyUilEXySh7P9CTpkpRitnoJ1MdFZ1+th1E", - "hGa3VbNj5wi+/OmnkhOVPwPQGns2f4j6//kVD/732z9+7JuctuyuetXGorw5mJLPe2jJgZqckdMcy5fs", - "ZFpO5qPlFL19c4HWy8l4+mE69s4hD4mS9e2FdsZ6lb2sRvuYs7W0MSYuGeyI2iaDsKbipJjW+amjqkrg", - "3JxXasq4WW7u0Bkaze9kZu+7KZMo4Qf1uxxD9Mef+SvDL+V0V/s+5Wi7hYSnCNMD+nm9mKvM+PiOnIRk", - "S7jZodSr7O4d2JUWW6tmhDVbDasN55QqxHI2+TeaT9abyTVab1a3483tyv8GNZatGi9zLeNuGWNcX5Gt", - "yPTrG4IqHmdqn2fqH2iKUbv7s7LVtLoT2ZzdOi9y3PVPue033PebfiOE44e07rc+3Lqq1Vf17TmmO/LQ", - "WBr0rF5EpZuyR+lC/SRP37P78luQAAdPmG6L310wK9I1xl1hOm0O6esGi5vR8sQGvhucnNicWZZBFc5P", - "wdmxH1N53dOFMHa0aR81yf8fAAD//ynhbdUwSwAA", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/default_values/default_values.yaml b/experimental/internal/codegen/test/default_values/default_values.yaml deleted file mode 100644 index 8f773e63ee..0000000000 --- a/experimental/internal/codegen/test/default_values/default_values.yaml +++ /dev/null @@ -1,158 +0,0 @@ -openapi: "3.1.0" -info: - title: Default Values Test - version: "1.0" -paths: {} -components: - schemas: - # Basic primitives with defaults - SimpleDefaults: - type: object - properties: - stringField: - type: string - default: "hello" - intField: - type: integer - default: 42 - boolField: - type: boolean - default: true - floatField: - type: number - default: 3.14 - int64Field: - type: integer - format: int64 - default: 9223372036854775807 - - # Nested objects - should recurse ApplyDefaults - NestedDefaults: - type: object - properties: - name: - type: string - default: "parent" - child: - $ref: '#/components/schemas/SimpleDefaults' - inlineChild: - type: object - properties: - label: - type: string - default: "inline-default" - value: - type: integer - default: 100 - - # Object with additionalProperties and defaults - MapWithDefaults: - type: object - properties: - prefix: - type: string - default: "map-" - additionalProperties: - type: string - - # Array field with defaults - ArrayDefaults: - type: object - properties: - items: - type: array - items: - type: string - default: [] - count: - type: integer - default: 0 - - # anyOf with defaults in members - AnyOfWithDefaults: - type: object - properties: - value: - anyOf: - - type: object - properties: - stringVal: - type: string - default: "default-string" - - type: object - properties: - intVal: - type: integer - default: 999 - - # oneOf with defaults in members - OneOfWithDefaults: - type: object - properties: - variant: - oneOf: - - type: object - properties: - optionA: - type: string - default: "option-a-default" - - type: object - properties: - optionB: - type: integer - default: 123 - - # allOf with defaults - AllOfWithDefaults: - allOf: - - type: object - properties: - base: - type: string - default: "base-value" - - type: object - properties: - extended: - type: integer - default: 50 - - # Deep nesting - multiple levels - DeepNesting: - type: object - properties: - level1: - type: object - properties: - name: - type: string - default: "level1-name" - level2: - type: object - properties: - count: - type: integer - default: 2 - level3: - type: object - properties: - enabled: - type: boolean - default: false - - # Required vs optional with defaults - RequiredAndOptional: - type: object - required: - - requiredWithDefault - - requiredNoDefault - properties: - requiredWithDefault: - type: string - default: "required-default" - requiredNoDefault: - type: string - optionalWithDefault: - type: string - default: "optional-default" - optionalNoDefault: - type: string diff --git a/experimental/internal/codegen/test/default_values/output/default_values.gen.go b/experimental/internal/codegen/test/default_values/output/default_values.gen.go deleted file mode 100644 index a8b988994d..0000000000 --- a/experimental/internal/codegen/test/default_values/output/default_values.gen.go +++ /dev/null @@ -1,514 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "encoding/json" - "fmt" -) - -// #/components/schemas/SimpleDefaults -type SimpleDefaultsSchemaComponent struct { - StringField *string `json:"stringField,omitempty" form:"stringField,omitempty"` - IntField *int `json:"intField,omitempty" form:"intField,omitempty"` - BoolField *bool `json:"boolField,omitempty" form:"boolField,omitempty"` - FloatField *float32 `json:"floatField,omitempty" form:"floatField,omitempty"` - Int64Field *int64 `json:"int64Field,omitempty" form:"int64Field,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *SimpleDefaultsSchemaComponent) ApplyDefaults() { - if s.StringField == nil { - v := "hello" - s.StringField = &v - } - if s.IntField == nil { - v := 42 - s.IntField = &v - } - if s.BoolField == nil { - v := true - s.BoolField = &v - } - if s.FloatField == nil { - v := float32(3.14) - s.FloatField = &v - } - if s.Int64Field == nil { - v := int64(9223372036854775807) - s.Int64Field = &v - } -} - -type SimpleDefaults = SimpleDefaultsSchemaComponent - -// #/components/schemas/NestedDefaults -type NestedDefaultsSchemaComponent struct { - Name *string `json:"name,omitempty" form:"name,omitempty"` - Child *SimpleDefaults `json:"child,omitempty" form:"child,omitempty"` - InlineChild *NestedDefaultsInlineChild `json:"inlineChild,omitempty" form:"inlineChild,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *NestedDefaultsSchemaComponent) ApplyDefaults() { - if s.Name == nil { - v := "parent" - s.Name = &v - } - if s.Child != nil { - s.Child.ApplyDefaults() - } - if s.InlineChild != nil { - s.InlineChild.ApplyDefaults() - } -} - -type NestedDefaults = NestedDefaultsSchemaComponent - -// #/components/schemas/NestedDefaults/properties/inlineChild -type NestedDefaultsInlineChildPropertySchemaComponent struct { - Label *string `json:"label,omitempty" form:"label,omitempty"` - Value *int `json:"value,omitempty" form:"value,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *NestedDefaultsInlineChildPropertySchemaComponent) ApplyDefaults() { - if s.Label == nil { - v := "inline-default" - s.Label = &v - } - if s.Value == nil { - v := 100 - s.Value = &v - } -} - -type NestedDefaultsInlineChild = NestedDefaultsInlineChildPropertySchemaComponent - -// #/components/schemas/MapWithDefaults -type MapWithDefaultsSchemaComponent struct { - Prefix *string `json:"prefix,omitempty" form:"prefix,omitempty"` - AdditionalProperties map[string]string `json:"-"` -} - -func (s MapWithDefaultsSchemaComponent) MarshalJSON() ([]byte, error) { - result := make(map[string]any) - - if s.Prefix != nil { - result["prefix"] = s.Prefix - } - - // Add additional properties - for k, v := range s.AdditionalProperties { - result[k] = v - } - - return json.Marshal(result) -} - -func (s *MapWithDefaultsSchemaComponent) UnmarshalJSON(data []byte) error { - // Known fields - knownFields := map[string]bool{ - "prefix": true, - } - - var raw map[string]json.RawMessage - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - - if v, ok := raw["prefix"]; ok { - var val string - if err := json.Unmarshal(v, &val); err != nil { - return err - } - s.Prefix = &val - } - - // Collect additional properties - s.AdditionalProperties = make(map[string]string) - for k, v := range raw { - if !knownFields[k] { - var val string - if err := json.Unmarshal(v, &val); err != nil { - return err - } - s.AdditionalProperties[k] = val - } - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *MapWithDefaultsSchemaComponent) ApplyDefaults() { - if s.Prefix == nil { - v := "map-" - s.Prefix = &v - } -} - -type MapWithDefaults = MapWithDefaultsSchemaComponent - -// #/components/schemas/ArrayDefaults -type ArrayDefaultsSchemaComponent struct { - Items []string `json:"items,omitempty" form:"items,omitempty"` - Count *int `json:"count,omitempty" form:"count,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ArrayDefaultsSchemaComponent) ApplyDefaults() { - if s.Count == nil { - v := 0 - s.Count = &v - } -} - -type ArrayDefaults = ArrayDefaultsSchemaComponent - -// #/components/schemas/AnyOfWithDefaults -type AnyOfWithDefaultsSchemaComponent struct { - Value *AnyOfWithDefaultsValue `json:"value,omitempty" form:"value,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AnyOfWithDefaultsSchemaComponent) ApplyDefaults() { -} - -type AnyOfWithDefaults = AnyOfWithDefaultsSchemaComponent - -// #/components/schemas/AnyOfWithDefaults/properties/value -type AnyOfWithDefaultsValuePropertySchemaComponent struct { - AnyOfWithDefaultsValueAnyOf0 *AnyOfWithDefaultsValueAnyOf0 - AnyOfWithDefaultsValueAnyOf1 *AnyOfWithDefaultsValueAnyOf1 -} - -func (u AnyOfWithDefaultsValuePropertySchemaComponent) MarshalJSON() ([]byte, error) { - result := make(map[string]any) - - if u.AnyOfWithDefaultsValueAnyOf0 != nil { - data, err := json.Marshal(u.AnyOfWithDefaultsValueAnyOf0) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err == nil { - for k, v := range m { - result[k] = v - } - } - } - if u.AnyOfWithDefaultsValueAnyOf1 != nil { - data, err := json.Marshal(u.AnyOfWithDefaultsValueAnyOf1) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err == nil { - for k, v := range m { - result[k] = v - } - } - } - - return json.Marshal(result) -} - -func (u *AnyOfWithDefaultsValuePropertySchemaComponent) UnmarshalJSON(data []byte) error { - var v0 AnyOfWithDefaultsValueAnyOf0 - if err := json.Unmarshal(data, &v0); err == nil { - u.AnyOfWithDefaultsValueAnyOf0 = &v0 - } - - var v1 AnyOfWithDefaultsValueAnyOf1 - if err := json.Unmarshal(data, &v1); err == nil { - u.AnyOfWithDefaultsValueAnyOf1 = &v1 - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *AnyOfWithDefaultsValuePropertySchemaComponent) ApplyDefaults() { - if u.AnyOfWithDefaultsValueAnyOf0 != nil { - u.AnyOfWithDefaultsValueAnyOf0.ApplyDefaults() - } - if u.AnyOfWithDefaultsValueAnyOf1 != nil { - u.AnyOfWithDefaultsValueAnyOf1.ApplyDefaults() - } -} - -type AnyOfWithDefaultsValue = AnyOfWithDefaultsValuePropertySchemaComponent - -// #/components/schemas/AnyOfWithDefaults/properties/value/anyOf/0 -type AnyOfWithDefaultsValueN0AnyOfPropertySchemaComponent struct { - StringVal *string `json:"stringVal,omitempty" form:"stringVal,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AnyOfWithDefaultsValueN0AnyOfPropertySchemaComponent) ApplyDefaults() { - if s.StringVal == nil { - v := "default-string" - s.StringVal = &v - } -} - -type AnyOfWithDefaultsValueAnyOf0 = AnyOfWithDefaultsValueN0AnyOfPropertySchemaComponent - -// #/components/schemas/AnyOfWithDefaults/properties/value/anyOf/1 -type AnyOfWithDefaultsValueN1AnyOfPropertySchemaComponent struct { - IntVal *int `json:"intVal,omitempty" form:"intVal,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AnyOfWithDefaultsValueN1AnyOfPropertySchemaComponent) ApplyDefaults() { - if s.IntVal == nil { - v := 999 - s.IntVal = &v - } -} - -type AnyOfWithDefaultsValueAnyOf1 = AnyOfWithDefaultsValueN1AnyOfPropertySchemaComponent - -// #/components/schemas/OneOfWithDefaults -type OneOfWithDefaultsSchemaComponent struct { - Variant *OneOfWithDefaultsVariant `json:"variant,omitempty" form:"variant,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *OneOfWithDefaultsSchemaComponent) ApplyDefaults() { -} - -type OneOfWithDefaults = OneOfWithDefaultsSchemaComponent - -// #/components/schemas/OneOfWithDefaults/properties/variant -type OneOfWithDefaultsVariantPropertySchemaComponent struct { - OneOfWithDefaultsVariantOneOf0 *OneOfWithDefaultsVariantOneOf0 - OneOfWithDefaultsVariantOneOf1 *OneOfWithDefaultsVariantOneOf1 -} - -func (u OneOfWithDefaultsVariantPropertySchemaComponent) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.OneOfWithDefaultsVariantOneOf0 != nil { - count++ - data, err = json.Marshal(u.OneOfWithDefaultsVariantOneOf0) - if err != nil { - return nil, err - } - } - if u.OneOfWithDefaultsVariantOneOf1 != nil { - count++ - data, err = json.Marshal(u.OneOfWithDefaultsVariantOneOf1) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("OneOfWithDefaultsVariantPropertySchemaComponent: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *OneOfWithDefaultsVariantPropertySchemaComponent) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 OneOfWithDefaultsVariantOneOf0 - if err := json.Unmarshal(data, &v0); err == nil { - u.OneOfWithDefaultsVariantOneOf0 = &v0 - successCount++ - } - - var v1 OneOfWithDefaultsVariantOneOf1 - if err := json.Unmarshal(data, &v1); err == nil { - u.OneOfWithDefaultsVariantOneOf1 = &v1 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("OneOfWithDefaultsVariantPropertySchemaComponent: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *OneOfWithDefaultsVariantPropertySchemaComponent) ApplyDefaults() { - if u.OneOfWithDefaultsVariantOneOf0 != nil { - u.OneOfWithDefaultsVariantOneOf0.ApplyDefaults() - } - if u.OneOfWithDefaultsVariantOneOf1 != nil { - u.OneOfWithDefaultsVariantOneOf1.ApplyDefaults() - } -} - -type OneOfWithDefaultsVariant = OneOfWithDefaultsVariantPropertySchemaComponent - -// #/components/schemas/OneOfWithDefaults/properties/variant/oneOf/0 -type OneOfWithDefaultsVariantN0OneOfPropertySchemaComponent struct { - OptionA *string `json:"optionA,omitempty" form:"optionA,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *OneOfWithDefaultsVariantN0OneOfPropertySchemaComponent) ApplyDefaults() { - if s.OptionA == nil { - v := "option-a-default" - s.OptionA = &v - } -} - -type OneOfWithDefaultsVariantOneOf0 = OneOfWithDefaultsVariantN0OneOfPropertySchemaComponent - -// #/components/schemas/OneOfWithDefaults/properties/variant/oneOf/1 -type OneOfWithDefaultsVariantN1OneOfPropertySchemaComponent struct { - OptionB *int `json:"optionB,omitempty" form:"optionB,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *OneOfWithDefaultsVariantN1OneOfPropertySchemaComponent) ApplyDefaults() { - if s.OptionB == nil { - v := 123 - s.OptionB = &v - } -} - -type OneOfWithDefaultsVariantOneOf1 = OneOfWithDefaultsVariantN1OneOfPropertySchemaComponent - -// #/components/schemas/AllOfWithDefaults -type AllOfWithDefaultsSchemaComponent struct { - Base *string `json:"base,omitempty" form:"base,omitempty"` - Extended *int `json:"extended,omitempty" form:"extended,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AllOfWithDefaultsSchemaComponent) ApplyDefaults() { - if s.Base == nil { - v := "base-value" - s.Base = &v - } - if s.Extended == nil { - v := 50 - s.Extended = &v - } -} - -type AllOfWithDefaults = AllOfWithDefaultsSchemaComponent - -// #/components/schemas/AllOfWithDefaults/allOf/0 -type AllOfWithDefaultsN0AllOfSchemaComponent struct { - Base *string `json:"base,omitempty" form:"base,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AllOfWithDefaultsN0AllOfSchemaComponent) ApplyDefaults() { - if s.Base == nil { - v := "base-value" - s.Base = &v - } -} - -type AllOfWithDefaultsAllOf0 = AllOfWithDefaultsN0AllOfSchemaComponent - -// #/components/schemas/AllOfWithDefaults/allOf/1 -type AllOfWithDefaultsN1AllOfSchemaComponent struct { - Extended *int `json:"extended,omitempty" form:"extended,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AllOfWithDefaultsN1AllOfSchemaComponent) ApplyDefaults() { - if s.Extended == nil { - v := 50 - s.Extended = &v - } -} - -type AllOfWithDefaultsAllOf1 = AllOfWithDefaultsN1AllOfSchemaComponent - -// #/components/schemas/DeepNesting -type DeepNestingSchemaComponent struct { - Level1 *DeepNestingLevel1 `json:"level1,omitempty" form:"level1,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *DeepNestingSchemaComponent) ApplyDefaults() { - if s.Level1 != nil { - s.Level1.ApplyDefaults() - } -} - -type DeepNesting = DeepNestingSchemaComponent - -// #/components/schemas/DeepNesting/properties/level1 -type DeepNestingLevel1PropertySchemaComponent struct { - Name *string `json:"name,omitempty" form:"name,omitempty"` - Level2 *DeepNestingLevel1Level2 `json:"level2,omitempty" form:"level2,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *DeepNestingLevel1PropertySchemaComponent) ApplyDefaults() { - if s.Name == nil { - v := "level1-name" - s.Name = &v - } - if s.Level2 != nil { - s.Level2.ApplyDefaults() - } -} - -type DeepNestingLevel1 = DeepNestingLevel1PropertySchemaComponent - -// #/components/schemas/DeepNesting/properties/level1/properties/level2 -type DeepNestingLevel1Level2PropertyPropertySchemaComponent struct { - Count *int `json:"count,omitempty" form:"count,omitempty"` - Level3 *DeepNestingLevel1Level2Level3 `json:"level3,omitempty" form:"level3,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *DeepNestingLevel1Level2PropertyPropertySchemaComponent) ApplyDefaults() { - if s.Count == nil { - v := 2 - s.Count = &v - } - if s.Level3 != nil { - s.Level3.ApplyDefaults() - } -} - -type DeepNestingLevel1Level2 = DeepNestingLevel1Level2PropertyPropertySchemaComponent - -// #/components/schemas/DeepNesting/properties/level1/properties/level2/properties/level3 -type DeepNestingLevel1Level2Level3PropertyPropertyPropertySchemaComponent struct { - Enabled *bool `json:"enabled,omitempty" form:"enabled,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *DeepNestingLevel1Level2Level3PropertyPropertyPropertySchemaComponent) ApplyDefaults() { - if s.Enabled == nil { - v := false - s.Enabled = &v - } -} - -type DeepNestingLevel1Level2Level3 = DeepNestingLevel1Level2Level3PropertyPropertyPropertySchemaComponent - -// #/components/schemas/RequiredAndOptional -type RequiredAndOptionalSchemaComponent struct { - RequiredWithDefault string `json:"requiredWithDefault" form:"requiredWithDefault"` - RequiredNoDefault string `json:"requiredNoDefault" form:"requiredNoDefault"` - OptionalWithDefault *string `json:"optionalWithDefault,omitempty" form:"optionalWithDefault,omitempty"` - OptionalNoDefault *string `json:"optionalNoDefault,omitempty" form:"optionalNoDefault,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *RequiredAndOptionalSchemaComponent) ApplyDefaults() { - if s.OptionalWithDefault == nil { - v := "optional-default" - s.OptionalWithDefault = &v - } -} - -type RequiredAndOptional = RequiredAndOptionalSchemaComponent diff --git a/experimental/internal/codegen/test/default_values/output/default_values_test.go b/experimental/internal/codegen/test/default_values/output/default_values_test.go deleted file mode 100644 index ccee0f18e3..0000000000 --- a/experimental/internal/codegen/test/default_values/output/default_values_test.go +++ /dev/null @@ -1,324 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func ptr[T any](v T) *T { - return &v -} - -// TestSimpleDefaults tests ApplyDefaults on basic primitive types -func TestSimpleDefaults(t *testing.T) { - t.Run("applies all defaults to empty struct", func(t *testing.T) { - s := SimpleDefaults{} - s.ApplyDefaults() - - require.NotNil(t, s.StringField) - assert.Equal(t, "hello", *s.StringField) - - require.NotNil(t, s.IntField) - assert.Equal(t, 42, *s.IntField) - - require.NotNil(t, s.BoolField) - assert.Equal(t, true, *s.BoolField) - - require.NotNil(t, s.FloatField) - assert.Equal(t, float32(3.14), *s.FloatField) - - require.NotNil(t, s.Int64Field) - assert.Equal(t, int64(9223372036854775807), *s.Int64Field) - }) - - t.Run("does not overwrite existing values", func(t *testing.T) { - s := SimpleDefaults{ - StringField: ptr("custom"), - IntField: ptr(100), - BoolField: ptr(false), - FloatField: ptr(float32(1.5)), - Int64Field: ptr(int64(123)), - } - s.ApplyDefaults() - - assert.Equal(t, "custom", *s.StringField) - assert.Equal(t, 100, *s.IntField) - assert.Equal(t, false, *s.BoolField) - assert.Equal(t, float32(1.5), *s.FloatField) - assert.Equal(t, int64(123), *s.Int64Field) - }) - - t.Run("applies defaults after unmarshaling empty object", func(t *testing.T) { - input := `{}` - var s SimpleDefaults - err := json.Unmarshal([]byte(input), &s) - require.NoError(t, err) - - s.ApplyDefaults() - - assert.Equal(t, "hello", *s.StringField) - assert.Equal(t, 42, *s.IntField) - }) - - t.Run("applies defaults after unmarshaling partial object", func(t *testing.T) { - input := `{"stringField": "from-json"}` - var s SimpleDefaults - err := json.Unmarshal([]byte(input), &s) - require.NoError(t, err) - - s.ApplyDefaults() - - assert.Equal(t, "from-json", *s.StringField) // from JSON - assert.Equal(t, 42, *s.IntField) // from default - }) -} - -// TestNestedDefaults tests ApplyDefaults recursion into nested structs -func TestNestedDefaults(t *testing.T) { - t.Run("applies defaults to parent and recurses to children", func(t *testing.T) { - n := NestedDefaults{ - Child: &SimpleDefaults{}, - InlineChild: &NestedDefaultsInlineChild{}, - } - n.ApplyDefaults() - - // Parent defaults - require.NotNil(t, n.Name) - assert.Equal(t, "parent", *n.Name) - - // Child defaults (recursion) - require.NotNil(t, n.Child.StringField) - assert.Equal(t, "hello", *n.Child.StringField) - require.NotNil(t, n.Child.IntField) - assert.Equal(t, 42, *n.Child.IntField) - - // Inline child defaults (recursion) - require.NotNil(t, n.InlineChild.Label) - assert.Equal(t, "inline-default", *n.InlineChild.Label) - require.NotNil(t, n.InlineChild.Value) - assert.Equal(t, 100, *n.InlineChild.Value) - }) - - t.Run("does not recurse into nil children", func(t *testing.T) { - n := NestedDefaults{} - n.ApplyDefaults() - - // Parent defaults applied - require.NotNil(t, n.Name) - assert.Equal(t, "parent", *n.Name) - - // Children are still nil (not created) - assert.Nil(t, n.Child) - assert.Nil(t, n.InlineChild) - }) - - t.Run("applies defaults after unmarshaling nested JSON", func(t *testing.T) { - input := `{"child": {"stringField": "from-child"}, "inlineChild": {}}` - var n NestedDefaults - err := json.Unmarshal([]byte(input), &n) - require.NoError(t, err) - - n.ApplyDefaults() - - // Parent defaults - assert.Equal(t, "parent", *n.Name) - - // Child - one field from JSON, others from defaults - assert.Equal(t, "from-child", *n.Child.StringField) - assert.Equal(t, 42, *n.Child.IntField) - - // Inline child - all defaults - assert.Equal(t, "inline-default", *n.InlineChild.Label) - assert.Equal(t, 100, *n.InlineChild.Value) - }) -} - -// TestMapWithDefaults tests ApplyDefaults on structs with additionalProperties -func TestMapWithDefaults(t *testing.T) { - t.Run("applies defaults to known fields", func(t *testing.T) { - m := MapWithDefaults{} - m.ApplyDefaults() - - require.NotNil(t, m.Prefix) - assert.Equal(t, "map-", *m.Prefix) - }) - - t.Run("does not affect additional properties", func(t *testing.T) { - m := MapWithDefaults{ - AdditionalProperties: map[string]string{ - "extra": "value", - }, - } - m.ApplyDefaults() - - assert.Equal(t, "map-", *m.Prefix) - assert.Equal(t, "value", m.AdditionalProperties["extra"]) - }) -} - -// TestArrayDefaults tests ApplyDefaults on structs with array fields -func TestArrayDefaults(t *testing.T) { - t.Run("applies defaults to non-array fields", func(t *testing.T) { - a := ArrayDefaults{} - a.ApplyDefaults() - - require.NotNil(t, a.Count) - assert.Equal(t, 0, *a.Count) - // Array field is not touched (no default generation for arrays currently) - assert.Nil(t, a.Items) - }) -} - -// TestAnyOfWithDefaults tests ApplyDefaults on anyOf variant members -func TestAnyOfWithDefaults(t *testing.T) { - t.Run("applies defaults to anyOf variant 0", func(t *testing.T) { - v0 := AnyOfWithDefaultsValueAnyOf0{} - v0.ApplyDefaults() - - require.NotNil(t, v0.StringVal) - assert.Equal(t, "default-string", *v0.StringVal) - }) - - t.Run("applies defaults to anyOf variant 1", func(t *testing.T) { - v1 := AnyOfWithDefaultsValueAnyOf1{} - v1.ApplyDefaults() - - require.NotNil(t, v1.IntVal) - assert.Equal(t, 999, *v1.IntVal) - }) -} - -// TestOneOfWithDefaults tests ApplyDefaults on oneOf variant members -func TestOneOfWithDefaults(t *testing.T) { - t.Run("applies defaults to oneOf variant 0", func(t *testing.T) { - v0 := OneOfWithDefaultsVariantOneOf0{} - v0.ApplyDefaults() - - require.NotNil(t, v0.OptionA) - assert.Equal(t, "option-a-default", *v0.OptionA) - }) - - t.Run("applies defaults to oneOf variant 1", func(t *testing.T) { - v1 := OneOfWithDefaultsVariantOneOf1{} - v1.ApplyDefaults() - - require.NotNil(t, v1.OptionB) - assert.Equal(t, 123, *v1.OptionB) - }) -} - -// TestAllOfWithDefaults tests ApplyDefaults on allOf merged structs -func TestAllOfWithDefaults(t *testing.T) { - t.Run("applies defaults from all merged schemas", func(t *testing.T) { - a := AllOfWithDefaults{} - a.ApplyDefaults() - - // Default from allOf/0 - require.NotNil(t, a.Base) - assert.Equal(t, "base-value", *a.Base) - - // Default from allOf/1 - require.NotNil(t, a.Extended) - assert.Equal(t, 50, *a.Extended) - }) -} - -// TestDeepNesting tests ApplyDefaults recursion through multiple levels -func TestDeepNesting(t *testing.T) { - t.Run("recurses through all levels", func(t *testing.T) { - d := DeepNesting{ - Level1: &DeepNestingLevel1{ - Level2: &DeepNestingLevel1Level2{ - Level3: &DeepNestingLevel1Level2Level3{}, - }, - }, - } - d.ApplyDefaults() - - // Level 1 defaults - require.NotNil(t, d.Level1.Name) - assert.Equal(t, "level1-name", *d.Level1.Name) - - // Level 2 defaults - require.NotNil(t, d.Level1.Level2.Count) - assert.Equal(t, 2, *d.Level1.Level2.Count) - - // Level 3 defaults - require.NotNil(t, d.Level1.Level2.Level3.Enabled) - assert.Equal(t, false, *d.Level1.Level2.Level3.Enabled) - }) - - t.Run("stops at nil levels", func(t *testing.T) { - d := DeepNesting{ - Level1: &DeepNestingLevel1{ - // Level2 is nil - }, - } - d.ApplyDefaults() - - assert.Equal(t, "level1-name", *d.Level1.Name) - assert.Nil(t, d.Level1.Level2) - }) -} - -// TestRequiredAndOptional tests ApplyDefaults behavior with required fields -func TestRequiredAndOptional(t *testing.T) { - t.Run("applies defaults only to optional pointer fields", func(t *testing.T) { - r := RequiredAndOptional{ - RequiredWithDefault: "set-by-user", - RequiredNoDefault: "also-set", - } - r.ApplyDefaults() - - // Required fields are value types, not pointers, so they don't get defaults applied - assert.Equal(t, "set-by-user", r.RequiredWithDefault) - assert.Equal(t, "also-set", r.RequiredNoDefault) - - // Optional fields with defaults get defaults applied - require.NotNil(t, r.OptionalWithDefault) - assert.Equal(t, "optional-default", *r.OptionalWithDefault) - - // Optional fields without defaults stay nil - assert.Nil(t, r.OptionalNoDefault) - }) -} - -// TestApplyDefaultsIdempotent tests that ApplyDefaults can be called multiple times -func TestApplyDefaultsIdempotent(t *testing.T) { - t.Run("multiple calls have same effect", func(t *testing.T) { - s := SimpleDefaults{} - s.ApplyDefaults() - s.ApplyDefaults() - s.ApplyDefaults() - - assert.Equal(t, "hello", *s.StringField) - assert.Equal(t, 42, *s.IntField) - }) -} - -// TestApplyDefaultsChain tests typical usage pattern: unmarshal then apply defaults -func TestApplyDefaultsChain(t *testing.T) { - t.Run("unmarshal partial JSON then apply defaults", func(t *testing.T) { - input := `{ - "level1": { - "level2": { - "level3": {} - } - } - }` - - var d DeepNesting - err := json.Unmarshal([]byte(input), &d) - require.NoError(t, err) - - d.ApplyDefaults() - - // All defaults applied at all levels - assert.Equal(t, "level1-name", *d.Level1.Name) - assert.Equal(t, 2, *d.Level1.Level2.Count) - assert.Equal(t, false, *d.Level1.Level2.Level3.Enabled) - }) -} diff --git a/experimental/internal/codegen/test/external_ref/config.yaml b/experimental/internal/codegen/test/external_ref/config.yaml deleted file mode 100644 index 919d5b9d47..0000000000 --- a/experimental/internal/codegen/test/external_ref/config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -package: externalref -output: internal/codegen/test/external_ref/spec.gen.go -import-mapping: - ./packagea/spec.yaml: github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packagea - ./packageb/spec.yaml: github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packageb diff --git a/experimental/internal/codegen/test/external_ref/external_ref_test.go b/experimental/internal/codegen/test/external_ref/external_ref_test.go deleted file mode 100644 index 885ddb05bb..0000000000 --- a/experimental/internal/codegen/test/external_ref/external_ref_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package externalref - -import ( - "testing" - - "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packagea" - "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packageb" -) - -// TestCrossPackageReferences verifies that types from external packages -// can be used correctly in the Container type. -func TestCrossPackageReferences(t *testing.T) { - // Create objects from each package - name := "test-name" - b := &packageb.ObjectB{ - Name: &name, - } - - a := &packagea.ObjectA{ - Name: &name, - ObjectB: b, - } - - // Create container that references both - container := Container{ - ObjectA: a, - ObjectB: b, - } - - // Verify the structure - if container.ObjectA == nil { - t.Error("ObjectA should not be nil") - } - if container.ObjectB == nil { - t.Error("ObjectB should not be nil") - } - if *container.ObjectA.Name != "test-name" { - t.Errorf("ObjectA.Name = %q, want %q", *container.ObjectA.Name, "test-name") - } - if *container.ObjectB.Name != "test-name" { - t.Errorf("ObjectB.Name = %q, want %q", *container.ObjectB.Name, "test-name") - } - - // Verify nested reference in ObjectA - if container.ObjectA.ObjectB == nil { - t.Error("ObjectA.ObjectB should not be nil") - } - if *container.ObjectA.ObjectB.Name != "test-name" { - t.Errorf("ObjectA.ObjectB.Name = %q, want %q", *container.ObjectA.ObjectB.Name, "test-name") - } -} - -// TestApplyDefaults verifies that ApplyDefaults works across package boundaries. -func TestApplyDefaults(t *testing.T) { - container := Container{ - ObjectA: &packagea.ObjectA{}, - ObjectB: &packageb.ObjectB{}, - } - - // Should not panic when calling ApplyDefaults across packages - container.ApplyDefaults() -} diff --git a/experimental/internal/codegen/test/external_ref/packagea/config.yaml b/experimental/internal/codegen/test/external_ref/packagea/config.yaml deleted file mode 100644 index 7cacfcb822..0000000000 --- a/experimental/internal/codegen/test/external_ref/packagea/config.yaml +++ /dev/null @@ -1,4 +0,0 @@ -package: packagea -output: internal/codegen/test/external_ref/packagea/spec.gen.go -import-mapping: - ../packageb/spec.yaml: github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packageb diff --git a/experimental/internal/codegen/test/external_ref/packagea/spec.gen.go b/experimental/internal/codegen/test/external_ref/packagea/spec.gen.go deleted file mode 100644 index b865bfb69d..0000000000 --- a/experimental/internal/codegen/test/external_ref/packagea/spec.gen.go +++ /dev/null @@ -1,22 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package packagea - -import ( - ext_a5fddf6c "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packageb" -) - -// #/components/schemas/ObjectA -type ObjectASchemaComponent struct { - Name *string `json:"name,omitempty" form:"name,omitempty"` - ObjectB *ext_a5fddf6c.ObjectB `json:"object_b,omitempty" form:"object_b,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ObjectASchemaComponent) ApplyDefaults() { - if s.ObjectB != nil { - s.ObjectB.ApplyDefaults() - } -} - -type ObjectA = ObjectASchemaComponent diff --git a/experimental/internal/codegen/test/external_ref/packagea/spec.yaml b/experimental/internal/codegen/test/external_ref/packagea/spec.yaml deleted file mode 100644 index 77f3ea45f5..0000000000 --- a/experimental/internal/codegen/test/external_ref/packagea/spec.yaml +++ /dev/null @@ -1,14 +0,0 @@ -openapi: "3.1.0" -info: - title: Package A - version: "1.0" -paths: {} -components: - schemas: - ObjectA: - type: object - properties: - name: - type: string - object_b: - $ref: ../packageb/spec.yaml#/components/schemas/ObjectB diff --git a/experimental/internal/codegen/test/external_ref/packageb/config.yaml b/experimental/internal/codegen/test/external_ref/packageb/config.yaml deleted file mode 100644 index 7b36e2c291..0000000000 --- a/experimental/internal/codegen/test/external_ref/packageb/config.yaml +++ /dev/null @@ -1,2 +0,0 @@ -package: packageb -output: internal/codegen/test/external_ref/packageb/spec.gen.go diff --git a/experimental/internal/codegen/test/external_ref/packageb/spec.gen.go b/experimental/internal/codegen/test/external_ref/packageb/spec.gen.go deleted file mode 100644 index e72bf4ac99..0000000000 --- a/experimental/internal/codegen/test/external_ref/packageb/spec.gen.go +++ /dev/null @@ -1,14 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package packageb - -// #/components/schemas/ObjectB -type ObjectBSchemaComponent struct { - Name *string `json:"name,omitempty" form:"name,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ObjectBSchemaComponent) ApplyDefaults() { -} - -type ObjectB = ObjectBSchemaComponent diff --git a/experimental/internal/codegen/test/external_ref/packageb/spec.yaml b/experimental/internal/codegen/test/external_ref/packageb/spec.yaml deleted file mode 100644 index bf9af943f5..0000000000 --- a/experimental/internal/codegen/test/external_ref/packageb/spec.yaml +++ /dev/null @@ -1,12 +0,0 @@ -openapi: "3.1.0" -info: - title: Package B - version: "1.0" -paths: {} -components: - schemas: - ObjectB: - type: object - properties: - name: - type: string diff --git a/experimental/internal/codegen/test/external_ref/spec.gen.go b/experimental/internal/codegen/test/external_ref/spec.gen.go deleted file mode 100644 index cf179d426b..0000000000 --- a/experimental/internal/codegen/test/external_ref/spec.gen.go +++ /dev/null @@ -1,26 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package externalref - -import ( - ext_95d82e90 "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packagea" - ext_a5fddf6c "github.com/oapi-codegen/oapi-codegen/experimental/internal/codegen/test/external_ref/packageb" -) - -// #/components/schemas/Container -type ContainerSchemaComponent struct { - ObjectA *ext_95d82e90.ObjectA `json:"object_a,omitempty" form:"object_a,omitempty"` - ObjectB *ext_a5fddf6c.ObjectB `json:"object_b,omitempty" form:"object_b,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ContainerSchemaComponent) ApplyDefaults() { - if s.ObjectA != nil { - s.ObjectA.ApplyDefaults() - } - if s.ObjectB != nil { - s.ObjectB.ApplyDefaults() - } -} - -type Container = ContainerSchemaComponent diff --git a/experimental/internal/codegen/test/external_ref/spec.yaml b/experimental/internal/codegen/test/external_ref/spec.yaml deleted file mode 100644 index 5a8e0ada72..0000000000 --- a/experimental/internal/codegen/test/external_ref/spec.yaml +++ /dev/null @@ -1,14 +0,0 @@ -openapi: "3.1.0" -info: - title: External Ref Test - version: "1.0" -paths: {} -components: - schemas: - Container: - type: object - properties: - object_a: - $ref: ./packagea/spec.yaml#/components/schemas/ObjectA - object_b: - $ref: ./packageb/spec.yaml#/components/schemas/ObjectB diff --git a/experimental/internal/codegen/test/files/comprehensive.yaml b/experimental/internal/codegen/test/files/comprehensive.yaml deleted file mode 100644 index 7bdef74eea..0000000000 --- a/experimental/internal/codegen/test/files/comprehensive.yaml +++ /dev/null @@ -1,861 +0,0 @@ -openapi: "3.1.0" -info: - title: Comprehensive Test Schema - version: "1.0.0" - description: Tests all schema patterns for oapi-codegen - -paths: - /simple: - get: - operationId: getSimple - responses: - "200": - description: Simple response - content: - application/json: - schema: - $ref: "#/components/schemas/SimpleObject" - - /inline-response: - get: - operationId: getInlineResponse - responses: - "200": - description: Inline object in response - content: - application/json: - schema: - type: object - required: - - id - - name - properties: - id: - type: integer - name: - type: string - - /parameters/{pathParam}: - parameters: - - name: pathParam - in: path - required: true - schema: - type: string - format: uuid - get: - operationId: getWithParameters - parameters: - - name: queryString - in: query - schema: - type: string - - name: queryInt - in: query - schema: - type: integer - - name: queryArray - in: query - schema: - type: array - items: - type: string - - name: headerParam - in: header - schema: - type: string - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/SimpleObject" - - /request-body: - post: - operationId: postRequestBody - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/AllTypesRequired" - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/SimpleObject" - - /multi-content: - post: - operationId: postMultiContent - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/SimpleObject" - application/x-www-form-urlencoded: - schema: - $ref: "#/components/schemas/SimpleObject" - multipart/form-data: - schema: - type: object - properties: - file: - type: string - format: binary - metadata: - type: string - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/SimpleObject" - text/plain: - schema: - type: string - -components: - schemas: - # =========================================== - # PRIMITIVE TYPES - All formats - # =========================================== - - AllTypesRequired: - type: object - required: - - intField - - int32Field - - int64Field - - floatField - - doubleField - - numberField - - stringField - - boolField - - dateField - - dateTimeField - - uuidField - - emailField - - uriField - - hostnameField - - ipv4Field - - ipv6Field - - byteField - - binaryField - - passwordField - properties: - intField: - type: integer - int32Field: - type: integer - format: int32 - int64Field: - type: integer - format: int64 - floatField: - type: number - format: float - doubleField: - type: number - format: double - numberField: - type: number - stringField: - type: string - boolField: - type: boolean - dateField: - type: string - format: date - dateTimeField: - type: string - format: date-time - uuidField: - type: string - format: uuid - emailField: - type: string - format: email - uriField: - type: string - format: uri - hostnameField: - type: string - format: hostname - ipv4Field: - type: string - format: ipv4 - ipv6Field: - type: string - format: ipv6 - byteField: - type: string - format: byte - binaryField: - type: string - format: binary - passwordField: - type: string - format: password - - AllTypesOptional: - type: object - properties: - intField: - type: integer - int32Field: - type: integer - format: int32 - int64Field: - type: integer - format: int64 - floatField: - type: number - format: float - doubleField: - type: number - format: double - numberField: - type: number - stringField: - type: string - boolField: - type: boolean - dateField: - type: string - format: date - dateTimeField: - type: string - format: date-time - uuidField: - type: string - format: uuid - emailField: - type: string - format: email - - # =========================================== - # NULLABLE TYPES - # =========================================== - - NullableRequired: - type: object - required: - - nullableString - - nullableInt - - nullableObject - properties: - nullableString: - type: - - string - - "null" - nullableInt: - type: - - integer - - "null" - nullableObject: - type: - - object - - "null" - properties: - name: - type: string - - NullableOptional: - type: object - properties: - nullableString: - type: - - string - - "null" - nullableInt: - type: - - integer - - "null" - - # =========================================== - # ARRAYS - # =========================================== - - ArrayTypes: - type: object - properties: - stringArray: - type: array - items: - type: string - intArray: - type: array - items: - type: integer - objectArray: - type: array - items: - $ref: "#/components/schemas/SimpleObject" - inlineObjectArray: - type: array - items: - type: object - properties: - id: - type: integer - name: - type: string - nestedArray: - type: array - items: - type: array - items: - type: string - nullableArray: - type: - - array - - "null" - items: - type: string - arrayWithConstraints: - type: array - minItems: 1 - maxItems: 10 - items: - type: string - - # =========================================== - # OBJECTS - # =========================================== - - SimpleObject: - type: object - required: - - id - properties: - id: - type: integer - name: - type: string - - NestedObject: - type: object - properties: - outer: - type: object - properties: - inner: - type: object - properties: - value: - type: string - - # =========================================== - # ADDITIONAL PROPERTIES - # =========================================== - - AdditionalPropsAny: - type: object - additionalProperties: true - - AdditionalPropsNone: - type: object - additionalProperties: false - properties: - known: - type: string - - AdditionalPropsTyped: - type: object - additionalProperties: - type: integer - - AdditionalPropsObject: - type: object - additionalProperties: - $ref: "#/components/schemas/SimpleObject" - - AdditionalPropsWithProps: - type: object - properties: - id: - type: integer - additionalProperties: - type: string - - # =========================================== - # ENUMS - # =========================================== - - StringEnum: - type: string - enum: - - value1 - - value2 - - value3 - - IntegerEnum: - type: integer - enum: - - 1 - - 2 - - 3 - - ObjectWithEnum: - type: object - properties: - status: - type: string - enum: - - pending - - active - - completed - priority: - type: integer - enum: - - 1 - - 2 - - 3 - - InlineEnumInProperty: - type: object - properties: - inlineStatus: - type: string - enum: - - on - - off - - # =========================================== - # ALLOF - Inheritance/Composition - # =========================================== - - BaseProperties: - type: object - properties: - id: - type: integer - createdAt: - type: string - format: date-time - - ExtendedObject: - allOf: - - $ref: "#/components/schemas/BaseProperties" - - type: object - required: - - name - properties: - name: - type: string - description: - type: string - - DeepInheritance: - allOf: - - $ref: "#/components/schemas/ExtendedObject" - - type: object - properties: - extra: - type: string - - AllOfMultipleRefs: - allOf: - - $ref: "#/components/schemas/BaseProperties" - - $ref: "#/components/schemas/SimpleObject" - - type: object - properties: - merged: - type: boolean - - AllOfInlineOnly: - allOf: - - type: object - properties: - first: - type: string - - type: object - properties: - second: - type: integer - - # =========================================== - # ANYOF - Union Types - # =========================================== - - AnyOfPrimitives: - anyOf: - - type: string - - type: integer - - AnyOfObjects: - anyOf: - - $ref: "#/components/schemas/SimpleObject" - - $ref: "#/components/schemas/BaseProperties" - - AnyOfMixed: - anyOf: - - type: string - - $ref: "#/components/schemas/SimpleObject" - - type: object - properties: - inline: - type: boolean - - AnyOfNullable: - anyOf: - - type: string - - type: "null" - - ObjectWithAnyOfProperty: - type: object - properties: - value: - anyOf: - - type: string - - type: integer - - type: boolean - - ArrayOfAnyOf: - type: array - items: - anyOf: - - $ref: "#/components/schemas/SimpleObject" - - $ref: "#/components/schemas/BaseProperties" - - # =========================================== - # ONEOF - Discriminated Unions - # =========================================== - - OneOfSimple: - oneOf: - - $ref: "#/components/schemas/SimpleObject" - - $ref: "#/components/schemas/BaseProperties" - - OneOfWithDiscriminator: - oneOf: - - $ref: "#/components/schemas/Cat" - - $ref: "#/components/schemas/Dog" - discriminator: - propertyName: petType - - OneOfWithDiscriminatorMapping: - oneOf: - - $ref: "#/components/schemas/Cat" - - $ref: "#/components/schemas/Dog" - discriminator: - propertyName: petType - mapping: - cat: "#/components/schemas/Cat" - dog: "#/components/schemas/Dog" - - Cat: - type: object - required: - - petType - - meow - properties: - petType: - type: string - meow: - type: string - whiskerLength: - type: number - - Dog: - type: object - required: - - petType - - bark - properties: - petType: - type: string - bark: - type: string - tailLength: - type: number - - OneOfInline: - oneOf: - - type: object - properties: - optionA: - type: string - - type: object - properties: - optionB: - type: integer - - OneOfPrimitives: - oneOf: - - type: string - - type: number - - type: boolean - - ObjectWithOneOfProperty: - type: object - properties: - id: - type: integer - variant: - oneOf: - - $ref: "#/components/schemas/Cat" - - $ref: "#/components/schemas/Dog" - discriminator: - propertyName: petType - - # =========================================== - # COMBINED COMPOSITION - # =========================================== - - AllOfWithOneOf: - allOf: - - $ref: "#/components/schemas/BaseProperties" - - oneOf: - - $ref: "#/components/schemas/Cat" - - $ref: "#/components/schemas/Dog" - discriminator: - propertyName: petType - - OneOfWithAllOf: - oneOf: - - allOf: - - $ref: "#/components/schemas/BaseProperties" - - type: object - properties: - variant: - type: string - const: "a" - - allOf: - - $ref: "#/components/schemas/BaseProperties" - - type: object - properties: - variant: - type: string - const: "b" - - # =========================================== - # RECURSIVE TYPES - # =========================================== - - TreeNode: - type: object - properties: - value: - type: string - children: - type: array - items: - $ref: "#/components/schemas/TreeNode" - - LinkedListNode: - type: object - properties: - value: - type: integer - next: - $ref: "#/components/schemas/LinkedListNode" - - RecursiveOneOf: - oneOf: - - type: string - - type: object - properties: - nested: - $ref: "#/components/schemas/RecursiveOneOf" - - # =========================================== - # READ/WRITE ONLY - # =========================================== - - ReadWriteOnly: - type: object - required: - - id - - password - properties: - id: - type: integer - readOnly: true - password: - type: string - writeOnly: true - name: - type: string - - # =========================================== - # DEFAULTS AND CONST - # =========================================== - - WithDefaults: - type: object - properties: - stringWithDefault: - type: string - default: "default_value" - intWithDefault: - type: integer - default: 42 - boolWithDefault: - type: boolean - default: true - arrayWithDefault: - type: array - items: - type: string - default: [] - - WithConst: - type: object - properties: - version: - type: string - const: "1.0.0" - type: - type: string - const: "fixed" - - # =========================================== - # CONSTRAINTS - # =========================================== - - WithConstraints: - type: object - properties: - boundedInt: - type: integer - minimum: 0 - maximum: 100 - exclusiveBoundedInt: - type: integer - exclusiveMinimum: 0 - exclusiveMaximum: 100 - multipleOf: - type: integer - multipleOf: 5 - boundedString: - type: string - minLength: 1 - maxLength: 255 - patternString: - type: string - pattern: "^[a-z]+$" - boundedArray: - type: array - minItems: 1 - maxItems: 10 - items: - type: string - uniqueArray: - type: array - uniqueItems: true - items: - type: string - - # =========================================== - # OPENAPI 3.1 SPECIFIC - # =========================================== - - TypeArray31: - type: - - object - - "null" - properties: - name: - type: string - - PrefixItems31: - type: array - prefixItems: - - type: string - - type: integer - - type: boolean - items: - type: string - - # =========================================== - # EMPTY / ANY TYPE - # =========================================== - - EmptySchema: {} - - AnyValue: - description: Accepts any JSON value - - ExplicitAny: - type: - - string - - number - - integer - - boolean - - array - - object - - "null" - - # =========================================== - # COMPLEX NESTED STRUCTURES - # =========================================== - - ComplexNested: - type: object - properties: - metadata: - type: object - additionalProperties: - anyOf: - - type: string - - type: integer - - type: array - items: - type: string - items: - type: array - items: - allOf: - - $ref: "#/components/schemas/BaseProperties" - - type: object - properties: - tags: - type: array - items: - type: string - config: - oneOf: - - type: object - properties: - mode: - type: string - const: "simple" - value: - type: string - - type: object - properties: - mode: - type: string - const: "advanced" - options: - type: object - additionalProperties: - type: string - - # =========================================== - # MAPS - # =========================================== - - StringMap: - type: object - additionalProperties: - type: string - - ObjectMap: - type: object - additionalProperties: - $ref: "#/components/schemas/SimpleObject" - - NestedMap: - type: object - additionalProperties: - type: object - additionalProperties: - type: string diff --git a/experimental/internal/codegen/test/issues/issue_1029/doc.go b/experimental/internal/codegen/test/issues/issue_1029/doc.go deleted file mode 100644 index dd7dc0eb30..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1029/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_1029 tests that oneOf with multiple single-value string enums generates valid code. -// https://github.com/oapi-codegen/oapi-codegen/issues/1029 -package issue_1029 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go deleted file mode 100644 index 7c5a5d2fa1..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1029/output/types.gen.go +++ /dev/null @@ -1,186 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "encoding/json" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/Registration -type Registration struct { - State *RegistrationState `json:"state,omitempty" form:"state,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Registration) ApplyDefaults() { -} - -// #/components/schemas/Registration/properties/state -type RegistrationState struct { - RegistrationStateOneOf0 *RegistrationStateOneOf0 - RegistrationStateOneOf1 *RegistrationStateOneOf1 - RegistrationStateOneOf2 *RegistrationStateOneOf2 - RegistrationStateOneOf3 *RegistrationStateOneOf3 -} - -func (u RegistrationState) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.RegistrationStateOneOf0 != nil { - count++ - data, err = json.Marshal(u.RegistrationStateOneOf0) - if err != nil { - return nil, err - } - } - if u.RegistrationStateOneOf1 != nil { - count++ - data, err = json.Marshal(u.RegistrationStateOneOf1) - if err != nil { - return nil, err - } - } - if u.RegistrationStateOneOf2 != nil { - count++ - data, err = json.Marshal(u.RegistrationStateOneOf2) - if err != nil { - return nil, err - } - } - if u.RegistrationStateOneOf3 != nil { - count++ - data, err = json.Marshal(u.RegistrationStateOneOf3) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("RegistrationState: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *RegistrationState) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 RegistrationStateOneOf0 - if err := json.Unmarshal(data, &v0); err == nil { - u.RegistrationStateOneOf0 = &v0 - successCount++ - } - - var v1 RegistrationStateOneOf1 - if err := json.Unmarshal(data, &v1); err == nil { - u.RegistrationStateOneOf1 = &v1 - successCount++ - } - - var v2 RegistrationStateOneOf2 - if err := json.Unmarshal(data, &v2); err == nil { - u.RegistrationStateOneOf2 = &v2 - successCount++ - } - - var v3 RegistrationStateOneOf3 - if err := json.Unmarshal(data, &v3); err == nil { - u.RegistrationStateOneOf3 = &v3 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("RegistrationState: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *RegistrationState) ApplyDefaults() { -} - -// #/components/schemas/Registration/properties/state/oneOf/0 -type RegistrationStateOneOf0 string - -const ( - RegistrationStateOneOf0_undefined RegistrationStateOneOf0 = "undefined" -) - -// #/components/schemas/Registration/properties/state/oneOf/1 -type RegistrationStateOneOf1 string - -const ( - RegistrationStateOneOf1_registered RegistrationStateOneOf1 = "registered" -) - -// #/components/schemas/Registration/properties/state/oneOf/2 -type RegistrationStateOneOf2 string - -const ( - RegistrationStateOneOf2_pending RegistrationStateOneOf2 = "pending" -) - -// #/components/schemas/Registration/properties/state/oneOf/3 -type RegistrationStateOneOf3 string - -const ( - RegistrationStateOneOf3_active RegistrationStateOneOf3 = "active" -) - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/6yQzY7UMBCE736KknJlMrPsibwBJySExNmb1CQNjm2527MaId4dOeFnZw4Iob25q7vr", - "a1eH96qVeDi9fTcgRX44Q61InMFYV8XZS2iVJcyMLN6IXFJmCVfXYTHLOhyPs9hSn/oxrcfksxzGNHFm", - "vC2kofTYWK5zHT4vjKja7Hfys9iCtQaTHIjWCDxcfKi8OeqN62ALfx80oRGgS6qhvdcsgfBxwnMqXzGm", - "UjhauPYuZUafZcBjf+ofncRzGhxgYoHDiyjwiWoOuLCopDjgoT/1J5e9LTrg23fXICkymrZ9HReufnsC", - "HzmLWvHWFjcFsGvmgPT0haP9lPYQTai/hgA1b/xTYo/lpQActgxutV2vceJZIqe73g7fA/xHq7L9geU1", - "vDLjdD/9X0Z+NLnwbz4/AgAA//+mSD78zgIAAA==", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_1029/output/types_test.go b/experimental/internal/codegen/test/issues/issue_1029/output/types_test.go deleted file mode 100644 index 4ea2eabfbe..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1029/output/types_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" -) - -// TestRegistrationStateOneOfEnums verifies that oneOf with string enums generates -// correctly with proper enum constants. -// https://github.com/oapi-codegen/oapi-codegen/issues/1029 -func TestRegistrationStateOneOfEnums(t *testing.T) { - // Verify enum constants exist and have correct values - tests := []struct { - name string - enum RegistrationStateOneOf0 - value string - }{ - {"undefined", RegistrationStateOneOf0_undefined, "undefined"}, - } - - for _, tt := range tests { - if string(tt.enum) != tt.value { - t.Errorf("%s enum = %q, want %q", tt.name, tt.enum, tt.value) - } - } -} - -func TestRegistrationStateMarshal(t *testing.T) { - // Test serialization of oneOf with string enum - state := RegistrationState{ - RegistrationStateOneOf0: ptrTo(RegistrationStateOneOf0(RegistrationStateOneOf0_undefined)), - } - - reg := Registration{ - State: &state, - } - - data, err := json.Marshal(reg) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - - // Verify the JSON contains the expected value - expected := `{"state":"undefined"}` - if string(data) != expected { - t.Errorf("Marshal result = %s, want %s", string(data), expected) - } -} - -func TestRegistrationStateUnmarshalLimitation(t *testing.T) { - // Note: Unmarshaling oneOf with multiple string enum types is inherently - // ambiguous without a discriminator, since any string can match any of the - // enum types. This test documents that limitation. - input := `{"state":"undefined"}` - - var decoded Registration - err := json.Unmarshal([]byte(input), &decoded) - - // The error is expected because all 4 string enum types can unmarshal - // from the same string value - if err == nil { - t.Log("Unmarshal succeeded (all variants matched)") - } else { - t.Logf("Unmarshal failed as expected for ambiguous oneOf: %v", err) - } -} - -func TestAllEnumConstants(t *testing.T) { - // Verify all enum constants are defined - _ = RegistrationStateOneOf0_undefined - _ = RegistrationStateOneOf1_registered - _ = RegistrationStateOneOf2_pending - _ = RegistrationStateOneOf3_active - - // Test values - if string(RegistrationStateOneOf1_registered) != "registered" { - t.Error("registered enum has wrong value") - } - if string(RegistrationStateOneOf2_pending) != "pending" { - t.Error("pending enum has wrong value") - } - if string(RegistrationStateOneOf3_active) != "active" { - t.Error("active enum has wrong value") - } -} - -func ptrTo[T any](v T) *T { - return &v -} diff --git a/experimental/internal/codegen/test/issues/issue_1029/spec.yaml b/experimental/internal/codegen/test/issues/issue_1029/spec.yaml deleted file mode 100644 index 698973cfbb..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1029/spec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# Issue 1029: oneOf string enums failing to generate properly -# https://github.com/oapi-codegen/oapi-codegen/issues/1029 -# -# When using oneOf with multiple single-value string enums, -# the generated code should compile and work correctly. -openapi: 3.0.3 -info: - title: Issue 1029 Test - version: 1.0.0 -paths: {} -components: - schemas: - Registration: - type: object - properties: - state: - oneOf: - - enum: - - undefined - type: string - - enum: - - registered - type: string - - enum: - - pending - type: string - - enum: - - active - type: string diff --git a/experimental/internal/codegen/test/issues/issue_1039/doc.go b/experimental/internal/codegen/test/issues/issue_1039/doc.go deleted file mode 100644 index 64016470f8..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1039/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_1039 tests nullable type generation. -// https://github.com/oapi-codegen/oapi-codegen/issues/1039 -package issue_1039 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go deleted file mode 100644 index d996cfb752..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1039/output/types.gen.go +++ /dev/null @@ -1,291 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/PatchRequest -// A request to patch an existing user object. -type PatchRequest struct { - SimpleRequiredNullable SimpleRequiredNullable `json:"simple_required_nullable" form:"simple_required_nullable"` - SimpleOptionalNullable SimpleOptionalNullable `json:"simple_optional_nullable,omitempty" form:"simple_optional_nullable,omitempty"` - SimpleOptionalNonNullable *any `json:"simple_optional_non_nullable,omitempty" form:"simple_optional_non_nullable,omitempty"` - ComplexRequiredNullable Nullable[ComplexRequiredNullable] `json:"complex_required_nullable" form:"complex_required_nullable"` - ComplexOptionalNullable Nullable[ComplexOptionalNullable] `json:"complex_optional_nullable,omitempty" form:"complex_optional_nullable,omitempty"` - AdditionalProperties map[string]any `json:"-"` -} - -func (s PatchRequest) MarshalJSON() ([]byte, error) { - result := make(map[string]any) - - result["simple_required_nullable"] = s.SimpleRequiredNullable - result["simple_optional_nullable"] = s.SimpleOptionalNullable - if s.SimpleOptionalNonNullable != nil { - result["simple_optional_non_nullable"] = s.SimpleOptionalNonNullable - } - result["complex_required_nullable"] = s.ComplexRequiredNullable - result["complex_optional_nullable"] = s.ComplexOptionalNullable - - // Add additional properties - for k, v := range s.AdditionalProperties { - result[k] = v - } - - return json.Marshal(result) -} - -func (s *PatchRequest) UnmarshalJSON(data []byte) error { - // Known fields - knownFields := map[string]bool{ - "simple_required_nullable": true, - "simple_optional_nullable": true, - "simple_optional_non_nullable": true, - "complex_required_nullable": true, - "complex_optional_nullable": true, - } - - var raw map[string]json.RawMessage - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - - if v, ok := raw["simple_required_nullable"]; ok { - if err := json.Unmarshal(v, &s.SimpleRequiredNullable); err != nil { - return err - } - } - if v, ok := raw["simple_optional_nullable"]; ok { - if err := json.Unmarshal(v, &s.SimpleOptionalNullable); err != nil { - return err - } - } - if v, ok := raw["simple_optional_non_nullable"]; ok { - var val any - if err := json.Unmarshal(v, &val); err != nil { - return err - } - s.SimpleOptionalNonNullable = &val - } - if v, ok := raw["complex_required_nullable"]; ok { - if err := json.Unmarshal(v, &s.ComplexRequiredNullable); err != nil { - return err - } - } - if v, ok := raw["complex_optional_nullable"]; ok { - if err := json.Unmarshal(v, &s.ComplexOptionalNullable); err != nil { - return err - } - } - - // Collect additional properties - s.AdditionalProperties = make(map[string]any) - for k, v := range raw { - if !knownFields[k] { - var val any - if err := json.Unmarshal(v, &val); err != nil { - return err - } - s.AdditionalProperties[k] = val - } - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *PatchRequest) ApplyDefaults() { -} - -// #/components/schemas/simple_required_nullable -// Simple required and nullable -type SimpleRequiredNullable = Nullable[int] - -// #/components/schemas/simple_optional_nullable -// Simple optional and nullable -type SimpleOptionalNullable = Nullable[int] - -// #/components/schemas/complex_required_nullable -// Complex required and nullable -type ComplexRequiredNullable struct { - Name *string `json:"name,omitempty" form:"name,omitempty"` // Optional and non nullable -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ComplexRequiredNullable) ApplyDefaults() { -} - -// #/components/schemas/complex_optional_nullable -// Complex, optional and nullable -type ComplexOptionalNullable struct { - AliasName Nullable[string] `json:"alias_name,omitempty" form:"alias_name,omitempty"` // Optional and nullable - Name *string `json:"name,omitempty" form:"name,omitempty"` // Optional and non nullable -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ComplexOptionalNullable) ApplyDefaults() { -} - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value -type Nullable[T any] map[bool]T - -// NewNullableWithValue creates a Nullable with the given value. -func NewNullableWithValue[T any](value T) Nullable[T] { - return Nullable[T]{true: value} -} - -// NewNullNullable creates a Nullable that is explicitly null. -func NewNullNullable[T any]() Nullable[T] { - return Nullable[T]{false: *new(T)} -} - -// Get returns the value if set, or an error if null or unspecified. -func (n Nullable[T]) Get() (T, error) { - if v, ok := n[true]; ok { - return v, nil - } - var zero T - if n.IsNull() { - return zero, ErrNullableIsNull - } - return zero, ErrNullableNotSpecified -} - -// MustGet returns the value or panics if null or unspecified. -func (n Nullable[T]) MustGet() T { - v, err := n.Get() - if err != nil { - panic(err) - } - return v -} - -// Set assigns a value. -func (n *Nullable[T]) Set(value T) { - *n = Nullable[T]{true: value} -} - -// SetNull marks the field as explicitly null. -func (n *Nullable[T]) SetNull() { - *n = Nullable[T]{false: *new(T)} -} - -// SetUnspecified clears the field (as if it was never set). -func (n *Nullable[T]) SetUnspecified() { - *n = nil -} - -// IsNull returns true if the field is explicitly null. -func (n Nullable[T]) IsNull() bool { - if n == nil { - return false - } - _, ok := n[false] - return ok -} - -// IsSpecified returns true if the field was provided (either null or a value). -func (n Nullable[T]) IsSpecified() bool { - return len(n) > 0 -} - -// MarshalJSON implements json.Marshaler. -func (n Nullable[T]) MarshalJSON() ([]byte, error) { - if n.IsNull() { - return []byte("null"), nil - } - if v, ok := n[true]; ok { - return json.Marshal(v) - } - // Unspecified - this shouldn't be called if omitempty is used correctly - return []byte("null"), nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (n *Nullable[T]) UnmarshalJSON(data []byte) error { - if string(data) == "null" { - n.SetNull() - return nil - } - var v T - if err := json.Unmarshal(data, &v); err != nil { - return err - } - n.Set(v) - return nil -} - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. -var ErrNullableIsNull = errors.New("nullable value is null") - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. -var ErrNullableNotSpecified = errors.New("nullable value is not specified") - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/7RUTY/TMBC9+1eMEqReaNNlT+QGnFYIdgXcKzeZJl5S23gmqJX48SjfSZu0ge7eEnvm", - "vTdvxuPDA1GOcLe+fx/C1zzL5DZD4KNFggQ1OsnKaOFDymwpDIJEcZpvV5HZB0ZatYxMjAnq4Y8qQCko", - "UIUvfPgifyJQ7hA4lQx6yCMdNlwYg3XGosuOwljU0qoQ7lfr1Z1QemdCAfAbHSmjQ1isi/OFAGDFGYaA", - "B7m3GQqAGClyynIZ90cAwP9JsJJTKkiDGjsssazkKK0+AYrQ0qSHuJXwVATU9w5/5Uj80cTHJuVE4I8U", - "K0jYmvjYxhSJymEcArsc2+PIaEbNHRaAtDZTUSkieCaj+3cAFKW4l8MzgDcOdyF4fhCZvTUaNVNQRVJQ", - "yv9W6fbaMsgaTUgd0OLder3o4w6q8h4/e0J06EVgTVDl9FkalKIbIZjtM0YsRkA/NHYCm9ozqQEPiljp", - "BHJCV2evxImLrc4lkCqatGmuNs0w9EIK3RkeJmOqEWHV92MKtm+R3woCqWM4Y77cmCkG71SDKf2S2YSG", - "5voGDWcMFzQYPUuH0ctbtfSYvN6LmejlyzVmkuJcxSu2ZpKiUSHjWFVXT938wk5mhELMGeHqfSrNmKCr", - "z9qY/p4avNrvJeqEwWLO2N7EO26pmDuqFTexUzqZzWL0CdPVIRxZfdcr/FShXpzdsVWl5R4nN/fjZCVd", - "wsAUMWu+r+32upa3F5/AWDEyU5I2/1DSlXK649EGvLR7o1R/AwAA///zkywGmQkAAA==", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_1039/output/types_test.go b/experimental/internal/codegen/test/issues/issue_1039/output/types_test.go deleted file mode 100644 index f165ca7ce8..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1039/output/types_test.go +++ /dev/null @@ -1,266 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" -) - -// TestNullableTypes verifies that nullable types are generated properly. -// https://github.com/oapi-codegen/oapi-codegen/issues/1039 -// -// The implementation uses Nullable[T] for nullable types: -// - Nullable primitive schemas generate type aliases: type SimpleRequiredNullable = Nullable[int] -// - Nullable object fields are wrapped: Nullable[ComplexType] -// - Inline nullable primitives use Nullable[T] directly -func TestNullableTypes(t *testing.T) { - // Create a patch request with various nullable fields - name := "test-name" - - // SimpleRequiredNullable is Nullable[int] - simpleRequired := NewNullableWithValue(42) - - // ComplexRequiredNullable is wrapped in Nullable - complexRequired := NewNullableWithValue(ComplexRequiredNullable{Name: &name}) - - req := PatchRequest{ - SimpleRequiredNullable: simpleRequired, - ComplexRequiredNullable: complexRequired, - } - - // Verify simple nullable value - val, err := req.SimpleRequiredNullable.Get() - if err != nil { - t.Fatalf("SimpleRequiredNullable.Get() failed: %v", err) - } - if val != 42 { - t.Errorf("SimpleRequiredNullable = %v, want 42", val) - } - - // Verify complex nullable can retrieve value - complexVal, err := req.ComplexRequiredNullable.Get() - if err != nil { - t.Fatalf("ComplexRequiredNullable.Get() failed: %v", err) - } - if *complexVal.Name != "test-name" { - t.Errorf("ComplexRequiredNullable.Name = %q, want %q", *complexVal.Name, "test-name") - } -} - -func TestPatchRequestJSONRoundTrip(t *testing.T) { - name := "test" - original := PatchRequest{ - SimpleRequiredNullable: NewNullableWithValue(100), - ComplexRequiredNullable: NewNullableWithValue(ComplexRequiredNullable{Name: &name}), - } - - data, err := json.Marshal(original) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - t.Logf("Marshaled: %s", string(data)) - - var decoded PatchRequest - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("Unmarshal failed: %v", err) - } - - // Verify simple nullable round-trips correctly - decodedSimple, err := decoded.SimpleRequiredNullable.Get() - if err != nil { - t.Fatalf("SimpleRequiredNullable.Get() failed: %v", err) - } - if decodedSimple != 100 { - t.Errorf("SimpleRequiredNullable mismatch: got %v, want %v", decodedSimple, 100) - } - - // Verify complex nullable round-trips correctly - complexVal, err := decoded.ComplexRequiredNullable.Get() - if err != nil { - t.Fatalf("ComplexRequiredNullable.Get() failed: %v", err) - } - if *complexVal.Name != "test" { - t.Errorf("ComplexRequiredNullable.Name = %q, want %q", *complexVal.Name, "test") - } -} - -func TestComplexNullableTypes(t *testing.T) { - // Complex nullable types use Nullable[T] - name := "name" - opt := ComplexOptionalNullable{ - AliasName: NewNullableWithValue("alias"), - Name: &name, - } - - req := PatchRequest{ - SimpleRequiredNullable: NewNullNullable[int](), // explicitly null - ComplexRequiredNullable: NewNullNullable[ComplexRequiredNullable](), - ComplexOptionalNullable: NewNullableWithValue(opt), - } - - // Check the complex optional nullable - if !req.ComplexOptionalNullable.IsSpecified() { - t.Fatal("ComplexOptionalNullable should be specified") - } - optVal := req.ComplexOptionalNullable.MustGet() - aliasVal := optVal.AliasName.MustGet() - if aliasVal != "alias" { - t.Errorf("AliasName = %q, want %q", aliasVal, "alias") - } - - // Check that required nullable can be null - if !req.ComplexRequiredNullable.IsNull() { - t.Error("ComplexRequiredNullable should be null") - } - if !req.SimpleRequiredNullable.IsNull() { - t.Error("SimpleRequiredNullable should be null") - } -} - -func TestNullableThreeStates(t *testing.T) { - // Test unspecified (nil/empty map) - unspecified := Nullable[string](nil) - if unspecified.IsSpecified() { - t.Error("unspecified should not be specified") - } - if unspecified.IsNull() { - t.Error("unspecified should not be null") - } - _, err := unspecified.Get() - if err != ErrNullableNotSpecified { - t.Errorf("Get() on unspecified should return ErrNullableNotSpecified, got %v", err) - } - - // Test explicitly null - null := NewNullNullable[string]() - if !null.IsSpecified() { - t.Error("null should be specified") - } - if !null.IsNull() { - t.Error("null should be null") - } - _, err = null.Get() - if err != ErrNullableIsNull { - t.Errorf("Get() on null should return ErrNullableIsNull, got %v", err) - } - - // Test with value - withValue := NewNullableWithValue("hello") - if !withValue.IsSpecified() { - t.Error("withValue should be specified") - } - if withValue.IsNull() { - t.Error("withValue should not be null") - } - val, err := withValue.Get() - if err != nil { - t.Errorf("Get() on withValue should succeed, got %v", err) - } - if val != "hello" { - t.Errorf("Get() = %q, want %q", val, "hello") - } -} - -func TestNullableJSONMarshal(t *testing.T) { - // Test marshaling each state - tests := []struct { - name string - nullable Nullable[string] - want string - }{ - {"with value", NewNullableWithValue("test"), `"test"`}, - {"explicitly null", NewNullNullable[string](), "null"}, - {"unspecified", Nullable[string](nil), "null"}, // unspecified marshals as null - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data, err := json.Marshal(tt.nullable) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - if string(data) != tt.want { - t.Errorf("Marshal() = %s, want %s", string(data), tt.want) - } - }) - } -} - -func TestNullableJSONUnmarshal(t *testing.T) { - tests := []struct { - name string - json string - wantNull bool - wantValue string - wantErr error - }{ - {"with value", `"test"`, false, "test", nil}, - {"explicitly null", "null", true, "", ErrNullableIsNull}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var n Nullable[string] - if err := json.Unmarshal([]byte(tt.json), &n); err != nil { - t.Fatalf("Unmarshal failed: %v", err) - } - if n.IsNull() != tt.wantNull { - t.Errorf("IsNull() = %v, want %v", n.IsNull(), tt.wantNull) - } - val, err := n.Get() - if err != tt.wantErr { - t.Errorf("Get() error = %v, want %v", err, tt.wantErr) - } - if err == nil && val != tt.wantValue { - t.Errorf("Get() = %q, want %q", val, tt.wantValue) - } - }) - } -} - -// TestNullablePrimitiveTypeAlias verifies that nullable primitive schemas -// generate proper type aliases. -func TestNullablePrimitiveTypeAlias(t *testing.T) { - // SimpleRequiredNullable should be a type alias to Nullable[int] - var simple SimpleRequiredNullable - simple.Set(42) - - val, err := simple.Get() - if err != nil { - t.Fatalf("Get() failed: %v", err) - } - if val != 42 { - t.Errorf("Get() = %d, want 42", val) - } - - // Test null state - simple.SetNull() - if !simple.IsNull() { - t.Error("should be null after SetNull()") - } - - // Test unspecified state - simple.SetUnspecified() - if simple.IsSpecified() { - t.Error("should not be specified after SetUnspecified()") - } -} - -// TestAdditionalPropertiesFalse verifies that additionalProperties: false -// generates proper marshal/unmarshal that rejects extra fields. -func TestAdditionalPropertiesFalse(t *testing.T) { - // The struct has AdditionalProperties field but additionalProperties: false - // means unknown fields are still collected but not expected - req := PatchRequest{ - SimpleRequiredNullable: NewNullableWithValue(1), - ComplexRequiredNullable: NewNullNullable[ComplexRequiredNullable](), - AdditionalProperties: map[string]any{"extra": "value"}, - } - - // Should marshal with additional properties - data, err := json.Marshal(req) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - - t.Logf("Marshaled: %s", string(data)) -} diff --git a/experimental/internal/codegen/test/issues/issue_1039/spec.yaml b/experimental/internal/codegen/test/issues/issue_1039/spec.yaml deleted file mode 100644 index 8d827acef5..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1039/spec.yaml +++ /dev/null @@ -1,86 +0,0 @@ -# Issue 1039: Nullable types generation -# https://github.com/oapi-codegen/oapi-codegen/issues/1039 -# -# Make sure that nullable types are generated properly -openapi: 3.0.1 -info: - version: '0.0.1' - title: example - description: | - Make sure that nullable types are generated properly -paths: - /example: - patch: - operationId: examplePatch - requestBody: - description: The patch body - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/PatchRequest" - responses: - '200': - description: "OK" - -components: - schemas: - PatchRequest: - type: object - description: A request to patch an existing user object. - required: - - simple_required_nullable - - complex_required_nullable - properties: - simple_required_nullable: - # required and nullable - $ref: "#/components/schemas/simple_required_nullable" - simple_optional_nullable: - # optional and nullable - $ref: "#/components/schemas/simple_optional_nullable" - simple_optional_non_nullable: - # optional and non-nullable - $ref: "#/components/schemas/simple_optional_non_nullable" - complex_required_nullable: - # required and nullable - $ref: "#/components/schemas/complex_required_nullable" - complex_optional_nullable: - # optional and nullable - $ref: "#/components/schemas/complex_optional_nullable" - additionalProperties: false - - simple_required_nullable: - type: integer - nullable: true - description: Simple required and nullable - - simple_optional_nullable: - type: integer - nullable: true - description: Simple optional and nullable - - simple_optional_non_nullable: - type: string - description: Simple optional and non nullable - - complex_required_nullable: - type: object - nullable: true - description: Complex required and nullable - properties: - name: - description: Optional and non nullable - type: string - - complex_optional_nullable: - type: object - description: Complex, optional and nullable - properties: - alias_name: - description: Optional and nullable - type: string - nullable: true - name: - description: Optional and non nullable - type: string - nullable: true diff --git a/experimental/internal/codegen/test/issues/issue_1397/doc.go b/experimental/internal/codegen/test/issues/issue_1397/doc.go deleted file mode 100644 index 94fef2bfb0..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1397/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_1397 tests basic type generation with x-go-type-name. -// https://github.com/oapi-codegen/oapi-codegen/issues/1397 -package issue_1397 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go deleted file mode 100644 index 7fd6bbf827..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1397/output/types.gen.go +++ /dev/null @@ -1,114 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/Test -type MyTestRequest struct { - Field1 []TestField1Item `json:"field1,omitempty" form:"field1,omitempty"` // A array of enum values - Field2 *MyTestRequestNestedField `json:"field2,omitempty" form:"field2,omitempty"` // A nested object with allocated name - Field3 *TestField3 `json:"field3,omitempty" form:"field3,omitempty"` // A nested object without allocated name -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *MyTestRequest) ApplyDefaults() { - if s.Field2 != nil { - s.Field2.ApplyDefaults() - } - if s.Field3 != nil { - s.Field3.ApplyDefaults() - } -} - -// #/components/schemas/Test/properties/field1 -// A array of enum values -type TestField1 = []TestField1Item - -// #/components/schemas/Test/properties/field1/items -type TestField1Item string - -const ( - TestField1Item_option1 TestField1Item = "option1" - TestField1Item_option2 TestField1Item = "option2" -) - -// #/components/schemas/Test/properties/field2 -// A nested object with allocated name -type MyTestRequestNestedField struct { - Field1 bool `json:"field1" form:"field1"` - Field2 string `json:"field2" form:"field2"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *MyTestRequestNestedField) ApplyDefaults() { -} - -// #/components/schemas/Test/properties/field3 -// A nested object without allocated name -type TestField3 struct { - Field1 bool `json:"field1" form:"field1"` - Field2 string `json:"field2" form:"field2"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *TestField3) ApplyDefaults() { -} - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/9xUzZKbMAy+8xSapFdCIO106lv30Jkc2kOnL+AYBbwDltcS283bd2zYAJt0Z3otJ/xZ", - "P58+WdrCkXlAKA9fPit40GwNyMUjNOgwaLHk4LeVFl7yhvJ4kzvdY7aFVsSzKorGSjucdob6grS3uaEa", - "G3Trg41JuIhZMvLotLcKNofdflduMuvOpDIAsdKhWhCCX8iSATxjYEtOQbnb7/aZod6TQyccvdi02Ov0", - "C8lh/INUhgI6PaKRCfKBPAaxyK9GAGeLXV3OZ4Aa2QTrJaX8CjoEfQE6A7qhh2fdDcgLayvY89IdkuEa", - "yYFSvPIuWq3QkTdLsK5ZXPT65ZhSwaclat2E7rO3IRLxdZ3VO3U6ZMF6Emxsuu46Mjqiqemz6z0h74s5", - "0zkRdajdrX113/5GgYBPgw1YL83zKeUtVN3osXoK8Vu/aQXfL/H9/MSnAVl+JDm+xVBrDQ//pCEN8j/L", - "+K6EmdfSptIKuQ5mg9cJjcWnFXOsFcg4668MkeWB6stM0pATdLJkrb3vrEkRikcmtxZgXAxvRfkQ8Kxg", - "sy3mLVJMK6SI1DdXDuzJ8bIz1f7jX1vfENXZnwAAAP//PTfE300FAAA=", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_1397/output/types_test.go b/experimental/internal/codegen/test/issues/issue_1397/output/types_test.go deleted file mode 100644 index 05ae316ffb..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1397/output/types_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" -) - -// TestNestedObjectTypes verifies that nested objects get proper types. -// https://github.com/oapi-codegen/oapi-codegen/issues/1397 -// -// Note: The x-go-type-name extension is not currently supported. Types are -// named based on their path in the schema rather than the specified names. -func TestNestedObjectTypes(t *testing.T) { - // Test schema should have array of enums, and two nested objects - test := MyTestRequest{ - Field1: []TestField1Item{ - TestField1Item_option1, - TestField1Item_option2, - }, - Field2: &MyTestRequestNestedField{ - Field1: true, - Field2: "value2", - }, - Field3: &TestField3{ - Field1: false, - Field2: "value3", - }, - } - - if len(test.Field1) != 2 { - t.Errorf("Field1 length = %d, want 2", len(test.Field1)) - } - if test.Field2.Field1 != true { - t.Errorf("Field2.Field1 = %v, want true", test.Field2.Field1) - } - if test.Field3.Field2 != "value3" { - t.Errorf("Field3.Field2 = %q, want %q", test.Field3.Field2, "value3") - } -} - -func TestEnumArrayField(t *testing.T) { - // Field1 is an array of enum values - _ = TestField1Item_option1 - _ = TestField1Item_option2 - - items := []TestField1Item{ - TestField1Item("option1"), - TestField1Item("option2"), - } - - if len(items) != 2 { - t.Errorf("items length = %d, want 2", len(items)) - } -} - -func TestTestJSONRoundTrip(t *testing.T) { - original := MyTestRequest{ - Field1: []TestField1Item{TestField1Item_option1}, - Field2: &MyTestRequestNestedField{Field1: true, Field2: "test"}, - } - - data, err := json.Marshal(original) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - - var decoded MyTestRequest - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("Unmarshal failed: %v", err) - } - - if len(decoded.Field1) != 1 { - t.Errorf("Field1 length = %d, want 1", len(decoded.Field1)) - } - if decoded.Field2 == nil { - t.Fatal("Field2 should not be nil") - } - if decoded.Field2.Field1 != true { - t.Errorf("Field2.Field1 = %v, want true", decoded.Field2.Field1) - } -} diff --git a/experimental/internal/codegen/test/issues/issue_1397/spec.yaml b/experimental/internal/codegen/test/issues/issue_1397/spec.yaml deleted file mode 100644 index 3f1289ceac..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1397/spec.yaml +++ /dev/null @@ -1,57 +0,0 @@ -# Issue 1397: Basic type generation with x-go-type-name -# https://github.com/oapi-codegen/oapi-codegen/issues/1397 -openapi: "3.0.1" -info: - title: Issue 1397 Test - version: 1.0.0 -components: - schemas: - Test: - type: object - properties: - field1: - description: A array of enum values - items: - enum: - - option1 - - option2 - type: string - maxItems: 5 - minItems: 0 - type: array - field2: - description: A nested object with allocated name - properties: - field1: - type: boolean - field2: - type: string - required: - - field1 - - field2 - type: object - x-go-type-name: MyTestRequestNestedField - field3: - description: A nested object without allocated name - properties: - field1: - type: boolean - field2: - type: string - required: - - field1 - - field2 - type: object - x-go-type-name: MyTestRequest -paths: - /test: - get: - operationId: test - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/Test" - responses: - 204: - description: good diff --git a/experimental/internal/codegen/test/issues/issue_1429/doc.go b/experimental/internal/codegen/test/issues/issue_1429/doc.go deleted file mode 100644 index ab6cbd6e21..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1429/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_1429 tests that enums inside anyOf members are generated. -// https://github.com/oapi-codegen/oapi-codegen/issues/1429 -package issue_1429 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go deleted file mode 100644 index 993992b0d7..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1429/output/types.gen.go +++ /dev/null @@ -1,149 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "encoding/json" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/test -type Test struct { - TestAnyOf0 *TestAnyOf0 - TestAnyOf1 *TestAnyOf1 -} - -func (u Test) MarshalJSON() ([]byte, error) { - result := make(map[string]any) - - if u.TestAnyOf0 != nil { - data, err := json.Marshal(u.TestAnyOf0) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err == nil { - for k, v := range m { - result[k] = v - } - } - } - if u.TestAnyOf1 != nil { - data, err := json.Marshal(u.TestAnyOf1) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err == nil { - for k, v := range m { - result[k] = v - } - } - } - - return json.Marshal(result) -} - -func (u *Test) UnmarshalJSON(data []byte) error { - var v0 TestAnyOf0 - if err := json.Unmarshal(data, &v0); err == nil { - u.TestAnyOf0 = &v0 - } - - var v1 TestAnyOf1 - if err := json.Unmarshal(data, &v1); err == nil { - u.TestAnyOf1 = &v1 - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *Test) ApplyDefaults() { - if u.TestAnyOf0 != nil { - u.TestAnyOf0.ApplyDefaults() - } - if u.TestAnyOf1 != nil { - u.TestAnyOf1.ApplyDefaults() - } -} - -// #/components/schemas/test/anyOf/0 -type TestAnyOf0 struct { - FieldA *string `json:"fieldA,omitempty" form:"fieldA,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *TestAnyOf0) ApplyDefaults() { -} - -// #/components/schemas/test/anyOf/1 -type TestAnyOf1 struct { - FieldA *string `json:"fieldA,omitempty" form:"fieldA,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *TestAnyOf1) ApplyDefaults() { -} - -// #/components/schemas/test/anyOf/1/properties/fieldA -type TestAnyOf1FieldA string - -const ( - TestAnyOf1FieldA_foo TestAnyOf1FieldA = "foo" - TestAnyOf1FieldA_bar TestAnyOf1FieldA = "bar" -) - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/6xSsW7cMAzd9RUPcYEsvfMl7VJtHTN1KdBZtmlLwZkURLrF/X0hOwdfrh3DiXqkHt8T", - "1eBFdSE8fX3+5kG8zGAxTMRUgtGAP5EYi9YsWUTgy4/RNYhmWX3bTsni0h17mVsJOR16GWgifn9IdYS2", - "dYZrXINflTMgF8lU7ILEmgZC4I0eM80dFcSgFauiPsMibfLsksk10CjLeUBHu9ajk0wccvL4cjwdTy7x", - "KN4BluxM/sYpfpKaA35T0STs8bT252BR64XWSK0mwERvCVDVBkvCL4O/MtQopFlYSa+NwMPz6fSwH4Fe", - "2IjtFgJCzufUr5Ttqwq/rwLaR5rDPQp8KjR6PDZtL3MWJjZtt15dlT+6vVBvv9U2ot0a1rf0kO6V+quZ", - "dQP7yMN1S+nWXo0x0Xn4fi9uY1QriaePJdmifoF/H+SAUeQ/aBeK+xsAAP//dJpKJ+ICAAA=", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_1429/output/types_test.go b/experimental/internal/codegen/test/issues/issue_1429/output/types_test.go deleted file mode 100644 index 9020a645b2..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1429/output/types_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" -) - -// TestEnumGenerated verifies that the enum type is generated for properties inside anyOf. -// Issue 1429: enum type was not being generated when used inside anyOf. -func TestEnumGenerated(t *testing.T) { - // The enum type should exist and have the expected constants - _ = TestAnyOf1FieldA_foo - _ = TestAnyOf1FieldA_bar - - // The alias should also exist - _ = TestAnyOf1FieldA(TestAnyOf1FieldA_foo) -} - -// TestAnyOfMarshal verifies that the anyOf type can be marshaled. -func TestAnyOfMarshal(t *testing.T) { - test := Test{ - TestAnyOf1: &TestAnyOf1{ - FieldA: ptr("foo"), - }, - } - - data, err := json.Marshal(test) - if err != nil { - t.Fatalf("Failed to marshal: %v", err) - } - - t.Logf("Marshaled: %s", string(data)) -} - -func ptr[T any](v T) *T { - return &v -} diff --git a/experimental/internal/codegen/test/issues/issue_1429/spec.yaml b/experimental/internal/codegen/test/issues/issue_1429/spec.yaml deleted file mode 100644 index a07aef195a..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1429/spec.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# Issue 1429: enum not generated when used with anyOf -# https://github.com/oapi-codegen/oapi-codegen/issues/1429 -# -# When a property inside an anyOf member has an enum, the enum type -# should be generated. -openapi: 3.0.0 -info: - title: Issue 1429 Test - version: 1.0.0 -paths: - /test: - get: - operationId: Test - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/test' -components: - schemas: - test: - type: object - anyOf: - - properties: - fieldA: - type: string - - properties: - fieldA: - type: string - enum: - - foo - - bar diff --git a/experimental/internal/codegen/test/issues/issue_1496/doc.go b/experimental/internal/codegen/test/issues/issue_1496/doc.go deleted file mode 100644 index e09cb2f78e..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1496/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_1496 tests that inline schemas generate valid Go identifiers. -// https://github.com/oapi-codegen/oapi-codegen/issues/1496 -package issue_1496 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go deleted file mode 100644 index 2f95e2dbf8..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1496/output/types.gen.go +++ /dev/null @@ -1,168 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "encoding/json" - "fmt" - "strings" - "sync" -) - -// #/paths//something/get/responses/200/content/application/json/schema -type GetSomethingJSONResponse struct { - Results []GetSomething200ResponseJSON2 `json:"results" form:"results"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *GetSomethingJSONResponse) ApplyDefaults() { -} - -// #/paths//something/get/responses/200/content/application/json/schema/properties/results -type GetSomething200ResponseJSON1 = []GetSomething200ResponseJSON2 - -// #/paths//something/get/responses/200/content/application/json/schema/properties/results/items -type GetSomething200ResponseJSON2 struct { - GetSomething200ResponseJSONAnyOf0 *GetSomething200ResponseJSONAnyOf0 - GetSomething200ResponseJSONAnyOf11 *GetSomething200ResponseJSONAnyOf11 -} - -func (u GetSomething200ResponseJSON2) MarshalJSON() ([]byte, error) { - result := make(map[string]any) - - if u.GetSomething200ResponseJSONAnyOf0 != nil { - data, err := json.Marshal(u.GetSomething200ResponseJSONAnyOf0) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err == nil { - for k, v := range m { - result[k] = v - } - } - } - if u.GetSomething200ResponseJSONAnyOf11 != nil { - data, err := json.Marshal(u.GetSomething200ResponseJSONAnyOf11) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err == nil { - for k, v := range m { - result[k] = v - } - } - } - - return json.Marshal(result) -} - -func (u *GetSomething200ResponseJSON2) UnmarshalJSON(data []byte) error { - var v0 GetSomething200ResponseJSONAnyOf0 - if err := json.Unmarshal(data, &v0); err == nil { - u.GetSomething200ResponseJSONAnyOf0 = &v0 - } - - var v1 GetSomething200ResponseJSONAnyOf11 - if err := json.Unmarshal(data, &v1); err == nil { - u.GetSomething200ResponseJSONAnyOf11 = &v1 - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *GetSomething200ResponseJSON2) ApplyDefaults() { - if u.GetSomething200ResponseJSONAnyOf0 != nil { - u.GetSomething200ResponseJSONAnyOf0.ApplyDefaults() - } - if u.GetSomething200ResponseJSONAnyOf11 != nil { - u.GetSomething200ResponseJSONAnyOf11.ApplyDefaults() - } -} - -// #/paths//something/get/responses/200/content/application/json/schema/properties/results/items/anyOf/0 -type GetSomething200ResponseJSONAnyOf0 struct { - Order *string `json:"order,omitempty" form:"order,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *GetSomething200ResponseJSONAnyOf0) ApplyDefaults() { -} - -// #/paths//something/get/responses/200/content/application/json/schema/properties/results/items/anyOf/1 -type GetSomething200ResponseJSONAnyOf11 struct { - Error *GetSomething200ResponseJSONAnyOf12 `json:"error,omitempty" form:"error,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *GetSomething200ResponseJSONAnyOf11) ApplyDefaults() { - if s.Error != nil { - s.Error.ApplyDefaults() - } -} - -// #/paths//something/get/responses/200/content/application/json/schema/properties/results/items/anyOf/1/properties/error -type GetSomething200ResponseJSONAnyOf12 struct { - Code *float32 `json:"code,omitempty" form:"code,omitempty"` - Message *string `json:"message,omitempty" form:"message,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *GetSomething200ResponseJSONAnyOf12) ApplyDefaults() { -} - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/7xTTW+bQBC98yue4ksrJeC0VaVy66nyKVJTqcdoDWOYyMxud4ZY/vfVgqlJncTJJZyY", - "fTP7PhgWWKn2hOsv376W+C5e9p3vFX59T5VBq5Y6hx1bCy90s0FDQtEZKVge3JZrcE1ivGGK2QKtWdCy", - "KBq2tl/nle8K7wJfVb6mhuRxwYlai8SdLbIFfrckcAKWLQtN5CxwiKTBixJ6JYWT/c2mGARdwlr6p6qG", - "7QNBXEfZAjunUHPRWJrRg4P03ZoiPlDe5Jf4tFze/STtt6Z3K6PuI3YtVy14Zk/ww+eZDyQucInP+TJf", - "ZiwbX2aAsW2pnIWIX6SWAQ8Ulb2UuB76g7NW00ChviNrWZpUAQ3Z+AL4kDywl1VdpvPbqfOATxnoNICk", - "/1gANWkVOdhAfHHbVxWpXswaKi9GYvMZwIWw5WpgLu7Vy2MUh+/w/ymGqMvDppyAkf70HKk+HQOukpWU", - "+QkWYgrBeO5xfucw9RQ0qXExuv2TOBt1z4xiXKjnwCT4Ra+v1X98fKwpvtwyOVKLxxV4B2kUo3+ltLOc", - "b+Ed17Om810T/fgrn23vSNU1b7j3kPjfAAAA///mDrwBGwUAAA==", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_1496/output/types_test.go b/experimental/internal/codegen/test/issues/issue_1496/output/types_test.go deleted file mode 100644 index 5056b4583e..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1496/output/types_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" -) - -// TestValidIdentifiers verifies that all generated type names are valid Go identifiers. -// Issue 1496: Inline schemas in responses were generating identifiers starting with numbers. -func TestValidIdentifiers(t *testing.T) { - // If this compiles, the identifiers are valid - response := GetSomethingJSONResponse{ - Results: []GetSomething200ResponseJSON2{ - { - GetSomething200ResponseJSONAnyOf0: &GetSomething200ResponseJSONAnyOf0{ - Order: ptr("order-123"), - }, - }, - { - GetSomething200ResponseJSONAnyOf11: &GetSomething200ResponseJSONAnyOf11{ - Error: &GetSomething200ResponseJSONAnyOf12{ - Code: ptr(float32(400)), - Message: ptr("Bad request"), - }, - }, - }, - }, - } - - // Should be able to marshal - data, err := json.Marshal(response) - if err != nil { - t.Fatalf("Failed to marshal response: %v", err) - } - - t.Logf("Marshaled response: %s", string(data)) -} - -func ptr[T any](v T) *T { - return &v -} diff --git a/experimental/internal/codegen/test/issues/issue_1496/spec.yaml b/experimental/internal/codegen/test/issues/issue_1496/spec.yaml deleted file mode 100644 index f4156e97ad..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1496/spec.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# Issue 1496: Anonymous object schema with oneOf generates invalid identifier -# https://github.com/oapi-codegen/oapi-codegen/issues/1496 -# -# When an inline schema in a response uses anyOf/oneOf, the generated type name -# was starting with a number (e.g., 200_Results_Item) which is invalid in Go. -openapi: 3.0.0 -info: - title: Issue 1496 Test - version: 1.0.0 -paths: - /something: - get: - operationId: getSomething - responses: - 200: - description: "Success" - content: - application/json: - schema: - type: object - required: - - results - properties: - results: - type: array - items: - anyOf: - - type: object - properties: - order: - type: string - - type: object - properties: - error: - type: object - properties: - code: - type: number - message: - type: string diff --git a/experimental/internal/codegen/test/issues/issue_1710/doc.go b/experimental/internal/codegen/test/issues/issue_1710/doc.go deleted file mode 100644 index 3ce6436c35..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1710/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_1710 tests that fields are not lost in nested allOf oneOf structures. -// https://github.com/oapi-codegen/oapi-codegen/issues/1710 -package issue_1710 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go deleted file mode 100644 index 6efb31f3a5..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1710/output/types.gen.go +++ /dev/null @@ -1,212 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "encoding/json" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/BasePrompt -type BasePrompt struct { - Name string `json:"name" form:"name"` - Version int `json:"version" form:"version"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *BasePrompt) ApplyDefaults() { -} - -// #/components/schemas/TextPrompt -type TextPrompt struct { - Prompt string `json:"prompt" form:"prompt"` - Name string `json:"name" form:"name"` - Version int `json:"version" form:"version"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *TextPrompt) ApplyDefaults() { -} - -// #/components/schemas/ChatMessage -type ChatMessage struct { - Role string `json:"role" form:"role"` - Content string `json:"content" form:"content"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ChatMessage) ApplyDefaults() { -} - -// #/components/schemas/ChatPrompt -type ChatPrompt struct { - Prompt []ChatMessage `json:"prompt" form:"prompt"` - Name string `json:"name" form:"name"` - Version int `json:"version" form:"version"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ChatPrompt) ApplyDefaults() { -} - -// #/components/schemas/ChatPrompt/properties/prompt -type ChatPromptPrompt = []ChatMessage - -// #/components/schemas/Prompt -type Prompt struct { - PromptOneOf0 *PromptOneOf0 - PromptOneOf1 *PromptOneOf1 -} - -func (u Prompt) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.PromptOneOf0 != nil { - count++ - data, err = json.Marshal(u.PromptOneOf0) - if err != nil { - return nil, err - } - } - if u.PromptOneOf1 != nil { - count++ - data, err = json.Marshal(u.PromptOneOf1) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("Prompt: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *Prompt) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 PromptOneOf0 - if err := json.Unmarshal(data, &v0); err == nil { - u.PromptOneOf0 = &v0 - successCount++ - } - - var v1 PromptOneOf1 - if err := json.Unmarshal(data, &v1); err == nil { - u.PromptOneOf1 = &v1 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("Prompt: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *Prompt) ApplyDefaults() { - if u.PromptOneOf0 != nil { - u.PromptOneOf0.ApplyDefaults() - } - if u.PromptOneOf1 != nil { - u.PromptOneOf1.ApplyDefaults() - } -} - -// #/components/schemas/Prompt/oneOf/0 -type PromptOneOf0 struct { - Type *string `json:"type,omitempty" form:"type,omitempty"` - Prompt []ChatMessage `json:"prompt" form:"prompt"` - Name string `json:"name" form:"name"` - Version int `json:"version" form:"version"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *PromptOneOf0) ApplyDefaults() { -} - -// #/components/schemas/Prompt/oneOf/0/allOf/0/properties/type -type PromptOneOf0AllOf0Type string - -const ( - PromptOneOf0AllOf0Type_chat PromptOneOf0AllOf0Type = "chat" -) - -// #/components/schemas/Prompt/oneOf/1 -type PromptOneOf1 struct { - Type *string `json:"type,omitempty" form:"type,omitempty"` - Prompt string `json:"prompt" form:"prompt"` - Name string `json:"name" form:"name"` - Version int `json:"version" form:"version"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *PromptOneOf1) ApplyDefaults() { -} - -// #/components/schemas/Prompt/oneOf/1/allOf/0/properties/type -type PromptOneOf1AllOf0Type string - -const ( - PromptOneOf1AllOf0Type_text PromptOneOf1AllOf0Type = "text" -) - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/9SUT4/TMBDF7/kUIwWpFzZpxQEpRzhxQMuhEmc3eYmNEtt4JvtHiO+O4qYbh2132RVC", - "oqfO68zzm5+b5PSJeQTt3u+2FbUGfUO9YyFjyYIFDam+v27JWVy3WU5axHNVlp0RPR6K2g2lU95c1a5B", - "B7suzOTN5WSe5VlOXzUsjWxstza/NaKPJ7ydlGMOJtZu7Bs6gHwAI9ygKbKc9hq08cENXjZz5FvFdMDk", - "O4UvMudhlTcVvSu2xTYztnVVRiRGelTJyrQHS0Z0g8DG2Yp2sd8r0VzRj59Z7QbvLKzwNM+1xqDiV6IP", - "ivElpjjWRHLvUZE7fEMts+SD8whiwKcmIqsGLNVpjCUY2z3Ip0SP+owVdAizHvB9NAHN0ncV/ZNydorK", - "Hnfyqsx+NXQh9bkwx8FZiLed/vomoK1ok5cL53KGXC58N3Hio1byGcyqwwvDB9c/D7x2VmBfs+Jkn5Sz", - "0UPmv8pbhaDuE9UIBk7b6EmmCcPNP7m09fLxEU+tztA4c+STvZcIrrg9Ui/8CZYP7DicG4s3rJX8Fu45", - "6CmTS9yXNf97QoK7lxBaXkt/TOhXAAAA//+WRkFKuQYAAA==", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_1710/output/types_test.go b/experimental/internal/codegen/test/issues/issue_1710/output/types_test.go deleted file mode 100644 index 345ab353e9..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1710/output/types_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" -) - -// TestPromptOneOfHasPromptField verifies that the 'prompt' field is not lost -// in nested allOf/oneOf structures. -// https://github.com/oapi-codegen/oapi-codegen/issues/1710 -func TestPromptOneOfHasPromptField(t *testing.T) { - // Test ChatPrompt variant (PromptOneOf0) - prompt is []ChatMessage - chatType := "chat" - chatPrompt := PromptOneOf0{ - Type: &chatType, - Prompt: []ChatMessage{ - {Role: "user", Content: "hello"}, - }, - } - - if chatPrompt.Type == nil || *chatPrompt.Type != "chat" { - t.Error("ChatPrompt variant should have Type='chat'") - } - if len(chatPrompt.Prompt) != 1 { - t.Errorf("ChatPrompt.Prompt should have 1 message, got %d", len(chatPrompt.Prompt)) - } - if chatPrompt.Prompt[0].Role != "user" { - t.Errorf("ChatPrompt.Prompt[0].Role = %q, want %q", chatPrompt.Prompt[0].Role, "user") - } - - // Test TextPrompt variant (PromptOneOf1) - prompt is string - textType := "text" - textPrompt := PromptOneOf1{ - Type: &textType, - Prompt: "Hello, world!", - } - - if textPrompt.Type == nil || *textPrompt.Type != "text" { - t.Error("TextPrompt variant should have Type='text'") - } - if textPrompt.Prompt != "Hello, world!" { - t.Errorf("TextPrompt.Prompt = %q, want %q", textPrompt.Prompt, "Hello, world!") - } -} - -func TestPromptJSONRoundTrip(t *testing.T) { - // Test chat prompt variant - chatType := "chat" - chatVariant := Prompt{ - PromptOneOf0: &PromptOneOf0{ - Type: &chatType, - Prompt: []ChatMessage{ - {Role: "user", Content: "test message"}, - }, - }, - } - - data, err := json.Marshal(chatVariant) - if err != nil { - t.Fatalf("Marshal chat variant failed: %v", err) - } - - var decoded Prompt - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("Unmarshal chat variant failed: %v", err) - } - - if decoded.PromptOneOf0 == nil { - t.Fatal("Expected PromptOneOf0 to be set after unmarshal") - } - if len(decoded.PromptOneOf0.Prompt) != 1 { - t.Errorf("Expected 1 message, got %d", len(decoded.PromptOneOf0.Prompt)) - } -} - -func TestTextPromptHasPromptField(t *testing.T) { - // Verify TextPrompt (from allOf) has the prompt field - tp := TextPrompt{ - Prompt: "my prompt", - Name: "test", - Version: 1, - } - - if tp.Prompt != "my prompt" { - t.Errorf("TextPrompt.Prompt = %q, want %q", tp.Prompt, "my prompt") - } -} - -func TestChatPromptHasPromptField(t *testing.T) { - // Verify ChatPrompt (from allOf) has the prompt field - cp := ChatPrompt{ - Prompt: []ChatMessage{ - {Role: "assistant", Content: "hello"}, - }, - Name: "test", - Version: 1, - } - - if len(cp.Prompt) != 1 { - t.Errorf("ChatPrompt.Prompt should have 1 message, got %d", len(cp.Prompt)) - } -} diff --git a/experimental/internal/codegen/test/issues/issue_1710/spec.yaml b/experimental/internal/codegen/test/issues/issue_1710/spec.yaml deleted file mode 100644 index b1219e00d7..0000000000 --- a/experimental/internal/codegen/test/issues/issue_1710/spec.yaml +++ /dev/null @@ -1,76 +0,0 @@ -# Issue 1710: field lost in nested allOf oneOf -# https://github.com/oapi-codegen/oapi-codegen/issues/1710 -# -# When using nested allOf with oneOf, all fields should be preserved. -# The 'prompt' field was being lost. -openapi: 3.0.0 -info: - title: Issue 1710 Test - version: 1.0.0 -paths: {} -components: - schemas: - BasePrompt: - type: object - properties: - name: - type: string - version: - type: integer - required: - - name - - version - TextPrompt: - type: object - properties: - prompt: - type: string - required: - - prompt - allOf: - - $ref: '#/components/schemas/BasePrompt' - ChatMessage: - type: object - properties: - role: - type: string - content: - type: string - required: - - role - - content - ChatPrompt: - type: object - properties: - prompt: - type: array - items: - $ref: '#/components/schemas/ChatMessage' - required: - - prompt - allOf: - - $ref: '#/components/schemas/BasePrompt' - Prompt: - oneOf: - - type: object - allOf: - - type: object - properties: - type: - type: string - enum: - - chat - - $ref: '#/components/schemas/ChatPrompt' - required: - - type - - type: object - allOf: - - type: object - properties: - type: - type: string - enum: - - text - - $ref: '#/components/schemas/TextPrompt' - required: - - type diff --git a/experimental/internal/codegen/test/issues/issue_193/doc.go b/experimental/internal/codegen/test/issues/issue_193/doc.go deleted file mode 100644 index dcf3fbff8c..0000000000 --- a/experimental/internal/codegen/test/issues/issue_193/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_193 tests allOf with additionalProperties merging. -// https://github.com/oapi-codegen/oapi-codegen/issues/193 -package issue_193 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_193/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_193/output/types.gen.go deleted file mode 100644 index 33a950f497..0000000000 --- a/experimental/internal/codegen/test/issues/issue_193/output/types.gen.go +++ /dev/null @@ -1,71 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/Person -type Person struct { - Metadata string `json:"metadata" form:"metadata"` - Name *string `json:"name,omitempty" form:"name,omitempty"` - Age *float32 `json:"age,omitempty" form:"age,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Person) ApplyDefaults() { -} - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/6yQvW7jQAyE+32KAVzbsuHK21151fkV1itK4kG73FtSFwRB3j2QHP8BMZAiHTn4SM5w", - "hd+qE2F32Hv8Gsc/HV7YBoS2ZWPJYTxWKVSNSZGo9px7t8JgVtQ3Tc82TKdNlNRIKLyO0lJP+bHh+YI2", - "u8PeSaEcCnvsN9vN1nHuxDvA2EbyMFKDxoFScMB/qsqSPXYLW4IN6vH27qKkIpmy6Tx75pcSOFJVyeca", - "CHOeSwOsECUlyeiYxlav+hr2WshDTn8p2lXGl0/wsDrRHVTp38SVWn+nzTsTWWiDhTu53LY8wBf0UcWn", - "LbU6P/2WoiwhoYUidxx/Ns4zjzkk+pa/5VL/hM1TOlF1HwEAAP//7z/Hg3YCAAA=", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_193/output/types_test.go b/experimental/internal/codegen/test/issues/issue_193/output/types_test.go deleted file mode 100644 index 301c97d923..0000000000 --- a/experimental/internal/codegen/test/issues/issue_193/output/types_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" -) - -// TestAllOfWithAdditionalProperties verifies that allOf with additionalProperties: true -// merges fields correctly from multiple allOf members. -// https://github.com/oapi-codegen/oapi-codegen/issues/193 -func TestAllOfWithAdditionalProperties(t *testing.T) { - name := "John" - age := float32(30) - - person := Person{ - Metadata: "some-metadata", - Name: &name, - Age: &age, - } - - // All fields from both allOf members should be present - if person.Metadata != "some-metadata" { - t.Errorf("Metadata = %q, want %q", person.Metadata, "some-metadata") - } - if *person.Name != "John" { - t.Errorf("Name = %q, want %q", *person.Name, "John") - } - if *person.Age != 30 { - t.Errorf("Age = %v, want %v", *person.Age, 30) - } -} - -func TestPersonJSONRoundTrip(t *testing.T) { - name := "Jane" - age := float32(25) - original := Person{ - Metadata: "meta", - Name: &name, - Age: &age, - } - - data, err := json.Marshal(original) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - - var decoded Person - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("Unmarshal failed: %v", err) - } - - if decoded.Metadata != original.Metadata { - t.Errorf("Metadata mismatch: got %q, want %q", decoded.Metadata, original.Metadata) - } - if *decoded.Name != *original.Name { - t.Errorf("Name mismatch: got %q, want %q", *decoded.Name, *original.Name) - } - if *decoded.Age != *original.Age { - t.Errorf("Age mismatch: got %v, want %v", *decoded.Age, *original.Age) - } -} - -func TestMetadataIsRequired(t *testing.T) { - // Metadata is required (no omitempty), so empty struct should marshal with empty string - person := Person{} - data, err := json.Marshal(person) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - - // Should contain "metadata" even if empty - expected := `{"metadata":""}` - if string(data) != expected { - t.Errorf("Marshal result = %s, want %s", string(data), expected) - } -} diff --git a/experimental/internal/codegen/test/issues/issue_193/spec.yaml b/experimental/internal/codegen/test/issues/issue_193/spec.yaml deleted file mode 100644 index 375d227fb4..0000000000 --- a/experimental/internal/codegen/test/issues/issue_193/spec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Issue 193: AllOf with additionalProperties merging -# https://github.com/oapi-codegen/oapi-codegen/issues/193 -openapi: 3.0.0 -info: - title: test schema - version: 1.0.0 -paths: {} -components: - schemas: - Person: - allOf: - # common fields - - type: object - additionalProperties: true - required: - - metadata - properties: - metadata: - type: string - # person specific fields - - type: object - additionalProperties: true - properties: - name: - type: string - age: - type: number diff --git a/experimental/internal/codegen/test/issues/issue_2102/doc.go b/experimental/internal/codegen/test/issues/issue_2102/doc.go deleted file mode 100644 index 753b091ce1..0000000000 --- a/experimental/internal/codegen/test/issues/issue_2102/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_2102 tests that properties defined at the same level as allOf are included. -// https://github.com/oapi-codegen/oapi-codegen/issues/2102 -package issue_2102 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go deleted file mode 100644 index 43d8ca049e..0000000000 --- a/experimental/internal/codegen/test/issues/issue_2102/output/types.gen.go +++ /dev/null @@ -1,82 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/Foo -type Foo struct { - Foo string `json:"foo" form:"foo"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Foo) ApplyDefaults() { -} - -// #/components/schemas/Bar -type Bar struct { - Bar string `json:"bar" form:"bar"` - Foo string `json:"foo" form:"foo"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Bar) ApplyDefaults() { -} - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/3xSwY6bMBC98xVPS6VcmpBNbxz3sFJPvVTqNQYP2CvwuJ5hq/x9BSQ16UbLBfzmzXsz", - "z5T4LjIRTs/HUw0zDD86/PHqEBNHSupJYBRiRsJA7zRgf1dKBN8HTmSLEk41Sl1VvVc3NYeWx4pN9PuW", - "LfUU7g9+9pVqNi7KosQvRwEG0joaDZwRNKwO5+x2hgkW52XI8zyVOtpM9rUoF2QznqXOB7KwPlGrwwUc", - "1qbVRBxPg0VDGCn1ZJfNP6p0iccFW+NJ1FGi0JIcCo4UTPQ1vh2Oh2PhQ8d1AajXgepNtPhJogXwTkk8", - "hxrPCz8adTI3VI1J8xvoSdcPQKZxNOlSozHpCiWSyEFIbhzg6XQ8PuUjYEna5KMuPrl1floOSkG3bMDE", - "OPjWzPzqTTjcV3EN638U+JKoq7Erq5bHyIGCSrVypXoxaVdkfG6+lladV+aboF4i1eDmjVr9t+XvySey", - "2XOPjvl6yjeT613Wy5qiyYd+gV9u8T70e6TY5I6Hilj/hu2InwXyyrzbUD9uuOLzdf0NAAD//90rMTaT", - "AwAA", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_2102/output/types_test.go b/experimental/internal/codegen/test/issues/issue_2102/output/types_test.go deleted file mode 100644 index b4916b2c33..0000000000 --- a/experimental/internal/codegen/test/issues/issue_2102/output/types_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" -) - -// TestBarHasBothProperties verifies that Bar has both foo and bar properties. -// Issue 2102: When a schema has both properties and allOf at the same level, -// the properties were being ignored. -func TestBarHasBothProperties(t *testing.T) { - // Bar should have both foo (from allOf ref to Foo) and bar (from direct properties) - bar := Bar{ - Foo: "test-foo", - Bar: "test-bar", - } - - // Should be able to marshal/unmarshal - data, err := json.Marshal(bar) - if err != nil { - t.Fatalf("Failed to marshal Bar: %v", err) - } - - var unmarshaled Bar - if err := json.Unmarshal(data, &unmarshaled); err != nil { - t.Fatalf("Failed to unmarshal Bar: %v", err) - } - - if unmarshaled.Foo != "test-foo" { - t.Errorf("Expected Foo to be 'test-foo', got %q", unmarshaled.Foo) - } - if unmarshaled.Bar != "test-bar" { - t.Errorf("Expected Bar to be 'test-bar', got %q", unmarshaled.Bar) - } -} - -// TestBarRequiredFields verifies that bar is required (from allOf member's required array). -func TestBarRequiredFields(t *testing.T) { - // Both foo and bar should be required (no omitempty), so an empty struct - // should marshal with empty string values - bar := Bar{} - data, err := json.Marshal(bar) - if err != nil { - t.Fatalf("Failed to marshal empty Bar: %v", err) - } - - // Both fields should be present in JSON - expected := `{"bar":"","foo":""}` - if string(data) != expected { - t.Errorf("Expected %s, got %s", expected, string(data)) - } -} diff --git a/experimental/internal/codegen/test/issues/issue_2102/spec.yaml b/experimental/internal/codegen/test/issues/issue_2102/spec.yaml deleted file mode 100644 index cbafb5a6d3..0000000000 --- a/experimental/internal/codegen/test/issues/issue_2102/spec.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Issue 2102: allOf with properties at same level - properties are ignored -# https://github.com/oapi-codegen/oapi-codegen/issues/2102 -# -# When a schema has both `properties` and `allOf` at the same level, -# the properties defined directly on the schema should be merged with -# the properties from the allOf references. -openapi: 3.0.0 -info: - title: Issue 2102 Test - version: 1.0.0 -paths: - /bar: - get: - summary: bar - responses: - "200": - description: bar - content: - application/json: - schema: - $ref: '#/components/schemas/Bar' -components: - schemas: - Foo: - type: object - required: - - foo - properties: - foo: - type: string - Bar: - type: object - properties: - bar: - type: string - allOf: - - $ref: '#/components/schemas/Foo' - - required: - - bar diff --git a/experimental/internal/codegen/test/issues/issue_312/doc.go b/experimental/internal/codegen/test/issues/issue_312/doc.go deleted file mode 100644 index 0f29af76ad..0000000000 --- a/experimental/internal/codegen/test/issues/issue_312/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package issue_312 tests proper escaping of paths with special characters. -// https://github.com/oapi-codegen/oapi-codegen/issues/312 -// This tests paths with colons like /pets:validate -package issue_312 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_312/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_312/output/types.gen.go deleted file mode 100644 index f5f81dbf6e..0000000000 --- a/experimental/internal/codegen/test/issues/issue_312/output/types.gen.go +++ /dev/null @@ -1,97 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/Pet -type Pet struct { - Name string `json:"name" form:"name"` // The name of the pet. -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Pet) ApplyDefaults() { -} - -// #/components/schemas/PetNames -type PetNames struct { - Names []string `json:"names" form:"names"` // The names of the pets. -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *PetNames) ApplyDefaults() { -} - -// #/components/schemas/Error -type Error struct { - Code int32 `json:"code" form:"code"` // Error code - Message string `json:"message" form:"message"` // Error message -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Error) ApplyDefaults() { -} - -// #/paths//pets:validate/post/responses/200/content/application/json/schema -type ValidatePetsJSONResponse = []Pet - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/8SVTW/bPAzH7/oURPMAOdVOk5uOzzAMvQw5DLurMm2rtSVNpLsFw777IDmu7TpxsQ3D", - "bjEpkb8/X5QN3BN1CIe7vYSj4hqQtPLGVmIDNbMnmeeV4bp7yLRrc6e8udWuwArt/MPEOJQf7vZiA+9q", - "1E8EPjiP4SUkuBK84prgq+EayKM2qgFdq6A0YyBozBOCdo2zJJxHq7yRcHPIdtnuRhhbOikAnjGQcVbC", - "XbQLADbcoByVACOxACiQdDCe0+E1pKBajPlFoos5co9M8lk1plCM0QLgHXH/C4C6tlXhJOHz+QjEC2fn", - "LO+lAxFBRfd9IWFIchz9Ab90SPy/K05Dwt5oAhYSOHT4YtbOMloezwEo7xujU4L8kZyd+gBI19iquQ3g", - "v4ClhO0m1671zqJlyvuTlB+RP6oWafuCR95ZQhqDbPe73XYac1aDJHFagCvgb6Ffgwfgk0cJKgR1WvgM", - "Y0vLK29q3opRTKm6hq/q6yx+86gZC8AQXPhbKteA38fE22F08+8e+b740ceocDm4H5BjR6Ayz2jBFGjZ", - "lAZDdmlGK+Qj8tkz7stIeAtWtSghZZ1wGyvTyk9MV+b4suq+r8Qhvkh/On3/oi1pjkZ7vHx29XGOY2t6", - "re7hETWLV7V6VeihE+kxYzOtReqDWK3gokKfakz34mvIdXqqMjHgpdX/DUZ6A5KWlK/39yIkTSgpE6t7", - "PhOfPGlN5Ap6/D+bfLZIpKq1gscLSynGMlY4fQlKF1rFyXPYXxOZ8OYMZ4Jf7WkfacD/GQAA//+iio0s", - "6AcAAA==", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_312/output/types_test.go b/experimental/internal/codegen/test/issues/issue_312/output/types_test.go deleted file mode 100644 index 3a2818f642..0000000000 --- a/experimental/internal/codegen/test/issues/issue_312/output/types_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" -) - -// TestPathWithColon verifies that paths with colons (like /pets:validate) generate properly. -// https://github.com/oapi-codegen/oapi-codegen/issues/312 -func TestPathWithColonGeneratesTypes(t *testing.T) { - // The path /pets:validate should generate a ValidatePetsJSONResponse type - response := ValidatePetsJSONResponse{ - {Name: "Fluffy"}, - {Name: "Spot"}, - } - - if len(response) != 2 { - t.Errorf("response length = %d, want 2", len(response)) - } - if response[0].Name != "Fluffy" { - t.Errorf("response[0].Name = %q, want %q", response[0].Name, "Fluffy") - } -} - -func TestPetSchema(t *testing.T) { - pet := Pet{ - Name: "Max", - } - - data, err := json.Marshal(pet) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - - expected := `{"name":"Max"}` - if string(data) != expected { - t.Errorf("Marshal result = %s, want %s", string(data), expected) - } -} - -func TestPetNamesSchema(t *testing.T) { - petNames := PetNames{ - Names: []string{"Fluffy", "Spot", "Max"}, - } - - data, err := json.Marshal(petNames) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - - var decoded PetNames - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("Unmarshal failed: %v", err) - } - - if len(decoded.Names) != 3 { - t.Errorf("Names length = %d, want 3", len(decoded.Names)) - } -} - -func TestErrorSchema(t *testing.T) { - err := Error{ - Code: 404, - Message: "Not Found", - } - - data, _ := json.Marshal(err) - expected := `{"code":404,"message":"Not Found"}` - if string(data) != expected { - t.Errorf("Marshal result = %s, want %s", string(data), expected) - } -} diff --git a/experimental/internal/codegen/test/issues/issue_312/spec.yaml b/experimental/internal/codegen/test/issues/issue_312/spec.yaml deleted file mode 100644 index fa458ec41f..0000000000 --- a/experimental/internal/codegen/test/issues/issue_312/spec.yaml +++ /dev/null @@ -1,86 +0,0 @@ -# Issue 312: Path escaping -# https://github.com/oapi-codegen/oapi-codegen/issues/312 -# Checks proper escaping of paths with special characters like colons -openapi: "3.0.0" -info: - version: 1.0.0 - title: Issue 312 test - description: Checks proper escaping of parameters -paths: - /pets:validate: - post: - summary: Validate pets - description: Validate pets - operationId: validatePets - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/PetNames' - responses: - '200': - description: valid pets - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Pet' - default: - description: unexpected error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /pets/{petId}: - get: - summary: Get pet given identifier. - operationId: getPet - parameters: - - name: petId - in: path - required: true - schema: - type: string - responses: - '200': - description: valid pet - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' -components: - schemas: - Pet: - type: object - required: - - name - properties: - name: - type: string - description: The name of the pet. - - PetNames: - type: object - required: - - names - properties: - names: - type: array - description: The names of the pets. - items: - type: string - - Error: - required: - - code - - message - properties: - code: - type: integer - format: int32 - description: Error code - message: - type: string - description: Error message diff --git a/experimental/internal/codegen/test/issues/issue_502/doc.go b/experimental/internal/codegen/test/issues/issue_502/doc.go deleted file mode 100644 index 992436f9e7..0000000000 --- a/experimental/internal/codegen/test/issues/issue_502/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_502 tests that anyOf with only one ref generates the referenced type. -// https://github.com/oapi-codegen/oapi-codegen/issues/502 -package issue_502 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_502/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_502/output/types.gen.go deleted file mode 100644 index cda70dd5ea..0000000000 --- a/experimental/internal/codegen/test/issues/issue_502/output/types.gen.go +++ /dev/null @@ -1,228 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/OptionalClaims -type OptionalClaims struct { - IDToken *string `json:"idToken,omitempty" form:"idToken,omitempty"` - AccessToken *string `json:"accessToken,omitempty" form:"accessToken,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *OptionalClaims) ApplyDefaults() { -} - -// #/components/schemas/Application -type Application struct { - Name *string `json:"name,omitempty" form:"name,omitempty"` - OptionalClaims Nullable[ApplicationOptionalClaims] `json:"optionalClaims,omitempty" form:"optionalClaims,omitempty"` // Optional claims configuration -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Application) ApplyDefaults() { -} - -// #/components/schemas/Application/properties/optionalClaims -// Optional claims configuration -type ApplicationOptionalClaims struct { - OptionalClaims *OptionalClaims -} - -func (u ApplicationOptionalClaims) MarshalJSON() ([]byte, error) { - result := make(map[string]any) - - if u.OptionalClaims != nil { - data, err := json.Marshal(u.OptionalClaims) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err == nil { - for k, v := range m { - result[k] = v - } - } - } - - return json.Marshal(result) -} - -func (u *ApplicationOptionalClaims) UnmarshalJSON(data []byte) error { - var v0 OptionalClaims - if err := json.Unmarshal(data, &v0); err == nil { - u.OptionalClaims = &v0 - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *ApplicationOptionalClaims) ApplyDefaults() { - if u.OptionalClaims != nil { - u.OptionalClaims.ApplyDefaults() - } -} - -// Nullable is a generic type that can distinguish between: -// - Field not provided (unspecified) -// - Field explicitly set to null -// - Field has a value -// -// This is implemented as a map[bool]T where: -// - Empty map: unspecified -// - map[false]T: explicitly null -// - map[true]T: has a value -type Nullable[T any] map[bool]T - -// NewNullableWithValue creates a Nullable with the given value. -func NewNullableWithValue[T any](value T) Nullable[T] { - return Nullable[T]{true: value} -} - -// NewNullNullable creates a Nullable that is explicitly null. -func NewNullNullable[T any]() Nullable[T] { - return Nullable[T]{false: *new(T)} -} - -// Get returns the value if set, or an error if null or unspecified. -func (n Nullable[T]) Get() (T, error) { - if v, ok := n[true]; ok { - return v, nil - } - var zero T - if n.IsNull() { - return zero, ErrNullableIsNull - } - return zero, ErrNullableNotSpecified -} - -// MustGet returns the value or panics if null or unspecified. -func (n Nullable[T]) MustGet() T { - v, err := n.Get() - if err != nil { - panic(err) - } - return v -} - -// Set assigns a value. -func (n *Nullable[T]) Set(value T) { - *n = Nullable[T]{true: value} -} - -// SetNull marks the field as explicitly null. -func (n *Nullable[T]) SetNull() { - *n = Nullable[T]{false: *new(T)} -} - -// SetUnspecified clears the field (as if it was never set). -func (n *Nullable[T]) SetUnspecified() { - *n = nil -} - -// IsNull returns true if the field is explicitly null. -func (n Nullable[T]) IsNull() bool { - if n == nil { - return false - } - _, ok := n[false] - return ok -} - -// IsSpecified returns true if the field was provided (either null or a value). -func (n Nullable[T]) IsSpecified() bool { - return len(n) > 0 -} - -// MarshalJSON implements json.Marshaler. -func (n Nullable[T]) MarshalJSON() ([]byte, error) { - if n.IsNull() { - return []byte("null"), nil - } - if v, ok := n[true]; ok { - return json.Marshal(v) - } - // Unspecified - this shouldn't be called if omitempty is used correctly - return []byte("null"), nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (n *Nullable[T]) UnmarshalJSON(data []byte) error { - if string(data) == "null" { - n.SetNull() - return nil - } - var v T - if err := json.Unmarshal(data, &v); err != nil { - return err - } - n.Set(v) - return nil -} - -// ErrNullableIsNull is returned when trying to get a value from a null Nullable. -var ErrNullableIsNull = errors.New("nullable value is null") - -// ErrNullableNotSpecified is returned when trying to get a value from an unspecified Nullable. -var ErrNullableNotSpecified = errors.New("nullable value is not specified") - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/5SRQWvcMBSE7/oVAy7kkqy3KbnoVnrqaS+BnrXys/1a+T0hPbcsIf+92NnueqEQcrNH", - "M8ynUYPvtc6Ep/2jR5DToW9DSocef9hGqKQTVAiFetgpEwYSKsGogsWo9CHSy6trMJrl6tt2YBvn4y7q", - "1GrI/BC1o4Hk9oeXyto+7R9d4xr8GEneuqEFb/VjqDftVEgi3YMNddQ5dRcS18DGjadbQe8halvGndNM", - "EjJ7fNntd3vH0qt3gLEl8tcZ8EzVHPCbSmUVj8+rPQcbq8fLq4s6ZRUSq0u8xpGmsH4Ch2ysEtK3FHg6", - "a1h5PPT4k6KdpVw0UzGmiwng7ll/kVyFf8lqhWW4yCFGqvV979ecE8ewEH2QRMJE72Lof++6Ai5PuRWA", - "B3wq1HvcNe11vva8XXs7290m2VGNhddjf1kXcfUhqvQ8zGW94iYkc0rhuDyqlZnc3wAAAP//6uhqZ+MC", - "AAA=", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_502/output/types_test.go b/experimental/internal/codegen/test/issues/issue_502/output/types_test.go deleted file mode 100644 index c0f7f6bb71..0000000000 --- a/experimental/internal/codegen/test/issues/issue_502/output/types_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" -) - -// TestAnyOfWithSingleRef verifies that anyOf with a single $ref generates -// correct types that can be used. -// https://github.com/oapi-codegen/oapi-codegen/issues/502 -func TestAnyOfWithSingleRef(t *testing.T) { - // OptionalClaims should be properly generated - claims := OptionalClaims{ - IDToken: ptrTo("id-token-value"), - AccessToken: ptrTo("access-token-value"), - } - - if *claims.IDToken != "id-token-value" { - t.Errorf("IDToken = %q, want %q", *claims.IDToken, "id-token-value") - } - if *claims.AccessToken != "access-token-value" { - t.Errorf("AccessToken = %q, want %q", *claims.AccessToken, "access-token-value") - } -} - -func TestApplicationWithAnyOfProperty(t *testing.T) { - // Application.OptionalClaims is an anyOf with a single ref + nullable: true - // It should be Nullable[ApplicationOptionalClaims] - app := Application{ - Name: ptrTo("my-app"), - OptionalClaims: NewNullableWithValue(ApplicationOptionalClaims{ - OptionalClaims: &OptionalClaims{ - IDToken: ptrTo("token"), - }, - }), - } - - if *app.Name != "my-app" { - t.Errorf("Name = %q, want %q", *app.Name, "my-app") - } - if !app.OptionalClaims.IsSpecified() { - t.Fatal("OptionalClaims should be specified") - } - optClaims := app.OptionalClaims.MustGet() - if optClaims.OptionalClaims == nil { - t.Fatal("OptionalClaims.OptionalClaims should not be nil") - } - if *optClaims.OptionalClaims.IDToken != "token" { - t.Errorf("IDToken = %q, want %q", *optClaims.OptionalClaims.IDToken, "token") - } -} - -func TestApplicationJSONRoundTrip(t *testing.T) { - original := Application{ - Name: ptrTo("test-app"), - OptionalClaims: NewNullableWithValue(ApplicationOptionalClaims{ - OptionalClaims: &OptionalClaims{ - IDToken: ptrTo("id"), - AccessToken: ptrTo("access"), - }, - }), - } - - data, err := json.Marshal(original) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - - var decoded Application - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("Unmarshal failed: %v", err) - } - - if *decoded.Name != *original.Name { - t.Errorf("Name mismatch: got %q, want %q", *decoded.Name, *original.Name) - } - if !decoded.OptionalClaims.IsSpecified() { - t.Fatal("OptionalClaims should be specified after round trip") - } - optClaims := decoded.OptionalClaims.MustGet() - if optClaims.OptionalClaims == nil { - t.Fatal("OptionalClaims.OptionalClaims should not be nil after round trip") - } -} - -func TestApplicationNullOptionalClaims(t *testing.T) { - // Test with explicitly null optional claims - app := Application{ - Name: ptrTo("null-test-app"), - OptionalClaims: NewNullNullable[ApplicationOptionalClaims](), - } - - if !app.OptionalClaims.IsNull() { - t.Error("OptionalClaims should be null") - } - - // Should marshal as null - data, err := json.Marshal(app) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - t.Logf("Marshaled with null optionalClaims: %s", string(data)) -} - -func ptrTo[T any](v T) *T { - return &v -} diff --git a/experimental/internal/codegen/test/issues/issue_502/spec.yaml b/experimental/internal/codegen/test/issues/issue_502/spec.yaml deleted file mode 100644 index 04e5a324e8..0000000000 --- a/experimental/internal/codegen/test/issues/issue_502/spec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# Issue 502: anyOf/allOf with only one ref type generates interface{} -# https://github.com/oapi-codegen/oapi-codegen/issues/502 -# -# When anyOf or allOf has only one reference, it should generate -# the referenced type, not interface{}. -openapi: 3.0.0 -info: - title: Issue 502 Test - version: 1.0.0 -paths: {} -components: - schemas: - OptionalClaims: - type: object - properties: - idToken: - type: string - accessToken: - type: string - Application: - type: object - properties: - name: - type: string - optionalClaims: - anyOf: - - $ref: '#/components/schemas/OptionalClaims' - description: Optional claims configuration - nullable: true diff --git a/experimental/internal/codegen/test/issues/issue_52/doc.go b/experimental/internal/codegen/test/issues/issue_52/doc.go deleted file mode 100644 index f4646cbdb6..0000000000 --- a/experimental/internal/codegen/test/issues/issue_52/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_52 tests that recursive types are handled properly. -// https://github.com/oapi-codegen/oapi-codegen/issues/52 -package issue_52 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_52/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_52/output/types.gen.go deleted file mode 100644 index 72354dfa93..0000000000 --- a/experimental/internal/codegen/test/issues/issue_52/output/types.gen.go +++ /dev/null @@ -1,87 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/Document -type Document struct { - Fields map[string]any `json:"fields,omitempty" form:"fields,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Document) ApplyDefaults() { -} - -// #/components/schemas/Document/properties/fields -type DocumentFields = map[string]any - -// #/components/schemas/Value -type Value struct { - StringValue *string `json:"stringValue,omitempty" form:"stringValue,omitempty"` - ArrayValue *ArrayValue `json:"arrayValue,omitempty" form:"arrayValue,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Value) ApplyDefaults() { -} - -// #/components/schemas/ArrayValue -type ArrayValue = []Value - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/5xQTWvcMBC961c8sgWdarlbctEtUCihlJYeelfkWVupLQnNOHShP77Y3g/vJvQjOknz", - "3rwPbXDPPBJutxbfyI+FwxNB9pkYnYtNH2KrNuhEMltj2iDd+FD5NJjkcnjrU0MtxctHmBTZ3G7VRm3w", - "2f0g8FgI0jlBuTJxhRYjapBLylT6vUqZosvB4n1VV1sV4i5ZBTxR4ZCiha6runqnFSBBerKgn27IPSmg", - "IfYlZJl5vxSAVyXITjqePM1B2s5SLclyASaim2zum5P/R5IDWohzikx8pAN6W9f6/LyKevPl080K8ykK", - "RVnTAe1y7oOfXc0jp6gvcYB9R4O7ngJvCu0s9Mb4NOQUKQqbhcvmQ/LjQFG0OoOTwgFfxI6ko/T0dRbp", - "4ZH8sfLydxLWnXeB+obXeV5YnI5rmjDVcv3XF2T+VuG760fSM32+/mdKlhJie7F53l7A09iV4vbPqH8K", - "d3faWBLePVNYjGblwyQIDauA/9D9dwAAAP//+4PlsMkDAAA=", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_52/output/types_test.go b/experimental/internal/codegen/test/issues/issue_52/output/types_test.go deleted file mode 100644 index 7bb4f39267..0000000000 --- a/experimental/internal/codegen/test/issues/issue_52/output/types_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" -) - -// TestRecursiveTypes verifies that recursive type definitions work correctly. -// https://github.com/oapi-codegen/oapi-codegen/issues/52 -func TestRecursiveTypes(t *testing.T) { - // Value references ArrayValue which is []Value - recursive - str := "test" - val := Value{ - StringValue: &str, - ArrayValue: &ArrayValue{ - {StringValue: &str}, - }, - } - - if *val.StringValue != "test" { - t.Errorf("StringValue = %q, want %q", *val.StringValue, "test") - } - if len(*val.ArrayValue) != 1 { - t.Errorf("ArrayValue length = %d, want 1", len(*val.ArrayValue)) - } -} - -func TestRecursiveJSONRoundTrip(t *testing.T) { - str := "test" - nested := "nested" - original := Value{ - StringValue: &str, - ArrayValue: &ArrayValue{ - {StringValue: &nested}, - }, - } - - data, err := json.Marshal(original) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - - var decoded Value - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("Unmarshal failed: %v", err) - } - - if *decoded.StringValue != *original.StringValue { - t.Errorf("StringValue mismatch: got %q, want %q", *decoded.StringValue, *original.StringValue) - } -} - -func TestDocumentWithRecursiveFields(t *testing.T) { - // Document.Fields is map[string]any (due to additionalProperties: $ref Value) - doc := Document{ - Fields: map[string]any{ - "key1": "value1", - }, - } - - if doc.Fields["key1"] != "value1" { - t.Errorf("Fields[key1] = %v, want %q", doc.Fields["key1"], "value1") - } -} diff --git a/experimental/internal/codegen/test/issues/issue_52/spec.yaml b/experimental/internal/codegen/test/issues/issue_52/spec.yaml deleted file mode 100644 index a29ee7a7be..0000000000 --- a/experimental/internal/codegen/test/issues/issue_52/spec.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Issue 52: Recursive types handling -# https://github.com/oapi-codegen/oapi-codegen/issues/52 -# -# Make sure that recursive types are handled properly -openapi: 3.0.2 -info: - version: '0.0.1' - title: example - description: | - Make sure that recursive types are handled properly -paths: - /example: - get: - operationId: exampleGet - responses: - '200': - description: "OK" - content: - 'application/json': - schema: - $ref: '#/components/schemas/Document' -components: - schemas: - Document: - type: object - properties: - fields: - type: object - additionalProperties: - $ref: '#/components/schemas/Value' - Value: - type: object - properties: - stringValue: - type: string - arrayValue: - $ref: '#/components/schemas/ArrayValue' - ArrayValue: - type: array - items: - $ref: '#/components/schemas/Value' diff --git a/experimental/internal/codegen/test/issues/issue_579/doc.go b/experimental/internal/codegen/test/issues/issue_579/doc.go deleted file mode 100644 index 10d90fa7f5..0000000000 --- a/experimental/internal/codegen/test/issues/issue_579/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_579 tests aliased types with date format. -// https://github.com/oapi-codegen/oapi-codegen/issues/579 -package issue_579 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_579/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_579/output/types.gen.go deleted file mode 100644 index 071c05c0f4..0000000000 --- a/experimental/internal/codegen/test/issues/issue_579/output/types.gen.go +++ /dev/null @@ -1,110 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "encoding/json" - "fmt" - "strings" - "sync" - "time" -) - -// #/components/schemas/Pet -type Pet struct { - Born *any `json:"born,omitempty" form:"born,omitempty"` - BornAt *Date `json:"born_at,omitempty" form:"born_at,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Pet) ApplyDefaults() { -} - -const DateFormat = "2006-01-02" - -type Date struct { - time.Time -} - -func (d Date) MarshalJSON() ([]byte, error) { - return json.Marshal(d.Format(DateFormat)) -} - -func (d *Date) UnmarshalJSON(data []byte) error { - var dateStr string - err := json.Unmarshal(data, &dateStr) - if err != nil { - return err - } - parsed, err := time.Parse(DateFormat, dateStr) - if err != nil { - return err - } - d.Time = parsed - return nil -} - -func (d Date) String() string { - return d.Format(DateFormat) -} - -func (d *Date) UnmarshalText(data []byte) error { - parsed, err := time.Parse(DateFormat, string(data)) - if err != nil { - return err - } - d.Time = parsed - return nil -} - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/2SSz86bMBDE7zzFiPTamLaKovhWqZfc8gaVYzbgCryWd9M/b18Z+ET8fZzY0c74N+AD", - "riJPwul8sfg+BSfUQ/8lEvwJOqJ3Snhwnp02B4yqSawxQ9DxeT96ng27FD577mmgWA+hBIs5nS8NJ4ou", - "BYv227E7dm0T4oNtA/ymLIGjxZeiN4AGncjuUFASbZLTUcq+SZPzNPLUUy4zMJCuLwAnyk4Dx2tvi37b", - "d7eNTJI4CsmbBfjadfsA9CQ+h6QLVPoQUB7PUSnqqwtQ+qsFLsRaB8SPNLv3KpaPbCGaQxwaz3PiSFEX", - "stWyQd72gquF77/I6yalXFpreK1051xRfMr0sGgPZj/FbEeY7Zf/cEptFfDTVQ0r2l1eb4Zdrskiv+TV", - "1JW1sv0PAAD//3OxuKeDAgAA", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_579/output/types_test.go b/experimental/internal/codegen/test/issues/issue_579/output/types_test.go deleted file mode 100644 index 895928b075..0000000000 --- a/experimental/internal/codegen/test/issues/issue_579/output/types_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" - "time" -) - -// TestAliasedDateType verifies that date format types work correctly. -// https://github.com/oapi-codegen/oapi-codegen/issues/579 -func TestDateType(t *testing.T) { - // Direct date type should use Date - date := Date{Time: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)} - - data, err := json.Marshal(date) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - - if string(data) != `"2024-01-15"` { - t.Errorf("Marshal result = %s, want %q", string(data), "2024-01-15") - } - - var decoded Date - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("Unmarshal failed: %v", err) - } - - if !decoded.Equal(date.Time) { - t.Errorf("Unmarshal result = %v, want %v", decoded.Time, date.Time) - } -} - -func TestPetWithDateFields(t *testing.T) { - // Pet has born_at as *Date (direct format: date) - date := Date{Time: time.Date(2020, 6, 15, 0, 0, 0, 0, time.UTC)} - pet := Pet{ - BornAt: &date, - } - - if pet.BornAt == nil { - t.Fatal("BornAt should not be nil") - } - if pet.BornAt.String() != "2020-06-15" { - t.Errorf("BornAt = %q, want %q", pet.BornAt.String(), "2020-06-15") - } -} - -// Note: The current implementation generates Born as *any instead of the ideal -// AliasedDate type. This is a known limitation with $ref to type aliases. -func TestPetBornFieldExists(t *testing.T) { - // Just verify the field exists and can hold a value - pet := Pet{ - Born: ptrTo[any]("2020-06-15"), - } - - if pet.Born == nil { - t.Fatal("Born should not be nil") - } -} - -func ptrTo[T any](v T) *T { - return &v -} diff --git a/experimental/internal/codegen/test/issues/issue_579/spec.yaml b/experimental/internal/codegen/test/issues/issue_579/spec.yaml deleted file mode 100644 index 8baf490045..0000000000 --- a/experimental/internal/codegen/test/issues/issue_579/spec.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# Issue 579: Aliased types with date format -# https://github.com/oapi-codegen/oapi-codegen/issues/579 -openapi: "3.0.0" -info: - version: 1.0.0 - title: Issue 579 test -paths: - /placeholder: - get: - operationId: getPlaceholder - responses: - 200: - description: placeholder - content: - text/plain: - schema: - type: string -components: - schemas: - Pet: - type: object - properties: - born: - $ref: "#/components/schemas/AliasedDate" - born_at: - type: string - format: date - AliasedDate: - type: string - format: date diff --git a/experimental/internal/codegen/test/issues/issue_697/doc.go b/experimental/internal/codegen/test/issues/issue_697/doc.go deleted file mode 100644 index 3b3d314ae0..0000000000 --- a/experimental/internal/codegen/test/issues/issue_697/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_697 tests that properties alongside allOf are included. -// https://github.com/oapi-codegen/oapi-codegen/issues/697 -package issue_697 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_697/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_697/output/types.gen.go deleted file mode 100644 index 1a342a9e92..0000000000 --- a/experimental/internal/codegen/test/issues/issue_697/output/types.gen.go +++ /dev/null @@ -1,81 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/YBase -type YBase struct { - BaseField *string `json:"baseField,omitempty" form:"baseField,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *YBase) ApplyDefaults() { -} - -// #/components/schemas/X -type X struct { - A *string `json:"a,omitempty" form:"a,omitempty"` - B *int `json:"b,omitempty" form:"b,omitempty"` - BaseField *string `json:"baseField,omitempty" form:"baseField,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *X) ApplyDefaults() { -} - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/3yQwWrcMBCG73qKH1zIpbE3KSRExx4KPbWHQNvj2Jq1psiS0MwGSum7lyhZ3NKyt/Hv", - "bzTfzICPqifG3cO9x+dWKjcTVmSmBkrp0xHUGLLm0ji4AdGsqp+mVSye5nEp21SoyvVSAq+c//6Q57d1", - "unu4d4Mb8CVyBkGXyBshkmIuFs9jckDdBchgkaG0MRI/cXrrhp78wWgspxQwMyQv6RQ4QHKHVs7cyDjA", - "flQe3YDHKApRqGySqMEKuh1ubw63oyuVM1XxeDcexoOTfCzeASaW2O83wiOrOeCJm0rJHjcdr2RRPX7+", - "ckvZasmcTZ/bXzbtJfDtPSm/lOhaHmX+zou9RvtiZwiYSfmDcAp7dO5Va5LXHn89/+2X3NFrvGl89Lga", - "pt1repWaus/VheF0cWjX+5eQbLxy+9+avwMAAP//FjslWWwCAAA=", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_697/output/types_test.go b/experimental/internal/codegen/test/issues/issue_697/output/types_test.go deleted file mode 100644 index f3d5263a93..0000000000 --- a/experimental/internal/codegen/test/issues/issue_697/output/types_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" -) - -// TestXHasAllFields verifies that schema X has properties from both its own -// definition AND from the allOf reference to YBase. -// https://github.com/oapi-codegen/oapi-codegen/issues/697 -func TestXHasAllFields(t *testing.T) { - a := "a-value" - b := 42 - baseField := "base-value" - - x := X{ - A: &a, - B: &b, - BaseField: &baseField, - } - - // Verify all fields are accessible - if *x.A != "a-value" { - t.Errorf("X.A = %q, want %q", *x.A, "a-value") - } - if *x.B != 42 { - t.Errorf("X.B = %d, want %d", *x.B, 42) - } - if *x.BaseField != "base-value" { - t.Errorf("X.BaseField = %q, want %q", *x.BaseField, "base-value") - } -} - -func TestXJSONRoundTrip(t *testing.T) { - a := "a-value" - b := 42 - baseField := "base-value" - - original := X{ - A: &a, - B: &b, - BaseField: &baseField, - } - - data, err := json.Marshal(original) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - - var decoded X - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("Unmarshal failed: %v", err) - } - - if *decoded.A != *original.A || *decoded.B != *original.B || *decoded.BaseField != *original.BaseField { - t.Errorf("Round trip failed: got %+v, want %+v", decoded, original) - } -} diff --git a/experimental/internal/codegen/test/issues/issue_697/spec.yaml b/experimental/internal/codegen/test/issues/issue_697/spec.yaml deleted file mode 100644 index 314044fc9b..0000000000 --- a/experimental/internal/codegen/test/issues/issue_697/spec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Issue 697: Properties near allOf are ignored -# https://github.com/oapi-codegen/oapi-codegen/issues/697 -# -# When a schema has both allOf and properties at the same level, -# the properties should be included in the generated type. -# This is similar to issue 2102. -openapi: 3.0.0 -info: - title: Issue 697 Test - version: 1.0.0 -paths: {} -components: - schemas: - YBase: - type: object - properties: - baseField: - type: string - X: - allOf: - - $ref: '#/components/schemas/YBase' - properties: - a: - type: string - b: - type: integer - type: object diff --git a/experimental/internal/codegen/test/issues/issue_775/doc.go b/experimental/internal/codegen/test/issues/issue_775/doc.go deleted file mode 100644 index f97f08fec2..0000000000 --- a/experimental/internal/codegen/test/issues/issue_775/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_775 tests that allOf with format specification works correctly. -// https://github.com/oapi-codegen/oapi-codegen/issues/775 -package issue_775 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_775/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_775/output/types.gen.go deleted file mode 100644 index 1cd65cb493..0000000000 --- a/experimental/internal/codegen/test/issues/issue_775/output/types.gen.go +++ /dev/null @@ -1,86 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/TestObject -type TestObject struct { - UUIDProperty *TestObjectUUIDProperty `json:"uuidProperty,omitempty" form:"uuidProperty,omitempty"` - DateProperty *TestObjectDateProperty `json:"dateProperty,omitempty" form:"dateProperty,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *TestObject) ApplyDefaults() { -} - -// #/components/schemas/TestObject/properties/uuidProperty -type TestObjectUUIDProperty struct { -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *TestObjectUUIDProperty) ApplyDefaults() { -} - -// #/components/schemas/TestObject/properties/dateProperty -type TestObjectDateProperty struct { -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *TestObjectDateProperty) ApplyDefaults() { -} - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/6xQQW7bMBC88xUD61hYclEYAviDntxD+wBKWkvbSFyCu4phBPl7IFmOnXtunOHM7OwW", - "+K06E+r66BHG8XTGD5wlT8GQAyspdm2IiGKYKPcEjq1MKRg3I21K3YFyluwKDGZJfVX1bMPclK1MlYTE", - "+1Y66il+BbyM1qquj65wBf4px34rYYLQdQj3LgtGE5Rg10TQQeaxw0XyCy5sg8x2q6Clk0QxJPb4VR7K", - "g+N4Fu8AYxvJP9bFX1JzwCtlZYkeP1d5Cjaox9u7W9aUSNF0sWs70BTWJ1brqflPrd0w1lYesnIblbIk", - "ysakdxEwz9z9ufHXB3u3q2WO/RO93uJZB+y3i/g16/OrC0bfFbxkuY8AAAD//xKKQGYZAgAA", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_775/output/types_test.go b/experimental/internal/codegen/test/issues/issue_775/output/types_test.go deleted file mode 100644 index c2efe59909..0000000000 --- a/experimental/internal/codegen/test/issues/issue_775/output/types_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package output - -import ( - "testing" -) - -// TestAllOfWithFormatCompiles verifies that using allOf to add format -// specifications doesn't cause generation errors. -// https://github.com/oapi-codegen/oapi-codegen/issues/775 -// -// Note: The current implementation generates empty struct types for these -// properties instead of the ideal Go types (uuid.UUID for format:uuid, -// time.Time for format:date). This is a known limitation. -func TestAllOfWithFormatCompiles(t *testing.T) { - // The fact that this compiles proves the original issue is fixed - // (generation no longer errors on allOf + format) - obj := TestObject{ - UUIDProperty: &TestObjectUUIDProperty{}, - DateProperty: &TestObjectDateProperty{}, - } - - // Access the fields to ensure they exist - _ = obj.UUIDProperty - _ = obj.DateProperty -} - -// TestIdealBehavior documents the expected ideal behavior. -// Currently this would require changes to handle format-only allOf schemas. -func TestIdealBehavior(t *testing.T) { - t.Skip("TODO: allOf with format-only schemas should produce proper Go types (uuid.UUID, time.Time)") - - // Ideal behavior would be: - // type TestObject struct { - // UUIDProperty *uuid.UUID `json:"uuidProperty,omitempty"` - // DateProperty *time.Time `json:"dateProperty,omitempty"` - // } -} diff --git a/experimental/internal/codegen/test/issues/issue_775/spec.yaml b/experimental/internal/codegen/test/issues/issue_775/spec.yaml deleted file mode 100644 index 353485dd5b..0000000000 --- a/experimental/internal/codegen/test/issues/issue_775/spec.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Issue 775: allOf + format raises "can not merge incompatible formats" error -# https://github.com/oapi-codegen/oapi-codegen/issues/775 -# -# Using allOf to add a format to a base type should work without errors. -openapi: 3.0.0 -info: - title: Issue 775 Test - version: 1.0.0 -paths: {} -components: - schemas: - TestObject: - type: object - properties: - uuidProperty: - type: string - allOf: - - format: uuid - dateProperty: - type: string - allOf: - - format: date diff --git a/experimental/internal/codegen/test/issues/issue_832/doc.go b/experimental/internal/codegen/test/issues/issue_832/doc.go deleted file mode 100644 index a1e8c49610..0000000000 --- a/experimental/internal/codegen/test/issues/issue_832/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_832 tests x-go-type-name override for enum types. -// https://github.com/oapi-codegen/oapi-codegen/issues/832 -package issue_832 - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_832/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_832/output/types.gen.go deleted file mode 100644 index d9dd314469..0000000000 --- a/experimental/internal/codegen/test/issues/issue_832/output/types.gen.go +++ /dev/null @@ -1,91 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/Document -type Document struct { - Name *string `json:"name,omitempty" form:"name,omitempty"` - Status *string `json:"status,omitempty" form:"status,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *Document) ApplyDefaults() { -} - -// #/components/schemas/Document/properties/status -type Document_Status string - -const ( - Document_Status_one Document_Status = "one" - Document_Status_two Document_Status = "two" - Document_Status_three Document_Status = "three" - Document_Status_four Document_Status = "four" -) - -// #/components/schemas/DocumentStatus -type DocumentStatus struct { - Value *string `json:"value,omitempty" form:"value,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *DocumentStatus) ApplyDefaults() { -} - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/9RSPYvbQBDt9Sse54AaW1Lk5tg6EI4UKS5dCGFvPZb2sHaWnZFzgfz4IMkfkiHE7XWa", - "maf3wb4VnkR6wuO2NnjbNLzR35E2wXaEPSdQ6DsMK8lWaFWjmLJsvLb9S+G4K9lGv3G8o4bCcvADr5SP", - "2zrjSMFGb7AtqqLOfNizyYAjJfEcDPKqqIqPeQao1wMZ0Jvt4oEyYEfiko864v5kAPCNRG+t8pFS8ruZ", - "55g4UlJPkkWrrQyC5YnXjDwN6fQBDFA7aDztLuKfSU/XRBI5CMkZDuR1VeXX8cbnw9cvD7Ob46AUdA4H", - "chvjwbtRtXwVDvnyDohrqbO3W+BDor1Bviodd5EDBZVywkr5iV3fUdB8lra+N279fvM+q9Ve8uwKGXhO", - "qInyDD0LDOUx4JdXcueHvnbm6mKo19zT9Jto8qG5rGWUn8OW/TQX9Z+T0/8RYiyxwXcOtIb+4jW0TURr", - "7LlPPxaBnhfid8Y62kP/71x/AwAA//+qyWhEFgQAAA==", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_832/output/types_test.go b/experimental/internal/codegen/test/issues/issue_832/output/types_test.go deleted file mode 100644 index 8537eb4648..0000000000 --- a/experimental/internal/codegen/test/issues/issue_832/output/types_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" -) - -// TestEnumTypeGeneration verifies that enum types in properties are generated. -// https://github.com/oapi-codegen/oapi-codegen/issues/832 -// -// Note: The x-go-type-name extension is not currently supported. The enum type -// is generated with a name derived from the property path rather than the -// specified x-go-type-name. -func TestEnumTypeGeneration(t *testing.T) { - // Enum constants should exist - _ = Document_Status_one - _ = Document_Status_two - _ = Document_Status_three - _ = Document_Status_four - - if string(Document_Status_one) != "one" { - t.Errorf("one = %q, want %q", Document_Status_one, "one") - } -} - -func TestDocumentWithStatus(t *testing.T) { - name := "test" - status := "one" - doc := Document{ - Name: &name, - Status: &status, - } - - data, err := json.Marshal(doc) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - - var decoded Document - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("Unmarshal failed: %v", err) - } - - if *decoded.Name != *doc.Name { - t.Errorf("Name = %q, want %q", *decoded.Name, *doc.Name) - } - if *decoded.Status != *doc.Status { - t.Errorf("Status = %q, want %q", *decoded.Status, *doc.Status) - } -} - -func TestDocumentStatusSchema(t *testing.T) { - // There's also a DocumentStatus schema (separate from the enum property) - value := "test-value" - ds := DocumentStatus{ - Value: &value, - } - - if *ds.Value != "test-value" { - t.Errorf("Value = %q, want %q", *ds.Value, "test-value") - } -} diff --git a/experimental/internal/codegen/test/issues/issue_832/spec.yaml b/experimental/internal/codegen/test/issues/issue_832/spec.yaml deleted file mode 100644 index fe2e387fdb..0000000000 --- a/experimental/internal/codegen/test/issues/issue_832/spec.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Issue 832: x-go-type-name for enum types -# https://github.com/oapi-codegen/oapi-codegen/issues/832 -openapi: 3.0.2 -info: - version: '0.0.1' - title: example - description: | - Test x-go-type-name override for enum properties -paths: - /example: - get: - operationId: exampleGet - responses: - '200': - description: "OK" - content: - 'application/json': - schema: - $ref: '#/components/schemas/Document' - /example2: - get: - operationId: exampleGet2 - responses: - '200': - description: "OK" - content: - 'application/json': - schema: - $ref: '#/components/schemas/DocumentStatus' -components: - schemas: - Document: - type: object - properties: - name: - type: string - status: - x-go-type-name: Document_Status - type: string - enum: [one, two, three, four] - DocumentStatus: - type: object - properties: - value: - type: string diff --git a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/doc.go b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/doc.go deleted file mode 100644 index db5bcd0016..0000000000 --- a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_head_digit_operation_id tests operation IDs starting with digits. -// https://github.com/oapi-codegen/oapi-codegen/issues/head-digit-of-operation-id -package issue_head_digit_operation_id - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go deleted file mode 100644 index 3bbe71257a..0000000000 --- a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types.gen.go +++ /dev/null @@ -1,69 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "fmt" - "strings" - "sync" -) - -// #/paths//3gpp/foo/get/responses/200/content/application/json/schema -type N3GPPFooJSONResponse struct { - Value *string `json:"value,omitempty" form:"value,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *N3GPPFooJSONResponse) ApplyDefaults() { -} - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/2yPsXLqMBBFe33FHdMbP+hUM48wKaDIDyj2Ii9xtBrtQiZ/nzGBsUmiStK5ujq7wE71", - "TB77TCUYS8JuA7VQjFPEB1uPjiObW+CF1BTWB4PM0oqB3wjVens4/BepECmNlHAJA3fYCrijZHxkKuok", - "UwqZPdZ1U68cp6N4B1yoKEvyqJq6qf9VDjC2gTyeKHTYjA6PkqOOy8F6Hd8v1zHn5VGuZUAk+95gct11", - "HjfJGyqkWZKS3rPAqmmmA9CRtoWzXdX2zzPSSjJKNg8DIeeB2+tvy5NKeqSAtj29h5+3gH1m8pDXE7X2", - "C+YyjmA815zWJQxn+gvcW9UKp+i+AgAA//9y+0ZQ6gEAAA==", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types_test.go b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types_test.go deleted file mode 100644 index df0bc5f952..0000000000 --- a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/output/types_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" -) - -// TestOperationIdStartingWithDigit verifies that operation IDs starting with -// digits generate valid Go identifiers with an N prefix. -func TestOperationIdStartingWithDigit(t *testing.T) { - // The operationId is "3GPPFoo" which should generate N3GPPFooJSONResponse - // (N prefix added to make it a valid Go identifier) - value := "test" - response := N3GPPFooJSONResponse{ - Value: &value, - } - - if *response.Value != "test" { - t.Errorf("Value = %q, want %q", *response.Value, "test") - } -} - -func TestN3GPPFooJSONRoundTrip(t *testing.T) { - value := "test-value" - original := N3GPPFooJSONResponse{ - Value: &value, - } - - data, err := json.Marshal(original) - if err != nil { - t.Fatalf("Marshal failed: %v", err) - } - - var decoded N3GPPFooJSONResponse - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("Unmarshal failed: %v", err) - } - - if *decoded.Value != *original.Value { - t.Errorf("Value = %q, want %q", *decoded.Value, *original.Value) - } -} - -// TestTypeNameIsValid ensures the type name is a valid Go identifier -func TestTypeNameIsValid(t *testing.T) { - // This test passes if it compiles - the type N3GPPFooJSONResponse - // must be a valid Go identifier - var _ N3GPPFooJSONResponse - var _ N3GPPFooJSONResponse -} diff --git a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/spec.yaml b/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/spec.yaml deleted file mode 100644 index 5bcd0f7d70..0000000000 --- a/experimental/internal/codegen/test/issues/issue_head_digit_operation_id/spec.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Issue: Operation ID starting with digit -# Tests that operation IDs like "3GPPFoo" generate valid Go identifiers -openapi: 3.0.2 -info: - version: "0.0.1" - title: Head Digit Operation ID Test -paths: - /3gpp/foo: - get: - operationId: 3GPPFoo - responses: - 200: - description: OK - content: - application/json: - schema: - type: object - properties: - value: - type: string diff --git a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/doc.go b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/doc.go deleted file mode 100644 index e40f41cd2e..0000000000 --- a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package issue_illegal_enum_names tests enum constant generation with edge cases. -// This tests various edge cases like empty strings, spaces, hyphens, leading digits, etc. -package issue_illegal_enum_names - -//go:generate go run ../../../../../cmd/oapi-codegen -package output -output output/types.gen.go spec.yaml diff --git a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go deleted file mode 100644 index ac2d373dde..0000000000 --- a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types.gen.go +++ /dev/null @@ -1,80 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/Bar -type Bar string - -const ( - Bar_Value Bar = "" - Bar_Foo Bar = "Foo" - Bar_Bar Bar = "Bar" - Bar_Foo_Bar Bar = "Foo Bar" - Bar_Foo_Bar_1 Bar = "Foo-Bar" - Bar_Foo_1 Bar = "1Foo" - Bar__Foo Bar = " Foo" - Bar__Foo_ Bar = " Foo " - Bar__Foo__1 Bar = "_Foo_" - Bar_Value_1 Bar = "1" -) - -// #/paths//foo/get/responses/200/content/application/json/schema -type GetFooJSONResponse = []Bar - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/2SRQYujQBCF7/0rHpMFT6PO7M1jYAfCwu5l76ExFdOLVjVdlSz590urQU1u1veq3ie6", - "w0H1Sg0OfU+d70F8HcB+IHU7/CE1nVArrObZ0BFT8haE8S/YBTefglwVdOoIrVdSJ5HYx9Dge1mXn84F", - "PkvjAAvWr1Q/cu+vrBpFDrhR0iDcoC7rsnYuertovqzOMjYAHdn0AEicX+RwajL/EpmTRBqFlfSxCnzW", - "9TIAJ9I2hWij7ffPVdIKG7GtlwEfYx/aUVb9VeFtCmh7ocE/U8DukRr4lPz9JQtGg76eAN8SnRsUu6qV", - "IQoTm1aTQKu9T4VzS5Dv52yq2vv06JzkailwN6P8KxflO4piNSzfL097n7bZK3nfko9tQZFXimeANTl+", - "iRxX89vHm/sfAAD//5W/OQySAgAA", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types_test.go b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types_test.go deleted file mode 100644 index 65a49f009f..0000000000 --- a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/output/types_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package output - -import ( - "testing" -) - -// TestIllegalEnumNames verifies that enum constants with various edge case values -// are generated with valid Go identifiers. -func TestIllegalEnumNames(t *testing.T) { - // All these enum constants should exist and have valid Go names - tests := []struct { - name string - constant Bar - value string - }{ - {"empty string", Bar_Value, ""}, - {"Foo", Bar_Foo, "Foo"}, - {"Bar", Bar_Bar, "Bar"}, - {"Foo Bar (with space)", Bar_Foo_Bar, "Foo Bar"}, - {"Foo-Bar (with hyphen)", Bar_Foo_Bar_1, "Foo-Bar"}, - {"1Foo (leading digit)", Bar_Foo_1, "1Foo"}, - {" Foo (leading space)", Bar__Foo, " Foo"}, - {" Foo (leading and trailing space)", Bar__Foo_, " Foo "}, - {"_Foo_ (underscores)", Bar__Foo__1, "_Foo_"}, - {"1 (just digit)", Bar_Value_1, "1"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if string(tt.constant) != tt.value { - t.Errorf("constant %q = %q, want %q", tt.name, tt.constant, tt.value) - } - }) - } -} - -func TestBarCanBeUsedInSlice(t *testing.T) { - // The response type is []Bar - response := GetFooJSONResponse{ - Bar_Foo, - Bar_Bar, - Bar_Value, // empty string - } - - if len(response) != 3 { - t.Errorf("response length = %d, want 3", len(response)) - } - if response[0] != "Foo" { - t.Errorf("response[0] = %q, want %q", response[0], "Foo") - } -} diff --git a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/spec.yaml b/experimental/internal/codegen/test/issues/issue_illegal_enum_names/spec.yaml deleted file mode 100644 index 5d80f246cf..0000000000 --- a/experimental/internal/codegen/test/issues/issue_illegal_enum_names/spec.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# Issue: Illegal enum names -# Tests enum constant generation with various edge cases -openapi: 3.0.2 - -info: - title: Illegal Enum Names Test - version: 0.0.0 - -paths: - /foo: - get: - operationId: getFoo - responses: - 200: - description: OK - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Bar' - -components: - schemas: - Bar: - type: string - enum: - - '' - - Foo - - Bar - - Foo Bar - - Foo-Bar - - 1Foo - - ' Foo' - - ' Foo ' - - _Foo_ - - "1" diff --git a/experimental/internal/codegen/test/nested_aggregate/doc.go b/experimental/internal/codegen/test/nested_aggregate/doc.go deleted file mode 100644 index 6d934ad0d9..0000000000 --- a/experimental/internal/codegen/test/nested_aggregate/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -package nested_aggregate - -//go:generate go run ../../../../cmd/oapi-codegen -package output -output output/nested_aggregate.gen.go nested_aggregate.yaml diff --git a/experimental/internal/codegen/test/nested_aggregate/nested_aggregate.yaml b/experimental/internal/codegen/test/nested_aggregate/nested_aggregate.yaml deleted file mode 100644 index 7a9c081ec2..0000000000 --- a/experimental/internal/codegen/test/nested_aggregate/nested_aggregate.yaml +++ /dev/null @@ -1,62 +0,0 @@ -openapi: "3.1.0" -info: - title: Nested Aggregate Test - version: "1.0" -paths: {} -components: - schemas: - # Case 1: Array with anyOf items - ArrayOfAnyOf: - type: array - items: - anyOf: - - type: string - - type: object - properties: - id: - type: integer - - # Case 2: Object with anyOf property - ObjectWithAnyOfProperty: - type: object - properties: - value: - anyOf: - - type: string - - type: integer - - # Case 3: Object with oneOf property containing inline objects - ObjectWithOneOfProperty: - type: object - properties: - variant: - oneOf: - - type: object - properties: - kind: - type: string - name: - type: string - - type: object - properties: - kind: - type: string - count: - type: integer - - # Case 4: allOf containing oneOf - AllOfWithOneOf: - allOf: - - type: object - properties: - base: - type: string - - oneOf: - - type: object - properties: - optionA: - type: boolean - - type: object - properties: - optionB: - type: integer diff --git a/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go b/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go deleted file mode 100644 index 8e9cb4c44b..0000000000 --- a/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate.gen.go +++ /dev/null @@ -1,400 +0,0 @@ -// Code generated by oapi-codegen; DO NOT EDIT. - -package output - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "encoding/json" - "fmt" - "strings" - "sync" -) - -// #/components/schemas/ArrayOfAnyOf -type ArrayOfAnyOf = []ArrayOfAnyOfItem - -// #/components/schemas/ArrayOfAnyOf/items -type ArrayOfAnyOfItem struct { - String0 *string - ArrayOfAnyOfAnyOf1 *ArrayOfAnyOfAnyOf1 -} - -func (u ArrayOfAnyOfItem) MarshalJSON() ([]byte, error) { - result := make(map[string]any) - - if u.String0 != nil { - return json.Marshal(u.String0) - } - if u.ArrayOfAnyOfAnyOf1 != nil { - data, err := json.Marshal(u.ArrayOfAnyOfAnyOf1) - if err != nil { - return nil, err - } - var m map[string]any - if err := json.Unmarshal(data, &m); err == nil { - for k, v := range m { - result[k] = v - } - } - } - - return json.Marshal(result) -} - -func (u *ArrayOfAnyOfItem) UnmarshalJSON(data []byte) error { - var v0 string - if err := json.Unmarshal(data, &v0); err == nil { - u.String0 = &v0 - } - - var v1 ArrayOfAnyOfAnyOf1 - if err := json.Unmarshal(data, &v1); err == nil { - u.ArrayOfAnyOfAnyOf1 = &v1 - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *ArrayOfAnyOfItem) ApplyDefaults() { - if u.ArrayOfAnyOfAnyOf1 != nil { - u.ArrayOfAnyOfAnyOf1.ApplyDefaults() - } -} - -// #/components/schemas/ArrayOfAnyOf/items/anyOf/1 -type ArrayOfAnyOfAnyOf1 struct { - ID *int `json:"id,omitempty" form:"id,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ArrayOfAnyOfAnyOf1) ApplyDefaults() { -} - -// #/components/schemas/ObjectWithAnyOfProperty -type ObjectWithAnyOfProperty struct { - Value *ObjectWithAnyOfPropertyValue `json:"value,omitempty" form:"value,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ObjectWithAnyOfProperty) ApplyDefaults() { -} - -// #/components/schemas/ObjectWithAnyOfProperty/properties/value -type ObjectWithAnyOfPropertyValue struct { - String0 *string - Int1 *int -} - -func (u ObjectWithAnyOfPropertyValue) MarshalJSON() ([]byte, error) { - if u.String0 != nil { - return json.Marshal(u.String0) - } - if u.Int1 != nil { - return json.Marshal(u.Int1) - } - return []byte("null"), nil -} - -func (u *ObjectWithAnyOfPropertyValue) UnmarshalJSON(data []byte) error { - var v0 string - if err := json.Unmarshal(data, &v0); err == nil { - u.String0 = &v0 - } - - var v1 int - if err := json.Unmarshal(data, &v1); err == nil { - u.Int1 = &v1 - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *ObjectWithAnyOfPropertyValue) ApplyDefaults() { -} - -// #/components/schemas/ObjectWithOneOfProperty -type ObjectWithOneOfProperty struct { - Variant *ObjectWithOneOfPropertyVariant `json:"variant,omitempty" form:"variant,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ObjectWithOneOfProperty) ApplyDefaults() { -} - -// #/components/schemas/ObjectWithOneOfProperty/properties/variant -type ObjectWithOneOfPropertyVariant struct { - ObjectWithOneOfPropertyVariantOneOf0 *ObjectWithOneOfPropertyVariantOneOf0 - ObjectWithOneOfPropertyVariantOneOf1 *ObjectWithOneOfPropertyVariantOneOf1 -} - -func (u ObjectWithOneOfPropertyVariant) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.ObjectWithOneOfPropertyVariantOneOf0 != nil { - count++ - data, err = json.Marshal(u.ObjectWithOneOfPropertyVariantOneOf0) - if err != nil { - return nil, err - } - } - if u.ObjectWithOneOfPropertyVariantOneOf1 != nil { - count++ - data, err = json.Marshal(u.ObjectWithOneOfPropertyVariantOneOf1) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("ObjectWithOneOfPropertyVariant: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *ObjectWithOneOfPropertyVariant) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 ObjectWithOneOfPropertyVariantOneOf0 - if err := json.Unmarshal(data, &v0); err == nil { - u.ObjectWithOneOfPropertyVariantOneOf0 = &v0 - successCount++ - } - - var v1 ObjectWithOneOfPropertyVariantOneOf1 - if err := json.Unmarshal(data, &v1); err == nil { - u.ObjectWithOneOfPropertyVariantOneOf1 = &v1 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("ObjectWithOneOfPropertyVariant: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *ObjectWithOneOfPropertyVariant) ApplyDefaults() { - if u.ObjectWithOneOfPropertyVariantOneOf0 != nil { - u.ObjectWithOneOfPropertyVariantOneOf0.ApplyDefaults() - } - if u.ObjectWithOneOfPropertyVariantOneOf1 != nil { - u.ObjectWithOneOfPropertyVariantOneOf1.ApplyDefaults() - } -} - -// #/components/schemas/ObjectWithOneOfProperty/properties/variant/oneOf/0 -type ObjectWithOneOfPropertyVariantOneOf0 struct { - Kind *string `json:"kind,omitempty" form:"kind,omitempty"` - Name *string `json:"name,omitempty" form:"name,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ObjectWithOneOfPropertyVariantOneOf0) ApplyDefaults() { -} - -// #/components/schemas/ObjectWithOneOfProperty/properties/variant/oneOf/1 -type ObjectWithOneOfPropertyVariantOneOf1 struct { - Kind *string `json:"kind,omitempty" form:"kind,omitempty"` - Count *int `json:"count,omitempty" form:"count,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *ObjectWithOneOfPropertyVariantOneOf1) ApplyDefaults() { -} - -// #/components/schemas/AllOfWithOneOf -type AllOfWithOneOf struct { - Base *string `json:"base,omitempty" form:"base,omitempty"` - AllOfWithOneOfAllOf1 *AllOfWithOneOfAllOf1 `json:"-"` -} - -func (s AllOfWithOneOf) MarshalJSON() ([]byte, error) { - result := make(map[string]any) - - if s.Base != nil { - result["base"] = s.Base - } - - if s.AllOfWithOneOfAllOf1 != nil { - unionData, err := json.Marshal(s.AllOfWithOneOfAllOf1) - if err != nil { - return nil, err - } - var unionMap map[string]any - if err := json.Unmarshal(unionData, &unionMap); err == nil { - for k, v := range unionMap { - result[k] = v - } - } - } - - return json.Marshal(result) -} - -func (s *AllOfWithOneOf) UnmarshalJSON(data []byte) error { - var raw map[string]json.RawMessage - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - - if v, ok := raw["base"]; ok { - var val string - if err := json.Unmarshal(v, &val); err != nil { - return err - } - s.Base = &val - } - - var AllOfWithOneOfAllOf1Val AllOfWithOneOfAllOf1 - if err := json.Unmarshal(data, &AllOfWithOneOfAllOf1Val); err != nil { - return err - } - s.AllOfWithOneOfAllOf1 = &AllOfWithOneOfAllOf1Val - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AllOfWithOneOf) ApplyDefaults() { -} - -// #/components/schemas/AllOfWithOneOf/allOf/1 -type AllOfWithOneOfAllOf1 struct { - AllOfWithOneOfAllOf1OneOf0 *AllOfWithOneOfAllOf1OneOf0 - AllOfWithOneOfAllOf1OneOf1 *AllOfWithOneOfAllOf1OneOf1 -} - -func (u AllOfWithOneOfAllOf1) MarshalJSON() ([]byte, error) { - var count int - var data []byte - var err error - - if u.AllOfWithOneOfAllOf1OneOf0 != nil { - count++ - data, err = json.Marshal(u.AllOfWithOneOfAllOf1OneOf0) - if err != nil { - return nil, err - } - } - if u.AllOfWithOneOfAllOf1OneOf1 != nil { - count++ - data, err = json.Marshal(u.AllOfWithOneOfAllOf1OneOf1) - if err != nil { - return nil, err - } - } - - if count != 1 { - return nil, fmt.Errorf("AllOfWithOneOfAllOf1: exactly one member must be set, got %d", count) - } - - return data, nil -} - -func (u *AllOfWithOneOfAllOf1) UnmarshalJSON(data []byte) error { - var successCount int - - var v0 AllOfWithOneOfAllOf1OneOf0 - if err := json.Unmarshal(data, &v0); err == nil { - u.AllOfWithOneOfAllOf1OneOf0 = &v0 - successCount++ - } - - var v1 AllOfWithOneOfAllOf1OneOf1 - if err := json.Unmarshal(data, &v1); err == nil { - u.AllOfWithOneOfAllOf1OneOf1 = &v1 - successCount++ - } - - if successCount != 1 { - return fmt.Errorf("AllOfWithOneOfAllOf1: expected exactly one type to match, got %d", successCount) - } - - return nil -} - -// ApplyDefaults sets default values for fields that are nil. -func (u *AllOfWithOneOfAllOf1) ApplyDefaults() { - if u.AllOfWithOneOfAllOf1OneOf0 != nil { - u.AllOfWithOneOfAllOf1OneOf0.ApplyDefaults() - } - if u.AllOfWithOneOfAllOf1OneOf1 != nil { - u.AllOfWithOneOfAllOf1OneOf1.ApplyDefaults() - } -} - -// #/components/schemas/AllOfWithOneOf/allOf/1/oneOf/0 -type AllOfWithOneOfAllOf1OneOf0 struct { - OptionA *bool `json:"optionA,omitempty" form:"optionA,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AllOfWithOneOfAllOf1OneOf0) ApplyDefaults() { -} - -// #/components/schemas/AllOfWithOneOf/allOf/1/oneOf/1 -type AllOfWithOneOfAllOf1OneOf1 struct { - OptionB *int `json:"optionB,omitempty" form:"optionB,omitempty"` -} - -// ApplyDefaults sets default values for fields that are nil. -func (s *AllOfWithOneOfAllOf1OneOf1) ApplyDefaults() { -} - -// Base64-encoded, gzip-compressed OpenAPI spec. -var swaggerSpecJSON = []string{ - "H4sIAAAAAAAC/7yTQY/TMBCF7/kVT+W8K5bl5FvgTjggcXbTSTKQjC17WlQh/juKk9KkTVqBKnpq5nnG", - "75uXOE9iPRtsXp9fnt9uMpbKmQxQ1pYMPlFU2iGv60C1VcIXipoBBwqRnRhsUpe32kSDn7+y0nXeCYnG", - "fkosG+ps+gu8wUcbCS8GeQj2iB+sDawciwqs1MV0KElFlffloQ3QoycD2ytjJZ0/yRiGnB+Bp7EnamCp", - "FwS3/UalTgTAB+cpKFM0szrAu8vKyROLUk0hm/K9MyjS9CngOHzwP8hfWZuE+XnU5rgzh0veDrbd09TY", - "1RZu7OEsLSG8zhGc0AQBpRO1LCw1WFoWGr3GC7iib/tnuMBWdEqTXCzjLaR5K0/gO8tCpljfVv8T29Ff", - "Nf1fe6Xbzxd2/1V9b2DbtqimkaY1D59iL/0J8jQ5NZyvWWVc49vaeLXFRa6nhybuvLKTfH0/W+dasvLI", - "2z7cT+N3AAAA//8nzKKtgAUAAA==", -} - -// decodeSwaggerSpec decodes and decompresses the embedded spec. -func decodeSwaggerSpec() ([]byte, error) { - joined := strings.Join(swaggerSpecJSON, "") - raw, err := base64.StdEncoding.DecodeString(joined) - if err != nil { - return nil, fmt.Errorf("decoding base64: %w", err) - } - r, err := gzip.NewReader(bytes.NewReader(raw)) - if err != nil { - return nil, fmt.Errorf("creating gzip reader: %w", err) - } - defer r.Close() - var out bytes.Buffer - if _, err := out.ReadFrom(r); err != nil { - return nil, fmt.Errorf("decompressing: %w", err) - } - return out.Bytes(), nil -} - -// decodeSwaggerSpecCached returns a closure that caches the decoded spec. -func decodeSwaggerSpecCached() func() ([]byte, error) { - var cached []byte - var cachedErr error - var once sync.Once - return func() ([]byte, error) { - once.Do(func() { - cached, cachedErr = decodeSwaggerSpec() - }) - return cached, cachedErr - } -} - -var swaggerSpec = decodeSwaggerSpecCached() - -// GetSwaggerSpecJSON returns the raw OpenAPI spec as JSON bytes. -func GetSwaggerSpecJSON() ([]byte, error) { - return swaggerSpec() -} diff --git a/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate_test.go b/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate_test.go deleted file mode 100644 index e70e8cb49e..0000000000 --- a/experimental/internal/codegen/test/nested_aggregate/output/nested_aggregate_test.go +++ /dev/null @@ -1,332 +0,0 @@ -package output - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func ptr[T any](v T) *T { - return &v -} - -// TestArrayOfAnyOf tests marshaling/unmarshaling of arrays with anyOf items -func TestArrayOfAnyOf(t *testing.T) { - t.Run("unmarshal string item", func(t *testing.T) { - input := `["hello", "world"]` - var arr ArrayOfAnyOf - err := json.Unmarshal([]byte(input), &arr) - require.NoError(t, err) - require.Len(t, arr, 2) - - // String items should populate the string field - assert.NotNil(t, arr[0].String0) - assert.Equal(t, "hello", *arr[0].String0) - assert.NotNil(t, arr[1].String0) - assert.Equal(t, "world", *arr[1].String0) - }) - - t.Run("unmarshal object item", func(t *testing.T) { - input := `[{"id": 42}]` - var arr ArrayOfAnyOf - err := json.Unmarshal([]byte(input), &arr) - require.NoError(t, err) - require.Len(t, arr, 1) - - // Object item should populate the object field - assert.NotNil(t, arr[0].ArrayOfAnyOfAnyOf1) - assert.NotNil(t, arr[0].ArrayOfAnyOfAnyOf1.ID) - assert.Equal(t, 42, *arr[0].ArrayOfAnyOfAnyOf1.ID) - }) - - t.Run("unmarshal mixed items", func(t *testing.T) { - input := `["hello", {"id": 1}, "world", {"id": 2}]` - var arr ArrayOfAnyOf - err := json.Unmarshal([]byte(input), &arr) - require.NoError(t, err) - require.Len(t, arr, 4) - - assert.NotNil(t, arr[0].String0) - assert.Equal(t, "hello", *arr[0].String0) - - assert.NotNil(t, arr[1].ArrayOfAnyOfAnyOf1) - assert.Equal(t, 1, *arr[1].ArrayOfAnyOfAnyOf1.ID) - - assert.NotNil(t, arr[2].String0) - assert.Equal(t, "world", *arr[2].String0) - - assert.NotNil(t, arr[3].ArrayOfAnyOfAnyOf1) - assert.Equal(t, 2, *arr[3].ArrayOfAnyOfAnyOf1.ID) - }) - - t.Run("marshal string item", func(t *testing.T) { - arr := ArrayOfAnyOf{ - {String0: ptr("hello")}, - } - data, err := json.Marshal(arr) - require.NoError(t, err) - assert.JSONEq(t, `["hello"]`, string(data)) - }) - - t.Run("marshal object item", func(t *testing.T) { - arr := ArrayOfAnyOf{ - {ArrayOfAnyOfAnyOf1: &ArrayOfAnyOfAnyOf1{ID: ptr(42)}}, - } - data, err := json.Marshal(arr) - require.NoError(t, err) - assert.JSONEq(t, `[{"id": 42}]`, string(data)) - }) - - t.Run("round trip mixed", func(t *testing.T) { - original := ArrayOfAnyOf{ - {String0: ptr("test")}, - {ArrayOfAnyOfAnyOf1: &ArrayOfAnyOfAnyOf1{ID: ptr(99)}}, - } - - data, err := json.Marshal(original) - require.NoError(t, err) - - var decoded ArrayOfAnyOf - err = json.Unmarshal(data, &decoded) - require.NoError(t, err) - - require.Len(t, decoded, 2) - assert.Equal(t, "test", *decoded[0].String0) - assert.Equal(t, 99, *decoded[1].ArrayOfAnyOfAnyOf1.ID) - }) -} - -// TestObjectWithAnyOfProperty tests marshaling/unmarshaling of objects with anyOf properties -func TestObjectWithAnyOfProperty(t *testing.T) { - t.Run("unmarshal string value", func(t *testing.T) { - input := `{"value": "hello"}` - var obj ObjectWithAnyOfProperty - err := json.Unmarshal([]byte(input), &obj) - require.NoError(t, err) - - require.NotNil(t, obj.Value) - assert.NotNil(t, obj.Value.String0) - assert.Equal(t, "hello", *obj.Value.String0) - }) - - t.Run("unmarshal integer value", func(t *testing.T) { - input := `{"value": 42}` - var obj ObjectWithAnyOfProperty - err := json.Unmarshal([]byte(input), &obj) - require.NoError(t, err) - - require.NotNil(t, obj.Value) - assert.NotNil(t, obj.Value.Int1) - assert.Equal(t, 42, *obj.Value.Int1) - }) - - t.Run("marshal string value", func(t *testing.T) { - obj := ObjectWithAnyOfProperty{ - Value: &ObjectWithAnyOfPropertyValue{ - String0: ptr("hello"), - }, - } - data, err := json.Marshal(obj) - require.NoError(t, err) - assert.JSONEq(t, `{"value": "hello"}`, string(data)) - }) - - t.Run("marshal integer value", func(t *testing.T) { - obj := ObjectWithAnyOfProperty{ - Value: &ObjectWithAnyOfPropertyValue{ - Int1: ptr(42), - }, - } - data, err := json.Marshal(obj) - require.NoError(t, err) - assert.JSONEq(t, `{"value": 42}`, string(data)) - }) - - t.Run("round trip string", func(t *testing.T) { - original := ObjectWithAnyOfProperty{ - Value: &ObjectWithAnyOfPropertyValue{String0: ptr("test")}, - } - - data, err := json.Marshal(original) - require.NoError(t, err) - - var decoded ObjectWithAnyOfProperty - err = json.Unmarshal(data, &decoded) - require.NoError(t, err) - - require.NotNil(t, decoded.Value) - assert.Equal(t, "test", *decoded.Value.String0) - }) -} - -// TestObjectWithOneOfProperty tests marshaling/unmarshaling of objects with oneOf properties -func TestObjectWithOneOfProperty(t *testing.T) { - t.Run("unmarshal ambiguous input errors", func(t *testing.T) { - // Both variants have optional "kind" field, so this JSON matches both - // oneOf requires exactly one match, so this should error - input := `{"variant": {"kind": "person", "name": "Alice"}}` - var obj ObjectWithOneOfProperty - err := json.Unmarshal([]byte(input), &obj) - require.Error(t, err) - assert.Contains(t, err.Error(), "expected exactly one type to match, got 2") - }) - - t.Run("unmarshal unambiguous variant 0", func(t *testing.T) { - // Only variant 0 has "name" as a field that can be set - // But since all fields are optional, both variants still match - // This demonstrates why discriminators are important for oneOf - input := `{"variant": {"name": "Alice"}}` - var obj ObjectWithOneOfProperty - err := json.Unmarshal([]byte(input), &obj) - // Still ambiguous because both variants can unmarshal (missing fields are just nil) - require.Error(t, err) - }) - - t.Run("unmarshal unambiguous variant 1", func(t *testing.T) { - // Only variant 1 has "count" field - // But since all fields are optional, both variants still match - input := `{"variant": {"count": 10}}` - var obj ObjectWithOneOfProperty - err := json.Unmarshal([]byte(input), &obj) - // Still ambiguous because both variants can unmarshal (missing fields are just nil) - require.Error(t, err) - }) - - t.Run("marshal variant 0", func(t *testing.T) { - obj := ObjectWithOneOfProperty{ - Variant: &ObjectWithOneOfPropertyVariant{ - ObjectWithOneOfPropertyVariantOneOf0: &ObjectWithOneOfPropertyVariantOneOf0{ - Kind: ptr("person"), - Name: ptr("Alice"), - }, - }, - } - data, err := json.Marshal(obj) - require.NoError(t, err) - assert.JSONEq(t, `{"variant": {"kind": "person", "name": "Alice"}}`, string(data)) - }) - - t.Run("marshal variant 1", func(t *testing.T) { - obj := ObjectWithOneOfProperty{ - Variant: &ObjectWithOneOfPropertyVariant{ - ObjectWithOneOfPropertyVariantOneOf1: &ObjectWithOneOfPropertyVariantOneOf1{ - Kind: ptr("counter"), - Count: ptr(10), - }, - }, - } - data, err := json.Marshal(obj) - require.NoError(t, err) - assert.JSONEq(t, `{"variant": {"kind": "counter", "count": 10}}`, string(data)) - }) - - t.Run("marshal fails with zero variants set", func(t *testing.T) { - obj := ObjectWithOneOfProperty{ - Variant: &ObjectWithOneOfPropertyVariant{}, - } - _, err := json.Marshal(obj) - assert.Error(t, err) - assert.Contains(t, err.Error(), "exactly one member must be set") - }) - - t.Run("marshal fails with two variants set", func(t *testing.T) { - obj := ObjectWithOneOfProperty{ - Variant: &ObjectWithOneOfPropertyVariant{ - ObjectWithOneOfPropertyVariantOneOf0: &ObjectWithOneOfPropertyVariantOneOf0{ - Kind: ptr("person"), - Name: ptr("Alice"), - }, - ObjectWithOneOfPropertyVariantOneOf1: &ObjectWithOneOfPropertyVariantOneOf1{ - Kind: ptr("counter"), - Count: ptr(10), - }, - }, - } - _, err := json.Marshal(obj) - assert.Error(t, err) - assert.Contains(t, err.Error(), "exactly one member must be set") - }) -} - -// TestAllOfWithOneOf tests marshaling/unmarshaling of allOf containing oneOf -func TestAllOfWithOneOf(t *testing.T) { - t.Run("unmarshal with optionA - ambiguous oneOf errors", func(t *testing.T) { - // The nested oneOf has same ambiguity issue - both variants match - input := `{"base": "test", "optionA": true}` - var obj AllOfWithOneOf - err := json.Unmarshal([]byte(input), &obj) - // The nested AllOfWithOneOfAllOf1 (oneOf) will error due to ambiguity - require.Error(t, err) - assert.Contains(t, err.Error(), "expected exactly one type to match") - }) - - t.Run("unmarshal with optionB - ambiguous oneOf errors", func(t *testing.T) { - input := `{"base": "test", "optionB": 42}` - var obj AllOfWithOneOf - err := json.Unmarshal([]byte(input), &obj) - require.Error(t, err) - assert.Contains(t, err.Error(), "expected exactly one type to match") - }) - - t.Run("marshal with optionA", func(t *testing.T) { - obj := AllOfWithOneOf{ - Base: ptr("test"), - AllOfWithOneOfAllOf1: &AllOfWithOneOfAllOf1{ - AllOfWithOneOfAllOf1OneOf0: &AllOfWithOneOfAllOf1OneOf0{ - OptionA: ptr(true), - }, - }, - } - - data, err := json.Marshal(obj) - require.NoError(t, err) - - // Should contain both base and optionA merged - var m map[string]any - err = json.Unmarshal(data, &m) - require.NoError(t, err) - - assert.Equal(t, "test", m["base"]) - assert.Equal(t, true, m["optionA"]) - }) - - t.Run("marshal with optionB", func(t *testing.T) { - obj := AllOfWithOneOf{ - Base: ptr("test"), - AllOfWithOneOfAllOf1: &AllOfWithOneOfAllOf1{ - AllOfWithOneOfAllOf1OneOf1: &AllOfWithOneOfAllOf1OneOf1{ - OptionB: ptr(42), - }, - }, - } - - data, err := json.Marshal(obj) - require.NoError(t, err) - - var m map[string]any - err = json.Unmarshal(data, &m) - require.NoError(t, err) - - assert.Equal(t, "test", m["base"]) - assert.Equal(t, float64(42), m["optionB"]) // JSON numbers are float64 - }) - - t.Run("marshal with nil union", func(t *testing.T) { - obj := AllOfWithOneOf{ - Base: ptr("only-base"), - } - - data, err := json.Marshal(obj) - require.NoError(t, err) - - var m map[string]any - err = json.Unmarshal(data, &m) - require.NoError(t, err) - - assert.Equal(t, "only-base", m["base"]) - assert.NotContains(t, m, "optionA") - assert.NotContains(t, m, "optionB") - }) -} diff --git a/experimental/internal/codegen/typegen.go b/experimental/internal/codegen/typegen.go deleted file mode 100644 index e27424bff0..0000000000 --- a/experimental/internal/codegen/typegen.go +++ /dev/null @@ -1,918 +0,0 @@ -package codegen - -import ( - "fmt" - "sort" - "strings" - - "github.com/pb33f/libopenapi/datamodel/high/base" -) - -// TypeGenerator converts OpenAPI schemas to Go type expressions. -// It tracks required imports and handles recursive type references. -type TypeGenerator struct { - typeMapping TypeMapping - converter *NameConverter - importResolver *ImportResolver - tagGenerator *StructTagGenerator - imports map[string]string // path -> alias (empty string = no alias) - - // schemaIndex maps JSON pointer refs to their descriptors - schemaIndex map[string]*SchemaDescriptor - - // requiredTemplates tracks which custom type templates are needed - requiredTemplates map[string]bool -} - -// NewTypeGenerator creates a TypeGenerator with the given configuration. -func NewTypeGenerator(typeMapping TypeMapping, converter *NameConverter, importResolver *ImportResolver, tagGenerator *StructTagGenerator) *TypeGenerator { - return &TypeGenerator{ - typeMapping: typeMapping, - converter: converter, - importResolver: importResolver, - tagGenerator: tagGenerator, - imports: make(map[string]string), - schemaIndex: make(map[string]*SchemaDescriptor), - requiredTemplates: make(map[string]bool), - } -} - -// IndexSchemas builds a lookup table from JSON pointer to schema descriptor. -// This is called before generation to enable $ref resolution. -func (g *TypeGenerator) IndexSchemas(schemas []*SchemaDescriptor) { - for _, s := range schemas { - ref := s.Path.String() - g.schemaIndex[ref] = s - } -} - -// AddImport records an import path needed by the generated code. -func (g *TypeGenerator) AddImport(path string) { - if path != "" { - g.imports[path] = "" - } -} - -// AddImportAlias records an import path with an alias. -func (g *TypeGenerator) AddImportAlias(path, alias string) { - if path != "" { - g.imports[path] = alias - } -} - -// AddJSONImport adds encoding/json import (used by marshal/unmarshal code). -func (g *TypeGenerator) AddJSONImport() { - g.AddImport("encoding/json") -} - -// AddJSONImports adds encoding/json and fmt imports (used by oneOf marshal/unmarshal code). -func (g *TypeGenerator) AddJSONImports() { - g.AddImport("encoding/json") - g.AddImport("fmt") -} - -// AddNullableTemplate adds the nullable type template to the output. -func (g *TypeGenerator) AddNullableTemplate() { - g.addTemplate("nullable") -} - -// Imports returns the collected imports as a map[path]alias. -func (g *TypeGenerator) Imports() map[string]string { - return g.imports -} - -// RequiredTemplates returns the set of template names needed for custom types. -func (g *TypeGenerator) RequiredTemplates() map[string]bool { - return g.requiredTemplates -} - -// addTemplate records that a custom type template is needed. -func (g *TypeGenerator) addTemplate(templateName string) { - if templateName != "" { - g.requiredTemplates[templateName] = true - } -} - -// GoTypeExpr returns the Go type expression for a schema descriptor. -// This handles references by looking up the target schema's name, -// and inline schemas by generating the appropriate Go type. -func (g *TypeGenerator) GoTypeExpr(desc *SchemaDescriptor) string { - // Handle $ref - return the referenced type's name - if desc.IsReference() { - // Check for external reference first - if desc.IsExternalReference() { - return g.externalRefType(desc) - } - - // Internal reference - look up in schema index - if target, ok := g.schemaIndex[desc.Ref]; ok { - return target.ShortName - } - // Fallback for unresolved references - return "any" - } - - return g.goTypeForSchema(desc.Schema, desc) -} - -// externalRefType resolves an external reference to a qualified Go type. -// Returns "any" if the external ref cannot be resolved. -func (g *TypeGenerator) externalRefType(desc *SchemaDescriptor) string { - filePath, internalPath := desc.ParseExternalRef() - if filePath == "" { - return "any" - } - - // Look up import mapping - if g.importResolver == nil { - // No import resolver configured - can't resolve external refs - return "any" - } - - imp := g.importResolver.Resolve(filePath) - if imp == nil { - // External file not in import mapping - return "any" - } - - // Extract type name from internal path (e.g., #/components/schemas/Pet -> Pet) - typeName := extractTypeNameFromRef(internalPath, g.converter) - if typeName == "" { - return "any" - } - - // If alias is empty, it's the current package (marked with "-") - if imp.Alias == "" { - return typeName - } - - // Add the import - g.AddImportAlias(imp.Path, imp.Alias) - - // Return qualified type - return imp.Alias + "." + typeName -} - -// extractTypeNameFromRef extracts a Go type name from an internal ref path. -// e.g., "#/components/schemas/Pet" -> "Pet" -func extractTypeNameFromRef(ref string, converter *NameConverter) string { - // Remove leading #/ - ref = strings.TrimPrefix(ref, "#/") - parts := strings.Split(ref, "/") - - if len(parts) < 3 { - return "" - } - - // For #/components/schemas/TypeName, the type name is the last part - // We assume external refs point to component schemas - typeName := parts[len(parts)-1] - return converter.ToTypeName(typeName) -} - -// goTypeForSchema generates a Go type expression from an OpenAPI schema. -// The desc parameter provides context (path, parent) for complex types. -func (g *TypeGenerator) goTypeForSchema(schema *base.Schema, desc *SchemaDescriptor) string { - if schema == nil { - return "any" - } - - // Handle composition types - if len(schema.AllOf) > 0 { - return g.allOfType(desc) - } - if len(schema.AnyOf) > 0 { - return g.anyOfType(desc) - } - if len(schema.OneOf) > 0 { - return g.oneOfType(desc) - } - - // Get the primary type from the type array - // OpenAPI 3.1 allows type to be an array like ["string", "null"] - primaryType := getPrimaryType(schema) - - // Check if this is a nullable primitive - wrap in Nullable[T] - nullable := isNullable(schema) - isPrimitive := primaryType == "string" || primaryType == "integer" || primaryType == "number" || primaryType == "boolean" - - var baseType string - switch primaryType { - case "object": - return g.objectType(schema, desc) - case "array": - return g.arrayType(schema, desc) - case "string": - baseType = g.stringType(schema) - case "integer": - baseType = g.integerType(schema) - case "number": - baseType = g.numberType(schema) - case "boolean": - baseType = g.booleanType(schema) - default: - // Unknown or empty type - could be a free-form object - if schema.Properties != nil && schema.Properties.Len() > 0 { - return g.objectType(schema, desc) - } - return "any" - } - - // Wrap nullable primitives in Nullable[T] - if nullable && isPrimitive { - g.AddNullableTemplate() - return "Nullable[" + baseType + "]" - } - - return baseType -} - -// getPrimaryType extracts the primary (non-null) type from a schema. -// OpenAPI 3.1 supports type arrays like ["string", "null"] for nullable. -func getPrimaryType(schema *base.Schema) string { - if len(schema.Type) == 0 { - return "" - } - for _, t := range schema.Type { - if t != "null" { - return t - } - } - return schema.Type[0] -} - -// objectType generates the Go type for an object schema. -// Simple objects with only additionalProperties become maps. -// Objects with properties become named struct types. -func (g *TypeGenerator) objectType(schema *base.Schema, desc *SchemaDescriptor) string { - hasProperties := schema.Properties != nil && schema.Properties.Len() > 0 - hasAdditionalProps := schema.AdditionalProperties != nil - - // Pure map case: no properties, only additionalProperties - if !hasProperties && hasAdditionalProps { - return g.mapType(schema, desc) - } - - // Empty object (no properties, no additionalProperties) - if !hasProperties && !hasAdditionalProps { - return "map[string]any" - } - - // Struct case: has properties (with or without additionalProperties) - // Return the type name - actual struct definition is generated separately - if desc != nil && desc.ShortName != "" { - return desc.ShortName - } - return "any" -} - -// mapType generates a map[string]T type for additionalProperties schemas. -func (g *TypeGenerator) mapType(schema *base.Schema, desc *SchemaDescriptor) string { - if schema.AdditionalProperties == nil { - return "map[string]any" - } - - // additionalProperties can be a boolean or a schema - // If it's a schema proxy (A), get the value type - if schema.AdditionalProperties.A != nil { - valueSchema := schema.AdditionalProperties.A.Schema() - if valueSchema != nil { - valueType := g.goTypeForSchema(valueSchema, nil) - return "map[string]" + valueType - } - } - - // additionalProperties: true or just present - return "map[string]any" -} - -// arrayType generates a []T type for array schemas. -func (g *TypeGenerator) arrayType(schema *base.Schema, desc *SchemaDescriptor) string { - if schema.Items == nil || schema.Items.A == nil { - return "[]any" - } - - // Check if items is a reference - itemProxy := schema.Items.A - if itemProxy.IsReference() { - ref := itemProxy.GetReference() - // Check for external reference first - if !strings.HasPrefix(ref, "#") && strings.Contains(ref, "#") { - // External reference - use import mapping - tempDesc := &SchemaDescriptor{Ref: ref} - itemType := g.externalRefType(tempDesc) - return "[]" + itemType - } - // Internal reference - look up in schema index - if target, ok := g.schemaIndex[ref]; ok { - return "[]" + target.ShortName - } - } - - // Check if we have a descriptor for the items schema - if desc != nil && desc.Items != nil && desc.Items.ShortName != "" { - return "[]" + desc.Items.ShortName - } - - // Inline items schema - itemSchema := itemProxy.Schema() - itemType := g.goTypeForSchema(itemSchema, nil) - return "[]" + itemType -} - -// stringType returns the Go type for a string schema. -func (g *TypeGenerator) stringType(schema *base.Schema) string { - spec := g.typeMapping.String.Default - if schema.Format != "" { - if formatSpec, ok := g.typeMapping.String.Formats[schema.Format]; ok { - spec = formatSpec - } - } - - g.AddImport(spec.Import) - g.addTemplate(spec.Template) - return spec.Type -} - -// integerType returns the Go type for an integer schema. -func (g *TypeGenerator) integerType(schema *base.Schema) string { - spec := g.typeMapping.Integer.Default - if schema.Format != "" { - if formatSpec, ok := g.typeMapping.Integer.Formats[schema.Format]; ok { - spec = formatSpec - } - } - - g.AddImport(spec.Import) - return spec.Type -} - -// numberType returns the Go type for a number schema. -func (g *TypeGenerator) numberType(schema *base.Schema) string { - spec := g.typeMapping.Number.Default - if schema.Format != "" { - if formatSpec, ok := g.typeMapping.Number.Formats[schema.Format]; ok { - spec = formatSpec - } - } - - g.AddImport(spec.Import) - return spec.Type -} - -// booleanType returns the Go type for a boolean schema. -func (g *TypeGenerator) booleanType(schema *base.Schema) string { - spec := g.typeMapping.Boolean.Default - g.AddImport(spec.Import) - return spec.Type -} - -// allOfType returns the type name for an allOf composition. -// allOf is typically used for struct embedding/inheritance. -func (g *TypeGenerator) allOfType(desc *SchemaDescriptor) string { - if desc != nil && desc.ShortName != "" { - return desc.ShortName - } - return "any" -} - -// anyOfType returns the type name for an anyOf composition. -func (g *TypeGenerator) anyOfType(desc *SchemaDescriptor) string { - if desc != nil && desc.ShortName != "" { - return desc.ShortName - } - return "any" -} - -// oneOfType returns the type name for a oneOf composition. -func (g *TypeGenerator) oneOfType(desc *SchemaDescriptor) string { - if desc != nil && desc.ShortName != "" { - return desc.ShortName - } - return "any" -} - -// StructField represents a field in a generated Go struct. -type StructField struct { - Name string // Go field name - Type string // Go type expression - JSONName string // Original JSON property name - Required bool // Is this field required in the schema - Nullable bool // Is this field nullable (type includes "null") - Pointer bool // Should this be a pointer type - OmitEmpty bool // Include omitempty in json tag - OmitZero bool // Include omitzero in json tag (Go 1.24+) - JSONIgnore bool // Use json:"-" tag to exclude from marshaling - Doc string // Field documentation - Default string // Go literal for default value (empty if no default) - IsStruct bool // True if this field is a struct type (for recursive ApplyDefaults) - IsNullableAlias bool // True if type is a type alias to Nullable[T] (don't wrap or pointer) - Order *int // Optional field ordering (lower values come first) -} - -// GenerateStructFields creates the list of struct fields for an object schema. -func (g *TypeGenerator) GenerateStructFields(desc *SchemaDescriptor) []StructField { - schema := desc.Schema - if schema == nil || schema.Properties == nil { - return nil - } - - // Build required set - required := make(map[string]bool) - for _, r := range schema.Required { - required[r] = true - } - - var fields []StructField - needsNullableImport := false - - for pair := schema.Properties.First(); pair != nil; pair = pair.Next() { - propName := pair.Key() - propProxy := pair.Value() - - field := StructField{ - Name: g.converter.ToPropertyName(propName), - JSONName: propName, - Required: required[propName], - } - - // Parse extensions from the property schema - var propExtensions *Extensions - var propSchema *base.Schema - - // Resolve the property schema - var propType string - if propProxy.IsReference() { - ref := propProxy.GetReference() - // Check if this is an external reference - if !strings.HasPrefix(ref, "#") && strings.Contains(ref, "#") { - // External reference - use import mapping - tempDesc := &SchemaDescriptor{Ref: ref} - propType = g.externalRefType(tempDesc) - field.IsStruct = true // external references are typically to struct types - } else if target, ok := g.schemaIndex[ref]; ok { - propType = target.ShortName - // Only set IsStruct if the referenced schema has ApplyDefaults - // This filters out array/map type aliases which don't have ApplyDefaults - field.IsStruct = schemaHasApplyDefaults(target.Schema) - // Check if the referenced schema is nullable - // BUT: if it's a nullable primitive, the type alias already wraps Nullable[T], - // so we shouldn't double-wrap it or add a pointer (Nullable handles "unspecified") - if isNullablePrimitive(target.Schema) { - // Already Nullable[T] - use as value type directly - field.IsNullableAlias = true - } else if isNullable(target.Schema) { - field.Nullable = true - } - // Extensions from referenced schema apply to the field - propExtensions = target.Extensions - } else { - propType = "any" - } - } else { - propSchema = propProxy.Schema() - field.Nullable = isNullable(propSchema) - field.Doc = extractDescription(propSchema) - - // Parse extensions from the property schema - if propSchema != nil && propSchema.Extensions != nil { - ext, err := ParseExtensions(propSchema.Extensions, desc.Path.Append("properties", propName).String()) - if err == nil { - propExtensions = ext - } - } - - // Generate the Go type for this property - // Always use goTypeForSchema to get the correct type expression - // This handles arrays, maps, and primitive types correctly - propType = g.goTypeForSchema(propSchema, desc.Properties[propName]) - - // Check if this is a struct type (object with properties, or a named type) - if propSchema != nil { - if propSchema.Properties != nil && propSchema.Properties.Len() > 0 { - field.IsStruct = true - } - // Extract default value - if propSchema.Default != nil { - field.Default = formatDefaultValue(propSchema.Default.Value, propType) - } - } - } - - // Apply extensions to the field - if propExtensions != nil { - // Name override - if propExtensions.NameOverride != "" { - field.Name = propExtensions.NameOverride - } - - // Type override replaces the generated type entirely - if propExtensions.TypeOverride != nil { - propType = propExtensions.TypeOverride.TypeName - if propExtensions.TypeOverride.ImportPath != "" { - if propExtensions.TypeOverride.ImportAlias != "" { - g.AddImportAlias(propExtensions.TypeOverride.ImportPath, propExtensions.TypeOverride.ImportAlias) - } else { - g.AddImport(propExtensions.TypeOverride.ImportPath) - } - } - // Type override bypasses nullable wrapping - the user specifies the exact type - field.IsNullableAlias = true // Don't wrap or add pointer - } - - // JSON ignore - if propExtensions.JSONIgnore != nil && *propExtensions.JSONIgnore { - field.JSONIgnore = true - } - - // Deprecated reason appended to documentation - if propExtensions.DeprecatedReason != "" { - if field.Doc != "" { - field.Doc = field.Doc + "\nDeprecated: " + propExtensions.DeprecatedReason - } else { - field.Doc = "Deprecated: " + propExtensions.DeprecatedReason - } - } - - // Order for field sorting - if propExtensions.Order != nil { - field.Order = propExtensions.Order - } - } - - // Determine type semantics: - // - Nullable fields: use Nullable[T] - // - Optional (not nullable) fields: use *T (pointer) - // - Required (not nullable) fields: use T (value type) - // - Collections (slices/maps) are never wrapped - // - Types already wrapped in Nullable[] are not double-wrapped - // - Type aliases to Nullable[T] are used as-is (IsNullableAlias) - isCollection := strings.HasPrefix(propType, "[]") || strings.HasPrefix(propType, "map[") - alreadyNullable := strings.HasPrefix(propType, "Nullable[") || field.IsNullableAlias - - if field.Nullable && !isCollection && !alreadyNullable { - // Use Nullable[T] for nullable fields (generated inline from template) - field.Type = "Nullable[" + propType + "]" - field.Pointer = false - needsNullableImport = true - } else if !field.Required && !isCollection && !alreadyNullable { - // Check for skip optional pointer extension - skipPointer := false - if propExtensions != nil && propExtensions.SkipOptionalPointer != nil && *propExtensions.SkipOptionalPointer { - skipPointer = true - } - - if skipPointer { - // Use value type even though optional - field.Type = propType - field.Pointer = false - } else { - // Use pointer for optional non-nullable fields - field.Type = "*" + propType - field.Pointer = true - } - } else { - // Value type for required non-nullable fields, collections, and Nullable aliases - field.Type = propType - field.Pointer = false - } - - // Determine omitempty/omitzero behavior - field.OmitEmpty = !field.Required - if propExtensions != nil { - // Explicit omitempty override - if propExtensions.OmitEmpty != nil { - field.OmitEmpty = *propExtensions.OmitEmpty - } - // Explicit omitzero - if propExtensions.OmitZero != nil && *propExtensions.OmitZero { - field.OmitZero = true - } - } - - fields = append(fields, field) - } - - if needsNullableImport { - g.AddNullableTemplate() - } - - // Sort fields by order if any have explicit ordering - sortFieldsByOrder(fields) - - return fields -} - -// collectFieldsRecursive returns the struct fields for a schema, recursively -// following allOf chains. For schemas with direct properties (no allOf), this -// falls through to GenerateStructFields. For allOf-composed schemas, it -// collects fields from all allOf members recursively, so that nested allOf -// references (e.g., A: allOf[$ref:B, ...] where B: allOf[$ref:C, ...]) are -// properly flattened. -func (g *TypeGenerator) collectFieldsRecursive(desc *SchemaDescriptor) []StructField { - schema := desc.Schema - if schema == nil { - return nil - } - - // If this schema has no allOf, use the standard field generation - if len(schema.AllOf) == 0 { - return g.GenerateStructFields(desc) - } - - // Collect fields from direct properties first - var fields []StructField - if schema.Properties != nil && schema.Properties.Len() > 0 { - fields = append(fields, g.GenerateStructFields(desc)...) - } - - // Recursively collect fields from each allOf member - for i, proxy := range schema.AllOf { - memberSchema := proxy.Schema() - if memberSchema == nil { - continue - } - - var memberFields []StructField - if proxy.IsReference() { - ref := proxy.GetReference() - if target, ok := g.schemaIndex[ref]; ok { - // Recurse: the target may itself be an allOf composition - memberFields = g.collectFieldsRecursive(target) - } - } else if memberSchema.Properties != nil && memberSchema.Properties.Len() > 0 { - if desc.AllOf != nil && i < len(desc.AllOf) { - memberFields = g.GenerateStructFields(desc.AllOf[i]) - } - } - - // Apply required array from this allOf member to collected fields - if len(memberSchema.Required) > 0 { - reqSet := make(map[string]bool) - for _, r := range memberSchema.Required { - reqSet[r] = true - } - for j := range memberFields { - if reqSet[memberFields[j].JSONName] && !memberFields[j].Required { - memberFields[j].Required = true - memberFields[j].OmitEmpty = false - if !memberFields[j].Nullable && !strings.HasPrefix(memberFields[j].Type, "[]") && !strings.HasPrefix(memberFields[j].Type, "map[") { - memberFields[j].Type = strings.TrimPrefix(memberFields[j].Type, "*") - memberFields[j].Pointer = false - } - } - } - } - - fields = append(fields, memberFields...) - } - - return fields -} - -// isNullable checks if a schema allows null values. -// In OpenAPI 3.1, this is expressed as type: ["string", "null"] -// In OpenAPI 3.0, this is expressed as nullable: true -func isNullable(schema *base.Schema) bool { - if schema == nil { - return false - } - - // OpenAPI 3.1 style: type array includes "null" - for _, t := range schema.Type { - if t == "null" { - return true - } - } - - // OpenAPI 3.0 style: nullable: true - if schema.Nullable != nil && *schema.Nullable { - return true - } - - return false -} - -// extractDescription gets the description from a schema. -func extractDescription(schema *base.Schema) string { - if schema == nil { - return "" - } - return schema.Description -} - -// formatDefaultValue converts an OpenAPI default value to a Go literal. -// goType is used to determine the correct format for the literal. -func formatDefaultValue(value any, goType string) string { - if value == nil { - return "" - } - - // Strip pointer prefix for type matching - baseType := strings.TrimPrefix(goType, "*") - - switch v := value.(type) { - case string: - // Check if the target type is not a string - // YAML/JSON might parse "10" or "true" as strings - switch baseType { - case "int", "int8", "int16", "int32", "int64", - "uint", "uint8", "uint16", "uint32", "uint64": - // Return the string as-is if it looks like a number - return v - case "bool": - // Return the string as-is if it looks like a bool - if v == "true" || v == "false" { - return v - } - case "float32", "float64": - return v - } - // It's actually a string type - quote it - return fmt.Sprintf("%q", v) - case bool: - return fmt.Sprintf("%t", v) - case float64: - // JSON numbers are always float64 - // Check if it's actually an integer - if v == float64(int64(v)) { - // It's a whole number - if strings.HasPrefix(baseType, "int") || strings.HasPrefix(baseType, "uint") { - return fmt.Sprintf("%d", int64(v)) - } - } - return fmt.Sprintf("%v", v) - case int, int64: - return fmt.Sprintf("%d", v) - case []any: - // Arrays - generate a slice literal - // For now, return empty slice if complex - if len(v) == 0 { - return fmt.Sprintf("%s{}", goType) - } - // Complex array defaults would need recursive handling - return "" - case map[string]any: - // Objects - for now, skip complex defaults - if len(v) == 0 { - return fmt.Sprintf("%s{}", goType) - } - return "" - default: - // Try a simple string conversion - return fmt.Sprintf("%v", v) - } -} - -// HasAdditionalProperties returns true if the schema has explicit additionalProperties. -func (g *TypeGenerator) HasAdditionalProperties(desc *SchemaDescriptor) bool { - if desc == nil || desc.Schema == nil { - return false - } - return desc.Schema.AdditionalProperties != nil -} - -// AdditionalPropertiesType returns the Go type for the additionalProperties. -func (g *TypeGenerator) AdditionalPropertiesType(desc *SchemaDescriptor) string { - if desc == nil || desc.Schema == nil || desc.Schema.AdditionalProperties == nil { - return "any" - } - - if desc.Schema.AdditionalProperties.A != nil { - valueSchema := desc.Schema.AdditionalProperties.A.Schema() - if valueSchema != nil { - return g.goTypeForSchema(valueSchema, nil) - } - } - - return "any" -} - -// SchemaKind represents the kind of schema for code generation. -type SchemaKind int - -const ( - KindStruct SchemaKind = iota - KindMap - KindAlias - KindEnum - KindAllOf - KindAnyOf - KindOneOf - KindReference -) - -// GetSchemaKind determines what kind of Go type to generate for a schema. -func GetSchemaKind(desc *SchemaDescriptor) SchemaKind { - if desc.IsReference() { - return KindReference - } - - schema := desc.Schema - if schema == nil { - return KindAlias - } - - // Enum check first - if len(schema.Enum) > 0 { - return KindEnum - } - - // Composition types - if len(schema.AllOf) > 0 { - return KindAllOf - } - if len(schema.AnyOf) > 0 { - return KindAnyOf - } - if len(schema.OneOf) > 0 { - return KindOneOf - } - - // Object with properties -> struct - if schema.Properties != nil && schema.Properties.Len() > 0 { - return KindStruct - } - - // Object with only additionalProperties -> map - primaryType := getPrimaryType(schema) - if primaryType == "object" { - if schema.AdditionalProperties != nil { - return KindMap - } - return KindStruct // empty struct - } - - // Everything else is an alias to a primitive type - return KindAlias -} - -// FormatJSONTag generates a JSON struct tag for a field. -// Deprecated: Use StructTagGenerator instead. -func FormatJSONTag(jsonName string, omitEmpty bool) string { - if omitEmpty { - return fmt.Sprintf("`json:\"%s,omitempty\"`", jsonName) - } - return fmt.Sprintf("`json:\"%s\"`", jsonName) -} - -// GenerateFieldTag generates struct tags for a field using the configured templates. -func (g *TypeGenerator) GenerateFieldTag(field StructField) string { - if g.tagGenerator == nil { - // Fallback to legacy behavior - if field.JSONIgnore { - return "`json:\"-\"`" - } - return FormatJSONTag(field.JSONName, field.OmitEmpty) - } - - info := StructTagInfo{ - FieldName: field.JSONName, - GoFieldName: field.Name, - IsOptional: !field.Required, - IsNullable: field.Nullable, - IsPointer: field.Pointer, - OmitEmpty: field.OmitEmpty, - OmitZero: field.OmitZero, - JSONIgnore: field.JSONIgnore, - } - return g.tagGenerator.GenerateTags(info) -} - -// TagGenerator returns the struct tag generator. -func (g *TypeGenerator) TagGenerator() *StructTagGenerator { - return g.tagGenerator -} - -// sortFieldsByOrder sorts fields by their Order value. -// Fields without an Order value are placed after fields with explicit ordering, -// maintaining their original relative order (stable sort). -func sortFieldsByOrder(fields []StructField) { - // Check if any fields have explicit ordering - hasOrder := false - for _, f := range fields { - if f.Order != nil { - hasOrder = true - break - } - } - if !hasOrder { - return - } - - // Stable sort to preserve relative order of fields without explicit ordering - sort.SliceStable(fields, func(i, j int) bool { - // Fields with Order come before fields without - if fields[i].Order == nil && fields[j].Order == nil { - return false // preserve original order - } - if fields[i].Order == nil { - return false // i (no order) comes after j - } - if fields[j].Order == nil { - return true // i (has order) comes before j - } - // Both have order - sort by value - return *fields[i].Order < *fields[j].Order - }) -} diff --git a/experimental/internal/codegen/typemapping.go b/experimental/internal/codegen/typemapping.go deleted file mode 100644 index 235b06260f..0000000000 --- a/experimental/internal/codegen/typemapping.go +++ /dev/null @@ -1,98 +0,0 @@ -package codegen - -// SimpleTypeSpec is used to define the Go typename of a simple type like -// an int or a string, along with the import required to use it. -type SimpleTypeSpec struct { - Type string `yaml:"type"` - Import string `yaml:"import,omitempty"` - Template string `yaml:"template,omitempty"` -} - -// FormatMapping defines the default Go type and format-specific overrides. -type FormatMapping struct { - Default SimpleTypeSpec `yaml:"default"` - Formats map[string]SimpleTypeSpec `yaml:"formats,omitempty"` -} - -// TypeMapping defines the mapping from OpenAPI types to Go types. -type TypeMapping struct { - Integer FormatMapping `yaml:"integer,omitempty"` - Number FormatMapping `yaml:"number,omitempty"` - Boolean FormatMapping `yaml:"boolean,omitempty"` - String FormatMapping `yaml:"string,omitempty"` -} - -// Merge returns a new TypeMapping with user overrides applied on top of base. -func (base TypeMapping) Merge(user TypeMapping) TypeMapping { - return TypeMapping{ - Integer: base.Integer.merge(user.Integer), - Number: base.Number.merge(user.Number), - Boolean: base.Boolean.merge(user.Boolean), - String: base.String.merge(user.String), - } -} - -func (base FormatMapping) merge(user FormatMapping) FormatMapping { - result := FormatMapping{ - Default: base.Default, - Formats: make(map[string]SimpleTypeSpec), - } - - // Copy base formats - for k, v := range base.Formats { - result.Formats[k] = v - } - - // Override with user default if specified - if user.Default.Type != "" { - result.Default = user.Default - } - - // Override/add user formats - for k, v := range user.Formats { - result.Formats[k] = v - } - - return result -} - -// DefaultTypeMapping provides the default OpenAPI type/format to Go type mappings. -var DefaultTypeMapping = TypeMapping{ - Integer: FormatMapping{ - Default: SimpleTypeSpec{Type: "int"}, - Formats: map[string]SimpleTypeSpec{ - "int": {Type: "int"}, - "int8": {Type: "int8"}, - "int16": {Type: "int16"}, - "int32": {Type: "int32"}, - "int64": {Type: "int64"}, - "uint": {Type: "uint"}, - "uint8": {Type: "uint8"}, - "uint16": {Type: "uint16"}, - "uint32": {Type: "uint32"}, - "uint64": {Type: "uint64"}, - }, - }, - Number: FormatMapping{ - Default: SimpleTypeSpec{Type: "float32"}, - Formats: map[string]SimpleTypeSpec{ - "float": {Type: "float32"}, - "double": {Type: "float64"}, - }, - }, - Boolean: FormatMapping{ - Default: SimpleTypeSpec{Type: "bool"}, - }, - String: FormatMapping{ - Default: SimpleTypeSpec{Type: "string"}, - Formats: map[string]SimpleTypeSpec{ - "byte": {Type: "[]byte"}, - "email": {Type: "Email", Template: "email.tmpl"}, - "date": {Type: "Date", Template: "date.tmpl"}, - "date-time": {Type: "time.Time", Import: "time"}, - "json": {Type: "json.RawMessage", Import: "encoding/json"}, - "uuid": {Type: "UUID", Template: "uuid.tmpl"}, - "binary": {Type: "File", Template: "file.tmpl"}, - }, - }, -} diff --git a/experimental/internal/codegen/typenames.go b/experimental/internal/codegen/typenames.go deleted file mode 100644 index c4d47b51af..0000000000 --- a/experimental/internal/codegen/typenames.go +++ /dev/null @@ -1 +0,0 @@ -package codegen diff --git a/scripts/foreach-module.sh b/scripts/foreach-module.sh deleted file mode 100755 index 7addae7825..0000000000 --- a/scripts/foreach-module.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -# Run a make target in each child go module whose go directive is compatible -# with the current Go toolchain. Modules requiring a newer Go are skipped. -# -# Usage: foreach-module.sh -set -euo pipefail - -target="${1:?usage: foreach-module.sh }" -cur_go="$(go env GOVERSION | sed 's/^go//')" - -git ls-files '**/*go.mod' -z | while IFS= read -r -d '' modfile; do - mod_go="$(sed -n 's/^go *//p' "$modfile")" - moddir="$(dirname "$modfile")" - - if [ "$(printf '%s\n%s' "$mod_go" "$cur_go" | sort -V | head -1)" = "$mod_go" ]; then - (set -x; cd "$moddir" && env GOBIN="${GOBIN:-}" make "$target") - else - echo "Skipping $moddir: requires go $mod_go, have go $cur_go" - fi -done From 35a6e0c76d169aafd1a1f3b8a6ef0f1b17c0ab8d Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Sat, 7 Feb 2026 12:49:12 +0000 Subject: [PATCH 10/44] build: use a re-usable, single, workflow for running CI As a way to keep updates to the CI pipelines more straightforward, we can extract this out to a shared, versioned (and updated by Renovate) Action. We can make sure to keep our binary builds going, but only for the current versions of Go - other build failures will be caught by our shared Action. --- .github/workflows/ci.yml | 43 +++++++++-------------------- .github/workflows/generate.yml | 50 ---------------------------------- .github/workflows/lint.yml | 47 -------------------------------- .github/workflows/tidy.yml | 50 ---------------------------------- 4 files changed, 13 insertions(+), 177 deletions(-) delete mode 100644 .github/workflows/generate.yml delete mode 100644 .github/workflows/lint.yml delete mode 100644 .github/workflows/tidy.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeb0208241..7a7d578881 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,22 +1,22 @@ -name: Build project -on: [ push, pull_request ] - +name: CI +on: + push: {} + pull_request: {} + workflow_dispatch: {} permissions: contents: read - jobs: build: - name: Build + uses: oapi-codegen/actions/.github/workflows/ci.yml@b9f2c274c1c631e648931dbbcc1942c2b2027837 # v0.4.0 + + build-binaries: runs-on: ubuntu-latest strategy: - fail-fast: false - # perform matrix testing to give us an earlier insight into issues with different versions of supported major versions of Go + fail-fast: true matrix: version: - - "1.22" - - "1.23" - - "1.24" - - "1.25" + - "stable" + - "oldstable" steps: - name: Check out source code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -26,25 +26,8 @@ jobs: with: go-version: ${{ matrix.version }} - - name: Test - run: make test + - name: Build + run: go build ./cmd/oapi-codegen env: # A combination of our GitHub Actions setup, with the Go toolchain, leads to inconsistencies in calling `go env`, in particular with Go 1.21, where having (the default) `GOTOOLCHAIN=auto` results in build failures GOTOOLCHAIN: local - - - name: Build - run: go build ./cmd/oapi-codegen - - results: - if: ${{ always() }} - runs-on: ubuntu-latest - name: Check build results - needs: [build] - steps: - - run: | - result="${{ needs.build.result }}" - if [[ $result == "success" || $result == "skipped" ]]; then - exit 0 - else - exit 1 - fi diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml deleted file mode 100644 index 4a0e5db377..0000000000 --- a/.github/workflows/generate.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Ensure generated files are up-to-date -on: [ push, pull_request ] - -permissions: - contents: read - -jobs: - build: - name: Build - runs-on: ubuntu-latest - strategy: - fail-fast: false - # perform matrix testing to give us an earlier insight into issues with different versions of supported major versions of Go - matrix: - version: - - "1.22" - - "1.23" - - "1.24" - - "1.25" - steps: - - name: Check out source code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - - name: Set up Go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 - with: - go-version: ${{ matrix.version }} - - - name: Run `make generate` - run: make generate - env: - # A combination of our GitHub Actions setup, with the Go toolchain, leads to inconsistencies in calling `go env`, in particular with Go 1.21, where having (the default) `GOTOOLCHAIN=auto` results in build failures - GOTOOLCHAIN: local - - - name: Check for no untracked files - run: git status && git diff-index --exit-code -p HEAD -- - - results: - if: ${{ always() }} - runs-on: ubuntu-latest - name: Check generation results - needs: [build] - steps: - - run: | - result="${{ needs.build.result }}" - if [[ $result == "success" || $result == "skipped" ]]; then - exit 0 - else - exit 1 - fi diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 8246442b1e..0000000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Lint project -on: [push, pull_request] - -permissions: - contents: read - -jobs: - build: - name: Build - runs-on: ubuntu-latest - strategy: - fail-fast: false - # perform matrix testing to give us an earlier insight into issues with different versions of supported major versions of Go - matrix: - version: - - "1.22" - - "1.23" - - "1.24" - - "1.25" - steps: - - name: Check out source code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - - name: Set up Go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 - with: - go-version: ${{ matrix.version }} - - - name: Run `make lint-ci` - run: make lint-ci - env: - # A combination of our GitHub Actions setup, with the Go toolchain, leads to inconsistencies in calling `go env`, in particular with Go 1.21, where having (the default) `GOTOOLCHAIN=auto` results in build failures - GOTOOLCHAIN: local - - results: - if: ${{ always() }} - runs-on: ubuntu-latest - name: Check linting results - needs: [build] - steps: - - run: | - result="${{ needs.build.result }}" - if [[ $result == "success" || $result == "skipped" ]]; then - exit 0 - else - exit 1 - fi diff --git a/.github/workflows/tidy.yml b/.github/workflows/tidy.yml deleted file mode 100644 index 4d5e66a992..0000000000 --- a/.github/workflows/tidy.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Ensure `go mod tidy` has been run -on: [ push, pull_request ] - -permissions: - contents: read - -jobs: - build: - name: Build - runs-on: ubuntu-latest - strategy: - fail-fast: false - # perform matrix testing to give us an earlier insight into issues with different versions of supported major versions of Go - matrix: - version: - - "1.22" - - "1.23" - - "1.24" - - "1.25" - steps: - - name: Check out source code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - - name: Set up Go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 - with: - go-version: ${{ matrix.version }} - - - name: Install `tidied` - run: go install gitlab.com/jamietanna/tidied@latest - - - name: Check for no untracked files - run: make tidy-ci - env: - # A combination of our GitHub Actions setup, with the Go toolchain, leads to inconsistencies in calling `go env`, in particular with Go 1.21, where having (the default) `GOTOOLCHAIN=auto` results in build failures - GOTOOLCHAIN: local - - results: - if: ${{ always() }} - runs-on: ubuntu-latest - name: Check tidy results - needs: [build] - steps: - - run: | - result="${{ needs.build.result }}" - if [[ $result == "success" || $result == "skipped" ]]; then - exit 0 - else - exit 1 - fi From bae5e574935f19c744ac71766fc16a7312fab4bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:41:04 -0800 Subject: [PATCH 11/44] fix(deps): update module github.com/gofiber/fiber/v2 to v2.52.11 [security] (#2207) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- examples/go.mod | 12 ++++++------ examples/go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/examples/go.mod b/examples/go.mod index 7c9b783567..c3aa80b848 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -8,8 +8,8 @@ require ( github.com/getkin/kin-openapi v0.133.0 github.com/gin-gonic/gin v1.10.0 github.com/go-chi/chi/v5 v5.0.10 - github.com/gofiber/fiber/v2 v2.52.4 - github.com/google/uuid v1.5.0 + github.com/gofiber/fiber/v2 v2.52.11 + github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 github.com/labstack/echo/v4 v4.12.0 @@ -32,7 +32,7 @@ require ( github.com/CloudyKit/jet/v6 v6.2.0 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect - github.com/andybalholm/brotli v1.0.5 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bytedance/sonic v1.11.6 // indirect @@ -64,7 +64,7 @@ require ( github.com/kataras/pio v0.0.12 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect - github.com/klauspost/compress v1.17.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -77,7 +77,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/microcosm-cc/bluemonday v1.0.25 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -112,7 +112,7 @@ require ( golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.25.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.20.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.25.1 // indirect diff --git a/examples/go.sum b/examples/go.sum index 97e7e8ae43..a28b5aa86a 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -13,8 +13,8 @@ github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= -github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= @@ -79,8 +79,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM= -github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs= +github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -106,8 +106,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -139,8 +139,8 @@ github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= -github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= -github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -180,8 +180,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -379,8 +379,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= From 2ec5a320dab48900f092cb26c59d17d781aaf5c3 Mon Sep 17 00:00:00 2001 From: Yuichiro Tsuji Date: Tue, 10 Feb 2026 07:23:23 +0900 Subject: [PATCH 12/44] docs(extensions): correct links to examples (#1836) Co-authored-by: Yuichiro Tsuji From 54868c73f060f46a64c55346ee5aa6cc1497f0eb Mon Sep 17 00:00:00 2001 From: Richard Kosegi Date: Mon, 9 Feb 2026 23:31:24 +0100 Subject: [PATCH 13/44] docs: fix link to example (#1884) Signed-off-by: Richard Kosegi Co-authored-by: Marcin Romaszewicz From 61057963748a269c3d082146b6cd49aef522a9f7 Mon Sep 17 00:00:00 2001 From: Ula <46541238+ula@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:32:16 -0500 Subject: [PATCH 14/44] Fixes type collision for enum values that start with _ (underscore) (#1438) Co-authored-by: Ula Co-authored-by: Marcin Romaszewicz --- pkg/codegen/utils_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/codegen/utils_test.go b/pkg/codegen/utils_test.go index af71438473..0666fc9909 100644 --- a/pkg/codegen/utils_test.go +++ b/pkg/codegen/utils_test.go @@ -604,6 +604,7 @@ func TestSchemaNameToTypeName(t *testing.T) { "=3": "Equal3", "#Tag": "HashTag", ".com": "DotCom", + "_1": "Underscore1", ">=": "GreaterThanEqual", "<=": "LessThanEqual", "<": "LessThan", From e8ada7a95863f9b630aaa839c66a12735113498c Mon Sep 17 00:00:00 2001 From: ShouheiNishi <96609867+ShouheiNishi@users.noreply.github.com> Date: Tue, 10 Feb 2026 07:34:12 +0900 Subject: [PATCH 15/44] Fix broken code when response content is not exist, but response headers are exist in Iris strict server. (#1411) Co-authored-by: Marcin Romaszewicz --- pkg/codegen/templates/strict/strict-iris-interface.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/codegen/templates/strict/strict-iris-interface.tmpl b/pkg/codegen/templates/strict/strict-iris-interface.tmpl index e93fdf6166..5ebbc4e1b3 100644 --- a/pkg/codegen/templates/strict/strict-iris-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-iris-interface.tmpl @@ -128,7 +128,7 @@ {{end -}} func (response {{$opid}}{{$statusCode}}Response) Visit{{$opid}}Response(ctx iris.Context) error { {{range $headers -}} - ctx.Response().Header.Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) + ctx.ResponseWriter().Header().Set("{{.Name}}", fmt.Sprint(response.Headers.{{.GoName}})) {{end -}} ctx.StatusCode({{if $fixedStatusCode}}{{$statusCode}}{{else}}response.StatusCode{{end}}) return nil From 5bde24dc8f18713a0dad0ea24657289f0c5c9b9f Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Mon, 9 Feb 2026 20:27:26 -0800 Subject: [PATCH 16/44] chore: readme update (#2209) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7bac0d2e98..a81ccc92d1 100644 --- a/README.md +++ b/README.md @@ -394,6 +394,8 @@ We can see that this provides the best means to focus on the implementation of t - Support multiple OpenAPI files by having a package-per-OpenAPI file - Support of OpenAPI 3.0 - OpenAPI 3.1 support is [awaiting upstream support](https://github.com/oapi-codegen/oapi-codegen/issues/373) + However, we do have an experimental version using a different OpenAPI parser which does support 3.1 and 3.2, which + you can play around with in our [experimental repo](https://github.com/oapi-codegen/oapi-codegen-exp/tree/main/experimental) - Note that this does not include OpenAPI 2.0 (aka Swagger) - Extract parameters from requests, to reduce work required by your implementation - Implicit `additionalProperties` are ignored by default ([more details](#additional-properties-additionalproperties)) From 5f38641c2bea522153b04d95069629489cf20cfb Mon Sep 17 00:00:00 2001 From: Alexey Boltunov <42418759+getBolted@users.noreply.github.com> Date: Tue, 10 Feb 2026 19:11:17 +0200 Subject: [PATCH 17/44] Adopt fiber middleware template for updated GetReqHeaders() method signature (#1419) * hotfix: - Bump fiber version to 2.52.0 - Adopted middleware template for fiber to handle new GetReqHeader() method, that has changed signature in fiber 2.50.0 (https://github.com/gofiber/fiber/releases/tag/v2.50.0) * Use latest fiber, and fix go deps The latest Fiber requires Go 1.24, therefore, we have to increase the version in the modules which use it. This is constrained to tests and examples, so it doesn't affect the main repo. Go 1.24 can't compile the version of golang.org/x/tools which we were using, so update that as well. * fix: use valueList[0] for fiber header IsPassThrough case The merge with upstream/main resolved a conflict in the fiber middleware template's header IsPassThrough handler by taking upstream's version, which still used the old single-string `value` variable. This is incorrect because fiber 2.50.0+ changed GetReqHeaders() to return map[string][]string. Fix by using valueList[0] to match the rest of the header handling block. Co-Authored-By: Claude Opus 4.6 * Tidy up modules This seems to be about the minimal set of changes to have everything build and test cleanly. * fix: add Go 1.24 version guards to Makefiles The internal/test and examples modules now require Go 1.24+ in their go.mod files, but their Makefiles lacked version guards, causing CI failures on Go 1.22 and 1.23. Add execute-if-go-124 guards matching the pattern used by other Go 1.24+ modules. Also bump the strict-server/stdhttp Makefile guard from 1.22 to 1.24. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Alexey Boltunov Co-authored-by: Marcin Romaszewicz Co-authored-by: Claude Opus 4.6 --- examples/Makefile | 31 ++++++-- examples/authenticated-api/stdhttp/go.mod | 6 +- examples/authenticated-api/stdhttp/go.sum | 22 +++--- examples/extensions/xomitzero/go.mod | 6 +- examples/extensions/xomitzero/go.sum | 22 +++--- examples/go.mod | 29 ++++---- examples/go.sum | 59 ++++++++------- .../minimal-server/stdhttp-go-tool/go.mod | 6 +- .../minimal-server/stdhttp-go-tool/go.sum | 22 +++--- examples/minimal-server/stdhttp/go.mod | 6 +- examples/minimal-server/stdhttp/go.sum | 22 +++--- .../go.mod | 6 +- .../go.sum | 22 +++--- examples/petstore-expanded/stdhttp/go.mod | 6 +- examples/petstore-expanded/stdhttp/go.sum | 22 +++--- go.mod | 6 +- go.sum | 22 +++--- internal/test/Makefile | 31 ++++++-- internal/test/go.mod | 35 +++++---- internal/test/go.sum | 71 +++++++++---------- .../test/strict-server/fiber/server.gen.go | 16 +++-- internal/test/strict-server/stdhttp/Makefile | 18 ++--- internal/test/strict-server/stdhttp/go.mod | 12 ++-- internal/test/strict-server/stdhttp/go.sum | 30 ++++---- .../templates/fiber/fiber-middleware.tmpl | 12 ++-- 25 files changed, 299 insertions(+), 241 deletions(-) diff --git a/examples/Makefile b/examples/Makefile index 5ec0edd058..bb37d63394 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -1,17 +1,36 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + lint: - $(GOBIN)/golangci-lint run ./... + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) lint-ci: - $(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m + + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) generate: - go generate ./... + $(call execute-if-go-124,go generate ./...) test: - go test -cover ./... + $(call execute-if-go-124,go test -cover ./...) tidy: - go mod tidy + $(call execute-if-go-124,go mod tidy) tidy-ci: - tidied -verbose + $(call execute-if-go-124,tidied -verbose) diff --git a/examples/authenticated-api/stdhttp/go.mod b/examples/authenticated-api/stdhttp/go.mod index 2ca2945993..3a2081ba2a 100644 --- a/examples/authenticated-api/stdhttp/go.mod +++ b/examples/authenticated-api/stdhttp/go.mod @@ -39,10 +39,10 @@ require ( github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect golang.org/x/crypto v0.22.0 // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/sync v0.9.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/text v0.20.0 // indirect - golang.org/x/tools v0.25.1 // indirect + golang.org/x/tools v0.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/examples/authenticated-api/stdhttp/go.sum b/examples/authenticated-api/stdhttp/go.sum index d38b743f8d..a6e130e447 100644 --- a/examples/authenticated-api/stdhttp/go.sum +++ b/examples/authenticated-api/stdhttp/go.sum @@ -37,6 +37,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= @@ -137,8 +139,8 @@ golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -151,15 +153,15 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -180,8 +182,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -202,8 +204,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.25.1 h1:YeIyhd0M7gStYR9jb2IFXVVT+QJhgXu1ZECOuRwofh4= -golang.org/x/tools v0.25.1/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/examples/extensions/xomitzero/go.mod b/examples/extensions/xomitzero/go.mod index e7b7295e3e..6b1e3678b8 100644 --- a/examples/extensions/xomitzero/go.mod +++ b/examples/extensions/xomitzero/go.mod @@ -26,10 +26,10 @@ require ( github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/sync v0.9.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/text v0.20.0 // indirect - golang.org/x/tools v0.25.1 // indirect + golang.org/x/tools v0.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/examples/extensions/xomitzero/go.sum b/examples/extensions/xomitzero/go.sum index 2319ae885e..fdce2066b1 100644 --- a/examples/extensions/xomitzero/go.sum +++ b/examples/extensions/xomitzero/go.sum @@ -32,6 +32,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -95,8 +97,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -104,13 +106,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -125,8 +127,8 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -138,8 +140,8 @@ golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.25.1 h1:YeIyhd0M7gStYR9jb2IFXVVT+QJhgXu1ZECOuRwofh4= -golang.org/x/tools v0.25.1/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/examples/go.mod b/examples/go.mod index c3aa80b848..011b9ad67a 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -1,6 +1,6 @@ module github.com/oapi-codegen/oapi-codegen/v2/examples -go 1.22.5 +go 1.24.0 replace github.com/oapi-codegen/oapi-codegen/v2 => ../ @@ -32,11 +32,12 @@ require ( github.com/CloudyKit/jet/v6 v6.2.0 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect - github.com/andybalholm/brotli v1.1.0 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.6.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -64,7 +65,7 @@ require ( github.com/kataras/pio v0.0.12 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect - github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -75,9 +76,9 @@ require ( github.com/lestrrat-go/option v1.0.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect github.com/microcosm-cc/bluemonday v1.0.25 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -88,7 +89,6 @@ require ( github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.4.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect @@ -99,23 +99,22 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/fasthttp v1.69.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/valyala/tcplisten v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.29.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.25.1 // indirect + golang.org/x/tools v0.39.0 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/examples/go.sum b/examples/go.sum index a28b5aa86a..0f5c1ca023 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -13,8 +13,8 @@ github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= @@ -27,6 +27,8 @@ github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= +github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= @@ -139,8 +141,8 @@ github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -175,13 +177,12 @@ github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqA github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -235,9 +236,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -284,12 +282,10 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= -github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= +github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= @@ -304,6 +300,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= @@ -323,8 +321,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -332,8 +330,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -347,16 +345,16 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -375,12 +373,11 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -391,8 +388,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -402,8 +399,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.25.1 h1:YeIyhd0M7gStYR9jb2IFXVVT+QJhgXu1ZECOuRwofh4= -golang.org/x/tools v0.25.1/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/examples/minimal-server/stdhttp-go-tool/go.mod b/examples/minimal-server/stdhttp-go-tool/go.mod index 7c900939fb..de08a03c54 100644 --- a/examples/minimal-server/stdhttp-go-tool/go.mod +++ b/examples/minimal-server/stdhttp-go-tool/go.mod @@ -22,10 +22,10 @@ require ( github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/sync v0.9.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/text v0.20.0 // indirect - golang.org/x/tools v0.25.1 // indirect + golang.org/x/tools v0.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/examples/minimal-server/stdhttp-go-tool/go.sum b/examples/minimal-server/stdhttp-go-tool/go.sum index 2319ae885e..fdce2066b1 100644 --- a/examples/minimal-server/stdhttp-go-tool/go.sum +++ b/examples/minimal-server/stdhttp-go-tool/go.sum @@ -32,6 +32,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -95,8 +97,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -104,13 +106,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -125,8 +127,8 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -138,8 +140,8 @@ golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.25.1 h1:YeIyhd0M7gStYR9jb2IFXVVT+QJhgXu1ZECOuRwofh4= -golang.org/x/tools v0.25.1/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/examples/minimal-server/stdhttp/go.mod b/examples/minimal-server/stdhttp/go.mod index 9661c9998f..f1ef1ba7b4 100644 --- a/examples/minimal-server/stdhttp/go.mod +++ b/examples/minimal-server/stdhttp/go.mod @@ -21,10 +21,10 @@ require ( github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/sync v0.9.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/text v0.20.0 // indirect - golang.org/x/tools v0.25.1 // indirect + golang.org/x/tools v0.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/examples/minimal-server/stdhttp/go.sum b/examples/minimal-server/stdhttp/go.sum index 2319ae885e..fdce2066b1 100644 --- a/examples/minimal-server/stdhttp/go.sum +++ b/examples/minimal-server/stdhttp/go.sum @@ -32,6 +32,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -95,8 +97,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -104,13 +106,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -125,8 +127,8 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -138,8 +140,8 @@ golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.25.1 h1:YeIyhd0M7gStYR9jb2IFXVVT+QJhgXu1ZECOuRwofh4= -golang.org/x/tools v0.25.1/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/examples/output-options/preferskipoptionalpointerwithomitzero/go.mod b/examples/output-options/preferskipoptionalpointerwithomitzero/go.mod index f3b0a6cfe8..6f75dac750 100644 --- a/examples/output-options/preferskipoptionalpointerwithomitzero/go.mod +++ b/examples/output-options/preferskipoptionalpointerwithomitzero/go.mod @@ -26,10 +26,10 @@ require ( github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/sync v0.9.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/text v0.20.0 // indirect - golang.org/x/tools v0.25.1 // indirect + golang.org/x/tools v0.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/examples/output-options/preferskipoptionalpointerwithomitzero/go.sum b/examples/output-options/preferskipoptionalpointerwithomitzero/go.sum index 2319ae885e..fdce2066b1 100644 --- a/examples/output-options/preferskipoptionalpointerwithomitzero/go.sum +++ b/examples/output-options/preferskipoptionalpointerwithomitzero/go.sum @@ -32,6 +32,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -95,8 +97,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -104,13 +106,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -125,8 +127,8 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -138,8 +140,8 @@ golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.25.1 h1:YeIyhd0M7gStYR9jb2IFXVVT+QJhgXu1ZECOuRwofh4= -golang.org/x/tools v0.25.1/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/examples/petstore-expanded/stdhttp/go.mod b/examples/petstore-expanded/stdhttp/go.mod index c1ef1e0491..536fc339df 100644 --- a/examples/petstore-expanded/stdhttp/go.mod +++ b/examples/petstore-expanded/stdhttp/go.mod @@ -32,10 +32,10 @@ require ( github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/sync v0.9.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/text v0.20.0 // indirect - golang.org/x/tools v0.25.1 // indirect + golang.org/x/tools v0.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/examples/petstore-expanded/stdhttp/go.sum b/examples/petstore-expanded/stdhttp/go.sum index 458fbf7291..7bf494f54d 100644 --- a/examples/petstore-expanded/stdhttp/go.sum +++ b/examples/petstore-expanded/stdhttp/go.sum @@ -36,6 +36,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -112,8 +114,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -121,13 +123,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -142,8 +144,8 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -155,8 +157,8 @@ golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.25.1 h1:YeIyhd0M7gStYR9jb2IFXVVT+QJhgXu1ZECOuRwofh4= -golang.org/x/tools v0.25.1/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/go.mod b/go.mod index 9431a8d978..12e391a6ee 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ require ( github.com/getkin/kin-openapi v0.133.0 github.com/speakeasy-api/openapi-overlay v0.10.2 github.com/stretchr/testify v1.11.1 - golang.org/x/mod v0.21.0 + golang.org/x/mod v0.23.0 golang.org/x/text v0.20.0 - golang.org/x/tools v0.25.1 + golang.org/x/tools v0.30.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -29,5 +29,5 @@ require ( github.com/ugorji/go/codec v1.2.11 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect - golang.org/x/sync v0.9.0 // indirect + golang.org/x/sync v0.11.0 // indirect ) diff --git a/go.sum b/go.sum index 2319ae885e..fdce2066b1 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -95,8 +97,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -104,13 +106,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -125,8 +127,8 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -138,8 +140,8 @@ golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.25.1 h1:YeIyhd0M7gStYR9jb2IFXVVT+QJhgXu1ZECOuRwofh4= -golang.org/x/tools v0.25.1/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/test/Makefile b/internal/test/Makefile index 5ec0edd058..bb37d63394 100644 --- a/internal/test/Makefile +++ b/internal/test/Makefile @@ -1,17 +1,36 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + lint: - $(GOBIN)/golangci-lint run ./... + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) lint-ci: - $(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m + + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) generate: - go generate ./... + $(call execute-if-go-124,go generate ./...) test: - go test -cover ./... + $(call execute-if-go-124,go test -cover ./...) tidy: - go mod tidy + $(call execute-if-go-124,go mod tidy) tidy-ci: - tidied -verbose + $(call execute-if-go-124,tidied -verbose) diff --git a/internal/test/go.mod b/internal/test/go.mod index 10ef9b25ed..6d321d86bf 100644 --- a/internal/test/go.mod +++ b/internal/test/go.mod @@ -1,6 +1,6 @@ module github.com/oapi-codegen/oapi-codegen/v2/internal/test -go 1.22.5 +go 1.24.0 replace github.com/oapi-codegen/oapi-codegen/v2 => ../../ @@ -8,8 +8,8 @@ require ( github.com/getkin/kin-openapi v0.133.0 github.com/gin-gonic/gin v1.9.1 github.com/go-chi/chi/v5 v5.0.10 - github.com/gofiber/fiber/v2 v2.49.1 - github.com/google/uuid v1.4.0 + github.com/gofiber/fiber/v2 v2.52.11 + github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 github.com/labstack/echo/v4 v4.11.3 @@ -27,12 +27,13 @@ require ( github.com/CloudyKit/jet/v6 v6.2.0 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect - github.com/andybalholm/brotli v1.0.5 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bytedance/sonic v1.10.0-rc3 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.0 // indirect + github.com/clipperhouse/uax29/v2 v2.6.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/fatih/structs v1.1.0 // indirect @@ -57,15 +58,15 @@ require ( github.com/kataras/pio v0.0.12 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect - github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/labstack/gommon v0.4.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect github.com/microcosm-cc/bluemonday v1.0.25 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -75,7 +76,6 @@ require ( github.com/pelletier/go-toml/v2 v2.0.9 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.4.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect @@ -87,23 +87,22 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.49.0 // indirect + github.com/valyala/fasthttp v1.69.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/valyala/tcplisten v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/arch v0.4.0 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.29.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.25.1 // indirect + golang.org/x/tools v0.39.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/internal/test/go.sum b/internal/test/go.sum index ca696cc621..336b745da8 100644 --- a/internal/test/go.sum +++ b/internal/test/go.sum @@ -13,8 +13,8 @@ github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= -github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= @@ -33,6 +33,8 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= +github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -78,8 +80,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/gofiber/fiber/v2 v2.49.1 h1:0W2DRWevSirc8pJl4o8r8QejDR8TV6ZUCawHxwbIdOk= -github.com/gofiber/fiber/v2 v2.49.1/go.mod h1:nPUeEBUeeYGgwbDm59Gp7vS8MDyScL6ezr/Np9A13WU= +github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs= +github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -105,8 +107,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -138,8 +140,8 @@ github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= -github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -162,14 +164,13 @@ github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNf github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -213,9 +214,6 @@ github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -263,13 +261,11 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.49.0 h1:9FdvCpmxB74LH4dPb7IJ1cOSsluR07XG3I1txXWwJpE= -github.com/valyala/fasthttp v1.49.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= +github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= @@ -284,6 +280,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= @@ -300,12 +298,12 @@ golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -315,14 +313,14 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -342,27 +340,26 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.25.1 h1:YeIyhd0M7gStYR9jb2IFXVVT+QJhgXu1ZECOuRwofh4= -golang.org/x/tools v0.25.1/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/test/strict-server/fiber/server.gen.go b/internal/test/strict-server/fiber/server.gen.go index b15d923fd0..edf81be1fb 100644 --- a/internal/test/strict-server/fiber/server.gen.go +++ b/internal/test/strict-server/fiber/server.gen.go @@ -151,10 +151,14 @@ func (siw *ServerInterfaceWrapper) HeadersExample(c *fiber.Ctx) error { headers := c.GetReqHeaders() // ------------- Required header parameter "header1" ------------- - if value, found := headers[http.CanonicalHeaderKey("header1")]; found { + if valueList, found := headers[http.CanonicalHeaderKey("header1")]; found { var Header1 string + n := len(valueList) + if n != 1 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Too many values for ParamName header1, 1 is required, but %d found", n)) + } - err = runtime.BindStyledParameterWithOptions("simple", "header1", value, &Header1, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true}) + err = runtime.BindStyledParameterWithOptions("simple", "header1", valueList[0], &Header1, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true}) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter header1: %w", err).Error()) } @@ -167,10 +171,14 @@ func (siw *ServerInterfaceWrapper) HeadersExample(c *fiber.Ctx) error { } // ------------- Optional header parameter "header2" ------------- - if value, found := headers[http.CanonicalHeaderKey("header2")]; found { + if valueList, found := headers[http.CanonicalHeaderKey("header2")]; found { var Header2 int + n := len(valueList) + if n != 1 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Too many values for ParamName header2, 1 is required, but %d found", n)) + } - err = runtime.BindStyledParameterWithOptions("simple", "header2", value, &Header2, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + err = runtime.BindStyledParameterWithOptions("simple", "header2", valueList[0], &Header2, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter header2: %w", err).Error()) } diff --git a/internal/test/strict-server/stdhttp/Makefile b/internal/test/strict-server/stdhttp/Makefile index 48fe768e30..bb37d63394 100644 --- a/internal/test/strict-server/stdhttp/Makefile +++ b/internal/test/strict-server/stdhttp/Makefile @@ -6,31 +6,31 @@ RESET := \e[0;0m GOVER := $(shell go env GOVERSION) GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") -define execute-if-go-122 +define execute-if-go-124 @{ \ -if [[ 22 -le $(GOMINOR) ]]; then \ +if [[ 24 -le $(GOMINOR) ]]; then \ $1; \ else \ - echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.22, which this module requires$(RESET)"; \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ fi \ } endef lint: - $(call execute-if-go-122,$(GOBIN)/golangci-lint run ./...) + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) lint-ci: - $(call execute-if-go-122,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) generate: - $(call execute-if-go-122,go generate ./...) + $(call execute-if-go-124,go generate ./...) test: - $(call execute-if-go-122,go test -cover ./...) + $(call execute-if-go-124,go test -cover ./...) tidy: - $(call execute-if-go-122,go mod tidy) + $(call execute-if-go-124,go mod tidy) tidy-ci: - $(call execute-if-go-122,tidied -verbose) + $(call execute-if-go-124,tidied -verbose) diff --git a/internal/test/strict-server/stdhttp/go.mod b/internal/test/strict-server/stdhttp/go.mod index 5d3c8860d2..cdd0f48f20 100644 --- a/internal/test/strict-server/stdhttp/go.mod +++ b/internal/test/strict-server/stdhttp/go.mod @@ -1,6 +1,6 @@ module github.com/oapi-codegen/oapi-codegen/v2/internal/test/strict-server/stdhttp -go 1.22.5 +go 1.24.0 replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../../ @@ -21,7 +21,7 @@ require ( github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/google/uuid v1.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect @@ -33,10 +33,10 @@ require ( github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/text v0.20.0 // indirect - golang.org/x/tools v0.25.1 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/tools v0.39.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/test/strict-server/stdhttp/go.sum b/internal/test/strict-server/stdhttp/go.sum index 7fe4b88d07..c5c399b374 100644 --- a/internal/test/strict-server/stdhttp/go.sum +++ b/internal/test/strict-server/stdhttp/go.sum @@ -36,9 +36,11 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -108,8 +110,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -117,13 +119,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -138,21 +140,21 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.25.1 h1:YeIyhd0M7gStYR9jb2IFXVVT+QJhgXu1ZECOuRwofh4= -golang.org/x/tools v0.25.1/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/codegen/templates/fiber/fiber-middleware.tmpl b/pkg/codegen/templates/fiber/fiber-middleware.tmpl index e44f4836cb..3213e22a7d 100644 --- a/pkg/codegen/templates/fiber/fiber-middleware.tmpl +++ b/pkg/codegen/templates/fiber/fiber-middleware.tmpl @@ -89,22 +89,26 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *fiber.Ctx) error { headers := c.GetReqHeaders() {{range .HeaderParams}}// ------------- {{if .Required}}Required{{else}}Optional{{end}} header parameter "{{.ParamName}}" ------------- - if value, found := headers[http.CanonicalHeaderKey("{{.ParamName}}")]; found { + 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}} - params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}valueList[0] {{end}} {{if .IsJson}} - err = json.Unmarshal([]byte(value), &{{.GoName}}) + 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}}", value, &{{.GoName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: {{.Explode}}, Required: {{.Required}}}) + err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", valueList[0], &{{.GoName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: {{.Explode}}, Required: {{.Required}}}) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter {{.ParamName}}: %w", err).Error()) } From 13eb5ac37e1919b8860e9d3a223e468ae1d7ddc8 Mon Sep 17 00:00:00 2001 From: Mehmet Gurevin Date: Tue, 10 Feb 2026 21:22:44 +0300 Subject: [PATCH 18/44] fixed duplicate type names (#200) * fixed duplicate type names * add typename dedup functions * Fixup: use full-word suffixes and add regression test for issue #200 - Rename auto-dedup suffixes to use full words: Parameter, Response, RequestBody (instead of Param, Resp, ReqBody) - Add internal/test/issues/issue-200/ with spec, config, generated code, and a compile-time regression test that instantiates every expected type Co-Authored-By: Claude Opus 4.6 * Gate duplicate type name resolution behind output-options config flag Add ResolveTypeNameCollisions bool to OutputOptions and the JSON schema. When false (the default), the codegen errors on duplicate type names as before. When true, FixDuplicateTypeNames auto-renames colliding types. Also cleans up ComponentType: removes unused constants, improves doc. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Marcin Romaszewicz Co-authored-by: Claude Opus 4.6 --- configuration-schema.json | 5 ++ internal/test/issues/issue-200/config.yaml | 7 ++ internal/test/issues/issue-200/doc.go | 3 + .../test/issues/issue-200/issue200.gen.go | 49 +++++++++++ .../test/issues/issue-200/issue200_test.go | 48 +++++++++++ internal/test/issues/issue-200/spec.yaml | 80 ++++++++++++++++++ pkg/codegen/codegen.go | 15 +++- pkg/codegen/configuration.go | 8 ++ pkg/codegen/schema.go | 25 +++++- pkg/codegen/utils.go | 83 +++++++++++++++++++ 10 files changed, 317 insertions(+), 6 deletions(-) create mode 100644 internal/test/issues/issue-200/config.yaml create mode 100644 internal/test/issues/issue-200/doc.go create mode 100644 internal/test/issues/issue-200/issue200.gen.go create mode 100644 internal/test/issues/issue-200/issue200_test.go create mode 100644 internal/test/issues/issue-200/spec.yaml diff --git a/configuration-schema.json b/configuration-schema.json index 9047914479..8ff58d94b6 100644 --- a/configuration-schema.json +++ b/configuration-schema.json @@ -252,6 +252,11 @@ "type": "boolean", "description": "Allows disabling the generation of an 'optional pointer' for an optional field that is a container type (such as a slice or a map), which ends up requiring an additional, unnecessary, `... != nil` check. A field can set `x-go-type-skip-optional-pointer: false` to still require the optional pointer.", "default": false + }, + "resolve-type-name-collisions": { + "type": "boolean", + "description": "When set to true, automatically renames types that collide across different OpenAPI component sections (schemas, parameters, requestBodies, responses, headers) by appending a suffix based on the component section (e.g., 'Parameter', 'Response', 'RequestBody'). Without this, the codegen will error on duplicate type names, requiring manual resolution via x-go-name.", + "default": false } } }, diff --git a/internal/test/issues/issue-200/config.yaml b/internal/test/issues/issue-200/config.yaml new file mode 100644 index 0000000000..d68804c987 --- /dev/null +++ b/internal/test/issues/issue-200/config.yaml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=../../../../configuration-schema.json +package: issue200 +generate: + models: true +output: issue200.gen.go +output-options: + resolve-type-name-collisions: true diff --git a/internal/test/issues/issue-200/doc.go b/internal/test/issues/issue-200/doc.go new file mode 100644 index 0000000000..733ebfce17 --- /dev/null +++ b/internal/test/issues/issue-200/doc.go @@ -0,0 +1,3 @@ +package issue200 + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-200/issue200.gen.go b/internal/test/issues/issue-200/issue200.gen.go new file mode 100644 index 0000000000..cc3c138314 --- /dev/null +++ b/internal/test/issues/issue-200/issue200.gen.go @@ -0,0 +1,49 @@ +// Package issue200 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package issue200 + +// Bar defines model for Bar. +type Bar struct { + Value *string `json:"value,omitempty"` +} + +// Bar2 defines model for Bar2. +type Bar2 struct { + Value *float32 `json:"value,omitempty"` +} + +// BarParam defines model for BarParam. +type BarParam = []int + +// BarParam2 defines model for BarParam2. +type BarParam2 = []int + +// BarParameter defines model for Bar. +type BarParameter = string + +// BarResponse defines model for Bar. +type BarResponse struct { + Value1 *Bar `json:"value1,omitempty"` + Value2 *Bar2 `json:"value2,omitempty"` + Value3 *BarParam `json:"value3,omitempty"` + Value4 *BarParam2 `json:"value4,omitempty"` +} + +// BarRequestBody defines model for Bar. +type BarRequestBody struct { + Value *int `json:"value,omitempty"` +} + +// PostFooJSONBody defines parameters for PostFoo. +type PostFooJSONBody struct { + Value *int `json:"value,omitempty"` +} + +// PostFooParams defines parameters for PostFoo. +type PostFooParams struct { + Bar *Bar `form:"Bar,omitempty" json:"Bar,omitempty"` +} + +// PostFooJSONRequestBody defines body for PostFoo for application/json ContentType. +type PostFooJSONRequestBody PostFooJSONBody diff --git a/internal/test/issues/issue-200/issue200_test.go b/internal/test/issues/issue-200/issue200_test.go new file mode 100644 index 0000000000..9f690d520c --- /dev/null +++ b/internal/test/issues/issue-200/issue200_test.go @@ -0,0 +1,48 @@ +package issue200 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestDuplicateTypeNamesCompile verifies that when the same name "Bar" is used +// across components/schemas, components/parameters, components/responses, +// components/requestBodies, and components/headers, the codegen produces +// distinct, compilable types with component-based suffixes. +// +// If the auto-rename logic breaks, this test will fail to compile. +func TestDuplicateTypeNamesCompile(t *testing.T) { + // Schema type: Bar (no suffix, first definition wins) + _ = Bar{Value: ptr("hello")} + + // Schema types with unique names (no collision) + _ = Bar2{Value: ptr(float32(1.0))} + _ = BarParam([]int{1, 2, 3}) + _ = BarParam2([]int{4, 5, 6}) + + // Parameter type: BarParameter (was "Bar" in components/parameters) + _ = BarParameter("query-value") + + // Response type: BarResponse (was "Bar" in components/responses) + _ = BarResponse{ + Value1: &Bar{Value: ptr("v1")}, + Value2: &Bar2{Value: ptr(float32(2.0))}, + Value3: &BarParam{1}, + Value4: &BarParam2{2}, + } + + // RequestBody type: BarRequestBody (was "Bar" in components/requestBodies) + _ = BarRequestBody{Value: ptr(42)} + + // Operation-derived types + _ = PostFooParams{Bar: &Bar{}} + _ = PostFooJSONBody{Value: ptr(99)} + _ = PostFooJSONRequestBody{Value: ptr(100)} + + assert.True(t, true, "all duplicate-named types resolved and compiled") +} + +func ptr[T any](v T) *T { + return &v +} diff --git a/internal/test/issues/issue-200/spec.yaml b/internal/test/issues/issue-200/spec.yaml new file mode 100644 index 0000000000..a18c7f403c --- /dev/null +++ b/internal/test/issues/issue-200/spec.yaml @@ -0,0 +1,80 @@ +openapi: 3.0.1 + +info: + title: "Duplicate type names test" + version: 0.0.0 + +paths: + /foo: + post: + operationId: postFoo + parameters: + - $ref: '#/components/parameters/Bar' + requestBody: + $ref: '#/components/requestBodies/Bar' + responses: + 200: + $ref: '#/components/responses/Bar' + +components: + schemas: + Bar: + type: object + properties: + value: + type: string + Bar2: + type: object + properties: + value: + type: number + BarParam: + type: array + items: + type: integer + BarParam2: + type: array + items: + type: integer + + headers: + Bar: + schema: + type: boolean + + parameters: + Bar: + name: Bar + in: query + schema: + type: string + + requestBodies: + Bar: + content: + application/json: + schema: + type: object + properties: + value: + type: integer + + responses: + Bar: + description: Bar response + headers: + X-Bar: + $ref: '#/components/headers/Bar' + content: + application/json: + schema: + type: object + properties: + value1: + $ref: '#/components/schemas/Bar' + value2: + $ref: '#/components/schemas/Bar2' + value3: + $ref: '#/components/schemas/BarParam' + value4: + $ref: '#/components/schemas/BarParam2' diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go index 04ac96cc66..279ed9d081 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -673,6 +673,8 @@ func GenerateTypesForResponses(t *template.Template, responses openapi3.Response return nil, fmt.Errorf("error making name for components/responses/%s: %w", responseName, err) } + goType.DefinedComp = ComponentTypeResponse + typeDef := TypeDefinition{ JsonName: responseName, Schema: goType, @@ -724,6 +726,8 @@ func GenerateTypesForRequestBodies(t *template.Template, bodies map[string]*open return nil, fmt.Errorf("error making name for components/schemas/%s: %w", requestBodyName, err) } + goType.DefinedComp = ComponentTypeRequestBody + typeDef := TypeDefinition{ JsonName: requestBodyName, Schema: goType, @@ -750,15 +754,18 @@ func GenerateTypes(t *template.Template, types []TypeDefinition) (string, error) m := map[string]TypeDefinition{} var ts []TypeDefinition + if globalState.options.OutputOptions.ResolveTypeNameCollisions { + types = FixDuplicateTypeNames(types) + } + for _, typ := range types { if prevType, found := m[typ.TypeName]; found { - // If type names collide, we need to see if they refer to the same - // exact type definition, in which case, we can de-dupe. If they don't - // match, we error out. + // If type names collide after auto-rename, we need to see if they + // refer to the same exact type definition, in which case, we can + // de-dupe. If they don't match, we error out. if TypeDefinitionsEquivalent(prevType, typ) { continue } - // We want to create an error when we try to define the same type twice. return "", fmt.Errorf("duplicate typename '%s' detected, can't auto-rename, "+ "please use x-go-name to specify your own name for one of them", typ.TypeName) } diff --git a/pkg/codegen/configuration.go b/pkg/codegen/configuration.go index 1d9ff3eaea..d3281fefec 100644 --- a/pkg/codegen/configuration.go +++ b/pkg/codegen/configuration.go @@ -300,6 +300,14 @@ type OutputOptions struct { // PreferSkipOptionalPointerOnContainerTypes allows disabling the generation of an "optional pointer" for an optional field that is a container type (such as a slice or a map), which ends up requiring an additional, unnecessary, `... != nil` check PreferSkipOptionalPointerOnContainerTypes bool `yaml:"prefer-skip-optional-pointer-on-container-types,omitempty"` + + // ResolveTypeNameCollisions, when set to true, automatically renames + // types that collide across different OpenAPI component sections + // (schemas, parameters, requestBodies, responses, headers) by appending + // a suffix based on the component section (e.g., "Parameter", "Response", + // "RequestBody"). Without this, the codegen will error on duplicate type + // names, requiring manual resolution via x-go-name. + ResolveTypeNameCollisions bool `yaml:"resolve-type-name-collisions,omitempty"` } func (oo OutputOptions) Validate() map[string]string { diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go index c099752d88..7435fa224d 100644 --- a/pkg/codegen/schema.go +++ b/pkg/codegen/schema.go @@ -39,8 +39,22 @@ type Schema struct { // The original OpenAPIv3 Schema. OAPISchema *openapi3.Schema + + DefinedComp ComponentType // Indicates which component section defined this type } +// ComponentType is used to keep track of where a given schema came from, in order +// to perform type name collision resolution. +type ComponentType int + +const ( + ComponentTypeSchema = iota + ComponentTypeParameter + ComponentTypeRequestBody + ComponentTypeResponse + ComponentTypeHeader +) + func (s Schema) IsRef() bool { return s.RefType != "" } @@ -311,6 +325,7 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) { Description: schema.Description, OAPISchema: schema, SkipOptionalPointer: skipOptionalPointer, + DefinedComp: ComponentTypeSchema, } // AllOf is interesting, and useful. It's the union of a number of other @@ -849,7 +864,9 @@ func paramToGoType(param *openapi3.Parameter, path []string) (Schema, error) { // We can process the schema through the generic schema processor if param.Schema != nil { - return GenerateGoSchema(param.Schema, path) + schema, err := GenerateGoSchema(param.Schema, path) + schema.DefinedComp = ComponentTypeParameter + return schema, err } // At this point, we have a content type. We know how to deal with @@ -859,6 +876,7 @@ func paramToGoType(param *openapi3.Parameter, path []string) (Schema, error) { return Schema{ GoType: "string", Description: StringToGoComment(param.Description), + DefinedComp: ComponentTypeParameter, }, nil } @@ -869,11 +887,14 @@ func paramToGoType(param *openapi3.Parameter, path []string) (Schema, error) { return Schema{ GoType: "string", Description: StringToGoComment(param.Description), + DefinedComp: ComponentTypeParameter, }, nil } // For json, we go through the standard schema mechanism - return GenerateGoSchema(mt.Schema, path) + schema, err := GenerateGoSchema(mt.Schema, path) + schema.DefinedComp = ComponentTypeParameter + return schema, err } func generateUnion(outSchema *Schema, elements openapi3.SchemaRefs, discriminator *openapi3.Discriminator, path []string) error { diff --git a/pkg/codegen/utils.go b/pkg/codegen/utils.go index 5326e672bf..691d887663 100644 --- a/pkg/codegen/utils.go +++ b/pkg/codegen/utils.go @@ -1112,3 +1112,86 @@ func sliceContains[E comparable](s []E, v E) bool { } return false } + +// FixDuplicateTypeNames renames duplicate type names. +func FixDuplicateTypeNames(typeDefs []TypeDefinition) []TypeDefinition { + if !hasDuplicatedTypeNames(typeDefs) { + return typeDefs + } + + // try to fix duplicate type names with their definition section + typeDefs = fixDuplicateTypeNamesWithCompName(typeDefs) + if !hasDuplicatedTypeNames(typeDefs) { + return typeDefs + } + + const maxIter = 100 + for i := 0; i < maxIter && hasDuplicatedTypeNames(typeDefs); i++ { + typeDefs = fixDuplicateTypeNamesDupCounts(typeDefs) + } + + if hasDuplicatedTypeNames(typeDefs) { + panic("too much duplicate type names") + } + + return typeDefs +} + +func hasDuplicatedTypeNames(typeDefs []TypeDefinition) bool { + dupCheck := make(map[string]int, len(typeDefs)) + + for _, d := range typeDefs { + dupCheck[d.TypeName]++ + + if dupCheck[d.TypeName] != 1 { + return true + } + } + + return false +} + +func fixDuplicateTypeNamesWithCompName(typeDefs []TypeDefinition) []TypeDefinition { + dupCheck := make(map[string]int, len(typeDefs)) + deDup := make([]TypeDefinition, len(typeDefs)) + + for i, d := range typeDefs { + dupCheck[d.TypeName]++ + + if dupCheck[d.TypeName] != 1 { + switch d.Schema.DefinedComp { + case ComponentTypeSchema: + d.TypeName += "Schema" + case ComponentTypeParameter: + d.TypeName += "Parameter" + case ComponentTypeRequestBody: + d.TypeName += "RequestBody" + case ComponentTypeResponse: + d.TypeName += "Response" + case ComponentTypeHeader: + d.TypeName += "Header" + } + } + + deDup[i] = d + } + + return deDup +} + +func fixDuplicateTypeNamesDupCounts(typeDefs []TypeDefinition) []TypeDefinition { + dupCheck := make(map[string]int, len(typeDefs)) + deDup := make([]TypeDefinition, len(typeDefs)) + + for i, d := range typeDefs { + dupCheck[d.TypeName]++ + + if dupCheck[d.TypeName] != 1 { + d.TypeName = d.TypeName + strconv.Itoa(dupCheck[d.TypeName]) + } + + deDup[i] = d + } + + return deDup +} From 15c1dafe763ea2cfc0b9be0c136a79fb33bca2d6 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Wed, 11 Feb 2026 20:53:30 -0800 Subject: [PATCH 19/44] refactor(internal): move Fiber tests into their own modules (#2212) * Fiber deps in their own modules Move the fiber dependences to their own modules, to contain Go 1.24 to the minimal subset of our code. Update make rules to exclude Go 1.24 tests on Go < 1.24 Update Readme with minimum Go version for each router. * Update Fiber in test issue Contain Fiber to its own module in test for Issue 1469 --- README.md | 28 ++- examples/Makefile | 31 +-- examples/go.mod | 27 +-- examples/go.sum | 54 ++--- examples/minimal-server/fiber/Makefile | 36 +++ examples/minimal-server/fiber/go.mod | 44 ++++ examples/minimal-server/fiber/go.sum | 198 +++++++++++++++ examples/petstore-expanded/fiber/Makefile | 36 +++ examples/petstore-expanded/fiber/go.mod | 53 ++++ examples/petstore-expanded/fiber/go.sum | 212 ++++++++++++++++ internal/test/Makefile | 31 +-- internal/test/go.mod | 30 +-- internal/test/go.sum | 60 ++--- internal/test/issues/issue1469/Makefile | 36 +++ internal/test/issues/issue1469/go.mod | 49 ++++ internal/test/issues/issue1469/go.sum | 198 +++++++++++++++ internal/test/strict-server/fiber/Makefile | 36 +++ .../strict-server/fiber/fiber_strict_test.go | 229 ++++++++++++++++++ internal/test/strict-server/fiber/go.mod | 55 +++++ internal/test/strict-server/fiber/go.sum | 210 ++++++++++++++++ internal/test/strict-server/stdhttp/Makefile | 18 +- internal/test/strict-server/stdhttp/go.mod | 12 +- internal/test/strict-server/stdhttp/go.sum | 28 +-- internal/test/strict-server/strict_test.go | 11 - 24 files changed, 1529 insertions(+), 193 deletions(-) create mode 100644 examples/minimal-server/fiber/Makefile create mode 100644 examples/minimal-server/fiber/go.mod create mode 100644 examples/minimal-server/fiber/go.sum create mode 100644 examples/petstore-expanded/fiber/Makefile create mode 100644 examples/petstore-expanded/fiber/go.mod create mode 100644 examples/petstore-expanded/fiber/go.sum create mode 100644 internal/test/issues/issue1469/Makefile create mode 100644 internal/test/issues/issue1469/go.mod create mode 100644 internal/test/issues/issue1469/go.sum create mode 100644 internal/test/strict-server/fiber/Makefile create mode 100644 internal/test/strict-server/fiber/fiber_strict_test.go create mode 100644 internal/test/strict-server/fiber/go.mod create mode 100644 internal/test/strict-server/fiber/go.sum diff --git a/README.md b/README.md index a81ccc92d1..40c1853278 100644 --- a/README.md +++ b/README.md @@ -423,6 +423,9 @@ Server generate flag to enable code generation +Required Go Version + + Example usage @@ -437,6 +440,9 @@ Example usage chi-server +1.22+ + + For a Chi server, you will want a configuration file such as: @@ -465,6 +471,9 @@ To implement this, check out [the Chi docs](#impl-chi). echo-server +1.22+ + + For an Echo server, you will want a configuration file such as: @@ -491,7 +500,9 @@ To implement this, check out [the Echo docs](#impl-echo). fiber-server - + +1.24+ + For a Fiber server, you will want a configuration file such as: @@ -521,6 +532,9 @@ To implement this, check out [the Fiber docs](#impl-fiber). gin-server +1.22+ + + For a Gin server, you will want a configuration file such as: @@ -548,7 +562,9 @@ To implement this, check out [the Gin docs](#impl-gin). gorilla-server - + +1.22+ + For a gorilla/mux server, you will want a configuration file such as: @@ -576,7 +592,9 @@ To implement this, check out [the gorilla/mux docs](#impl-gorillamux). iris-server - + +1.22+ + For a Iris server, you will want a configuration file such as: @@ -604,7 +622,9 @@ To implement this, check out [the Iris docs](#impl-iris). std-http-server - + +1.22+ + To use purely `net/http` (for Go 1.22+), you will want a configuration file such as: diff --git a/examples/Makefile b/examples/Makefile index bb37d63394..5ec0edd058 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -1,36 +1,17 @@ -SHELL:=/bin/bash - -YELLOW := \e[0;33m -RESET := \e[0;0m - -GOVER := $(shell go env GOVERSION) -GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") - -define execute-if-go-124 -@{ \ -if [[ 24 -le $(GOMINOR) ]]; then \ - $1; \ -else \ - echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ -fi \ -} -endef - lint: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + $(GOBIN)/golangci-lint run ./... lint-ci: - - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + $(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m generate: - $(call execute-if-go-124,go generate ./...) + go generate ./... test: - $(call execute-if-go-124,go test -cover ./...) + go test -cover ./... tidy: - $(call execute-if-go-124,go mod tidy) + go mod tidy tidy-ci: - $(call execute-if-go-124,tidied -verbose) + tidied -verbose diff --git a/examples/go.mod b/examples/go.mod index 011b9ad67a..89ce0af263 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -1,6 +1,6 @@ module github.com/oapi-codegen/oapi-codegen/v2/examples -go 1.24.0 +go 1.22.5 replace github.com/oapi-codegen/oapi-codegen/v2 => ../ @@ -8,14 +8,12 @@ require ( github.com/getkin/kin-openapi v0.133.0 github.com/gin-gonic/gin v1.10.0 github.com/go-chi/chi/v5 v5.0.10 - github.com/gofiber/fiber/v2 v2.52.11 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 github.com/labstack/echo/v4 v4.12.0 github.com/lestrrat-go/jwx v1.2.26 github.com/oapi-codegen/echo-middleware v1.0.2 - github.com/oapi-codegen/fiber-middleware v1.0.2 github.com/oapi-codegen/gin-middleware v1.0.2 github.com/oapi-codegen/iris-middleware v1.0.5 github.com/oapi-codegen/nethttp-middleware v1.0.2 @@ -32,12 +30,11 @@ require ( github.com/CloudyKit/jet/v6 v6.2.0 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect - github.com/andybalholm/brotli v1.2.0 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.6.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -65,7 +62,7 @@ require ( github.com/kataras/pio v0.0.12 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect - github.com/klauspost/compress v1.18.4 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -76,9 +73,8 @@ require ( github.com/lestrrat-go/option v1.0.1 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect github.com/microcosm-cc/bluemonday v1.0.25 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -99,7 +95,6 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.69.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect @@ -107,14 +102,14 @@ require ( github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.30.0 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/examples/go.sum b/examples/go.sum index 0f5c1ca023..d7a256ee0e 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -13,8 +13,8 @@ github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= -github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= @@ -27,8 +27,6 @@ github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= -github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= @@ -81,8 +79,6 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs= -github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -141,8 +137,8 @@ github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -177,12 +173,11 @@ github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqA github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -200,8 +195,6 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oapi-codegen/echo-middleware v1.0.2 h1:oNBqiE7jd/9bfGNk/bpbX2nqWrtPc+LL4Boya8Wl81U= github.com/oapi-codegen/echo-middleware v1.0.2/go.mod h1:5J6MFcGqrpWLXpbKGZtRPZViLIHyyyUHlkqg6dT2R4E= -github.com/oapi-codegen/fiber-middleware v1.0.2 h1:f4KPdjyRTYh2GyAv9wsDP+Q9akOND17wuMSbmMwDkJI= -github.com/oapi-codegen/fiber-middleware v1.0.2/go.mod h1:+lGj+802Ajp/+fQG9d8t1SuYP8r7lnOc6wnOwwRArYg= github.com/oapi-codegen/gin-middleware v1.0.2 h1:/H99UzvHQAUxXK8pzdcGAZgjCVeXdFDAUUWaJT0k0eI= github.com/oapi-codegen/gin-middleware v1.0.2/go.mod h1:2HJDQjH8jzK2/k/VKcWl+/T41H7ai2bKa6dN3AA2GpA= github.com/oapi-codegen/iris-middleware v1.0.5 h1:eO33pCvapaf1Xa0esEP0PYcdqPZSeq1eze4mamhT5hU= @@ -282,8 +275,6 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= -github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= @@ -300,8 +291,6 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= @@ -321,8 +310,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -330,8 +319,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -345,16 +334,16 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -373,11 +362,12 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -388,8 +378,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -399,8 +389,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/examples/minimal-server/fiber/Makefile b/examples/minimal-server/fiber/Makefile new file mode 100644 index 0000000000..bb37d63394 --- /dev/null +++ b/examples/minimal-server/fiber/Makefile @@ -0,0 +1,36 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + +generate: + $(call execute-if-go-124,go generate ./...) + +test: + $(call execute-if-go-124,go test -cover ./...) + +tidy: + $(call execute-if-go-124,go mod tidy) + +tidy-ci: + $(call execute-if-go-124,tidied -verbose) diff --git a/examples/minimal-server/fiber/go.mod b/examples/minimal-server/fiber/go.mod new file mode 100644 index 0000000000..58abd2fa35 --- /dev/null +++ b/examples/minimal-server/fiber/go.mod @@ -0,0 +1,44 @@ +module github.com/oapi-codegen/oapi-codegen/v2/examples/minimal-server/fiber + +go 1.24.0 + +replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../ + +tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen + +require github.com/gofiber/fiber/v2 v2.52.11 + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oapi-codegen/oapi-codegen/v2 v2.0.0-00010101000000-000000000000 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/speakeasy-api/jsonpath v0.6.0 // indirect + github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/tools v0.30.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/examples/minimal-server/fiber/go.sum b/examples/minimal-server/fiber/go.sum new file mode 100644 index 0000000000..e2954b1efc --- /dev/null +++ b/examples/minimal-server/fiber/go.sum @@ -0,0 +1,198 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs= +github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= +github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= +github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= +github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= +github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/petstore-expanded/fiber/Makefile b/examples/petstore-expanded/fiber/Makefile new file mode 100644 index 0000000000..bb37d63394 --- /dev/null +++ b/examples/petstore-expanded/fiber/Makefile @@ -0,0 +1,36 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + +generate: + $(call execute-if-go-124,go generate ./...) + +test: + $(call execute-if-go-124,go test -cover ./...) + +tidy: + $(call execute-if-go-124,go mod tidy) + +tidy-ci: + $(call execute-if-go-124,tidied -verbose) diff --git a/examples/petstore-expanded/fiber/go.mod b/examples/petstore-expanded/fiber/go.mod new file mode 100644 index 0000000000..a93bf57445 --- /dev/null +++ b/examples/petstore-expanded/fiber/go.mod @@ -0,0 +1,53 @@ +module github.com/oapi-codegen/oapi-codegen/v2/examples/petstore-expanded/fiber + +go 1.24.0 + +replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../ + +tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen + +require ( + github.com/getkin/kin-openapi v0.133.0 + github.com/gofiber/fiber/v2 v2.52.11 + github.com/oapi-codegen/fiber-middleware v1.0.2 + github.com/oapi-codegen/runtime v1.1.0 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oapi-codegen/oapi-codegen/v2 v2.0.0-00010101000000-000000000000 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/speakeasy-api/jsonpath v0.6.0 // indirect + github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/tools v0.30.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/examples/petstore-expanded/fiber/go.sum b/examples/petstore-expanded/fiber/go.sum new file mode 100644 index 0000000000..62ff2f51fd --- /dev/null +++ b/examples/petstore-expanded/fiber/go.sum @@ -0,0 +1,212 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs= +github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/fiber-middleware v1.0.2 h1:f4KPdjyRTYh2GyAv9wsDP+Q9akOND17wuMSbmMwDkJI= +github.com/oapi-codegen/fiber-middleware v1.0.2/go.mod h1:+lGj+802Ajp/+fQG9d8t1SuYP8r7lnOc6wnOwwRArYg= +github.com/oapi-codegen/runtime v1.1.0 h1:rJpoNUawn5XTvekgfkvSZr0RqEnoYpFkyvrzfWeFKWM= +github.com/oapi-codegen/runtime v1.1.0/go.mod h1:BeSfBkWWWnAnGdyS+S/GnlbmHKzf8/hwkvelJZDeKA8= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= +github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= +github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= +github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= +github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/test/Makefile b/internal/test/Makefile index bb37d63394..5ec0edd058 100644 --- a/internal/test/Makefile +++ b/internal/test/Makefile @@ -1,36 +1,17 @@ -SHELL:=/bin/bash - -YELLOW := \e[0;33m -RESET := \e[0;0m - -GOVER := $(shell go env GOVERSION) -GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") - -define execute-if-go-124 -@{ \ -if [[ 24 -le $(GOMINOR) ]]; then \ - $1; \ -else \ - echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ -fi \ -} -endef - lint: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + $(GOBIN)/golangci-lint run ./... lint-ci: - - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + $(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m generate: - $(call execute-if-go-124,go generate ./...) + go generate ./... test: - $(call execute-if-go-124,go test -cover ./...) + go test -cover ./... tidy: - $(call execute-if-go-124,go mod tidy) + go mod tidy tidy-ci: - $(call execute-if-go-124,tidied -verbose) + tidied -verbose diff --git a/internal/test/go.mod b/internal/test/go.mod index 6d321d86bf..4d9e591610 100644 --- a/internal/test/go.mod +++ b/internal/test/go.mod @@ -1,6 +1,6 @@ module github.com/oapi-codegen/oapi-codegen/v2/internal/test -go 1.24.0 +go 1.22.5 replace github.com/oapi-codegen/oapi-codegen/v2 => ../../ @@ -8,8 +8,7 @@ require ( github.com/getkin/kin-openapi v0.133.0 github.com/gin-gonic/gin v1.9.1 github.com/go-chi/chi/v5 v5.0.10 - github.com/gofiber/fiber/v2 v2.52.11 - github.com/google/uuid v1.6.0 + github.com/google/uuid v1.4.0 github.com/gorilla/mux v1.8.1 github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 github.com/labstack/echo/v4 v4.11.3 @@ -27,13 +26,12 @@ require ( github.com/CloudyKit/jet/v6 v6.2.0 // indirect github.com/Joker/jade v1.1.3 // indirect github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect - github.com/andybalholm/brotli v1.2.0 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bytedance/sonic v1.10.0-rc3 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.0 // indirect - github.com/clipperhouse/uax29/v2 v2.6.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/fatih/structs v1.1.0 // indirect @@ -58,15 +56,14 @@ require ( github.com/kataras/pio v0.0.12 // indirect github.com/kataras/sitemap v0.0.6 // indirect github.com/kataras/tunnel v0.0.4 // indirect - github.com/klauspost/compress v1.18.4 // indirect + github.com/klauspost/compress v1.16.7 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/labstack/gommon v0.4.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/microcosm-cc/bluemonday v1.0.25 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -87,7 +84,6 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.69.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect @@ -95,14 +91,14 @@ require ( github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/yosssi/ace v0.0.5 // indirect golang.org/x/arch v0.4.0 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.30.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/internal/test/go.sum b/internal/test/go.sum index 336b745da8..994b539a63 100644 --- a/internal/test/go.sum +++ b/internal/test/go.sum @@ -13,8 +13,8 @@ github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= -github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= @@ -33,8 +33,6 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= -github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -80,8 +78,6 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs= -github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -107,8 +103,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -140,8 +136,8 @@ github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -164,13 +160,12 @@ github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNf github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -261,8 +256,6 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= -github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= @@ -280,8 +273,6 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= @@ -298,12 +289,12 @@ golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -313,14 +304,14 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -340,26 +331,27 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/test/issues/issue1469/Makefile b/internal/test/issues/issue1469/Makefile new file mode 100644 index 0000000000..bb37d63394 --- /dev/null +++ b/internal/test/issues/issue1469/Makefile @@ -0,0 +1,36 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + +generate: + $(call execute-if-go-124,go generate ./...) + +test: + $(call execute-if-go-124,go test -cover ./...) + +tidy: + $(call execute-if-go-124,go mod tidy) + +tidy-ci: + $(call execute-if-go-124,tidied -verbose) diff --git a/internal/test/issues/issue1469/go.mod b/internal/test/issues/issue1469/go.mod new file mode 100644 index 0000000000..fbd28f728a --- /dev/null +++ b/internal/test/issues/issue1469/go.mod @@ -0,0 +1,49 @@ +module github.com/oapi-codegen/oapi-codegen/v2/internal/test/issues/issue1469 + +go 1.24.0 + +replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../../ + +tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen + +require ( + github.com/gofiber/fiber/v2 v2.52.11 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oapi-codegen/oapi-codegen/v2 v2.0.0-00010101000000-000000000000 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/speakeasy-api/jsonpath v0.6.0 // indirect + github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/tools v0.30.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/internal/test/issues/issue1469/go.sum b/internal/test/issues/issue1469/go.sum new file mode 100644 index 0000000000..e2954b1efc --- /dev/null +++ b/internal/test/issues/issue1469/go.sum @@ -0,0 +1,198 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs= +github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= +github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= +github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= +github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= +github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/test/strict-server/fiber/Makefile b/internal/test/strict-server/fiber/Makefile new file mode 100644 index 0000000000..bb37d63394 --- /dev/null +++ b/internal/test/strict-server/fiber/Makefile @@ -0,0 +1,36 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + +generate: + $(call execute-if-go-124,go generate ./...) + +test: + $(call execute-if-go-124,go test -cover ./...) + +tidy: + $(call execute-if-go-124,go mod tidy) + +tidy-ci: + $(call execute-if-go-124,tidied -verbose) diff --git a/internal/test/strict-server/fiber/fiber_strict_test.go b/internal/test/strict-server/fiber/fiber_strict_test.go new file mode 100644 index 0000000000..cb8b509b08 --- /dev/null +++ b/internal/test/strict-server/fiber/fiber_strict_test.go @@ -0,0 +1,229 @@ +package api + +import ( + "bytes" + "encoding/json" + "io" + "mime" + "mime/multipart" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" + "github.com/stretchr/testify/assert" + + clientAPI "github.com/oapi-codegen/oapi-codegen/v2/internal/test/strict-server/client" + "github.com/oapi-codegen/runtime" + "github.com/oapi-codegen/testutil" +) + +func TestFiberServer(t *testing.T) { + server := StrictServer{} + strictHandler := NewStrictHandler(server, nil) + r := fiber.New() + RegisterHandlers(r, strictHandler) + testImpl(t, adaptor.FiberApp(r)) +} + +func testImpl(t *testing.T, handler http.Handler) { + t.Run("JSONExample", func(t *testing.T) { + value := "123" + requestBody := clientAPI.Example{Value: &value} + rr := testutil.NewRequest().Post("/json").WithJsonBody(requestBody).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + assert.True(t, strings.HasPrefix(rr.Header().Get("Content-Type"), "application/json")) + var responseBody clientAPI.Example + err := json.NewDecoder(rr.Body).Decode(&responseBody) + assert.NoError(t, err) + assert.Equal(t, requestBody, responseBody) + }) + t.Run("URLEncodedExample", func(t *testing.T) { + value := "456" + requestBody := clientAPI.Example{Value: &value} + requestBodyEncoded, err := runtime.MarshalForm(&requestBody, nil) + assert.NoError(t, err) + rr := testutil.NewRequest().Post("/urlencoded").WithContentType("application/x-www-form-urlencoded").WithBody([]byte(requestBodyEncoded.Encode())).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "application/x-www-form-urlencoded", rr.Header().Get("Content-Type")) + values, err := url.ParseQuery(rr.Body.String()) + assert.NoError(t, err) + var responseBody clientAPI.Example + err = runtime.BindForm(&responseBody, values, nil, nil) + assert.NoError(t, err) + assert.Equal(t, requestBody, responseBody) + }) + t.Run("MultipartExample", func(t *testing.T) { + value := "789" + fieldName := "value" + var writer bytes.Buffer + mw := multipart.NewWriter(&writer) + field, err := mw.CreateFormField(fieldName) + assert.NoError(t, err) + _, _ = field.Write([]byte(value)) + assert.NoError(t, mw.Close()) + rr := testutil.NewRequest().Post("/multipart").WithContentType(mw.FormDataContentType()).WithBody(writer.Bytes()).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + contentType, params, err := mime.ParseMediaType(rr.Header().Get("Content-Type")) + assert.NoError(t, err) + assert.Equal(t, "multipart/form-data", contentType) + reader := multipart.NewReader(rr.Body, params["boundary"]) + part, err := reader.NextPart() + assert.NoError(t, err) + assert.Equal(t, part.FormName(), fieldName) + readValue, err := io.ReadAll(part) + assert.NoError(t, err) + assert.Equal(t, value, string(readValue)) + _, err = reader.NextPart() + assert.Equal(t, io.EOF, err) + }) + t.Run("MultipartRelatedExample", func(t *testing.T) { + value := "789" + fieldName := "value" + var writer bytes.Buffer + mw := multipart.NewWriter(&writer) + field, err := mw.CreateFormField(fieldName) + assert.NoError(t, err) + _, _ = field.Write([]byte(value)) + assert.NoError(t, mw.Close()) + rr := testutil.NewRequest().Post("/multipart-related").WithContentType(mime.FormatMediaType("multipart/related", map[string]string{"boundary": mw.Boundary()})).WithBody(writer.Bytes()).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + contentType, params, err := mime.ParseMediaType(rr.Header().Get("Content-Type")) + assert.NoError(t, err) + assert.Equal(t, "multipart/related", contentType) + reader := multipart.NewReader(rr.Body, params["boundary"]) + part, err := reader.NextPart() + assert.NoError(t, err) + assert.Equal(t, part.FormName(), fieldName) + readValue, err := io.ReadAll(part) + assert.NoError(t, err) + assert.Equal(t, value, string(readValue)) + _, err = reader.NextPart() + assert.Equal(t, io.EOF, err) + }) + t.Run("TextExample", func(t *testing.T) { + value := "text" + rr := testutil.NewRequest().Post("/text").WithContentType("text/plain").WithBody([]byte(value)).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "text/plain", rr.Header().Get("Content-Type")) + assert.Equal(t, value, rr.Body.String()) + }) + t.Run("UnknownExample", func(t *testing.T) { + data := []byte("unknown data") + rr := testutil.NewRequest().Post("/unknown").WithContentType("image/png").WithBody(data).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "video/mp4", rr.Header().Get("Content-Type")) + assert.Equal(t, data, rr.Body.Bytes()) + }) + t.Run("MultipleRequestAndResponseTypesJSON", func(t *testing.T) { + value := "123" + requestBody := clientAPI.Example{Value: &value} + rr := testutil.NewRequest().Post("/multiple").WithJsonBody(requestBody).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + assert.True(t, strings.HasPrefix(rr.Header().Get("Content-Type"), "application/json")) + var responseBody clientAPI.Example + err := json.NewDecoder(rr.Body).Decode(&responseBody) + assert.NoError(t, err) + assert.Equal(t, requestBody, responseBody) + }) + t.Run("MultipleRequestAndResponseTypesFormdata", func(t *testing.T) { + value := "456" + requestBody := clientAPI.Example{Value: &value} + requestBodyEncoded, err := runtime.MarshalForm(&requestBody, nil) + assert.NoError(t, err) + rr := testutil.NewRequest().Post("/multiple").WithContentType("application/x-www-form-urlencoded").WithBody([]byte(requestBodyEncoded.Encode())).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "application/x-www-form-urlencoded", rr.Header().Get("Content-Type")) + values, err := url.ParseQuery(rr.Body.String()) + assert.NoError(t, err) + var responseBody clientAPI.Example + err = runtime.BindForm(&responseBody, values, nil, nil) + assert.NoError(t, err) + assert.Equal(t, requestBody, responseBody) + }) + t.Run("MultipleRequestAndResponseTypesMultipart", func(t *testing.T) { + value := "789" + fieldName := "value" + var writer bytes.Buffer + mw := multipart.NewWriter(&writer) + field, err := mw.CreateFormField(fieldName) + assert.NoError(t, err) + _, _ = field.Write([]byte(value)) + assert.NoError(t, mw.Close()) + rr := testutil.NewRequest().Post("/multiple").WithContentType(mw.FormDataContentType()).WithBody(writer.Bytes()).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + contentType, params, err := mime.ParseMediaType(rr.Header().Get("Content-Type")) + assert.NoError(t, err) + assert.Equal(t, "multipart/form-data", contentType) + reader := multipart.NewReader(rr.Body, params["boundary"]) + part, err := reader.NextPart() + assert.NoError(t, err) + assert.Equal(t, part.FormName(), fieldName) + readValue, err := io.ReadAll(part) + assert.NoError(t, err) + assert.Equal(t, value, string(readValue)) + _, err = reader.NextPart() + assert.Equal(t, io.EOF, err) + }) + t.Run("MultipleRequestAndResponseTypesText", func(t *testing.T) { + value := "text" + rr := testutil.NewRequest().Post("/multiple").WithContentType("text/plain").WithBody([]byte(value)).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "text/plain", rr.Header().Get("Content-Type")) + assert.Equal(t, value, rr.Body.String()) + }) + t.Run("MultipleRequestAndResponseTypesImage", func(t *testing.T) { + data := []byte("unknown data") + rr := testutil.NewRequest().Post("/multiple").WithContentType("image/png").WithBody(data).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "image/png", rr.Header().Get("Content-Type")) + assert.Equal(t, data, rr.Body.Bytes()) + }) + t.Run("HeadersExample", func(t *testing.T) { + header1 := "value1" + header2 := "890" + value := "asdf" + requestBody := clientAPI.Example{Value: &value} + rr := testutil.NewRequest().Post("/with-headers").WithHeader("header1", header1).WithHeader("header2", header2).WithJsonBody(requestBody).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + assert.True(t, strings.HasPrefix(rr.Header().Get("Content-Type"), "application/json")) + var responseBody clientAPI.Example + err := json.NewDecoder(rr.Body).Decode(&responseBody) + assert.NoError(t, err) + assert.Equal(t, requestBody, responseBody) + assert.Equal(t, header1, rr.Header().Get("header1")) + assert.Equal(t, header2, rr.Header().Get("header2")) + }) + t.Run("UnspecifiedContentType", func(t *testing.T) { + data := []byte("image data") + contentType := "image/jpeg" + rr := testutil.NewRequest().Post("/unspecified-content-type").WithContentType(contentType).WithBody(data).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, contentType, rr.Header().Get("Content-Type")) + assert.Equal(t, data, rr.Body.Bytes()) + }) + t.Run("ReusableResponses", func(t *testing.T) { + value := "jkl;" + requestBody := clientAPI.Example{Value: &value} + rr := testutil.NewRequest().Post("/reusable-responses").WithJsonBody(requestBody).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + assert.True(t, strings.HasPrefix(rr.Header().Get("Content-Type"), "application/json")) + var responseBody clientAPI.Example + err := json.NewDecoder(rr.Body).Decode(&responseBody) + assert.NoError(t, err) + assert.Equal(t, requestBody, responseBody) + }) + t.Run("UnionResponses", func(t *testing.T) { + value := "union" + requestBody := clientAPI.Example{Value: &value} + rr := testutil.NewRequest().Post("/with-union").WithJsonBody(requestBody).GoWithHTTPHandler(t, handler).Recorder + assert.Equal(t, http.StatusOK, rr.Code) + assert.True(t, strings.HasPrefix(rr.Header().Get("Content-Type"), "application/json")) + var responseBody clientAPI.Example + err := json.NewDecoder(rr.Body).Decode(&responseBody) + assert.NoError(t, err) + assert.Equal(t, requestBody, responseBody) + }) +} diff --git a/internal/test/strict-server/fiber/go.mod b/internal/test/strict-server/fiber/go.mod new file mode 100644 index 0000000000..add341aacf --- /dev/null +++ b/internal/test/strict-server/fiber/go.mod @@ -0,0 +1,55 @@ +module github.com/oapi-codegen/oapi-codegen/v2/internal/test/strict-server/fiber + +go 1.24.0 + +replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../../ + +replace github.com/oapi-codegen/oapi-codegen/v2/internal/test => ../.. + +tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen + +require ( + github.com/getkin/kin-openapi v0.133.0 + github.com/gofiber/fiber/v2 v2.52.11 + github.com/oapi-codegen/oapi-codegen/v2/internal/test v0.0.0-00010101000000-000000000000 + github.com/oapi-codegen/runtime v1.1.0 + github.com/oapi-codegen/testutil v1.0.0 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oapi-codegen/oapi-codegen/v2 v2.0.0-00010101000000-000000000000 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/speakeasy-api/jsonpath v0.6.0 // indirect + github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/tools v0.30.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/internal/test/strict-server/fiber/go.sum b/internal/test/strict-server/fiber/go.sum new file mode 100644 index 0000000000..f585043c1b --- /dev/null +++ b/internal/test/strict-server/fiber/go.sum @@ -0,0 +1,210 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs= +github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/runtime v1.1.0 h1:rJpoNUawn5XTvekgfkvSZr0RqEnoYpFkyvrzfWeFKWM= +github.com/oapi-codegen/runtime v1.1.0/go.mod h1:BeSfBkWWWnAnGdyS+S/GnlbmHKzf8/hwkvelJZDeKA8= +github.com/oapi-codegen/testutil v1.0.0 h1:1GI2IiMMLh2vDHr1OkNacaYU/VaApKdcmfgl4aeXAa8= +github.com/oapi-codegen/testutil v1.0.0/go.mod h1:ttCaYbHvJtHuiyeBF0tPIX+4uhEPTeizXKx28okijLw= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= +github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= +github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= +github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= +github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/test/strict-server/stdhttp/Makefile b/internal/test/strict-server/stdhttp/Makefile index bb37d63394..48fe768e30 100644 --- a/internal/test/strict-server/stdhttp/Makefile +++ b/internal/test/strict-server/stdhttp/Makefile @@ -6,31 +6,31 @@ RESET := \e[0;0m GOVER := $(shell go env GOVERSION) GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") -define execute-if-go-124 +define execute-if-go-122 @{ \ -if [[ 24 -le $(GOMINOR) ]]; then \ +if [[ 22 -le $(GOMINOR) ]]; then \ $1; \ else \ - echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.22, which this module requires$(RESET)"; \ fi \ } endef lint: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + $(call execute-if-go-122,$(GOBIN)/golangci-lint run ./...) lint-ci: - $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + $(call execute-if-go-122,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) generate: - $(call execute-if-go-124,go generate ./...) + $(call execute-if-go-122,go generate ./...) test: - $(call execute-if-go-124,go test -cover ./...) + $(call execute-if-go-122,go test -cover ./...) tidy: - $(call execute-if-go-124,go mod tidy) + $(call execute-if-go-122,go mod tidy) tidy-ci: - $(call execute-if-go-124,tidied -verbose) + $(call execute-if-go-122,tidied -verbose) diff --git a/internal/test/strict-server/stdhttp/go.mod b/internal/test/strict-server/stdhttp/go.mod index cdd0f48f20..b56e4799a0 100644 --- a/internal/test/strict-server/stdhttp/go.mod +++ b/internal/test/strict-server/stdhttp/go.mod @@ -1,6 +1,6 @@ module github.com/oapi-codegen/oapi-codegen/v2/internal/test/strict-server/stdhttp -go 1.24.0 +go 1.22.5 replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../../ @@ -21,7 +21,7 @@ require ( github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/google/uuid v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect @@ -33,10 +33,10 @@ require ( github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/text v0.32.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/tools v0.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/test/strict-server/stdhttp/go.sum b/internal/test/strict-server/stdhttp/go.sum index c5c399b374..880c5ff529 100644 --- a/internal/test/strict-server/stdhttp/go.sum +++ b/internal/test/strict-server/stdhttp/go.sum @@ -39,8 +39,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -110,8 +110,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -119,13 +119,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -140,21 +140,21 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/test/strict-server/strict_test.go b/internal/test/strict-server/strict_test.go index 0ae094df02..03cade9cc9 100644 --- a/internal/test/strict-server/strict_test.go +++ b/internal/test/strict-server/strict_test.go @@ -13,8 +13,6 @@ import ( "github.com/gin-gonic/gin" "github.com/go-chi/chi/v5" - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/adaptor" "github.com/kataras/iris/v12" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" @@ -22,7 +20,6 @@ import ( chiAPI "github.com/oapi-codegen/oapi-codegen/v2/internal/test/strict-server/chi" clientAPI "github.com/oapi-codegen/oapi-codegen/v2/internal/test/strict-server/client" echoAPI "github.com/oapi-codegen/oapi-codegen/v2/internal/test/strict-server/echo" - fiberAPI "github.com/oapi-codegen/oapi-codegen/v2/internal/test/strict-server/fiber" ginAPI "github.com/oapi-codegen/oapi-codegen/v2/internal/test/strict-server/gin" irisAPI "github.com/oapi-codegen/oapi-codegen/v2/internal/test/strict-server/iris" @@ -63,14 +60,6 @@ func TestGinServer(t *testing.T) { testImpl(t, r) } -func TestFiberServer(t *testing.T) { - server := fiberAPI.StrictServer{} - strictHandler := fiberAPI.NewStrictHandler(server, nil) - r := fiber.New() - fiberAPI.RegisterHandlers(r, strictHandler) - testImpl(t, adaptor.FiberApp(r)) -} - func testImpl(t *testing.T, handler http.Handler) { t.Run("JSONExample", func(t *testing.T) { value := "123" From db8d6b5a99c8ac7bf063f930cf8eff86c3291ff9 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Thu, 12 Feb 2026 10:16:51 -0800 Subject: [PATCH 20/44] Run golangci-lint on a supported Go version We only need to run lint on a single version of Go, as the output isn't Go runtime dependent, however, we need to be careful to run it in an environment where the Go runtime isn't newer than what the golangci-lint executable was built with, because it panics on some kind of version check in that case. Resolves: #2214 --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a7d578881..b21f3f88ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,9 @@ permissions: contents: read jobs: build: - uses: oapi-codegen/actions/.github/workflows/ci.yml@b9f2c274c1c631e648931dbbcc1942c2b2027837 # v0.4.0 + uses: oapi-codegen/actions/.github/workflows/ci.yml@75566d848d25021f137594c947f26171094fb511 # v0.5.0 + with: + lint_versions: '["1.25"]' build-binaries: runs-on: ubuntu-latest From b7ac5dbc080606ca893ae3ff2218c444c9760ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Haylee=20Sch=C3=A4fer?= Date: Fri, 13 Feb 2026 20:30:00 +0100 Subject: [PATCH 21/44] feat: add support for custom package alias for external ref imports (#2211) * support specifying package alias * update test * fix local package * simplify --- .../test/externalref/externalref.cfg.yaml | 2 +- internal/test/externalref/externalref.gen.go | 12 ++++---- pkg/codegen/codegen.go | 29 +++++++++++++++---- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/internal/test/externalref/externalref.cfg.yaml b/internal/test/externalref/externalref.cfg.yaml index 22a76c3ffd..dd7a93d019 100644 --- a/internal/test/externalref/externalref.cfg.yaml +++ b/internal/test/externalref/externalref.cfg.yaml @@ -5,7 +5,7 @@ generate: embedded-spec: true import-mapping: ./packageA/spec.yaml: github.com/oapi-codegen/oapi-codegen/v2/internal/test/externalref/packageA - ./packageB/spec.yaml: github.com/oapi-codegen/oapi-codegen/v2/internal/test/externalref/packageB + ./packageB/spec.yaml: package_b github.com/oapi-codegen/oapi-codegen/v2/internal/test/externalref/packageB https://petstore3.swagger.io/api/v3/openapi.json: github.com/oapi-codegen/oapi-codegen/v2/internal/test/externalref/petstore output: externalref.gen.go output-options: diff --git a/internal/test/externalref/externalref.gen.go b/internal/test/externalref/externalref.gen.go index a1bd9f6ea2..158a922d02 100644 --- a/internal/test/externalref/externalref.gen.go +++ b/internal/test/externalref/externalref.gen.go @@ -14,16 +14,16 @@ import ( "github.com/getkin/kin-openapi/openapi3" externalRef0 "github.com/oapi-codegen/oapi-codegen/v2/internal/test/externalref/packageA" - externalRef1 "github.com/oapi-codegen/oapi-codegen/v2/internal/test/externalref/packageB" - externalRef2 "github.com/oapi-codegen/oapi-codegen/v2/internal/test/externalref/petstore" + package_b "github.com/oapi-codegen/oapi-codegen/v2/internal/test/externalref/packageB" + externalRef1 "github.com/oapi-codegen/oapi-codegen/v2/internal/test/externalref/petstore" ) // Container defines model for Container. type Container struct { ObjectA *externalRef0.ObjectA `json:"object_a,omitempty"` - ObjectB *externalRef1.ObjectB `json:"object_b,omitempty"` + ObjectB *package_b.ObjectB `json:"object_b,omitempty"` ObjectC *map[string]interface{} `json:"object_c,omitempty"` - Pet *externalRef2.Pet `json:"pet,omitempty"` + Pet *externalRef1.Pet `json:"pet,omitempty"` } // Base64 encoded, gzipped, json marshaled Swagger object @@ -82,13 +82,13 @@ func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { } res[rawPath] = rawFunc } - for rawPath, rawFunc := range externalRef1.PathToRawSpec(path.Join(path.Dir(pathToFile), "./packageB/spec.yaml")) { + for rawPath, rawFunc := range package_b.PathToRawSpec(path.Join(path.Dir(pathToFile), "./packageB/spec.yaml")) { if _, ok := res[rawPath]; ok { // it is not possible to compare functions in golang, so always overwrite the old value } res[rawPath] = rawFunc } - for rawPath, rawFunc := range externalRef2.PathToRawSpec(path.Join(path.Dir(pathToFile), "https://petstore3.swagger.io/api/v3/openapi.json")) { + for rawPath, rawFunc := range externalRef1.PathToRawSpec(path.Join(path.Dir(pathToFile), "https://petstore3.swagger.io/api/v3/openapi.json")) { if _, ok := res[rawPath]; ok { // it is not possible to compare functions in golang, so always overwrite the old value } diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go index 279ed9d081..ce259c42f4 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -90,8 +90,8 @@ func (im importMap) GoImports() []string { func constructImportMapping(importMapping map[string]string) importMap { var ( - pathToName = map[string]string{} - result = importMap{} + pathToImport = importMap{} + result = importMap{} ) { @@ -102,13 +102,32 @@ func constructImportMapping(importMapping map[string]string) importMap { sort.Strings(packagePaths) for _, packagePath := range packagePaths { - if _, ok := pathToName[packagePath]; !ok && packagePath != importMappingCurrentPackage { - pathToName[packagePath] = fmt.Sprintf("externalRef%d", len(pathToName)) + if _, ok := pathToImport[packagePath]; !ok && packagePath != importMappingCurrentPackage { + split := strings.Split(packagePath, " ") + if len(split) == 2 { + // if we have 2 parts, we assume both the package name and path are provided, and we use them as is + pathToImport[packagePath] = goImport{ + Name: split[0], + Path: split[1], + } + } else { + // otherwise, we auto-generate a package name based on the order of the imports, to ensure deterministic output + pathToImport[packagePath] = goImport{ + Name: fmt.Sprintf("externalRef%d", len(pathToImport)), + Path: packagePath, + } + } } } } for specPath, packagePath := range importMapping { - result[specPath] = goImport{Name: pathToName[packagePath], Path: packagePath} + if packagePath == importMappingCurrentPackage { + result[specPath] = goImport{ + Path: importMappingCurrentPackage, + } + } else { + result[specPath] = pathToImport[packagePath] + } } return result } From 01d4fc0aae7f8802f2d2ece9222d5e59b6f20bf1 Mon Sep 17 00:00:00 2001 From: Brahm Lower Date: Fri, 13 Feb 2026 14:34:20 -0800 Subject: [PATCH 22/44] fix: escape quoted media type directives (#2217) * fix: escape quoted media type directives fixes https://github.com/oapi-codegen/oapi-codegen/issues/1529 * squashme: expanded tests to cover iris, fiber & echo tests * squashme: added makefile to fiber test directory, fixing root level tests * squashme: actually fixed the generate issues with fiber tests * squashme: skip fiber test for go versions <1.24 * squashme: make actually uses the execute-if-go-124 function --- .../issues/issue-1529/strict-echo/config.yaml | 9 + .../test/issues/issue-1529/strict-echo/doc.go | 3 + .../issue-1529/strict-echo/issue1529.gen.go | 471 ++++++++++++++++++ .../issues/issue-1529/strict-echo/spec.yaml | 21 + .../issues/issue-1529/strict-fiber/Makefile | 36 ++ .../issue-1529/strict-fiber/config.yaml | 9 + .../issues/issue-1529/strict-fiber/doc.go | 3 + .../issues/issue-1529/strict-fiber/go.mod | 46 ++ .../issues/issue-1529/strict-fiber/go.sum | 198 ++++++++ .../issue-1529/strict-fiber/issue1529.gen.go | 465 +++++++++++++++++ .../issues/issue-1529/strict-fiber/spec.yaml | 21 + .../issues/issue-1529/strict-iris/config.yaml | 9 + .../test/issues/issue-1529/strict-iris/doc.go | 3 + .../issue-1529/strict-iris/issue1529.gen.go | 466 +++++++++++++++++ .../issues/issue-1529/strict-iris/spec.yaml | 21 + pkg/codegen/template_helpers.go | 7 +- .../strict/strict-fiber-interface.tmpl | 2 +- .../templates/strict/strict-interface.tmpl | 2 +- .../strict/strict-iris-interface.tmpl | 2 +- pkg/codegen/utils.go | 7 + pkg/codegen/utils_test.go | 30 ++ 21 files changed, 1826 insertions(+), 5 deletions(-) create mode 100644 internal/test/issues/issue-1529/strict-echo/config.yaml create mode 100644 internal/test/issues/issue-1529/strict-echo/doc.go create mode 100644 internal/test/issues/issue-1529/strict-echo/issue1529.gen.go create mode 100644 internal/test/issues/issue-1529/strict-echo/spec.yaml create mode 100644 internal/test/issues/issue-1529/strict-fiber/Makefile create mode 100644 internal/test/issues/issue-1529/strict-fiber/config.yaml create mode 100644 internal/test/issues/issue-1529/strict-fiber/doc.go create mode 100644 internal/test/issues/issue-1529/strict-fiber/go.mod create mode 100644 internal/test/issues/issue-1529/strict-fiber/go.sum create mode 100644 internal/test/issues/issue-1529/strict-fiber/issue1529.gen.go create mode 100644 internal/test/issues/issue-1529/strict-fiber/spec.yaml create mode 100644 internal/test/issues/issue-1529/strict-iris/config.yaml create mode 100644 internal/test/issues/issue-1529/strict-iris/doc.go create mode 100644 internal/test/issues/issue-1529/strict-iris/issue1529.gen.go create mode 100644 internal/test/issues/issue-1529/strict-iris/spec.yaml diff --git a/internal/test/issues/issue-1529/strict-echo/config.yaml b/internal/test/issues/issue-1529/strict-echo/config.yaml new file mode 100644 index 0000000000..56abc0bee7 --- /dev/null +++ b/internal/test/issues/issue-1529/strict-echo/config.yaml @@ -0,0 +1,9 @@ +# yaml-language-server: $schema=../../../../../configuration-schema.json +package: issue1529 +generate: + client: true + models: true + embedded-spec: true + echo-server: true + strict-server: true +output: issue1529.gen.go diff --git a/internal/test/issues/issue-1529/strict-echo/doc.go b/internal/test/issues/issue-1529/strict-echo/doc.go new file mode 100644 index 0000000000..4bf78249fa --- /dev/null +++ b/internal/test/issues/issue-1529/strict-echo/doc.go @@ -0,0 +1,3 @@ +package issue1529 + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-1529/strict-echo/issue1529.gen.go b/internal/test/issues/issue-1529/strict-echo/issue1529.gen.go new file mode 100644 index 0000000000..cd5260fae4 --- /dev/null +++ b/internal/test/issues/issue-1529/strict-echo/issue1529.gen.go @@ -0,0 +1,471 @@ +// Package issue1529 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package issue1529 + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/labstack/echo/v4" + strictecho "github.com/oapi-codegen/runtime/strictmiddleware/echo" +) + +// Test defines model for Test. +type Test = map[string]interface{} + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // 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 +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // Test request + Test(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) Test(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewTestRequest(c.Server) + 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) +} + +// NewTestRequest generates requests for Test +func NewTestRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/test") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.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 +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // TestWithResponse request + TestWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*TestResponse, error) +} + +type TestResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Test + ApplicationjsonProfileBar200 *Test + ApplicationjsonProfileFoo200 *Test +} + +// Status returns HTTPResponse.Status +func (r TestResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r TestResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// TestWithResponse request returning *TestResponse +func (c *ClientWithResponses) TestWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*TestResponse, error) { + rsp, err := c.Test(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseTestResponse(rsp) +} + +// ParseTestResponse parses an HTTP response from a TestWithResponse call +func ParseTestResponse(rsp *http.Response) (*TestResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &TestResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case rsp.Header.Get("Content-Type") == "application/json" && rsp.StatusCode == 200: + var dest Test + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case rsp.Header.Get("Content-Type") == "application/json; profile=\"Bar\"" && rsp.StatusCode == 200: + var dest Test + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationjsonProfileBar200 = &dest + + case rsp.Header.Get("Content-Type") == "application/json; profile=\"Foo\"" && rsp.StatusCode == 200: + var dest Test + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationjsonProfileFoo200 = &dest + + } + + return response, nil +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + + // (GET /test) + Test(ctx echo.Context) error +} + +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +// Test converts echo context to params. +func (w *ServerInterfaceWrapper) Test(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.Test(ctx) + return err +} + +// 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.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 +} + +// RegisterHandlers adds each server route to the EchoRouter. +func RegisterHandlers(router EchoRouter, si ServerInterface) { + RegisterHandlersWithBaseURL(router, si, "") +} + +// Registers handlers, and prepends BaseURL to the paths, so that the paths +// can be served under a prefix. +func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { + + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + router.GET(baseURL+"/test", wrapper.Test) + +} + +type TestRequestObject struct { +} + +type TestResponseObject interface { + VisitTestResponse(w http.ResponseWriter) error +} + +type Test200JSONResponse Test + +func (response Test200JSONResponse) VisitTestResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type Test200ApplicationJSONProfileBarResponse Test + +func (response Test200ApplicationJSONProfileBarResponse) VisitTestResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; profile=\"Bar\"") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type Test200ApplicationJSONProfileFooResponse Test + +func (response Test200ApplicationJSONProfileFooResponse) VisitTestResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; profile=\"Foo\"") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + + // (GET /test) + Test(ctx context.Context, request TestRequestObject) (TestResponseObject, error) +} + +type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc +type StrictMiddlewareFunc = strictecho.StrictEchoMiddlewareFunc + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc +} + +// Test operation middleware +func (sh *strictHandler) Test(ctx echo.Context) error { + var request TestRequestObject + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.Test(ctx.Request().Context(), request.(TestRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "Test") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(TestResponseObject); ok { + return validResponse.VisitTestResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "H4sIAAAAAAAC/6zPMUsEMRAF4L8iT8twWbWL2FgI9pbXxDjr5cjNDMlYyLL/XRIXFizFad4072NmQZKL", + "ChNbQ1jQ0okucayv1KynfSkhQN7OlAzrujpkngWBP0txECWOmhFwf5gOt3DQaKcheNuIDxohSjVaFn55", + "R/jxHSo1FW40GnfT1CMJG/HoRNWS02j5cxPej+zbTaUZAdd+/8JvL/jh92t/Ew9XWmXOhR6PeIr1iH82", + "n0X+YG7zHQAA//9fnz6pkQEAAA==", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/internal/test/issues/issue-1529/strict-echo/spec.yaml b/internal/test/issues/issue-1529/strict-echo/spec.yaml new file mode 100644 index 0000000000..ca7aae80a3 --- /dev/null +++ b/internal/test/issues/issue-1529/strict-echo/spec.yaml @@ -0,0 +1,21 @@ +openapi: "3.0.1" +components: + schemas: + Test: + type: object +paths: + /test: + get: + operationId: test + responses: + 200: + content: + application/json: + schema: + $ref: "#/components/schemas/Test" + application/json; profile="Foo": + schema: + $ref: "#/components/schemas/Test" + application/json; profile="Bar": + schema: + $ref: "#/components/schemas/Test" diff --git a/internal/test/issues/issue-1529/strict-fiber/Makefile b/internal/test/issues/issue-1529/strict-fiber/Makefile new file mode 100644 index 0000000000..bb37d63394 --- /dev/null +++ b/internal/test/issues/issue-1529/strict-fiber/Makefile @@ -0,0 +1,36 @@ +SHELL:=/bin/bash + +YELLOW := \e[0;33m +RESET := \e[0;0m + +GOVER := $(shell go env GOVERSION) +GOMINOR := $(shell bash -c "cut -f1 -d' ' <<< \"$(GOVER)\" | cut -f2 -d.") + +define execute-if-go-124 +@{ \ +if [[ 24 -le $(GOMINOR) ]]; then \ + $1; \ +else \ + echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \ +fi \ +} +endef + +lint: + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...) + +lint-ci: + + $(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m) + +generate: + $(call execute-if-go-124,go generate ./...) + +test: + $(call execute-if-go-124,go test -cover ./...) + +tidy: + $(call execute-if-go-124,go mod tidy) + +tidy-ci: + $(call execute-if-go-124,tidied -verbose) diff --git a/internal/test/issues/issue-1529/strict-fiber/config.yaml b/internal/test/issues/issue-1529/strict-fiber/config.yaml new file mode 100644 index 0000000000..e03a3f678b --- /dev/null +++ b/internal/test/issues/issue-1529/strict-fiber/config.yaml @@ -0,0 +1,9 @@ +# yaml-language-server: $schema=../../../../../configuration-schema.json +package: issue1529 +generate: + client: true + models: true + embedded-spec: true + fiber-server: true + strict-server: true +output: issue1529.gen.go diff --git a/internal/test/issues/issue-1529/strict-fiber/doc.go b/internal/test/issues/issue-1529/strict-fiber/doc.go new file mode 100644 index 0000000000..4bf78249fa --- /dev/null +++ b/internal/test/issues/issue-1529/strict-fiber/doc.go @@ -0,0 +1,3 @@ +package issue1529 + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-1529/strict-fiber/go.mod b/internal/test/issues/issue-1529/strict-fiber/go.mod new file mode 100644 index 0000000000..5e881be269 --- /dev/null +++ b/internal/test/issues/issue-1529/strict-fiber/go.mod @@ -0,0 +1,46 @@ +module github.com/oapi-codegen/oapi-codegen/v2/internal/test/issues/issue-1529/strict-fiber + +go 1.24.0 + +replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../../../ + +tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen + +require ( + github.com/getkin/kin-openapi v0.133.0 + github.com/gofiber/fiber/v2 v2.52.11 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oapi-codegen/oapi-codegen/v2 v2.5.1 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/speakeasy-api/jsonpath v0.6.0 // indirect + github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/tools v0.30.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/internal/test/issues/issue-1529/strict-fiber/go.sum b/internal/test/issues/issue-1529/strict-fiber/go.sum new file mode 100644 index 0000000000..e2954b1efc --- /dev/null +++ b/internal/test/issues/issue-1529/strict-fiber/go.sum @@ -0,0 +1,198 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs= +github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= +github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= +github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= +github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= +github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/test/issues/issue-1529/strict-fiber/issue1529.gen.go b/internal/test/issues/issue-1529/strict-fiber/issue1529.gen.go new file mode 100644 index 0000000000..4d6ab516f5 --- /dev/null +++ b/internal/test/issues/issue-1529/strict-fiber/issue1529.gen.go @@ -0,0 +1,465 @@ +// Package issue1529 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. +package issue1529 + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/gofiber/fiber/v2" +) + +// Test defines model for Test. +type Test = map[string]interface{} + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // 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 +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // Test request + Test(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) Test(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewTestRequest(c.Server) + 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) +} + +// NewTestRequest generates requests for Test +func NewTestRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/test") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.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 +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // TestWithResponse request + TestWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*TestResponse, error) +} + +type TestResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Test + ApplicationjsonProfileBar200 *Test + ApplicationjsonProfileFoo200 *Test +} + +// Status returns HTTPResponse.Status +func (r TestResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r TestResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// TestWithResponse request returning *TestResponse +func (c *ClientWithResponses) TestWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*TestResponse, error) { + rsp, err := c.Test(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseTestResponse(rsp) +} + +// ParseTestResponse parses an HTTP response from a TestWithResponse call +func ParseTestResponse(rsp *http.Response) (*TestResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &TestResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case rsp.Header.Get("Content-Type") == "application/json" && rsp.StatusCode == 200: + var dest Test + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case rsp.Header.Get("Content-Type") == "application/json; profile=\"Bar\"" && rsp.StatusCode == 200: + var dest Test + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationjsonProfileBar200 = &dest + + case rsp.Header.Get("Content-Type") == "application/json; profile=\"Foo\"" && rsp.StatusCode == 200: + var dest Test + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationjsonProfileFoo200 = &dest + + } + + return response, nil +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + + // (GET /test) + Test(c *fiber.Ctx) error +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +type MiddlewareFunc fiber.Handler + +// Test operation middleware +func (siw *ServerInterfaceWrapper) Test(c *fiber.Ctx) error { + + return siw.Handler.Test(c) +} + +// FiberServerOptions provides options for the Fiber server. +type FiberServerOptions struct { + BaseURL string + Middlewares []MiddlewareFunc +} + +// 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) { + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + for _, m := range options.Middlewares { + router.Use(fiber.Handler(m)) + } + + router.Get(options.BaseURL+"/test", wrapper.Test) + +} + +type TestRequestObject struct { +} + +type TestResponseObject interface { + VisitTestResponse(ctx *fiber.Ctx) error +} + +type Test200JSONResponse Test + +func (response Test200JSONResponse) VisitTestResponse(ctx *fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(200) + + return ctx.JSON(&response) +} + +type Test200ApplicationJSONProfileBarResponse Test + +func (response Test200ApplicationJSONProfileBarResponse) VisitTestResponse(ctx *fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json; profile=\"Bar\"") + ctx.Status(200) + + return ctx.JSON(&response) +} + +type Test200ApplicationJSONProfileFooResponse Test + +func (response Test200ApplicationJSONProfileFooResponse) VisitTestResponse(ctx *fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json; profile=\"Foo\"") + ctx.Status(200) + + return ctx.JSON(&response) +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + + // (GET /test) + Test(ctx context.Context, request TestRequestObject) (TestResponseObject, error) +} + +type StrictHandlerFunc func(ctx *fiber.Ctx, args interface{}) (interface{}, 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 +} + +// Test operation middleware +func (sh *strictHandler) Test(ctx *fiber.Ctx) error { + var request TestRequestObject + + handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { + return sh.ssi.Test(ctx.UserContext(), request.(TestRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "Test") + } + + response, err := handler(ctx, request) + + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else if validResponse, ok := response.(TestResponseObject); ok { + if err := validResponse.VisitTestResponse(ctx); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "H4sIAAAAAAAC/6zPMUsEMRAF4L8iT8twWbWL2FgI9pbXxDjr5cjNDMlYyLL/XRIXFizFad4072NmQZKL", + "ChNbQ1jQ0okucayv1KynfSkhQN7OlAzrujpkngWBP0txECWOmhFwf5gOt3DQaKcheNuIDxohSjVaFn55", + "R/jxHSo1FW40GnfT1CMJG/HoRNWS02j5cxPej+zbTaUZAdd+/8JvL/jh92t/Ew9XWmXOhR6PeIr1iH82", + "n0X+YG7zHQAA//9fnz6pkQEAAA==", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/internal/test/issues/issue-1529/strict-fiber/spec.yaml b/internal/test/issues/issue-1529/strict-fiber/spec.yaml new file mode 100644 index 0000000000..ca7aae80a3 --- /dev/null +++ b/internal/test/issues/issue-1529/strict-fiber/spec.yaml @@ -0,0 +1,21 @@ +openapi: "3.0.1" +components: + schemas: + Test: + type: object +paths: + /test: + get: + operationId: test + responses: + 200: + content: + application/json: + schema: + $ref: "#/components/schemas/Test" + application/json; profile="Foo": + schema: + $ref: "#/components/schemas/Test" + application/json; profile="Bar": + schema: + $ref: "#/components/schemas/Test" diff --git a/internal/test/issues/issue-1529/strict-iris/config.yaml b/internal/test/issues/issue-1529/strict-iris/config.yaml new file mode 100644 index 0000000000..835b035300 --- /dev/null +++ b/internal/test/issues/issue-1529/strict-iris/config.yaml @@ -0,0 +1,9 @@ +# yaml-language-server: $schema=../../../../../configuration-schema.json +package: issue1529 +generate: + client: true + models: true + embedded-spec: true + iris-server: true + strict-server: true +output: issue1529.gen.go diff --git a/internal/test/issues/issue-1529/strict-iris/doc.go b/internal/test/issues/issue-1529/strict-iris/doc.go new file mode 100644 index 0000000000..4bf78249fa --- /dev/null +++ b/internal/test/issues/issue-1529/strict-iris/doc.go @@ -0,0 +1,3 @@ +package issue1529 + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-1529/strict-iris/issue1529.gen.go b/internal/test/issues/issue-1529/strict-iris/issue1529.gen.go new file mode 100644 index 0000000000..a214576e80 --- /dev/null +++ b/internal/test/issues/issue-1529/strict-iris/issue1529.gen.go @@ -0,0 +1,466 @@ +// Package issue1529 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package issue1529 + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/kataras/iris/v12" + strictiris "github.com/oapi-codegen/runtime/strictmiddleware/iris" +) + +// Test defines model for Test. +type Test = map[string]interface{} + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // 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 +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // Test request + Test(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) Test(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewTestRequest(c.Server) + 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) +} + +// NewTestRequest generates requests for Test +func NewTestRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/test") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.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 +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // TestWithResponse request + TestWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*TestResponse, error) +} + +type TestResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Test + ApplicationjsonProfileBar200 *Test + ApplicationjsonProfileFoo200 *Test +} + +// Status returns HTTPResponse.Status +func (r TestResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r TestResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// TestWithResponse request returning *TestResponse +func (c *ClientWithResponses) TestWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*TestResponse, error) { + rsp, err := c.Test(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseTestResponse(rsp) +} + +// ParseTestResponse parses an HTTP response from a TestWithResponse call +func ParseTestResponse(rsp *http.Response) (*TestResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &TestResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case rsp.Header.Get("Content-Type") == "application/json" && rsp.StatusCode == 200: + var dest Test + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case rsp.Header.Get("Content-Type") == "application/json; profile=\"Bar\"" && rsp.StatusCode == 200: + var dest Test + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationjsonProfileBar200 = &dest + + case rsp.Header.Get("Content-Type") == "application/json; profile=\"Foo\"" && rsp.StatusCode == 200: + var dest Test + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationjsonProfileFoo200 = &dest + + } + + return response, nil +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + + // (GET /test) + Test(ctx iris.Context) +} + +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +type MiddlewareFunc iris.Handler + +// Test converts iris context to params. +func (w *ServerInterfaceWrapper) Test(ctx iris.Context) { + + // Invoke the callback with all the unmarshaled arguments + w.Handler.Test(ctx) +} + +// IrisServerOption is the option for iris server +type IrisServerOptions struct { + BaseURL string + Middlewares []MiddlewareFunc +} + +// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. +func RegisterHandlers(router *iris.Application, si ServerInterface) { + RegisterHandlersWithOptions(router, si, IrisServerOptions{}) +} + +// RegisterHandlersWithOptions creates http.Handler with additional options +func RegisterHandlersWithOptions(router *iris.Application, si ServerInterface, options IrisServerOptions) { + + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + router.Get(options.BaseURL+"/test", wrapper.Test) + + router.Build() +} + +type TestRequestObject struct { +} + +type TestResponseObject interface { + VisitTestResponse(ctx iris.Context) error +} + +type Test200JSONResponse Test + +func (response Test200JSONResponse) VisitTestResponse(ctx iris.Context) error { + ctx.ResponseWriter().Header().Set("Content-Type", "application/json") + ctx.StatusCode(200) + + return ctx.JSON(&response) +} + +type Test200ApplicationJSONProfileBarResponse Test + +func (response Test200ApplicationJSONProfileBarResponse) VisitTestResponse(ctx iris.Context) error { + ctx.ResponseWriter().Header().Set("Content-Type", "application/json; profile=\"Bar\"") + ctx.StatusCode(200) + + return ctx.JSON(&response) +} + +type Test200ApplicationJSONProfileFooResponse Test + +func (response Test200ApplicationJSONProfileFooResponse) VisitTestResponse(ctx iris.Context) error { + ctx.ResponseWriter().Header().Set("Content-Type", "application/json; profile=\"Foo\"") + ctx.StatusCode(200) + + return ctx.JSON(&response) +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + + // (GET /test) + Test(ctx context.Context, request TestRequestObject) (TestResponseObject, error) +} + +type StrictHandlerFunc = strictiris.StrictIrisHandlerFunc +type StrictMiddlewareFunc = strictiris.StrictIrisMiddlewareFunc + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc +} + +// Test operation middleware +func (sh *strictHandler) Test(ctx iris.Context) { + var request TestRequestObject + + handler := func(ctx iris.Context, request interface{}) (interface{}, error) { + return sh.ssi.Test(ctx, request.(TestRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "Test") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.StopWithError(http.StatusBadRequest, err) + return + } else if validResponse, ok := response.(TestResponseObject); ok { + if err := validResponse.VisitTestResponse(ctx); err != nil { + ctx.StopWithError(http.StatusBadRequest, err) + return + } + } else if response != nil { + ctx.Writef("Unexpected response type: %T", response) + return + } +} + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "H4sIAAAAAAAC/6zPMUsEMRAF4L8iT8twWbWL2FgI9pbXxDjr5cjNDMlYyLL/XRIXFizFad4072NmQZKL", + "ChNbQ1jQ0okucayv1KynfSkhQN7OlAzrujpkngWBP0txECWOmhFwf5gOt3DQaKcheNuIDxohSjVaFn55", + "R/jxHSo1FW40GnfT1CMJG/HoRNWS02j5cxPej+zbTaUZAdd+/8JvL/jh92t/Ew9XWmXOhR6PeIr1iH82", + "n0X+YG7zHQAA//9fnz6pkQEAAA==", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/internal/test/issues/issue-1529/strict-iris/spec.yaml b/internal/test/issues/issue-1529/strict-iris/spec.yaml new file mode 100644 index 0000000000..ca7aae80a3 --- /dev/null +++ b/internal/test/issues/issue-1529/strict-iris/spec.yaml @@ -0,0 +1,21 @@ +openapi: "3.0.1" +components: + schemas: + Test: + type: object +paths: + /test: + get: + operationId: test + responses: + 200: + content: + application/json: + schema: + $ref: "#/components/schemas/Test" + application/json; profile="Foo": + schema: + $ref: "#/components/schemas/Test" + application/json; profile="Bar": + schema: + $ref: "#/components/schemas/Test" diff --git a/pkg/codegen/template_helpers.go b/pkg/codegen/template_helpers.go index 49ee3aba64..64caf12828 100644 --- a/pkg/codegen/template_helpers.go +++ b/pkg/codegen/template_helpers.go @@ -246,14 +246,16 @@ func genResponseUnmarshal(op *OperationDefinition) string { func buildUnmarshalCase(typeDefinition ResponseTypeDefinition, caseAction string, contentType string) (caseKey string, caseClause string) { caseKey = fmt.Sprintf("%s.%s.%s", prefixLeastSpecific, contentType, typeDefinition.ResponseName) caseClauseKey := getConditionOfResponseName("rsp.StatusCode", typeDefinition.ResponseName) - caseClause = fmt.Sprintf("case strings.Contains(rsp.Header.Get(\"%s\"), \"%s\") && %s:\n%s\n", "Content-Type", contentType, caseClauseKey, caseAction) + contentTypeLiteral := StringToGoString(contentType) + caseClause = fmt.Sprintf("case strings.Contains(rsp.Header.Get(\"%s\"), %s) && %s:\n%s\n", "Content-Type", contentTypeLiteral, caseClauseKey, caseAction) return caseKey, caseClause } func buildUnmarshalCaseStrict(typeDefinition ResponseTypeDefinition, caseAction string, contentType string) (caseKey string, caseClause string) { caseKey = fmt.Sprintf("%s.%s.%s", prefixLeastSpecific, contentType, typeDefinition.ResponseName) caseClauseKey := getConditionOfResponseName("rsp.StatusCode", typeDefinition.ResponseName) - caseClause = fmt.Sprintf("case rsp.Header.Get(\"%s\") == \"%s\" && %s:\n%s\n", "Content-Type", contentType, caseClauseKey, caseAction) + contentTypeLiteral := StringToGoString(contentType) + caseClause = fmt.Sprintf("case rsp.Header.Get(\"%s\") == %s && %s:\n%s\n", "Content-Type", contentTypeLiteral, caseClauseKey, caseAction) return caseKey, caseClause } @@ -343,6 +345,7 @@ var TemplateFunctions = template.FuncMap{ "title": titleCaser.String, "stripNewLines": stripNewLines, "sanitizeGoIdentity": SanitizeGoIdentity, + "toGoString": StringToGoString, "toGoComment": StringWithTypeNameToGoComment, "genServerURLWithVariablesFunctionParams": genServerURLWithVariablesFunctionParams, diff --git a/pkg/codegen/templates/strict/strict-fiber-interface.tmpl b/pkg/codegen/templates/strict/strict-fiber-interface.tmpl index 8bcfc89b10..e83f7e38aa 100644 --- a/pkg/codegen/templates/strict/strict-fiber-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-fiber-interface.tmpl @@ -77,7 +77,7 @@ {{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}}", map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}"{{.ContentType}}"{{else}}response.ContentType{{end}}) + ctx.Response().Header.Set("Content-Type", {{if eq .NameTag "Multipart"}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType("{{.ContentType}}", 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)) diff --git a/pkg/codegen/templates/strict/strict-interface.tmpl b/pkg/codegen/templates/strict/strict-interface.tmpl index e19936d6a7..ac4b56fdbb 100644 --- a/pkg/codegen/templates/strict/strict-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-interface.tmpl @@ -74,7 +74,7 @@ {{if eq .NameTag "Multipart" -}} writer := multipart.NewWriter(w) {{end -}} - w.Header().Set("Content-Type", {{if eq .NameTag "Multipart"}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType("{{.ContentType}}", map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}"{{.ContentType}}"{{else}}response.ContentType{{end}}) + w.Header().Set("Content-Type", {{if eq .NameTag "Multipart"}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType("{{.ContentType}}", map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}{{.ContentType | toGoString}}{{else}}response.ContentType{{end}}) {{if not .IsSupported -}} if response.ContentLength != 0 { w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) diff --git a/pkg/codegen/templates/strict/strict-iris-interface.tmpl b/pkg/codegen/templates/strict/strict-iris-interface.tmpl index 5ebbc4e1b3..95b78d0645 100644 --- a/pkg/codegen/templates/strict/strict-iris-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-iris-interface.tmpl @@ -79,7 +79,7 @@ {{if eq .NameTag "Multipart" -}} writer := multipart.NewWriter(ctx.ResponseWriter()) {{end -}} - ctx.ResponseWriter().Header().Set("Content-Type", {{if eq .NameTag "Multipart"}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType("{{.ContentType}}", map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}"{{.ContentType}}"{{else}}response.ContentType{{end}}) + ctx.ResponseWriter().Header().Set("Content-Type", {{if eq .NameTag "Multipart"}}{{if eq .ContentType "multipart/form-data"}}writer.FormDataContentType(){{else}}mime.FormatMediaType("{{.ContentType}}", map[string]string{"boundary": writer.Boundary()}){{end}}{{else if .HasFixedContentType }}{{.ContentType | toGoString}}{{else}}response.ContentType{{end}}) {{if not .IsSupported -}} if response.ContentLength != 0 { ctx.ResponseWriter().Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) diff --git a/pkg/codegen/utils.go b/pkg/codegen/utils.go index 691d887663..89a9c31256 100644 --- a/pkg/codegen/utils.go +++ b/pkg/codegen/utils.go @@ -864,6 +864,13 @@ func PathToTypeName(path []string) string { return strings.Join(path, "_") } +// StringToGoString takes an arbitrary string and converts it to a valid Go string literal, +// including the quotes. For instance, `foo "bar"` would be converted to `"foo \"bar\""` +func StringToGoString(in string) string { + esc := strings.ReplaceAll(in, "\"", "\\\"") + return fmt.Sprintf("\"%s\"", esc) +} + // StringToGoComment renders a possible multi-line string as a valid Go-Comment. // Each line is prefixed as a comment. func StringToGoComment(in string) string { diff --git a/pkg/codegen/utils_test.go b/pkg/codegen/utils_test.go index 0666fc9909..aaea643721 100644 --- a/pkg/codegen/utils_test.go +++ b/pkg/codegen/utils_test.go @@ -469,6 +469,36 @@ func TestReplacePathParamsWithStr(t *testing.T) { assert.EqualValues(t, "/path/%s/%s/%s/foo", result) } +func TestStringToGoStringValue(t *testing.T) { + testCases := []struct { + input string + expected string + message string + }{ + { + input: ``, + expected: `""`, + message: "blank string should be converted to empty Go string literal", + }, + { + input: `application/json`, + expected: `"application/json"`, + message: "typical string should be returned as-is", + }, + { + input: `application/json; foo="bar"`, + expected: `"application/json; foo=\"bar\""`, + message: "string with quotes should include escape characters", + }, + } + for _, testCase := range testCases { + t.Run(testCase.message, func(t *testing.T) { + result := StringToGoString(testCase.input) + assert.EqualValues(t, testCase.expected, result, testCase.message) + }) + } +} + func TestStringToGoComment(t *testing.T) { testCases := []struct { input string From 1650807a81afff00eb4447f8ef332e41fcc01c18 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Sun, 15 Feb 2026 07:01:44 -0800 Subject: [PATCH 23/44] fix: handle duplicate path parameters in OpenAPI specs (#2220) Some real-world OpenAPI specs (e.g. Keycloak) reuse the same path parameter more than once in a single URI, such as: /clients/{client-uuid}/roles/{role-name}/composites/clients/{client-uuid} Previously this caused a "has 4 positional parameters, but spec has 3 declared" error because SortParamsByPath compared raw URI placeholder count against unique declared parameters. Fix this by deduplicating the path parameters extracted from the URI (preserving first-occurrence order) before matching them against the spec declared parameters. This is the right level to fix the issue rather than scattering dedup logic across template helpers. Fixes #1574 Supersedes #2175 Co-authored-by: Junior Rantila Co-authored-by: Claude Opus 4.6 --- pkg/codegen/codegen_test.go | 60 +++++++++++++++++++++++++++++++++++++ pkg/codegen/utils.go | 22 +++++++++++--- pkg/codegen/utils_test.go | 47 +++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 4 deletions(-) diff --git a/pkg/codegen/codegen_test.go b/pkg/codegen/codegen_test.go index 7b22faed01..ff3c86d9ba 100644 --- a/pkg/codegen/codegen_test.go +++ b/pkg/codegen/codegen_test.go @@ -227,5 +227,65 @@ func (t *ExampleSchema_Item) FromExternalRef0NewPet(v externalRef0.NewPet) error `) } +func TestDuplicatePathParameter(t *testing.T) { + // Regression test for https://github.com/oapi-codegen/oapi-codegen/issues/1574 + // Some real-world specs (e.g. Keycloak) have paths where the same parameter + // appears more than once: /clients/{client-uuid}/.../clients/{client-uuid} + spec := ` +openapi: "3.0.0" +info: + version: 1.0.0 + title: Duplicate path param test +paths: + /admin/realms/{realm}/clients/{client-uuid}/roles/{role-name}/composites/clients/{client-uuid}: + get: + operationId: getCompositeRoles + parameters: + - name: realm + in: path + required: true + schema: + type: string + - name: client-uuid + in: path + required: true + schema: + type: string + - name: role-name + in: path + required: true + schema: + type: string + responses: + '200': + description: Success +` + loader := openapi3.NewLoader() + swagger, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + opts := Configuration{ + PackageName: "api", + Generate: GenerateOptions{ + EchoServer: true, + Client: true, + Models: true, + }, + } + + code, err := Generate(swagger, opts) + require.NoError(t, err) + assert.NotEmpty(t, code) + + // Verify the generated code is valid Go. + _, err = format.Source([]byte(code)) + require.NoError(t, err) + + // The path params should appear exactly once in the function signature. + assert.Contains(t, code, "realm string") + assert.Contains(t, code, "clientUuid string") + assert.Contains(t, code, "roleName string") +} + //go:embed test_spec.yaml var testOpenAPIDefinition string diff --git a/pkg/codegen/utils.go b/pkg/codegen/utils.go index 89a9c31256..79e274f7ee 100644 --- a/pkg/codegen/utils.go +++ b/pkg/codegen/utils.go @@ -654,15 +654,29 @@ func ReplacePathParamsWithStr(uri string) string { } // SortParamsByPath reorders the given parameter definitions to match those in the path URI. +// If a parameter appears more than once in the path (e.g. Keycloak's +// /clients/{client-uuid}/roles/{role-name}/composites/clients/{client-uuid}), +// duplicates are removed and only the first occurrence determines the order. func SortParamsByPath(path string, in []ParameterDefinition) ([]ParameterDefinition, error) { pathParams := OrderedParamsFromUri(path) + + // Deduplicate, preserving first-occurrence order. + seen := make(map[string]struct{}, len(pathParams)) + uniqueParams := make([]string, 0, len(pathParams)) + for _, name := range pathParams { + if _, exists := seen[name]; !exists { + seen[name] = struct{}{} + uniqueParams = append(uniqueParams, name) + } + } + n := len(in) - if len(pathParams) != n { + if len(uniqueParams) != n { return nil, fmt.Errorf("path '%s' has %d positional parameters, but spec has %d declared", - path, len(pathParams), n) + path, len(uniqueParams), n) } - out := make([]ParameterDefinition, len(in)) - for i, name := range pathParams { + out := make([]ParameterDefinition, n) + for i, name := range uniqueParams { p := ParameterDefinitions(in).FindByName(name) if p == nil { return nil, fmt.Errorf("path '%s' refers to parameter '%s', which doesn't exist in specification", diff --git a/pkg/codegen/utils_test.go b/pkg/codegen/utils_test.go index aaea643721..f204fc50c8 100644 --- a/pkg/codegen/utils_test.go +++ b/pkg/codegen/utils_test.go @@ -462,6 +462,53 @@ func TestOrderedParamsFromUri(t *testing.T) { result = OrderedParamsFromUri("/path/foo") assert.EqualValues(t, []string{}, result) + + // A parameter can appear more than once in the URI (e.g. Keycloak API). + // OrderedParamsFromUri faithfully returns all occurrences. + result = OrderedParamsFromUri("/admin/realms/{realm}/clients/{client-uuid}/roles/{role-name}/composites/clients/{client-uuid}") + assert.EqualValues(t, []string{"realm", "client-uuid", "role-name", "client-uuid"}, result) +} + +func TestSortParamsByPath(t *testing.T) { + strSchema := &openapi3.Schema{Type: &openapi3.Types{"string"}} + + t.Run("reorders params to match path order", func(t *testing.T) { + params := []ParameterDefinition{ + {ParamName: "b", In: "path", Spec: &openapi3.Parameter{Name: "b", Schema: &openapi3.SchemaRef{Value: strSchema}}}, + {ParamName: "a", In: "path", Spec: &openapi3.Parameter{Name: "a", Schema: &openapi3.SchemaRef{Value: strSchema}}}, + } + sorted, err := SortParamsByPath("/foo/{a}/bar/{b}", params) + require.NoError(t, err) + require.Len(t, sorted, 2) + assert.Equal(t, "a", sorted[0].ParamName) + assert.Equal(t, "b", sorted[1].ParamName) + }) + + t.Run("errors on missing parameter", func(t *testing.T) { + params := []ParameterDefinition{ + {ParamName: "a", In: "path", Spec: &openapi3.Parameter{Name: "a", Schema: &openapi3.SchemaRef{Value: strSchema}}}, + } + _, err := SortParamsByPath("/foo/{a}/bar/{b}", params) + assert.Error(t, err) + }) + + t.Run("handles duplicate path parameters", func(t *testing.T) { + // This is the Keycloak-style path where {client-uuid} appears twice. + // The spec only declares 3 unique parameters. + params := []ParameterDefinition{ + {ParamName: "realm", In: "path", Spec: &openapi3.Parameter{Name: "realm", Schema: &openapi3.SchemaRef{Value: strSchema}}}, + {ParamName: "client-uuid", In: "path", Spec: &openapi3.Parameter{Name: "client-uuid", Schema: &openapi3.SchemaRef{Value: strSchema}}}, + {ParamName: "role-name", In: "path", Spec: &openapi3.Parameter{Name: "role-name", Schema: &openapi3.SchemaRef{Value: strSchema}}}, + } + path := "/admin/realms/{realm}/clients/{client-uuid}/roles/{role-name}/composites/clients/{client-uuid}" + sorted, err := SortParamsByPath(path, params) + require.NoError(t, err) + // Should return 3 unique params in first-occurrence order + require.Len(t, sorted, 3) + assert.Equal(t, "realm", sorted[0].ParamName) + assert.Equal(t, "client-uuid", sorted[1].ParamName) + assert.Equal(t, "role-name", sorted[2].ParamName) + }) } func TestReplacePathParamsWithStr(t *testing.T) { From fca3f0fedd22c8e372ecc0811aefaea33f33a845 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Sun, 15 Feb 2026 07:02:07 -0800 Subject: [PATCH 24/44] Fix schema gathering oversight (#2219) Response and RequestBodies need to include any additional types into model generation. This is an ancient bug, from the very first days of oapi-codegen. Extend the issue-200 spec with inline objects containing additionalProperties inside components/responses and components/requestBodies. The test confirms the codegen produces named types (Bar_Pagination, Bar_Metadata) for these inline schemas. Co-authored-by: Claude Opus 4.6 --- .../test/issues/issue-200/issue200.gen.go | 258 +++++++++++++++++- .../test/issues/issue-200/issue200_test.go | 15 + internal/test/issues/issue-200/spec.yaml | 16 ++ pkg/codegen/codegen.go | 2 + 4 files changed, 285 insertions(+), 6 deletions(-) diff --git a/internal/test/issues/issue-200/issue200.gen.go b/internal/test/issues/issue-200/issue200.gen.go index cc3c138314..530c042c7a 100644 --- a/internal/test/issues/issue-200/issue200.gen.go +++ b/internal/test/issues/issue-200/issue200.gen.go @@ -3,6 +3,11 @@ // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. package issue200 +import ( + "encoding/json" + "fmt" +) + // Bar defines model for Bar. type Bar struct { Value *string `json:"value,omitempty"` @@ -24,20 +29,36 @@ type BarParameter = string // BarResponse defines model for Bar. type BarResponse struct { - Value1 *Bar `json:"value1,omitempty"` - Value2 *Bar2 `json:"value2,omitempty"` - Value3 *BarParam `json:"value3,omitempty"` - Value4 *BarParam2 `json:"value4,omitempty"` + Pagination *Bar_Pagination `json:"pagination,omitempty"` + Value1 *Bar `json:"value1,omitempty"` + Value2 *Bar2 `json:"value2,omitempty"` + Value3 *BarParam `json:"value3,omitempty"` + Value4 *BarParam2 `json:"value4,omitempty"` +} + +// Bar_Pagination defines model for Bar.Pagination. +type Bar_Pagination struct { + Page *int `json:"page,omitempty"` + TotalPages *int `json:"totalPages,omitempty"` + AdditionalProperties map[string]string `json:"-"` } // BarRequestBody defines model for Bar. type BarRequestBody struct { - Value *int `json:"value,omitempty"` + Metadata *Bar_Metadata `json:"metadata,omitempty"` + Value *int `json:"value,omitempty"` +} + +// Bar_Metadata defines model for Bar.Metadata. +type Bar_Metadata struct { + Key *string `json:"key,omitempty"` + AdditionalProperties map[string]string `json:"-"` } // PostFooJSONBody defines parameters for PostFoo. type PostFooJSONBody struct { - Value *int `json:"value,omitempty"` + Metadata *PostFooJSONBody_Metadata `json:"metadata,omitempty"` + Value *int `json:"value,omitempty"` } // PostFooParams defines parameters for PostFoo. @@ -45,5 +66,230 @@ type PostFooParams struct { Bar *Bar `form:"Bar,omitempty" json:"Bar,omitempty"` } +// PostFooJSONBody_Metadata defines parameters for PostFoo. +type PostFooJSONBody_Metadata struct { + Key *string `json:"key,omitempty"` + AdditionalProperties map[string]string `json:"-"` +} + // PostFooJSONRequestBody defines body for PostFoo for application/json ContentType. type PostFooJSONRequestBody PostFooJSONBody + +// Getter for additional properties for PostFooJSONBody_Metadata. Returns the specified +// element and whether it was found +func (a PostFooJSONBody_Metadata) Get(fieldName string) (value string, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for PostFooJSONBody_Metadata +func (a *PostFooJSONBody_Metadata) Set(fieldName string, value string) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]string) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for PostFooJSONBody_Metadata to handle AdditionalProperties +func (a *PostFooJSONBody_Metadata) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["key"]; found { + err = json.Unmarshal(raw, &a.Key) + if err != nil { + return fmt.Errorf("error reading 'key': %w", err) + } + delete(object, "key") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]string) + for fieldName, fieldBuf := range object { + var fieldVal string + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for PostFooJSONBody_Metadata to handle AdditionalProperties +func (a PostFooJSONBody_Metadata) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Key != nil { + object["key"], err = json.Marshal(a.Key) + if err != nil { + return nil, fmt.Errorf("error marshaling 'key': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for Bar_Pagination. Returns the specified +// element and whether it was found +func (a Bar_Pagination) Get(fieldName string) (value string, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Bar_Pagination +func (a *Bar_Pagination) Set(fieldName string, value string) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]string) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Bar_Pagination to handle AdditionalProperties +func (a *Bar_Pagination) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["page"]; found { + err = json.Unmarshal(raw, &a.Page) + if err != nil { + return fmt.Errorf("error reading 'page': %w", err) + } + delete(object, "page") + } + + if raw, found := object["totalPages"]; found { + err = json.Unmarshal(raw, &a.TotalPages) + if err != nil { + return fmt.Errorf("error reading 'totalPages': %w", err) + } + delete(object, "totalPages") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]string) + for fieldName, fieldBuf := range object { + var fieldVal string + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Bar_Pagination to handle AdditionalProperties +func (a Bar_Pagination) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Page != nil { + object["page"], err = json.Marshal(a.Page) + if err != nil { + return nil, fmt.Errorf("error marshaling 'page': %w", err) + } + } + + if a.TotalPages != nil { + object["totalPages"], err = json.Marshal(a.TotalPages) + if err != nil { + return nil, fmt.Errorf("error marshaling 'totalPages': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for Bar_Metadata. Returns the specified +// element and whether it was found +func (a Bar_Metadata) Get(fieldName string) (value string, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Bar_Metadata +func (a *Bar_Metadata) Set(fieldName string, value string) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]string) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Bar_Metadata to handle AdditionalProperties +func (a *Bar_Metadata) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["key"]; found { + err = json.Unmarshal(raw, &a.Key) + if err != nil { + return fmt.Errorf("error reading 'key': %w", err) + } + delete(object, "key") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]string) + for fieldName, fieldBuf := range object { + var fieldVal string + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Bar_Metadata to handle AdditionalProperties +func (a Bar_Metadata) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Key != nil { + object["key"], err = json.Marshal(a.Key) + if err != nil { + return nil, fmt.Errorf("error marshaling 'key': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} diff --git a/internal/test/issues/issue-200/issue200_test.go b/internal/test/issues/issue-200/issue200_test.go index 9f690d520c..96fdb62e8b 100644 --- a/internal/test/issues/issue-200/issue200_test.go +++ b/internal/test/issues/issue-200/issue200_test.go @@ -35,6 +35,21 @@ func TestDuplicateTypeNamesCompile(t *testing.T) { // RequestBody type: BarRequestBody (was "Bar" in components/requestBodies) _ = BarRequestBody{Value: ptr(42)} + // Inline nested object with additionalProperties inside a response + // must produce a named AdditionalType (not get silently dropped). + _ = Bar_Pagination{ + Page: ptr(1), + TotalPages: ptr(10), + AdditionalProperties: map[string]string{"cursor": "abc"}, + } + + // Inline nested object with additionalProperties inside a requestBody + // must produce a named AdditionalType (not get silently dropped). + _ = Bar_Metadata{ + Key: ptr("k"), + AdditionalProperties: map[string]string{"extra": "val"}, + } + // Operation-derived types _ = PostFooParams{Bar: &Bar{}} _ = PostFooJSONBody{Value: ptr(99)} diff --git a/internal/test/issues/issue-200/spec.yaml b/internal/test/issues/issue-200/spec.yaml index a18c7f403c..2a68f71694 100644 --- a/internal/test/issues/issue-200/spec.yaml +++ b/internal/test/issues/issue-200/spec.yaml @@ -58,6 +58,13 @@ components: properties: value: type: integer + metadata: + type: object + properties: + key: + type: string + additionalProperties: + type: string responses: Bar: @@ -78,3 +85,12 @@ components: $ref: '#/components/schemas/BarParam' value4: $ref: '#/components/schemas/BarParam2' + pagination: + type: object + properties: + page: + type: integer + totalPages: + type: integer + additionalProperties: + type: string diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go index ce259c42f4..355de4f7a7 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -714,6 +714,7 @@ func GenerateTypesForResponses(t *template.Template, responses openapi3.Response } types = append(types, typeDef) + types = append(types, goType.AdditionalTypes...) } } return types, nil @@ -762,6 +763,7 @@ func GenerateTypesForRequestBodies(t *template.Template, bodies map[string]*open typeDef.TypeName = SchemaNameToTypeName(refType) } types = append(types, typeDef) + types = append(types, goType.AdditionalTypes...) } } return types, nil From b658fcffa2b8baa059df5a81ff6cf75eeb1d6e9a Mon Sep 17 00:00:00 2001 From: Toby Brain Date: Tue, 17 Feb 2026 03:39:56 +1100 Subject: [PATCH 25/44] Support unions with multiple mappings pointing to a single underlying type (#2071) --- internal/test/issues/issue-1530/config.yaml | 4 + internal/test/issues/issue-1530/doc.go | 3 + .../test/issues/issue-1530/issue1530.gen.go | 126 ++++++++++++++++++ .../test/issues/issue-1530/issue1530.yaml | 57 ++++++++ .../test/issues/issue-1530/issue1530_test.go | 51 +++++++ pkg/codegen/schema.go | 3 +- pkg/codegen/templates/union.tmpl | 37 ++--- 7 files changed, 263 insertions(+), 18 deletions(-) create mode 100644 internal/test/issues/issue-1530/config.yaml create mode 100644 internal/test/issues/issue-1530/doc.go create mode 100644 internal/test/issues/issue-1530/issue1530.gen.go create mode 100644 internal/test/issues/issue-1530/issue1530.yaml create mode 100644 internal/test/issues/issue-1530/issue1530_test.go diff --git a/internal/test/issues/issue-1530/config.yaml b/internal/test/issues/issue-1530/config.yaml new file mode 100644 index 0000000000..3cb9f3dd04 --- /dev/null +++ b/internal/test/issues/issue-1530/config.yaml @@ -0,0 +1,4 @@ +package: issue1530 +generate: + models: true +output: issue1530.gen.go \ No newline at end of file diff --git a/internal/test/issues/issue-1530/doc.go b/internal/test/issues/issue-1530/doc.go new file mode 100644 index 0000000000..c6a0133735 --- /dev/null +++ b/internal/test/issues/issue-1530/doc.go @@ -0,0 +1,3 @@ +package issue1530 + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml issue1530.yaml diff --git a/internal/test/issues/issue-1530/issue1530.gen.go b/internal/test/issues/issue-1530/issue1530.gen.go new file mode 100644 index 0000000000..8a60251b2c --- /dev/null +++ b/internal/test/issues/issue-1530/issue1530.gen.go @@ -0,0 +1,126 @@ +// Package issue1530 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package issue1530 + +import ( + "encoding/json" + "errors" + + "github.com/oapi-codegen/runtime" +) + +// ConfigHttp defines model for ConfigHttp. +type ConfigHttp struct { + ConfigType string `json:"config_type"` + Host string `json:"host"` + Password *string `json:"password,omitempty"` + Port int `json:"port"` + User *string `json:"user,omitempty"` +} + +// ConfigSaveReq defines model for ConfigSaveReq. +type ConfigSaveReq struct { + union json.RawMessage +} + +// ConfigSsh defines model for ConfigSsh. +type ConfigSsh struct { + ConfigType string `json:"config_type"` + Host *string `json:"host,omitempty"` + Port *int `json:"port,omitempty"` + PrivateKey *string `json:"private_key,omitempty"` + User *string `json:"user,omitempty"` +} + +// PostConfigJSONRequestBody defines body for PostConfig for application/json ContentType. +type PostConfigJSONRequestBody = ConfigSaveReq + +// AsConfigHttp returns the union data inside the ConfigSaveReq as a ConfigHttp +func (t ConfigSaveReq) AsConfigHttp() (ConfigHttp, error) { + var body ConfigHttp + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromConfigHttp overwrites any union data inside the ConfigSaveReq as the provided ConfigHttp +func (t *ConfigSaveReq) FromConfigHttp(v ConfigHttp) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeConfigHttp performs a merge with any union data inside the ConfigSaveReq, using the provided ConfigHttp +func (t *ConfigSaveReq) MergeConfigHttp(v ConfigHttp) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsConfigSsh returns the union data inside the ConfigSaveReq as a ConfigSsh +func (t ConfigSaveReq) AsConfigSsh() (ConfigSsh, error) { + var body ConfigSsh + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromConfigSsh overwrites any union data inside the ConfigSaveReq as the provided ConfigSsh +func (t *ConfigSaveReq) FromConfigSsh(v ConfigSsh) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeConfigSsh performs a merge with any union data inside the ConfigSaveReq, using the provided ConfigSsh +func (t *ConfigSaveReq) MergeConfigSsh(v ConfigSsh) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t ConfigSaveReq) Discriminator() (string, error) { + var discriminator struct { + Discriminator string `json:"config_type"` + } + err := json.Unmarshal(t.union, &discriminator) + return discriminator.Discriminator, err +} + +func (t ConfigSaveReq) ValueByDiscriminator() (interface{}, error) { + discriminator, err := t.Discriminator() + if err != nil { + return nil, err + } + switch discriminator { + case "another_server": + return t.AsConfigHttp() + case "apache_server": + return t.AsConfigHttp() + case "ssh_server": + return t.AsConfigSsh() + case "web_server": + return t.AsConfigHttp() + default: + return nil, errors.New("unknown discriminator value: " + discriminator) + } +} + +func (t ConfigSaveReq) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *ConfigSaveReq) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} diff --git a/internal/test/issues/issue-1530/issue1530.yaml b/internal/test/issues/issue-1530/issue1530.yaml new file mode 100644 index 0000000000..222d074b7b --- /dev/null +++ b/internal/test/issues/issue-1530/issue1530.yaml @@ -0,0 +1,57 @@ +paths: + /config: + post: + summary: Save configuration + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ConfigSaveReq" + responses: + "200": + description: Configuration saved successfully +components: + schemas: + ConfigHttp: + type: object + properties: + config_type: + type: string + host: + type: string + port: + type: integer + user: + type: string + password: + type: string + required: + - config_type + - host + - port + ConfigSaveReq: + oneOf: + - $ref: "#/components/schemas/ConfigHttp" + - $ref: "#/components/schemas/ConfigSsh" + discriminator: + propertyName: config_type + mapping: + ssh_server: "#/components/schemas/ConfigSsh" + apache_server: "#/components/schemas/ConfigHttp" + web_server: "#/components/schemas/ConfigHttp" + another_server: "#/components/schemas/ConfigHttp" + ConfigSsh: + type: object + properties: + config_type: + type: string + host: + type: string + port: + type: integer + user: + type: string + private_key: + type: string + required: + - config_type \ No newline at end of file diff --git a/internal/test/issues/issue-1530/issue1530_test.go b/internal/test/issues/issue-1530/issue1530_test.go new file mode 100644 index 0000000000..a23f7ed2d8 --- /dev/null +++ b/internal/test/issues/issue-1530/issue1530_test.go @@ -0,0 +1,51 @@ +package issue1530_test + +import ( + "testing" + + issue1530 "github.com/oapi-codegen/oapi-codegen/v2/internal/test/issues/issue-1530" + "github.com/stretchr/testify/require" +) + +func TestIssue1530(t *testing.T) { + httpConfigTypes := []string{ + "another_server", + "apache_server", + "web_server", + } + + for _, configType := range httpConfigTypes { + t.Run("http-"+configType, func(t *testing.T) { + saveReq := issue1530.ConfigSaveReq{} + err := saveReq.FromConfigHttp(issue1530.ConfigHttp{ + ConfigType: configType, + Host: "example.com", + }) + require.NoError(t, err) + + cfg, err := saveReq.AsConfigHttp() + require.NoError(t, err) + require.Equal(t, configType, cfg.ConfigType) + + cfgByDiscriminator, err := saveReq.ValueByDiscriminator() + require.NoError(t, err) + require.Equal(t, cfg, cfgByDiscriminator) + }) + } + + t.Run("ssh", func(t *testing.T) { + saveReq := issue1530.ConfigSaveReq{} + err := saveReq.FromConfigSsh(issue1530.ConfigSsh{ + ConfigType: "ssh_server", + }) + require.NoError(t, err) + + cfg, err := saveReq.AsConfigSsh() + require.NoError(t, err) + require.Equal(t, "ssh_server", cfg.ConfigType) + + cfgByDiscriminator, err := saveReq.ValueByDiscriminator() + require.NoError(t, err) + require.Equal(t, cfg, cfgByDiscriminator) + }) +} diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go index 7435fa224d..f361ab3465 100644 --- a/pkg/codegen/schema.go +++ b/pkg/codegen/schema.go @@ -938,7 +938,6 @@ func generateUnion(outSchema *Schema, elements openapi3.SchemaRefs, discriminato if v == element.Ref { outSchema.Discriminator.Mapping[k] = elementSchema.GoType mapped = true - break } } // Implicit mapping. @@ -949,7 +948,7 @@ func generateUnion(outSchema *Schema, elements openapi3.SchemaRefs, discriminato outSchema.UnionElements = append(outSchema.UnionElements, UnionElement(elementSchema.GoType)) } - if (outSchema.Discriminator != nil) && len(outSchema.Discriminator.Mapping) != len(elements) { + if (outSchema.Discriminator != nil) && len(outSchema.Discriminator.Mapping) < len(elements) { return errors.New("discriminator: not all schemas were mapped") } diff --git a/pkg/codegen/templates/union.tmpl b/pkg/codegen/templates/union.tmpl index b27a2aca3f..61c2bf37fd 100644 --- a/pkg/codegen/templates/union.tmpl +++ b/pkg/codegen/templates/union.tmpl @@ -2,6 +2,7 @@ {{$typeName := .TypeName -}} {{$discriminator := .Schema.Discriminator}} {{$properties := .Schema.Properties -}} + {{$numberOfUnionTypes := len .Schema.UnionElements -}} {{range .Schema.UnionElements}} {{$element := . -}} // As{{ .Method }} returns the union data inside the {{$typeName}} as a {{.}} @@ -14,16 +15,18 @@ // From{{ .Method }} overwrites any union data inside the {{$typeName}} as the provided {{.}} func (t *{{$typeName}}) From{{ .Method }} (v {{.}}) error { {{if $discriminator -}} - {{range $value, $type := $discriminator.Mapping -}} - {{if eq $type $element -}} - {{$hasProperty := false -}} - {{range $properties -}} - {{if eq .GoFieldName $discriminator.PropertyName -}} - t.{{$discriminator.PropertyName}} = "{{$value}}" - {{$hasProperty = true -}} + {{if eq $numberOfUnionTypes (len $discriminator.Mapping) -}} + {{range $value, $type := $discriminator.Mapping -}} + {{if eq $type $element -}} + {{$hasProperty := false -}} + {{range $properties -}} + {{if eq .GoFieldName $discriminator.PropertyName -}} + t.{{$discriminator.PropertyName}} = "{{$value}}" + {{$hasProperty = true -}} + {{end -}} {{end -}} + {{if not $hasProperty}}v.{{$discriminator.PropertyName}} = "{{$value}}"{{end}} {{end -}} - {{if not $hasProperty}}v.{{$discriminator.PropertyName}} = "{{$value}}"{{end}} {{end -}} {{end -}} {{end -}} @@ -35,16 +38,18 @@ // Merge{{ .Method }} performs a merge with any union data inside the {{$typeName}}, using the provided {{.}} func (t *{{$typeName}}) Merge{{ .Method }} (v {{.}}) error { {{if $discriminator -}} - {{range $value, $type := $discriminator.Mapping -}} - {{if eq $type $element -}} - {{$hasProperty := false -}} - {{range $properties -}} - {{if eq .GoFieldName $discriminator.PropertyName -}} - t.{{$discriminator.PropertyName}} = "{{$value}}" - {{$hasProperty = true -}} + {{if eq $numberOfUnionTypes (len $discriminator.Mapping) -}} + {{range $value, $type := $discriminator.Mapping -}} + {{if eq $type $element -}} + {{$hasProperty := false -}} + {{range $properties -}} + {{if eq .GoFieldName $discriminator.PropertyName -}} + t.{{$discriminator.PropertyName}} = "{{$value}}" + {{$hasProperty = true -}} + {{end -}} {{end -}} + {{if not $hasProperty}}v.{{$discriminator.PropertyName}} = "{{$value}}"{{end}} {{end -}} - {{if not $hasProperty}}v.{{$discriminator.PropertyName}} = "{{$value}}"{{end}} {{end -}} {{end -}} {{end -}} From 091742e84c1ac2b9ad41c5be25a0983cf6330976 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Mon, 16 Feb 2026 13:08:29 -0800 Subject: [PATCH 26/44] fix(strict-server): generate correct type for `$ref` text responses (#2225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: add regression test for issue #2190 Add a minimal reproduction for invalid generated code when reusing response components. The generated VisitGetTestResponse method attempts []byte(response) on a struct type, which does not compile. Co-Authored-By: Claude Opus 4.6 * fix: generate correct type for $ref text responses in strict server (#2190) When a response is defined as a $ref to a component response with text/plain content type (and no headers), the strict-server template generated a struct-embedding type whose Visit method tried to call []byte(response) on a struct, which failed to compile. The root cause was the revert of PR #1132 (commit 891a067), which had originally fixed this by making all text responses string types. That PR was reverted because it broke text responses with headers (#1676), which require the struct form with a Body field. The fix extends the existing multipart special case in Branch 1A of the strict-interface template to also cover text responses without headers. This generates a named type alias (e.g. `type GetTest401TextResponse UnauthorizedTextResponse`) instead of a struct embedding, so []byte(response) compiles and works correctly. Text responses with headers continue to go through Branch 1C (struct with Body + Headers fields), so #1676 is unaffected — confirmed by verifying no diff in internal/test/issues/issue-1676/ping.gen.go. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- internal/test/issues/issue-2190/config.yaml | 9 + internal/test/issues/issue-2190/generate.go | 3 + .../test/issues/issue-2190/issue2190.gen.go | 486 ++++++++++++++++++ .../test/issues/issue-2190/issue2190_test.go | 37 ++ internal/test/issues/issue-2190/spec.yaml | 30 ++ .../templates/strict/strict-interface.tmpl | 2 +- 6 files changed, 566 insertions(+), 1 deletion(-) create mode 100644 internal/test/issues/issue-2190/config.yaml create mode 100644 internal/test/issues/issue-2190/generate.go create mode 100644 internal/test/issues/issue-2190/issue2190.gen.go create mode 100644 internal/test/issues/issue-2190/issue2190_test.go create mode 100644 internal/test/issues/issue-2190/spec.yaml diff --git a/internal/test/issues/issue-2190/config.yaml b/internal/test/issues/issue-2190/config.yaml new file mode 100644 index 0000000000..da89951cdc --- /dev/null +++ b/internal/test/issues/issue-2190/config.yaml @@ -0,0 +1,9 @@ +package: issue2190 +output: issue2190.gen.go +generate: + std-http-server: true + strict-server: true + models: true + client: true +output-options: + nullable-type: true diff --git a/internal/test/issues/issue-2190/generate.go b/internal/test/issues/issue-2190/generate.go new file mode 100644 index 0000000000..74169c3fe3 --- /dev/null +++ b/internal/test/issues/issue-2190/generate.go @@ -0,0 +1,3 @@ +package issue2190 + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-2190/issue2190.gen.go b/internal/test/issues/issue-2190/issue2190.gen.go new file mode 100644 index 0000000000..c22e32724c --- /dev/null +++ b/internal/test/issues/issue-2190/issue2190.gen.go @@ -0,0 +1,486 @@ +//go:build go1.22 + +// Package issue2190 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package issue2190 + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + strictnethttp "github.com/oapi-codegen/runtime/strictmiddleware/nethttp" +) + +// Success defines model for Success. +type Success = string + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // 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 +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GetTest request + GetTest(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetTest(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetTestRequest(c.Server) + 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) +} + +// NewGetTestRequest generates requests for GetTest +func NewGetTestRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/test") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.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 +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GetTestWithResponse request + GetTestWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetTestResponse, error) +} + +type GetTestResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Success +} + +// Status returns HTTPResponse.Status +func (r GetTestResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetTestResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetTestWithResponse request returning *GetTestResponse +func (c *ClientWithResponses) GetTestWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetTestResponse, error) { + rsp, err := c.GetTest(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetTestResponse(rsp) +} + +// ParseGetTestResponse parses an HTTP response from a GetTestWithResponse call +func ParseGetTestResponse(rsp *http.Response) (*GetTestResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetTestResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Success + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + + // (GET /v1/test) + GetTest(w http.ResponseWriter, r *http.Request) +} + +// 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 + +// GetTest operation middleware +func (siw *ServerInterfaceWrapper) GetTest(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetTest(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +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) +} + +// 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)) + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + +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) + } + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + m.HandleFunc("GET "+options.BaseURL+"/v1/test", wrapper.GetTest) + + return m +} + +type SuccessJSONResponse string + +type UnauthorizedTextResponse string + +type GetTestRequestObject struct { +} + +type GetTestResponseObject interface { + VisitGetTestResponse(w http.ResponseWriter) error +} + +type GetTest200JSONResponse struct{ SuccessJSONResponse } + +func (response GetTest200JSONResponse) VisitGetTestResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetTest401TextResponse UnauthorizedTextResponse + +func (response GetTest401TextResponse) VisitGetTestResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(401) + + _, err := w.Write([]byte(response)) + return err +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + + // (GET /v1/test) + GetTest(ctx context.Context, request GetTestRequestObject) (GetTestResponseObject, error) +} + +type StrictHandlerFunc = strictnethttp.StrictHTTPHandlerFunc +type StrictMiddlewareFunc = strictnethttp.StrictHTTPMiddlewareFunc + +type StrictHTTPServerOptions struct { + RequestErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) + ResponseErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: StrictHTTPServerOptions{ + RequestErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + }, + ResponseErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) + }, + }} +} + +func NewStrictHandlerWithOptions(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc, options StrictHTTPServerOptions) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: options} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc + options StrictHTTPServerOptions +} + +// GetTest operation middleware +func (sh *strictHandler) GetTest(w http.ResponseWriter, r *http.Request) { + var request GetTestRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetTest(ctx, request.(GetTestRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetTest") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetTestResponseObject); ok { + if err := validResponse.VisitGetTestResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} diff --git a/internal/test/issues/issue-2190/issue2190_test.go b/internal/test/issues/issue-2190/issue2190_test.go new file mode 100644 index 0000000000..8dc7a0b075 --- /dev/null +++ b/internal/test/issues/issue-2190/issue2190_test.go @@ -0,0 +1,37 @@ +package issue2190 + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestGetTest401TextResponse verifies that the generated VisitGetTestResponse +// method on GetTest401TextResponse produces a valid text/plain 401 response. +// This is a regression test for https://github.com/oapi-codegen/oapi-codegen/issues/2190 +// where the generated code tried to do []byte(response) on a struct type, +// which does not compile. +func TestGetTest401TextResponse(t *testing.T) { + resp := GetTest401TextResponse("Unauthorized") + w := httptest.NewRecorder() + + err := resp.VisitGetTestResponse(w) + require.NoError(t, err) + assert.Equal(t, 401, w.Code) + assert.Equal(t, "text/plain", w.Header().Get("Content-Type")) + assert.Equal(t, "Unauthorized", w.Body.String()) +} + +// TestGetTest200JSONResponse verifies that the 200 JSON response path also works. +func TestGetTest200JSONResponse(t *testing.T) { + resp := GetTest200JSONResponse{SuccessJSONResponse("hello")} + w := httptest.NewRecorder() + + err := resp.VisitGetTestResponse(w) + require.NoError(t, err) + assert.Equal(t, 200, w.Code) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) + assert.Contains(t, w.Body.String(), "hello") +} diff --git a/internal/test/issues/issue-2190/spec.yaml b/internal/test/issues/issue-2190/spec.yaml new file mode 100644 index 0000000000..2844100e3d --- /dev/null +++ b/internal/test/issues/issue-2190/spec.yaml @@ -0,0 +1,30 @@ +openapi: 3.0.3 +info: + title: test + version: 1.0.0 +servers: + - url: https://te.st +paths: + /v1/test: + get: + operationId: GetTest + responses: + "200": + $ref: "#/components/responses/Success" + "401": + $ref: "#/components/responses/Unauthorized" +components: + responses: + Success: + description: Success + content: + application/json: + schema: + type: string + Unauthorized: + description: Unauthorized + content: + text/plain: + schema: + type: string + example: "Unauthorized" diff --git a/pkg/codegen/templates/strict/strict-interface.tmpl b/pkg/codegen/templates/strict/strict-interface.tmpl index ac4b56fdbb..62f3d4d9c6 100644 --- a/pkg/codegen/templates/strict/strict-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-interface.tmpl @@ -40,7 +40,7 @@ {{range .Contents}} {{$receiverTypeName := printf "%s%s%s%s" $opid $statusCode .NameTagOrContentType "Response"}} {{if and $fixedStatusCode $isRef -}} - {{ if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) (eq .NameTag "Multipart") -}} + {{ if and (not $hasHeaders) ($fixedStatusCode) (.IsSupported) (or (eq .NameTag "Multipart") (eq .NameTag "Text")) -}} type {{$receiverTypeName}} {{$ref}}{{.NameTagOrContentType}}Response {{else if $isExternalRef -}} type {{$receiverTypeName}} struct { {{$ref}} } From 99615d042afbb684c08cca144cd52e4e8e04c9d9 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Mon, 16 Feb 2026 13:09:24 -0800 Subject: [PATCH 27/44] fix: handle optional request bodies in strict server mode (#2222) Fixes #2116 Strict server handlers unconditionally decoded the request body, causing an EOF error when no body was sent for operations with optional request bodies (requestBody.required defaults to false in OpenAPI 3.0). For optional JSON bodies, the generated handlers now treat io.EOF from the decoder as "no body provided" and leave request.Body as nil rather than returning an error. For optional text bodies, an empty read is similarly treated as absent. Required bodies retain the original unconditional error handling. All five strict server templates are updated: stdhttp (used by chi and gorilla), echo, gin, fiber, and iris. Two new test operations (RequiredJSONBody, RequiredTextBody) with required: true are added to the strict server test schema to verify both code paths are generated correctly. Co-authored-by: Claude Opus 4.6 --- .../test/issues/issue-1298/issue1298.gen.go | 12 +- internal/test/strict-server/chi/server.gen.go | 281 +++++++++++++++--- internal/test/strict-server/chi/server.go | 8 + internal/test/strict-server/chi/types.gen.go | 9 + .../test/strict-server/client/client.gen.go | 272 +++++++++++++++++ .../test/strict-server/echo/server.gen.go | 243 +++++++++++++-- internal/test/strict-server/echo/server.go | 8 + internal/test/strict-server/echo/types.gen.go | 9 + .../test/strict-server/fiber/server.gen.go | 240 +++++++++++++-- internal/test/strict-server/fiber/server.go | 8 + .../test/strict-server/fiber/types.gen.go | 9 + internal/test/strict-server/gin/server.gen.go | 278 ++++++++++++++--- internal/test/strict-server/gin/server.go | 8 + internal/test/strict-server/gin/types.gen.go | 9 + .../test/strict-server/gorilla/server.gen.go | 269 ++++++++++++++--- internal/test/strict-server/gorilla/server.go | 8 + .../test/strict-server/gorilla/types.gen.go | 9 + .../test/strict-server/iris/server.gen.go | 259 +++++++++++++--- internal/test/strict-server/iris/server.go | 8 + internal/test/strict-server/iris/types.gen.go | 9 + .../test/strict-server/stdhttp/server.gen.go | 267 ++++++++++++++--- internal/test/strict-server/stdhttp/server.go | 8 + .../test/strict-server/stdhttp/types.gen.go | 9 + .../test/strict-server/strict-schema.yaml | 42 +++ pkg/codegen/templates/strict/strict-echo.tmpl | 15 +- .../templates/strict/strict-fiber.tmpl | 15 +- pkg/codegen/templates/strict/strict-gin.tmpl | 17 +- pkg/codegen/templates/strict/strict-http.tmpl | 16 +- pkg/codegen/templates/strict/strict-iris.tmpl | 16 +- 29 files changed, 2098 insertions(+), 263 deletions(-) diff --git a/internal/test/issues/issue-1298/issue1298.gen.go b/internal/test/issues/issue-1298/issue1298.gen.go index 94734a5772..2e5a48dc57 100644 --- a/internal/test/issues/issue-1298/issue1298.gen.go +++ b/internal/test/issues/issue-1298/issue1298.gen.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -372,11 +373,14 @@ func (sh *strictHandler) Test(ctx *gin.Context) { var body TestApplicationTestPlusJSONRequestBody if err := ctx.ShouldBindJSON(&body); err != nil { - ctx.Status(http.StatusBadRequest) - ctx.Error(err) - return + if !errors.Is(err, io.EOF) { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { return sh.ssi.Test(ctx, request.(TestRequestObject)) diff --git a/internal/test/strict-server/chi/server.gen.go b/internal/test/strict-server/chi/server.gen.go index c8c7a09245..443fb094bf 100644 --- a/internal/test/strict-server/chi/server.gen.go +++ b/internal/test/strict-server/chi/server.gen.go @@ -9,6 +9,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "mime" @@ -39,6 +40,12 @@ type ServerInterface interface { // (POST /multiple) MultipleRequestAndResponseTypes(w http.ResponseWriter, r *http.Request) + // (POST /required-json-body) + RequiredJSONBody(w http.ResponseWriter, r *http.Request) + + // (POST /required-text-body) + RequiredTextBody(w http.ResponseWriter, r *http.Request) + // (GET /reserved-go-keyword-parameters/{type}) ReservedGoKeywordParameters(w http.ResponseWriter, r *http.Request, pType string) @@ -88,6 +95,16 @@ func (_ Unimplemented) MultipleRequestAndResponseTypes(w http.ResponseWriter, r w.WriteHeader(http.StatusNotImplemented) } +// (POST /required-json-body) +func (_ Unimplemented) RequiredJSONBody(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// (POST /required-text-body) +func (_ Unimplemented) RequiredTextBody(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // (GET /reserved-go-keyword-parameters/{type}) func (_ Unimplemented) ReservedGoKeywordParameters(w http.ResponseWriter, r *http.Request, pType string) { w.WriteHeader(http.StatusNotImplemented) @@ -193,6 +210,34 @@ func (siw *ServerInterfaceWrapper) MultipleRequestAndResponseTypes(w http.Respon handler.ServeHTTP(w, r) } +// RequiredJSONBody operation middleware +func (siw *ServerInterfaceWrapper) RequiredJSONBody(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.RequiredJSONBody(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// RequiredTextBody operation middleware +func (siw *ServerInterfaceWrapper) RequiredTextBody(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.RequiredTextBody(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ReservedGoKeywordParameters operation middleware func (siw *ServerInterfaceWrapper) ReservedGoKeywordParameters(w http.ResponseWriter, r *http.Request) { @@ -490,6 +535,12 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/multiple", wrapper.MultipleRequestAndResponseTypes) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/required-json-body", wrapper.RequiredJSONBody) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/required-text-body", wrapper.RequiredTextBody) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/reserved-go-keyword-parameters/{type}", wrapper.ReservedGoKeywordParameters) }) @@ -716,6 +767,73 @@ func (response MultipleRequestAndResponseTypes400Response) VisitMultipleRequestA return nil } +type RequiredJSONBodyRequestObject struct { + Body *RequiredJSONBodyJSONRequestBody +} + +type RequiredJSONBodyResponseObject interface { + VisitRequiredJSONBodyResponse(w http.ResponseWriter) error +} + +type RequiredJSONBody200JSONResponse Example + +func (response RequiredJSONBody200JSONResponse) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type RequiredJSONBody400Response = BadrequestResponse + +func (response RequiredJSONBody400Response) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type RequiredJSONBodydefaultResponse struct { + StatusCode int +} + +func (response RequiredJSONBodydefaultResponse) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(response.StatusCode) + return nil +} + +type RequiredTextBodyRequestObject struct { + Body *RequiredTextBodyTextRequestBody +} + +type RequiredTextBodyResponseObject interface { + VisitRequiredTextBodyResponse(w http.ResponseWriter) error +} + +type RequiredTextBody200TextResponse string + +func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(200) + + _, err := w.Write([]byte(response)) + return err +} + +type RequiredTextBody400Response = BadrequestResponse + +func (response RequiredTextBody400Response) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type RequiredTextBodydefaultResponse struct { + StatusCode int +} + +func (response RequiredTextBodydefaultResponse) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(response.StatusCode) + return nil +} + type ReservedGoKeywordParametersRequestObject struct { Type string `json:"type"` } @@ -1063,6 +1181,12 @@ type StrictServerInterface interface { // (POST /multiple) MultipleRequestAndResponseTypes(ctx context.Context, request MultipleRequestAndResponseTypesRequestObject) (MultipleRequestAndResponseTypesResponseObject, error) + // (POST /required-json-body) + RequiredJSONBody(ctx context.Context, request RequiredJSONBodyRequestObject) (RequiredJSONBodyResponseObject, error) + + // (POST /required-text-body) + RequiredTextBody(ctx context.Context, request RequiredTextBodyRequestObject) (RequiredTextBodyResponseObject, error) + // (GET /reserved-go-keyword-parameters/{type}) ReservedGoKeywordParameters(ctx context.Context, request ReservedGoKeywordParametersRequestObject) (ReservedGoKeywordParametersResponseObject, error) @@ -1123,10 +1247,13 @@ func (sh *strictHandler) JSONExample(w http.ResponseWriter, r *http.Request) { var body JSONExampleJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.JSONExample(ctx, request.(JSONExampleRequestObject)) @@ -1221,10 +1348,13 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(w http.ResponseWriter, var body MultipleRequestAndResponseTypesJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.JSONBody = &body } - request.JSONBody = &body } if strings.HasPrefix(r.Header.Get("Content-Type"), "application/x-www-form-urlencoded") { if err := r.ParseForm(); err != nil { @@ -1255,8 +1385,10 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(w http.ResponseWriter, sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't read body: %w", err)) return } - body := MultipleRequestAndResponseTypesTextRequestBody(data) - request.TextBody = &body + if len(data) > 0 { + body := MultipleRequestAndResponseTypesTextRequestBody(data) + request.TextBody = &body + } } handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { @@ -1279,6 +1411,69 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(w http.ResponseWriter, } } +// RequiredJSONBody operation middleware +func (sh *strictHandler) RequiredJSONBody(w http.ResponseWriter, r *http.Request) { + var request RequiredJSONBodyRequestObject + + var body RequiredJSONBodyJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.RequiredJSONBody(ctx, request.(RequiredJSONBodyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RequiredJSONBody") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(RequiredJSONBodyResponseObject); ok { + if err := validResponse.VisitRequiredJSONBodyResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// RequiredTextBody operation middleware +func (sh *strictHandler) RequiredTextBody(w http.ResponseWriter, r *http.Request) { + var request RequiredTextBodyRequestObject + + data, err := io.ReadAll(r.Body) + if err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't read body: %w", err)) + return + } + body := RequiredTextBodyTextRequestBody(data) + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.RequiredTextBody(ctx, request.(RequiredTextBodyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RequiredTextBody") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(RequiredTextBodyResponseObject); ok { + if err := validResponse.VisitRequiredTextBodyResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // ReservedGoKeywordParameters operation middleware func (sh *strictHandler) ReservedGoKeywordParameters(w http.ResponseWriter, r *http.Request, pType string) { var request ReservedGoKeywordParametersRequestObject @@ -1311,10 +1506,13 @@ func (sh *strictHandler) ReusableResponses(w http.ResponseWriter, r *http.Reques var body ReusableResponsesJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.ReusableResponses(ctx, request.(ReusableResponsesRequestObject)) @@ -1345,8 +1543,10 @@ func (sh *strictHandler) TextExample(w http.ResponseWriter, r *http.Request) { sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't read body: %w", err)) return } - body := TextExampleTextRequestBody(data) - request.Body = &body + if len(data) > 0 { + body := TextExampleTextRequestBody(data) + request.Body = &body + } handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.TextExample(ctx, request.(TextExampleRequestObject)) @@ -1465,10 +1665,13 @@ func (sh *strictHandler) HeadersExample(w http.ResponseWriter, r *http.Request, var body HeadersExampleJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.HeadersExample(ctx, request.(HeadersExampleRequestObject)) @@ -1496,10 +1699,13 @@ func (sh *strictHandler) UnionExample(w http.ResponseWriter, r *http.Request) { var body UnionExampleJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.UnionExample(ctx, request.(UnionExampleRequestObject)) @@ -1524,24 +1730,25 @@ func (sh *strictHandler) UnionExample(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5QUZccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQb0sWpUS", - "PTqZ3PhY7C6+b3ex2BnPTGmNBk2OpzOO4KzRDpqXoZAI/1TgyL9JcBkqS8ponvJ3Qg7af/OII1RODAtY", - "LPfymdEEulkqrC1UJvzS5JPz62fcZWMohX/6ESHnKf8hWbmShL8ugWdR2gL4fD6PXnhw95FHfAxCAjbe", - "hserTd00tcBT7giVHnGvJIhdd4opTTAC9Na8aOuEF1j4kc64RWMBSQWMJqKooNtS+8UMP0FGYQdK52Yb", - "y1ujSSjtmFR5DgiaWAse8zocc5W1BgkkG06Zt5ARc4ATQB5xUuQd4/fr31nrsOMRnwC6YOiq1+/1PV/G", - "ghZW8ZS/bT5F3AoaNxtaEmRNF++/3d/9yZRjoiJTClKZKIopKwW6sShAMqXJeBerjFyPN5awIf5X2a5+", - "30Lpo6YJoHdGTk8RME1croXzdb9/pricR/wmGOvSsXQqWUuwRk0uqqID80f9WZtaM0A02O4sKauClBVI", - "61xtov3HQmQfyJf6ktxgGUtB4kSoH8vSpYGPEQpBIPcgYBAkD+NhTf1JWfgaOxfloK3HnXXqfmxqx8am", - "ZmSYBFGwWtGYLRa+KLBKM8Gc0qMC2MKpqJPMAtpj72ctB+1eHryOk9ezaEPLc1zXddwkUIUF6MzIL6Mw", - "4qoUI0isHm0u97oF8ZQPp+RDdvuAO1IiR5zgmRJbCKV3n95nKunfkT5aYod0RWi6EhmPTPwZprVBGVuB", - "ogQCdMnMW597xSPoSOW/lpIsE5oNgWlRgmQiJ0D2wbBWpdtK2UFr94P5GERWqpqWZ/mS/j3jHpKmDeIR", - "9wZ4GlAJea3Qk05YQbQDtqf/jM+vImCBZmi24w1T3WVwUaKW0CHkzpfELuY68AuWBmsSl2nadkfc1vXj", - "HGeQZ/L1o/8Bnvdqu45Y+s6d24cCVoWPr2PWrtoHti+spHugOFESTFLamwM1XwxUZyFTuQIZt7uIg2+v", - "lYRbozME2myB/JVOG2JLZf6mSWNgAYGIOcNqYGXliFnhHFPUVJFChduqhK3i8bjy7DZYeliV012svjkR", - "p28uxehN/+rwJW9PHDcbrcwr+Tj4/X2QOfTOfrSe6cCO73h2L5TO/pISrw21ulP4lyCwOtMzUBPfEWnJ", - "EKhCDZJNlFgMYrZys1WworWrFwpurLqhxYDtkIYo2qnrmke7hnBP3/CI6JSjy3PFaaXVrlHho//N2h76", - "5dmgjP6fDgJFQYBakJrAT8e5QW5rMRru8ibTXrAc7Wnh6duLqnnEw+w6lKAKC18niGyaJGHm3XO1GI0A", - "e8okwiqPwr8BAAD//4h9qqfAGAAA", + "H4sIAAAAAAAC/+xZTXPbNhP+Kzt431NKmrbjE2+NJ5O2aeuObJ86PkDESkJCAiiwFK3R6L93QEBfFq1K", + "qT4ynt5Ecr/wPLuLBTRlha6MVqjIsXzKLDqjlcP2oc+Fxb9qdOSfBLrCSkNSK5azD1z04rdZwizWjvdL", + "nKt7+UIrQtWqcmNKWXCvmn1xXn/KXDHCivtf/7c4YDn7X7YMJQtfXYbPvDIlstlslryI4O4zS9gIuUDb", + "Rht+Xq3bpolBljNHVqoh80aC2HWnmFSEQ7TemxeNQXiBeRz5lBmrDVqSAaMxL2vs9hTf6P4XLCisQKqB", + "3sTyViviUjkQcjBAi4ogggfehgNXG6MtoYD+BLyHgsChHaNlCSNJPjB2v/oeYsCOJWyM1gVHVxeXF5ee", + "L21QcSNZzt63rxJmOI3aBS0IMrqL91/u734H6YDXpCtOsuBlOYGKWzfiJQqQirQPsS7IXbDWk22J/1lE", + "7Y8RSp81bQJ90GJyjIRp83Ilna8vL0+Ul7OE3QRnXTYWQWUrBdaaGfC67MD8UX1VulGA1mobV5ZVdUnS", + "cEurXK2j/dtcZBfIF/aygbZVKjjxI6F+KE/nBj61WHJCsQMBvSC5Hw8r5o/Kwr/xc1YOYj/u7FP3I904", + "GOkGSINAXkIjaQRzxRcNVirg4KQalgjzoJJOMkuM296PSvTiWh68jaP3s2TNynPaNE3aFlBtS1SFFt9G", + "YcJkxYeYGTVcV/e2ObGc9SfkU3ZzgztQISeM8JkyU3Kptu/eJ2rp/yF9sMIO5eofpUWRekbSfqyP7sKN", + "5QVeyg8ac90EnIZKOl+l4aMb6boUwMuGT1zoD5sTRy+q+8mjLcyjjx3BgffJcrI1vu0xZEGtz6zzUPuA", + "z/SP1O6R+PvSd+qa2p+i9kwg0qFOv+Kk0VakhlteIaF12dTHOfO2hthh8o+FJBRcQR9B8QoF8AGhhU8a", + "oknXwU/w+0l/DiJLU+2BY/GQ/zllHrz2EMIS5h2wPOD3ko5kC8BPx6VqjmY46qZrrl5L+Cgyh87iwPmB", + "pIvjDvyCp96KxHmOTNtzc+Pwf4qk9ky+Pnj7lrDLsH3AweN77wJ1ePk6ZlFrF9i+cY7ZAcWxFKizytzs", + "aflsoDqDhRxIFGlcRRpie60l3GpVWKT1A4jfDJUmWBiD/gRohBAQaPfHBqGqHYHhzoGktouUMtwVCdxo", + "Ho/LyG6Dp4dlO93G6rsjcfruXIzeXF7tr/L+yHmzdpB4pR57v34MMvvemB3sxLLneetwfs9Uzo2kUbpy", + "pdxdwj8FgeWeXqAc+4lICbBItVUoYCz5/Bp0ozajgSWtXbNQCGM5Dc2vt/cZiJKttq5Zsu0K/OkNX9Ae", + "84+DU+VpreS2i/pH/xniDP1yb5BafafX8LwktIqTHOMPh7m/2bSiFd4N2kp7wXKyo4ent5dVs4SFf45C", + "C6pt6fsEkcmzLPzjdOEaPhyivZA640Z6FP4OAAD//0nTejA+HAAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/strict-server/chi/server.go b/internal/test/strict-server/chi/server.go index c135a2a445..914f967268 100644 --- a/internal/test/strict-server/chi/server.go +++ b/internal/test/strict-server/chi/server.go @@ -126,6 +126,14 @@ func (s StrictServer) ReusableResponses(ctx context.Context, request ReusableRes return ReusableResponses200JSONResponse{ReusableresponseJSONResponse: ReusableresponseJSONResponse{Body: *request.Body}}, nil } +func (s StrictServer) RequiredJSONBody(ctx context.Context, request RequiredJSONBodyRequestObject) (RequiredJSONBodyResponseObject, error) { + return RequiredJSONBody200JSONResponse(*request.Body), nil +} + +func (s StrictServer) RequiredTextBody(ctx context.Context, request RequiredTextBodyRequestObject) (RequiredTextBodyResponseObject, error) { + return RequiredTextBody200TextResponse(*request.Body), nil +} + func (s StrictServer) ReservedGoKeywordParameters(ctx context.Context, request ReservedGoKeywordParametersRequestObject) (ReservedGoKeywordParametersResponseObject, error) { return ReservedGoKeywordParameters200TextResponse(""), nil } diff --git a/internal/test/strict-server/chi/types.gen.go b/internal/test/strict-server/chi/types.gen.go index 6682c1acab..fac014c4bb 100644 --- a/internal/test/strict-server/chi/types.gen.go +++ b/internal/test/strict-server/chi/types.gen.go @@ -14,6 +14,9 @@ type Reusableresponse = Example // MultipleRequestAndResponseTypesTextBody defines parameters for MultipleRequestAndResponseTypes. type MultipleRequestAndResponseTypesTextBody = string +// RequiredTextBodyTextBody defines parameters for RequiredTextBody. +type RequiredTextBodyTextBody = string + // TextExampleTextBody defines parameters for TextExample. type TextExampleTextBody = string @@ -44,6 +47,12 @@ type MultipleRequestAndResponseTypesMultipartRequestBody = Example // MultipleRequestAndResponseTypesTextRequestBody defines body for MultipleRequestAndResponseTypes for text/plain ContentType. type MultipleRequestAndResponseTypesTextRequestBody = MultipleRequestAndResponseTypesTextBody +// RequiredJSONBodyJSONRequestBody defines body for RequiredJSONBody for application/json ContentType. +type RequiredJSONBodyJSONRequestBody = Example + +// RequiredTextBodyTextRequestBody defines body for RequiredTextBody for text/plain ContentType. +type RequiredTextBodyTextRequestBody = RequiredTextBodyTextBody + // ReusableResponsesJSONRequestBody defines body for ReusableResponses for application/json ContentType. type ReusableResponsesJSONRequestBody = Example diff --git a/internal/test/strict-server/client/client.gen.go b/internal/test/strict-server/client/client.gen.go index 5bf1dffb3f..fcdef6c6d9 100644 --- a/internal/test/strict-server/client/client.gen.go +++ b/internal/test/strict-server/client/client.gen.go @@ -27,6 +27,9 @@ type Reusableresponse = Example // MultipleRequestAndResponseTypesTextBody defines parameters for MultipleRequestAndResponseTypes. type MultipleRequestAndResponseTypesTextBody = string +// RequiredTextBodyTextBody defines parameters for RequiredTextBody. +type RequiredTextBodyTextBody = string + // TextExampleTextBody defines parameters for TextExample. type TextExampleTextBody = string @@ -57,6 +60,12 @@ type MultipleRequestAndResponseTypesMultipartRequestBody = Example // MultipleRequestAndResponseTypesTextRequestBody defines body for MultipleRequestAndResponseTypes for text/plain ContentType. type MultipleRequestAndResponseTypesTextRequestBody = MultipleRequestAndResponseTypesTextBody +// RequiredJSONBodyJSONRequestBody defines body for RequiredJSONBody for application/json ContentType. +type RequiredJSONBodyJSONRequestBody = Example + +// RequiredTextBodyTextRequestBody defines body for RequiredTextBody for text/plain ContentType. +type RequiredTextBodyTextRequestBody = RequiredTextBodyTextBody + // ReusableResponsesJSONRequestBody defines body for ReusableResponses for application/json ContentType. type ReusableResponsesJSONRequestBody = Example @@ -165,6 +174,16 @@ type ClientInterface interface { MultipleRequestAndResponseTypesWithTextBody(ctx context.Context, body MultipleRequestAndResponseTypesTextRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // RequiredJSONBodyWithBody request with any body + RequiredJSONBodyWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + RequiredJSONBody(ctx context.Context, body RequiredJSONBodyJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // RequiredTextBodyWithBody request with any body + RequiredTextBodyWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + RequiredTextBodyWithTextBody(ctx context.Context, body RequiredTextBodyTextRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ReservedGoKeywordParameters request ReservedGoKeywordParameters(ctx context.Context, pType string, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -296,6 +315,54 @@ func (c *Client) MultipleRequestAndResponseTypesWithTextBody(ctx context.Context return c.Client.Do(req) } +func (c *Client) RequiredJSONBodyWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRequiredJSONBodyRequestWithBody(c.Server, contentType, body) + 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) +} + +func (c *Client) RequiredJSONBody(ctx context.Context, body RequiredJSONBodyJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRequiredJSONBodyRequest(c.Server, body) + 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) +} + +func (c *Client) RequiredTextBodyWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRequiredTextBodyRequestWithBody(c.Server, contentType, body) + 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) +} + +func (c *Client) RequiredTextBodyWithTextBody(ctx context.Context, body RequiredTextBodyTextRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRequiredTextBodyRequestWithTextBody(c.Server, body) + 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) +} + func (c *Client) ReservedGoKeywordParameters(ctx context.Context, pType string, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewReservedGoKeywordParametersRequest(c.Server, pType) if err != nil { @@ -608,6 +675,82 @@ func NewMultipleRequestAndResponseTypesRequestWithBody(server string, contentTyp return req, nil } +// NewRequiredJSONBodyRequest calls the generic RequiredJSONBody builder with application/json body +func NewRequiredJSONBodyRequest(server string, body RequiredJSONBodyJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewRequiredJSONBodyRequestWithBody(server, "application/json", bodyReader) +} + +// NewRequiredJSONBodyRequestWithBody generates requests for RequiredJSONBody with any type of body +func NewRequiredJSONBodyRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/required-json-body") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewRequiredTextBodyRequestWithTextBody calls the generic RequiredTextBody builder with text/plain body +func NewRequiredTextBodyRequestWithTextBody(server string, body RequiredTextBodyTextRequestBody) (*http.Request, error) { + var bodyReader io.Reader + bodyReader = strings.NewReader(string(body)) + return NewRequiredTextBodyRequestWithBody(server, "text/plain", bodyReader) +} + +// NewRequiredTextBodyRequestWithBody generates requests for RequiredTextBody with any type of body +func NewRequiredTextBodyRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/required-text-body") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewReservedGoKeywordParametersRequest generates requests for ReservedGoKeywordParameters func NewReservedGoKeywordParametersRequest(server string, pType string) (*http.Request, error) { var err error @@ -983,6 +1126,16 @@ type ClientWithResponsesInterface interface { MultipleRequestAndResponseTypesWithTextBodyWithResponse(ctx context.Context, body MultipleRequestAndResponseTypesTextRequestBody, reqEditors ...RequestEditorFn) (*MultipleRequestAndResponseTypesResponse, error) + // RequiredJSONBodyWithBodyWithResponse request with any body + RequiredJSONBodyWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RequiredJSONBodyResponse, error) + + RequiredJSONBodyWithResponse(ctx context.Context, body RequiredJSONBodyJSONRequestBody, reqEditors ...RequestEditorFn) (*RequiredJSONBodyResponse, error) + + // RequiredTextBodyWithBodyWithResponse request with any body + RequiredTextBodyWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RequiredTextBodyResponse, error) + + RequiredTextBodyWithTextBodyWithResponse(ctx context.Context, body RequiredTextBodyTextRequestBody, reqEditors ...RequestEditorFn) (*RequiredTextBodyResponse, error) + // ReservedGoKeywordParametersWithResponse request ReservedGoKeywordParametersWithResponse(ctx context.Context, pType string, reqEditors ...RequestEditorFn) (*ReservedGoKeywordParametersResponse, error) @@ -1104,6 +1257,49 @@ func (r MultipleRequestAndResponseTypesResponse) StatusCode() int { return 0 } +type RequiredJSONBodyResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Example +} + +// Status returns HTTPResponse.Status +func (r RequiredJSONBodyResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r RequiredJSONBodyResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type RequiredTextBodyResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r RequiredTextBodyResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r RequiredTextBodyResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type ReservedGoKeywordParametersResponse struct { Body []byte HTTPResponse *http.Response @@ -1347,6 +1543,40 @@ func (c *ClientWithResponses) MultipleRequestAndResponseTypesWithTextBodyWithRes return ParseMultipleRequestAndResponseTypesResponse(rsp) } +// RequiredJSONBodyWithBodyWithResponse request with arbitrary body returning *RequiredJSONBodyResponse +func (c *ClientWithResponses) RequiredJSONBodyWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RequiredJSONBodyResponse, error) { + rsp, err := c.RequiredJSONBodyWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRequiredJSONBodyResponse(rsp) +} + +func (c *ClientWithResponses) RequiredJSONBodyWithResponse(ctx context.Context, body RequiredJSONBodyJSONRequestBody, reqEditors ...RequestEditorFn) (*RequiredJSONBodyResponse, error) { + rsp, err := c.RequiredJSONBody(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRequiredJSONBodyResponse(rsp) +} + +// RequiredTextBodyWithBodyWithResponse request with arbitrary body returning *RequiredTextBodyResponse +func (c *ClientWithResponses) RequiredTextBodyWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RequiredTextBodyResponse, error) { + rsp, err := c.RequiredTextBodyWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRequiredTextBodyResponse(rsp) +} + +func (c *ClientWithResponses) RequiredTextBodyWithTextBodyWithResponse(ctx context.Context, body RequiredTextBodyTextRequestBody, reqEditors ...RequestEditorFn) (*RequiredTextBodyResponse, error) { + rsp, err := c.RequiredTextBodyWithTextBody(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRequiredTextBodyResponse(rsp) +} + // ReservedGoKeywordParametersWithResponse request returning *ReservedGoKeywordParametersResponse func (c *ClientWithResponses) ReservedGoKeywordParametersWithResponse(ctx context.Context, pType string, reqEditors ...RequestEditorFn) (*ReservedGoKeywordParametersResponse, error) { rsp, err := c.ReservedGoKeywordParameters(ctx, pType, reqEditors...) @@ -1546,6 +1776,48 @@ func ParseMultipleRequestAndResponseTypesResponse(rsp *http.Response) (*Multiple return response, nil } +// ParseRequiredJSONBodyResponse parses an HTTP response from a RequiredJSONBodyWithResponse call +func ParseRequiredJSONBodyResponse(rsp *http.Response) (*RequiredJSONBodyResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &RequiredJSONBodyResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Example + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseRequiredTextBodyResponse parses an HTTP response from a RequiredTextBodyWithResponse call +func ParseRequiredTextBodyResponse(rsp *http.Response) (*RequiredTextBodyResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &RequiredTextBodyResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + // ParseReservedGoKeywordParametersResponse parses an HTTP response from a ReservedGoKeywordParametersWithResponse call func ParseReservedGoKeywordParametersResponse(rsp *http.Response) (*ReservedGoKeywordParametersResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/internal/test/strict-server/echo/server.gen.go b/internal/test/strict-server/echo/server.gen.go index f07dfa5e46..78eccb1770 100644 --- a/internal/test/strict-server/echo/server.gen.go +++ b/internal/test/strict-server/echo/server.gen.go @@ -9,6 +9,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "mime" @@ -39,6 +40,12 @@ type ServerInterface interface { // (POST /multiple) MultipleRequestAndResponseTypes(ctx echo.Context) error + // (POST /required-json-body) + RequiredJSONBody(ctx echo.Context) error + + // (POST /required-text-body) + RequiredTextBody(ctx echo.Context) error + // (GET /reserved-go-keyword-parameters/{type}) ReservedGoKeywordParameters(ctx echo.Context, pType string) error @@ -105,6 +112,24 @@ func (w *ServerInterfaceWrapper) MultipleRequestAndResponseTypes(ctx echo.Contex return err } +// RequiredJSONBody converts echo context to params. +func (w *ServerInterfaceWrapper) RequiredJSONBody(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.RequiredJSONBody(ctx) + return err +} + +// RequiredTextBody converts echo context to params. +func (w *ServerInterfaceWrapper) RequiredTextBody(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.RequiredTextBody(ctx) + return err +} + // ReservedGoKeywordParameters converts echo context to params. func (w *ServerInterfaceWrapper) ReservedGoKeywordParameters(ctx echo.Context) error { var err error @@ -253,6 +278,8 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.POST(baseURL+"/multipart", wrapper.MultipartExample) router.POST(baseURL+"/multipart-related", wrapper.MultipartRelatedExample) router.POST(baseURL+"/multiple", wrapper.MultipleRequestAndResponseTypes) + router.POST(baseURL+"/required-json-body", wrapper.RequiredJSONBody) + router.POST(baseURL+"/required-text-body", wrapper.RequiredTextBody) router.GET(baseURL+"/reserved-go-keyword-parameters/:type", wrapper.ReservedGoKeywordParameters) router.POST(baseURL+"/reusable-responses", wrapper.ReusableResponses) router.POST(baseURL+"/text", wrapper.TextExample) @@ -462,6 +489,73 @@ func (response MultipleRequestAndResponseTypes400Response) VisitMultipleRequestA return nil } +type RequiredJSONBodyRequestObject struct { + Body *RequiredJSONBodyJSONRequestBody +} + +type RequiredJSONBodyResponseObject interface { + VisitRequiredJSONBodyResponse(w http.ResponseWriter) error +} + +type RequiredJSONBody200JSONResponse Example + +func (response RequiredJSONBody200JSONResponse) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type RequiredJSONBody400Response = BadrequestResponse + +func (response RequiredJSONBody400Response) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type RequiredJSONBodydefaultResponse struct { + StatusCode int +} + +func (response RequiredJSONBodydefaultResponse) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(response.StatusCode) + return nil +} + +type RequiredTextBodyRequestObject struct { + Body *RequiredTextBodyTextRequestBody +} + +type RequiredTextBodyResponseObject interface { + VisitRequiredTextBodyResponse(w http.ResponseWriter) error +} + +type RequiredTextBody200TextResponse string + +func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(200) + + _, err := w.Write([]byte(response)) + return err +} + +type RequiredTextBody400Response = BadrequestResponse + +func (response RequiredTextBody400Response) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type RequiredTextBodydefaultResponse struct { + StatusCode int +} + +func (response RequiredTextBodydefaultResponse) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(response.StatusCode) + return nil +} + type ReservedGoKeywordParametersRequestObject struct { Type string `json:"type"` } @@ -809,6 +903,12 @@ type StrictServerInterface interface { // (POST /multiple) MultipleRequestAndResponseTypes(ctx context.Context, request MultipleRequestAndResponseTypesRequestObject) (MultipleRequestAndResponseTypesResponseObject, error) + // (POST /required-json-body) + RequiredJSONBody(ctx context.Context, request RequiredJSONBodyRequestObject) (RequiredJSONBodyResponseObject, error) + + // (POST /required-text-body) + RequiredTextBody(ctx context.Context, request RequiredTextBodyRequestObject) (RequiredTextBodyResponseObject, error) + // (GET /reserved-go-keyword-parameters/{type}) ReservedGoKeywordParameters(ctx context.Context, request ReservedGoKeywordParametersRequestObject) (ReservedGoKeywordParametersResponseObject, error) @@ -852,9 +952,12 @@ func (sh *strictHandler) JSONExample(ctx echo.Context) error { var body JSONExampleJSONRequestBody if err := ctx.Bind(&body); err != nil { - return err + if !errors.Is(err, io.EOF) { + return err + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx echo.Context, request interface{}) (interface{}, error) { return sh.ssi.JSONExample(ctx.Request().Context(), request.(JSONExampleRequestObject)) @@ -942,9 +1045,12 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(ctx echo.Context) error if strings.HasPrefix(ctx.Request().Header.Get("Content-Type"), "application/json") { var body MultipleRequestAndResponseTypesJSONRequestBody if err := ctx.Bind(&body); err != nil { - return err + if !errors.Is(err, io.EOF) { + return err + } + } else { + request.JSONBody = &body } - request.JSONBody = &body } if strings.HasPrefix(ctx.Request().Header.Get("Content-Type"), "application/x-www-form-urlencoded") { if form, err := ctx.FormParams(); err == nil { @@ -972,8 +1078,10 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(ctx echo.Context) error 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) { @@ -995,6 +1103,65 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(ctx echo.Context) error return nil } +// RequiredJSONBody operation middleware +func (sh *strictHandler) RequiredJSONBody(ctx echo.Context) error { + var request RequiredJSONBodyRequestObject + + var body RequiredJSONBodyJSONRequestBody + if err := ctx.Bind(&body); err != nil { + return err + } + request.Body = &body + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.RequiredJSONBody(ctx.Request().Context(), request.(RequiredJSONBodyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RequiredJSONBody") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(RequiredJSONBodyResponseObject); ok { + return validResponse.VisitRequiredJSONBodyResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// RequiredTextBody operation middleware +func (sh *strictHandler) RequiredTextBody(ctx echo.Context) error { + var request RequiredTextBodyRequestObject + + data, err := io.ReadAll(ctx.Request().Body) + if err != nil { + return err + } + body := RequiredTextBodyTextRequestBody(data) + request.Body = &body + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.RequiredTextBody(ctx.Request().Context(), request.(RequiredTextBodyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RequiredTextBody") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(RequiredTextBodyResponseObject); ok { + return validResponse.VisitRequiredTextBodyResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // ReservedGoKeywordParameters operation middleware func (sh *strictHandler) ReservedGoKeywordParameters(ctx echo.Context, pType string) error { var request ReservedGoKeywordParametersRequestObject @@ -1026,9 +1193,12 @@ func (sh *strictHandler) ReusableResponses(ctx echo.Context) error { var body ReusableResponsesJSONRequestBody if err := ctx.Bind(&body); err != nil { - return err + if !errors.Is(err, io.EOF) { + return err + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx echo.Context, request interface{}) (interface{}, error) { return sh.ssi.ReusableResponses(ctx.Request().Context(), request.(ReusableResponsesRequestObject)) @@ -1057,8 +1227,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)) @@ -1172,9 +1344,12 @@ func (sh *strictHandler) HeadersExample(ctx echo.Context, params HeadersExampleP var body HeadersExampleJSONRequestBody if err := ctx.Bind(&body); err != nil { - return err + if !errors.Is(err, io.EOF) { + return err + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx echo.Context, request interface{}) (interface{}, error) { return sh.ssi.HeadersExample(ctx.Request().Context(), request.(HeadersExampleRequestObject)) @@ -1201,9 +1376,12 @@ func (sh *strictHandler) UnionExample(ctx echo.Context) error { var body UnionExampleJSONRequestBody if err := ctx.Bind(&body); err != nil { - return err + if !errors.Is(err, io.EOF) { + return err + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx echo.Context, request interface{}) (interface{}, error) { return sh.ssi.UnionExample(ctx.Request().Context(), request.(UnionExampleRequestObject)) @@ -1227,24 +1405,25 @@ func (sh *strictHandler) UnionExample(ctx echo.Context) error { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5QUZccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQb0sWpUS", - "PTqZ3PhY7C6+b3ex2BnPTGmNBk2OpzOO4KzRDpqXoZAI/1TgyL9JcBkqS8ponvJ3Qg7af/OII1RODAtY", - "LPfymdEEulkqrC1UJvzS5JPz62fcZWMohX/6ESHnKf8hWbmShL8ugWdR2gL4fD6PXnhw95FHfAxCAjbe", - "hserTd00tcBT7giVHnGvJIhdd4opTTAC9Na8aOuEF1j4kc64RWMBSQWMJqKooNtS+8UMP0FGYQdK52Yb", - "y1ujSSjtmFR5DgiaWAse8zocc5W1BgkkG06Zt5ARc4ATQB5xUuQd4/fr31nrsOMRnwC6YOiq1+/1PV/G", - "ghZW8ZS/bT5F3AoaNxtaEmRNF++/3d/9yZRjoiJTClKZKIopKwW6sShAMqXJeBerjFyPN5awIf5X2a5+", - "30Lpo6YJoHdGTk8RME1croXzdb9/pricR/wmGOvSsXQqWUuwRk0uqqID80f9WZtaM0A02O4sKauClBVI", - "61xtov3HQmQfyJf6ktxgGUtB4kSoH8vSpYGPEQpBIPcgYBAkD+NhTf1JWfgaOxfloK3HnXXqfmxqx8am", - "ZmSYBFGwWtGYLRa+KLBKM8Gc0qMC2MKpqJPMAtpj72ctB+1eHryOk9ezaEPLc1zXddwkUIUF6MzIL6Mw", - "4qoUI0isHm0u97oF8ZQPp+RDdvuAO1IiR5zgmRJbCKV3n95nKunfkT5aYod0RWi6EhmPTPwZprVBGVuB", - "ogQCdMnMW597xSPoSOW/lpIsE5oNgWlRgmQiJ0D2wbBWpdtK2UFr94P5GERWqpqWZ/mS/j3jHpKmDeIR", - "9wZ4GlAJea3Qk05YQbQDtqf/jM+vImCBZmi24w1T3WVwUaKW0CHkzpfELuY68AuWBmsSl2nadkfc1vXj", - "HGeQZ/L1o/8Bnvdqu45Y+s6d24cCVoWPr2PWrtoHti+spHugOFESTFLamwM1XwxUZyFTuQIZt7uIg2+v", - "lYRbozME2myB/JVOG2JLZf6mSWNgAYGIOcNqYGXliFnhHFPUVJFChduqhK3i8bjy7DZYeliV012svjkR", - "p28uxehN/+rwJW9PHDcbrcwr+Tj4/X2QOfTOfrSe6cCO73h2L5TO/pISrw21ulP4lyCwOtMzUBPfEWnJ", - "EKhCDZJNlFgMYrZys1WworWrFwpurLqhxYDtkIYo2qnrmke7hnBP3/CI6JSjy3PFaaXVrlHho//N2h76", - "5dmgjP6fDgJFQYBakJrAT8e5QW5rMRru8ibTXrAc7Wnh6duLqnnEw+w6lKAKC18niGyaJGHm3XO1GI0A", - "e8okwiqPwr8BAAD//4h9qqfAGAAA", + "H4sIAAAAAAAC/+xZTXPbNhP+Kzt431NKmrbjE2+NJ5O2aeuObJ86PkDESkJCAiiwFK3R6L93QEBfFq1K", + "qT4ynt5Ecr/wPLuLBTRlha6MVqjIsXzKLDqjlcP2oc+Fxb9qdOSfBLrCSkNSK5azD1z04rdZwizWjvdL", + "nKt7+UIrQtWqcmNKWXCvmn1xXn/KXDHCivtf/7c4YDn7X7YMJQtfXYbPvDIlstlslryI4O4zS9gIuUDb", + "Rht+Xq3bpolBljNHVqoh80aC2HWnmFSEQ7TemxeNQXiBeRz5lBmrDVqSAaMxL2vs9hTf6P4XLCisQKqB", + "3sTyViviUjkQcjBAi4ogggfehgNXG6MtoYD+BLyHgsChHaNlCSNJPjB2v/oeYsCOJWyM1gVHVxeXF5ee", + "L21QcSNZzt63rxJmOI3aBS0IMrqL91/u734H6YDXpCtOsuBlOYGKWzfiJQqQirQPsS7IXbDWk22J/1lE", + "7Y8RSp81bQJ90GJyjIRp83Ilna8vL0+Ul7OE3QRnXTYWQWUrBdaaGfC67MD8UX1VulGA1mobV5ZVdUnS", + "cEurXK2j/dtcZBfIF/aygbZVKjjxI6F+KE/nBj61WHJCsQMBvSC5Hw8r5o/Kwr/xc1YOYj/u7FP3I904", + "GOkGSINAXkIjaQRzxRcNVirg4KQalgjzoJJOMkuM296PSvTiWh68jaP3s2TNynPaNE3aFlBtS1SFFt9G", + "YcJkxYeYGTVcV/e2ObGc9SfkU3ZzgztQISeM8JkyU3Kptu/eJ2rp/yF9sMIO5eofpUWRekbSfqyP7sKN", + "5QVeyg8ac90EnIZKOl+l4aMb6boUwMuGT1zoD5sTRy+q+8mjLcyjjx3BgffJcrI1vu0xZEGtz6zzUPuA", + "z/SP1O6R+PvSd+qa2p+i9kwg0qFOv+Kk0VakhlteIaF12dTHOfO2hthh8o+FJBRcQR9B8QoF8AGhhU8a", + "oknXwU/w+0l/DiJLU+2BY/GQ/zllHrz2EMIS5h2wPOD3ko5kC8BPx6VqjmY46qZrrl5L+Cgyh87iwPmB", + "pIvjDvyCp96KxHmOTNtzc+Pwf4qk9ky+Pnj7lrDLsH3AweN77wJ1ePk6ZlFrF9i+cY7ZAcWxFKizytzs", + "aflsoDqDhRxIFGlcRRpie60l3GpVWKT1A4jfDJUmWBiD/gRohBAQaPfHBqGqHYHhzoGktouUMtwVCdxo", + "Ho/LyG6Dp4dlO93G6rsjcfruXIzeXF7tr/L+yHmzdpB4pR57v34MMvvemB3sxLLneetwfs9Uzo2kUbpy", + "pdxdwj8FgeWeXqAc+4lICbBItVUoYCz5/Bp0ozajgSWtXbNQCGM5Dc2vt/cZiJKttq5Zsu0K/OkNX9Ae", + "84+DU+VpreS2i/pH/xniDP1yb5BafafX8LwktIqTHOMPh7m/2bSiFd4N2kp7wXKyo4ent5dVs4SFf45C", + "C6pt6fsEkcmzLPzjdOEaPhyivZA640Z6FP4OAAD//0nTejA+HAAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/strict-server/echo/server.go b/internal/test/strict-server/echo/server.go index c135a2a445..914f967268 100644 --- a/internal/test/strict-server/echo/server.go +++ b/internal/test/strict-server/echo/server.go @@ -126,6 +126,14 @@ func (s StrictServer) ReusableResponses(ctx context.Context, request ReusableRes return ReusableResponses200JSONResponse{ReusableresponseJSONResponse: ReusableresponseJSONResponse{Body: *request.Body}}, nil } +func (s StrictServer) RequiredJSONBody(ctx context.Context, request RequiredJSONBodyRequestObject) (RequiredJSONBodyResponseObject, error) { + return RequiredJSONBody200JSONResponse(*request.Body), nil +} + +func (s StrictServer) RequiredTextBody(ctx context.Context, request RequiredTextBodyRequestObject) (RequiredTextBodyResponseObject, error) { + return RequiredTextBody200TextResponse(*request.Body), nil +} + func (s StrictServer) ReservedGoKeywordParameters(ctx context.Context, request ReservedGoKeywordParametersRequestObject) (ReservedGoKeywordParametersResponseObject, error) { return ReservedGoKeywordParameters200TextResponse(""), nil } diff --git a/internal/test/strict-server/echo/types.gen.go b/internal/test/strict-server/echo/types.gen.go index 6682c1acab..fac014c4bb 100644 --- a/internal/test/strict-server/echo/types.gen.go +++ b/internal/test/strict-server/echo/types.gen.go @@ -14,6 +14,9 @@ type Reusableresponse = Example // MultipleRequestAndResponseTypesTextBody defines parameters for MultipleRequestAndResponseTypes. type MultipleRequestAndResponseTypesTextBody = string +// RequiredTextBodyTextBody defines parameters for RequiredTextBody. +type RequiredTextBodyTextBody = string + // TextExampleTextBody defines parameters for TextExample. type TextExampleTextBody = string @@ -44,6 +47,12 @@ type MultipleRequestAndResponseTypesMultipartRequestBody = Example // MultipleRequestAndResponseTypesTextRequestBody defines body for MultipleRequestAndResponseTypes for text/plain ContentType. type MultipleRequestAndResponseTypesTextRequestBody = MultipleRequestAndResponseTypesTextBody +// RequiredJSONBodyJSONRequestBody defines body for RequiredJSONBody for application/json ContentType. +type RequiredJSONBodyJSONRequestBody = Example + +// RequiredTextBodyTextRequestBody defines body for RequiredTextBody for text/plain ContentType. +type RequiredTextBodyTextRequestBody = RequiredTextBodyTextBody + // ReusableResponsesJSONRequestBody defines body for ReusableResponses for application/json ContentType. type ReusableResponsesJSONRequestBody = Example diff --git a/internal/test/strict-server/fiber/server.gen.go b/internal/test/strict-server/fiber/server.gen.go index edf81be1fb..f33ca45430 100644 --- a/internal/test/strict-server/fiber/server.gen.go +++ b/internal/test/strict-server/fiber/server.gen.go @@ -9,6 +9,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "mime" @@ -38,6 +39,12 @@ type ServerInterface interface { // (POST /multiple) MultipleRequestAndResponseTypes(c *fiber.Ctx) error + // (POST /required-json-body) + RequiredJSONBody(c *fiber.Ctx) error + + // (POST /required-text-body) + RequiredTextBody(c *fiber.Ctx) error + // (GET /reserved-go-keyword-parameters/{type}) ReservedGoKeywordParameters(c *fiber.Ctx, pType string) error @@ -94,6 +101,18 @@ func (siw *ServerInterfaceWrapper) MultipleRequestAndResponseTypes(c *fiber.Ctx) return siw.Handler.MultipleRequestAndResponseTypes(c) } +// RequiredJSONBody operation middleware +func (siw *ServerInterfaceWrapper) RequiredJSONBody(c *fiber.Ctx) error { + + return siw.Handler.RequiredJSONBody(c) +} + +// RequiredTextBody operation middleware +func (siw *ServerInterfaceWrapper) RequiredTextBody(c *fiber.Ctx) error { + + return siw.Handler.RequiredTextBody(c) +} + // ReservedGoKeywordParameters operation middleware func (siw *ServerInterfaceWrapper) ReservedGoKeywordParameters(c *fiber.Ctx) error { @@ -225,6 +244,10 @@ func RegisterHandlersWithOptions(router fiber.Router, si ServerInterface, option router.Post(options.BaseURL+"/multiple", wrapper.MultipleRequestAndResponseTypes) + router.Post(options.BaseURL+"/required-json-body", wrapper.RequiredJSONBody) + + router.Post(options.BaseURL+"/required-text-body", wrapper.RequiredTextBody) + router.Get(options.BaseURL+"/reserved-go-keyword-parameters/:type", wrapper.ReservedGoKeywordParameters) router.Post(options.BaseURL+"/reusable-responses", wrapper.ReusableResponses) @@ -441,6 +464,73 @@ func (response MultipleRequestAndResponseTypes400Response) VisitMultipleRequestA return nil } +type RequiredJSONBodyRequestObject struct { + Body *RequiredJSONBodyJSONRequestBody +} + +type RequiredJSONBodyResponseObject interface { + VisitRequiredJSONBodyResponse(ctx *fiber.Ctx) error +} + +type RequiredJSONBody200JSONResponse Example + +func (response RequiredJSONBody200JSONResponse) VisitRequiredJSONBodyResponse(ctx *fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(200) + + return ctx.JSON(&response) +} + +type RequiredJSONBody400Response = BadrequestResponse + +func (response RequiredJSONBody400Response) VisitRequiredJSONBodyResponse(ctx *fiber.Ctx) error { + ctx.Status(400) + return nil +} + +type RequiredJSONBodydefaultResponse struct { + StatusCode int +} + +func (response RequiredJSONBodydefaultResponse) VisitRequiredJSONBodyResponse(ctx *fiber.Ctx) error { + ctx.Status(response.StatusCode) + return nil +} + +type RequiredTextBodyRequestObject struct { + Body *RequiredTextBodyTextRequestBody +} + +type RequiredTextBodyResponseObject interface { + VisitRequiredTextBodyResponse(ctx *fiber.Ctx) error +} + +type RequiredTextBody200TextResponse string + +func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(ctx *fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "text/plain") + ctx.Status(200) + + _, err := ctx.WriteString(string(response)) + return err +} + +type RequiredTextBody400Response = BadrequestResponse + +func (response RequiredTextBody400Response) VisitRequiredTextBodyResponse(ctx *fiber.Ctx) error { + ctx.Status(400) + return nil +} + +type RequiredTextBodydefaultResponse struct { + StatusCode int +} + +func (response RequiredTextBodydefaultResponse) VisitRequiredTextBodyResponse(ctx *fiber.Ctx) error { + ctx.Status(response.StatusCode) + return nil +} + type ReservedGoKeywordParametersRequestObject struct { Type string `json:"type"` } @@ -788,6 +878,12 @@ type StrictServerInterface interface { // (POST /multiple) MultipleRequestAndResponseTypes(ctx context.Context, request MultipleRequestAndResponseTypesRequestObject) (MultipleRequestAndResponseTypesResponseObject, error) + // (POST /required-json-body) + RequiredJSONBody(ctx context.Context, request RequiredJSONBodyRequestObject) (RequiredJSONBodyResponseObject, error) + + // (POST /required-text-body) + RequiredTextBody(ctx context.Context, request RequiredTextBodyRequestObject) (RequiredTextBodyResponseObject, error) + // (GET /reserved-go-keyword-parameters/{type}) ReservedGoKeywordParameters(ctx context.Context, request ReservedGoKeywordParametersRequestObject) (ReservedGoKeywordParametersResponseObject, error) @@ -832,9 +928,12 @@ func (sh *strictHandler) JSONExample(ctx *fiber.Ctx) error { var body JSONExampleJSONRequestBody if err := ctx.BodyParser(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + if !errors.Is(err, io.EOF) { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { return sh.ssi.JSONExample(ctx.UserContext(), request.(JSONExampleRequestObject)) @@ -925,9 +1024,12 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(ctx *fiber.Ctx) error { var body MultipleRequestAndResponseTypesJSONRequestBody if err := ctx.BodyParser(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + if !errors.Is(err, io.EOF) { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + } else { + request.JSONBody = &body } - request.JSONBody = &body } if strings.HasPrefix(string(ctx.Request().Header.ContentType()), "application/x-www-form-urlencoded") { var body MultipleRequestAndResponseTypesFormdataRequestBody @@ -944,8 +1046,10 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(ctx *fiber.Ctx) error { } if strings.HasPrefix(string(ctx.Request().Header.ContentType()), "text/plain") { data := ctx.Request().Body() - body := MultipleRequestAndResponseTypesTextRequestBody(data) - request.TextBody = &body + if len(data) > 0 { + body := MultipleRequestAndResponseTypesTextRequestBody(data) + request.TextBody = &body + } } handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { @@ -969,6 +1073,66 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(ctx *fiber.Ctx) error { return nil } +// RequiredJSONBody operation middleware +func (sh *strictHandler) RequiredJSONBody(ctx *fiber.Ctx) error { + var request RequiredJSONBodyRequestObject + + var body RequiredJSONBodyJSONRequestBody + if err := ctx.BodyParser(&body); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + request.Body = &body + + handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { + return sh.ssi.RequiredJSONBody(ctx.UserContext(), request.(RequiredJSONBodyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RequiredJSONBody") + } + + response, err := handler(ctx, request) + + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else if validResponse, ok := response.(RequiredJSONBodyResponseObject); ok { + if err := validResponse.VisitRequiredJSONBodyResponse(ctx); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// RequiredTextBody operation middleware +func (sh *strictHandler) RequiredTextBody(ctx *fiber.Ctx) error { + var request RequiredTextBodyRequestObject + + data := ctx.Request().Body() + body := RequiredTextBodyTextRequestBody(data) + request.Body = &body + + handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { + return sh.ssi.RequiredTextBody(ctx.UserContext(), request.(RequiredTextBodyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RequiredTextBody") + } + + response, err := handler(ctx, request) + + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else if validResponse, ok := response.(RequiredTextBodyResponseObject); ok { + if err := validResponse.VisitRequiredTextBodyResponse(ctx); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // ReservedGoKeywordParameters operation middleware func (sh *strictHandler) ReservedGoKeywordParameters(ctx *fiber.Ctx, pType string) error { var request ReservedGoKeywordParametersRequestObject @@ -1002,9 +1166,12 @@ func (sh *strictHandler) ReusableResponses(ctx *fiber.Ctx) error { var body ReusableResponsesJSONRequestBody if err := ctx.BodyParser(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + if !errors.Is(err, io.EOF) { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { return sh.ssi.ReusableResponses(ctx.UserContext(), request.(ReusableResponsesRequestObject)) @@ -1032,8 +1199,10 @@ func (sh *strictHandler) TextExample(ctx *fiber.Ctx) error { var request TextExampleRequestObject data := ctx.Request().Body() - body := TextExampleTextRequestBody(data) - request.Body = &body + if len(data) > 0 { + body := TextExampleTextRequestBody(data) + request.Body = &body + } handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { return sh.ssi.TextExample(ctx.UserContext(), request.(TextExampleRequestObject)) @@ -1151,9 +1320,12 @@ func (sh *strictHandler) HeadersExample(ctx *fiber.Ctx, params HeadersExamplePar var body HeadersExampleJSONRequestBody if err := ctx.BodyParser(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + if !errors.Is(err, io.EOF) { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { return sh.ssi.HeadersExample(ctx.UserContext(), request.(HeadersExampleRequestObject)) @@ -1182,9 +1354,12 @@ func (sh *strictHandler) UnionExample(ctx *fiber.Ctx) error { var body UnionExampleJSONRequestBody if err := ctx.BodyParser(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + if !errors.Is(err, io.EOF) { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { return sh.ssi.UnionExample(ctx.UserContext(), request.(UnionExampleRequestObject)) @@ -1210,24 +1385,25 @@ func (sh *strictHandler) UnionExample(ctx *fiber.Ctx) error { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5QUZccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQb0sWpUS", - "PTqZ3PhY7C6+b3ex2BnPTGmNBk2OpzOO4KzRDpqXoZAI/1TgyL9JcBkqS8ponvJ3Qg7af/OII1RODAtY", - "LPfymdEEulkqrC1UJvzS5JPz62fcZWMohX/6ESHnKf8hWbmShL8ugWdR2gL4fD6PXnhw95FHfAxCAjbe", - "hserTd00tcBT7giVHnGvJIhdd4opTTAC9Na8aOuEF1j4kc64RWMBSQWMJqKooNtS+8UMP0FGYQdK52Yb", - "y1ujSSjtmFR5DgiaWAse8zocc5W1BgkkG06Zt5ARc4ATQB5xUuQd4/fr31nrsOMRnwC6YOiq1+/1PV/G", - "ghZW8ZS/bT5F3AoaNxtaEmRNF++/3d/9yZRjoiJTClKZKIopKwW6sShAMqXJeBerjFyPN5awIf5X2a5+", - "30Lpo6YJoHdGTk8RME1croXzdb9/pricR/wmGOvSsXQqWUuwRk0uqqID80f9WZtaM0A02O4sKauClBVI", - "61xtov3HQmQfyJf6ktxgGUtB4kSoH8vSpYGPEQpBIPcgYBAkD+NhTf1JWfgaOxfloK3HnXXqfmxqx8am", - "ZmSYBFGwWtGYLRa+KLBKM8Gc0qMC2MKpqJPMAtpj72ctB+1eHryOk9ezaEPLc1zXddwkUIUF6MzIL6Mw", - "4qoUI0isHm0u97oF8ZQPp+RDdvuAO1IiR5zgmRJbCKV3n95nKunfkT5aYod0RWi6EhmPTPwZprVBGVuB", - "ogQCdMnMW597xSPoSOW/lpIsE5oNgWlRgmQiJ0D2wbBWpdtK2UFr94P5GERWqpqWZ/mS/j3jHpKmDeIR", - "9wZ4GlAJea3Qk05YQbQDtqf/jM+vImCBZmi24w1T3WVwUaKW0CHkzpfELuY68AuWBmsSl2nadkfc1vXj", - "HGeQZ/L1o/8Bnvdqu45Y+s6d24cCVoWPr2PWrtoHti+spHugOFESTFLamwM1XwxUZyFTuQIZt7uIg2+v", - "lYRbozME2myB/JVOG2JLZf6mSWNgAYGIOcNqYGXliFnhHFPUVJFChduqhK3i8bjy7DZYeliV012svjkR", - "p28uxehN/+rwJW9PHDcbrcwr+Tj4/X2QOfTOfrSe6cCO73h2L5TO/pISrw21ulP4lyCwOtMzUBPfEWnJ", - "EKhCDZJNlFgMYrZys1WworWrFwpurLqhxYDtkIYo2qnrmke7hnBP3/CI6JSjy3PFaaXVrlHho//N2h76", - "5dmgjP6fDgJFQYBakJrAT8e5QW5rMRru8ibTXrAc7Wnh6duLqnnEw+w6lKAKC18niGyaJGHm3XO1GI0A", - "e8okwiqPwr8BAAD//4h9qqfAGAAA", + "H4sIAAAAAAAC/+xZTXPbNhP+Kzt431NKmrbjE2+NJ5O2aeuObJ86PkDESkJCAiiwFK3R6L93QEBfFq1K", + "qT4ynt5Ecr/wPLuLBTRlha6MVqjIsXzKLDqjlcP2oc+Fxb9qdOSfBLrCSkNSK5azD1z04rdZwizWjvdL", + "nKt7+UIrQtWqcmNKWXCvmn1xXn/KXDHCivtf/7c4YDn7X7YMJQtfXYbPvDIlstlslryI4O4zS9gIuUDb", + "Rht+Xq3bpolBljNHVqoh80aC2HWnmFSEQ7TemxeNQXiBeRz5lBmrDVqSAaMxL2vs9hTf6P4XLCisQKqB", + "3sTyViviUjkQcjBAi4ogggfehgNXG6MtoYD+BLyHgsChHaNlCSNJPjB2v/oeYsCOJWyM1gVHVxeXF5ee", + "L21QcSNZzt63rxJmOI3aBS0IMrqL91/u734H6YDXpCtOsuBlOYGKWzfiJQqQirQPsS7IXbDWk22J/1lE", + "7Y8RSp81bQJ90GJyjIRp83Ilna8vL0+Ul7OE3QRnXTYWQWUrBdaaGfC67MD8UX1VulGA1mobV5ZVdUnS", + "cEurXK2j/dtcZBfIF/aygbZVKjjxI6F+KE/nBj61WHJCsQMBvSC5Hw8r5o/Kwr/xc1YOYj/u7FP3I904", + "GOkGSINAXkIjaQRzxRcNVirg4KQalgjzoJJOMkuM296PSvTiWh68jaP3s2TNynPaNE3aFlBtS1SFFt9G", + "YcJkxYeYGTVcV/e2ObGc9SfkU3ZzgztQISeM8JkyU3Kptu/eJ2rp/yF9sMIO5eofpUWRekbSfqyP7sKN", + "5QVeyg8ac90EnIZKOl+l4aMb6boUwMuGT1zoD5sTRy+q+8mjLcyjjx3BgffJcrI1vu0xZEGtz6zzUPuA", + "z/SP1O6R+PvSd+qa2p+i9kwg0qFOv+Kk0VakhlteIaF12dTHOfO2hthh8o+FJBRcQR9B8QoF8AGhhU8a", + "oknXwU/w+0l/DiJLU+2BY/GQ/zllHrz2EMIS5h2wPOD3ko5kC8BPx6VqjmY46qZrrl5L+Cgyh87iwPmB", + "pIvjDvyCp96KxHmOTNtzc+Pwf4qk9ky+Pnj7lrDLsH3AweN77wJ1ePk6ZlFrF9i+cY7ZAcWxFKizytzs", + "aflsoDqDhRxIFGlcRRpie60l3GpVWKT1A4jfDJUmWBiD/gRohBAQaPfHBqGqHYHhzoGktouUMtwVCdxo", + "Ho/LyG6Dp4dlO93G6rsjcfruXIzeXF7tr/L+yHmzdpB4pR57v34MMvvemB3sxLLneetwfs9Uzo2kUbpy", + "pdxdwj8FgeWeXqAc+4lICbBItVUoYCz5/Bp0ozajgSWtXbNQCGM5Dc2vt/cZiJKttq5Zsu0K/OkNX9Ae", + "84+DU+VpreS2i/pH/xniDP1yb5BafafX8LwktIqTHOMPh7m/2bSiFd4N2kp7wXKyo4ent5dVs4SFf45C", + "C6pt6fsEkcmzLPzjdOEaPhyivZA640Z6FP4OAAD//0nTejA+HAAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/strict-server/fiber/server.go b/internal/test/strict-server/fiber/server.go index c135a2a445..914f967268 100644 --- a/internal/test/strict-server/fiber/server.go +++ b/internal/test/strict-server/fiber/server.go @@ -126,6 +126,14 @@ func (s StrictServer) ReusableResponses(ctx context.Context, request ReusableRes return ReusableResponses200JSONResponse{ReusableresponseJSONResponse: ReusableresponseJSONResponse{Body: *request.Body}}, nil } +func (s StrictServer) RequiredJSONBody(ctx context.Context, request RequiredJSONBodyRequestObject) (RequiredJSONBodyResponseObject, error) { + return RequiredJSONBody200JSONResponse(*request.Body), nil +} + +func (s StrictServer) RequiredTextBody(ctx context.Context, request RequiredTextBodyRequestObject) (RequiredTextBodyResponseObject, error) { + return RequiredTextBody200TextResponse(*request.Body), nil +} + func (s StrictServer) ReservedGoKeywordParameters(ctx context.Context, request ReservedGoKeywordParametersRequestObject) (ReservedGoKeywordParametersResponseObject, error) { return ReservedGoKeywordParameters200TextResponse(""), nil } diff --git a/internal/test/strict-server/fiber/types.gen.go b/internal/test/strict-server/fiber/types.gen.go index 6682c1acab..fac014c4bb 100644 --- a/internal/test/strict-server/fiber/types.gen.go +++ b/internal/test/strict-server/fiber/types.gen.go @@ -14,6 +14,9 @@ type Reusableresponse = Example // MultipleRequestAndResponseTypesTextBody defines parameters for MultipleRequestAndResponseTypes. type MultipleRequestAndResponseTypesTextBody = string +// RequiredTextBodyTextBody defines parameters for RequiredTextBody. +type RequiredTextBodyTextBody = string + // TextExampleTextBody defines parameters for TextExample. type TextExampleTextBody = string @@ -44,6 +47,12 @@ type MultipleRequestAndResponseTypesMultipartRequestBody = Example // MultipleRequestAndResponseTypesTextRequestBody defines body for MultipleRequestAndResponseTypes for text/plain ContentType. type MultipleRequestAndResponseTypesTextRequestBody = MultipleRequestAndResponseTypesTextBody +// RequiredJSONBodyJSONRequestBody defines body for RequiredJSONBody for application/json ContentType. +type RequiredJSONBodyJSONRequestBody = Example + +// RequiredTextBodyTextRequestBody defines body for RequiredTextBody for text/plain ContentType. +type RequiredTextBodyTextRequestBody = RequiredTextBodyTextBody + // ReusableResponsesJSONRequestBody defines body for ReusableResponses for application/json ContentType. type ReusableResponsesJSONRequestBody = Example diff --git a/internal/test/strict-server/gin/server.gen.go b/internal/test/strict-server/gin/server.gen.go index 21f9f32763..5a5b9c7582 100644 --- a/internal/test/strict-server/gin/server.gen.go +++ b/internal/test/strict-server/gin/server.gen.go @@ -9,6 +9,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "mime" @@ -39,6 +40,12 @@ type ServerInterface interface { // (POST /multiple) MultipleRequestAndResponseTypes(c *gin.Context) + // (POST /required-json-body) + RequiredJSONBody(c *gin.Context) + + // (POST /required-text-body) + RequiredTextBody(c *gin.Context) + // (GET /reserved-go-keyword-parameters/{type}) ReservedGoKeywordParameters(c *gin.Context, pType string) @@ -125,6 +132,32 @@ func (siw *ServerInterfaceWrapper) MultipleRequestAndResponseTypes(c *gin.Contex siw.Handler.MultipleRequestAndResponseTypes(c) } +// RequiredJSONBody operation middleware +func (siw *ServerInterfaceWrapper) RequiredJSONBody(c *gin.Context) { + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.RequiredJSONBody(c) +} + +// RequiredTextBody operation middleware +func (siw *ServerInterfaceWrapper) RequiredTextBody(c *gin.Context) { + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.RequiredTextBody(c) +} + // ReservedGoKeywordParameters operation middleware func (siw *ServerInterfaceWrapper) ReservedGoKeywordParameters(c *gin.Context) { @@ -319,6 +352,8 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options router.POST(options.BaseURL+"/multipart", wrapper.MultipartExample) router.POST(options.BaseURL+"/multipart-related", wrapper.MultipartRelatedExample) router.POST(options.BaseURL+"/multiple", wrapper.MultipleRequestAndResponseTypes) + router.POST(options.BaseURL+"/required-json-body", wrapper.RequiredJSONBody) + router.POST(options.BaseURL+"/required-text-body", wrapper.RequiredTextBody) router.GET(options.BaseURL+"/reserved-go-keyword-parameters/:type", wrapper.ReservedGoKeywordParameters) router.POST(options.BaseURL+"/reusable-responses", wrapper.ReusableResponses) router.POST(options.BaseURL+"/text", wrapper.TextExample) @@ -527,6 +562,73 @@ func (response MultipleRequestAndResponseTypes400Response) VisitMultipleRequestA return nil } +type RequiredJSONBodyRequestObject struct { + Body *RequiredJSONBodyJSONRequestBody +} + +type RequiredJSONBodyResponseObject interface { + VisitRequiredJSONBodyResponse(w http.ResponseWriter) error +} + +type RequiredJSONBody200JSONResponse Example + +func (response RequiredJSONBody200JSONResponse) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type RequiredJSONBody400Response = BadrequestResponse + +func (response RequiredJSONBody400Response) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type RequiredJSONBodydefaultResponse struct { + StatusCode int +} + +func (response RequiredJSONBodydefaultResponse) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(response.StatusCode) + return nil +} + +type RequiredTextBodyRequestObject struct { + Body *RequiredTextBodyTextRequestBody +} + +type RequiredTextBodyResponseObject interface { + VisitRequiredTextBodyResponse(w http.ResponseWriter) error +} + +type RequiredTextBody200TextResponse string + +func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(200) + + _, err := w.Write([]byte(response)) + return err +} + +type RequiredTextBody400Response = BadrequestResponse + +func (response RequiredTextBody400Response) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type RequiredTextBodydefaultResponse struct { + StatusCode int +} + +func (response RequiredTextBodydefaultResponse) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(response.StatusCode) + return nil +} + type ReservedGoKeywordParametersRequestObject struct { Type string `json:"type"` } @@ -874,6 +976,12 @@ type StrictServerInterface interface { // (POST /multiple) MultipleRequestAndResponseTypes(ctx context.Context, request MultipleRequestAndResponseTypesRequestObject) (MultipleRequestAndResponseTypesResponseObject, error) + // (POST /required-json-body) + RequiredJSONBody(ctx context.Context, request RequiredJSONBodyRequestObject) (RequiredJSONBodyResponseObject, error) + + // (POST /required-text-body) + RequiredTextBody(ctx context.Context, request RequiredTextBodyRequestObject) (RequiredTextBodyResponseObject, error) + // (GET /reserved-go-keyword-parameters/{type}) ReservedGoKeywordParameters(ctx context.Context, request ReservedGoKeywordParametersRequestObject) (ReservedGoKeywordParametersResponseObject, error) @@ -917,11 +1025,14 @@ func (sh *strictHandler) JSONExample(ctx *gin.Context) { var body JSONExampleJSONRequestBody if err := ctx.ShouldBindJSON(&body); err != nil { - ctx.Status(http.StatusBadRequest) - ctx.Error(err) - return + if !errors.Is(err, io.EOF) { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { return sh.ssi.JSONExample(ctx, request.(JSONExampleRequestObject)) @@ -1019,11 +1130,14 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(ctx *gin.Context) { var body MultipleRequestAndResponseTypesJSONRequestBody if err := ctx.ShouldBindJSON(&body); err != nil { - ctx.Status(http.StatusBadRequest) - ctx.Error(err) - return + if !errors.Is(err, io.EOF) { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + } else { + request.JSONBody = &body } - request.JSONBody = &body } if strings.HasPrefix(ctx.GetHeader("Content-Type"), "application/x-www-form-urlencoded") { if err := ctx.Request.ParseForm(); err != nil { @@ -1054,8 +1168,10 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(ctx *gin.Context) { ctx.Error(err) return } - body := MultipleRequestAndResponseTypesTextRequestBody(data) - request.TextBody = &body + if len(data) > 0 { + body := MultipleRequestAndResponseTypesTextRequestBody(data) + request.TextBody = &body + } } handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { @@ -1079,6 +1195,72 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(ctx *gin.Context) { } } +// RequiredJSONBody operation middleware +func (sh *strictHandler) RequiredJSONBody(ctx *gin.Context) { + var request RequiredJSONBodyRequestObject + + var body RequiredJSONBodyJSONRequestBody + if err := ctx.ShouldBindJSON(&body); err != nil { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + request.Body = &body + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.RequiredJSONBody(ctx, request.(RequiredJSONBodyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RequiredJSONBody") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(RequiredJSONBodyResponseObject); ok { + if err := validResponse.VisitRequiredJSONBodyResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// RequiredTextBody operation middleware +func (sh *strictHandler) RequiredTextBody(ctx *gin.Context) { + var request RequiredTextBodyRequestObject + + data, err := io.ReadAll(ctx.Request.Body) + if err != nil { + ctx.Error(err) + return + } + body := RequiredTextBodyTextRequestBody(data) + request.Body = &body + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.RequiredTextBody(ctx, request.(RequiredTextBodyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RequiredTextBody") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(RequiredTextBodyResponseObject); ok { + if err := validResponse.VisitRequiredTextBodyResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + // ReservedGoKeywordParameters operation middleware func (sh *strictHandler) ReservedGoKeywordParameters(ctx *gin.Context, pType string) { var request ReservedGoKeywordParametersRequestObject @@ -1112,11 +1294,14 @@ func (sh *strictHandler) ReusableResponses(ctx *gin.Context) { var body ReusableResponsesJSONRequestBody if err := ctx.ShouldBindJSON(&body); err != nil { - ctx.Status(http.StatusBadRequest) - ctx.Error(err) - return + if !errors.Is(err, io.EOF) { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { return sh.ssi.ReusableResponses(ctx, request.(ReusableResponsesRequestObject)) @@ -1148,8 +1333,10 @@ func (sh *strictHandler) TextExample(ctx *gin.Context) { ctx.Error(err) return } - body := TextExampleTextRequestBody(data) - request.Body = &body + if len(data) > 0 { + body := TextExampleTextRequestBody(data) + request.Body = &body + } handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { return sh.ssi.TextExample(ctx, request.(TextExampleRequestObject)) @@ -1272,11 +1459,14 @@ func (sh *strictHandler) HeadersExample(ctx *gin.Context, params HeadersExampleP var body HeadersExampleJSONRequestBody if err := ctx.ShouldBindJSON(&body); err != nil { - ctx.Status(http.StatusBadRequest) - ctx.Error(err) - return + if !errors.Is(err, io.EOF) { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { return sh.ssi.HeadersExample(ctx, request.(HeadersExampleRequestObject)) @@ -1305,11 +1495,14 @@ func (sh *strictHandler) UnionExample(ctx *gin.Context) { var body UnionExampleJSONRequestBody if err := ctx.ShouldBindJSON(&body); err != nil { - ctx.Status(http.StatusBadRequest) - ctx.Error(err) - return + if !errors.Is(err, io.EOF) { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { return sh.ssi.UnionExample(ctx, request.(UnionExampleRequestObject)) @@ -1335,24 +1528,25 @@ func (sh *strictHandler) UnionExample(ctx *gin.Context) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5QUZccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQb0sWpUS", - "PTqZ3PhY7C6+b3ex2BnPTGmNBk2OpzOO4KzRDpqXoZAI/1TgyL9JcBkqS8ponvJ3Qg7af/OII1RODAtY", - "LPfymdEEulkqrC1UJvzS5JPz62fcZWMohX/6ESHnKf8hWbmShL8ugWdR2gL4fD6PXnhw95FHfAxCAjbe", - "hserTd00tcBT7giVHnGvJIhdd4opTTAC9Na8aOuEF1j4kc64RWMBSQWMJqKooNtS+8UMP0FGYQdK52Yb", - "y1ujSSjtmFR5DgiaWAse8zocc5W1BgkkG06Zt5ARc4ATQB5xUuQd4/fr31nrsOMRnwC6YOiq1+/1PV/G", - "ghZW8ZS/bT5F3AoaNxtaEmRNF++/3d/9yZRjoiJTClKZKIopKwW6sShAMqXJeBerjFyPN5awIf5X2a5+", - "30Lpo6YJoHdGTk8RME1croXzdb9/pricR/wmGOvSsXQqWUuwRk0uqqID80f9WZtaM0A02O4sKauClBVI", - "61xtov3HQmQfyJf6ktxgGUtB4kSoH8vSpYGPEQpBIPcgYBAkD+NhTf1JWfgaOxfloK3HnXXqfmxqx8am", - "ZmSYBFGwWtGYLRa+KLBKM8Gc0qMC2MKpqJPMAtpj72ctB+1eHryOk9ezaEPLc1zXddwkUIUF6MzIL6Mw", - "4qoUI0isHm0u97oF8ZQPp+RDdvuAO1IiR5zgmRJbCKV3n95nKunfkT5aYod0RWi6EhmPTPwZprVBGVuB", - "ogQCdMnMW597xSPoSOW/lpIsE5oNgWlRgmQiJ0D2wbBWpdtK2UFr94P5GERWqpqWZ/mS/j3jHpKmDeIR", - "9wZ4GlAJea3Qk05YQbQDtqf/jM+vImCBZmi24w1T3WVwUaKW0CHkzpfELuY68AuWBmsSl2nadkfc1vXj", - "HGeQZ/L1o/8Bnvdqu45Y+s6d24cCVoWPr2PWrtoHti+spHugOFESTFLamwM1XwxUZyFTuQIZt7uIg2+v", - "lYRbozME2myB/JVOG2JLZf6mSWNgAYGIOcNqYGXliFnhHFPUVJFChduqhK3i8bjy7DZYeliV012svjkR", - "p28uxehN/+rwJW9PHDcbrcwr+Tj4/X2QOfTOfrSe6cCO73h2L5TO/pISrw21ulP4lyCwOtMzUBPfEWnJ", - "EKhCDZJNlFgMYrZys1WworWrFwpurLqhxYDtkIYo2qnrmke7hnBP3/CI6JSjy3PFaaXVrlHho//N2h76", - "5dmgjP6fDgJFQYBakJrAT8e5QW5rMRru8ibTXrAc7Wnh6duLqnnEw+w6lKAKC18niGyaJGHm3XO1GI0A", - "e8okwiqPwr8BAAD//4h9qqfAGAAA", + "H4sIAAAAAAAC/+xZTXPbNhP+Kzt431NKmrbjE2+NJ5O2aeuObJ86PkDESkJCAiiwFK3R6L93QEBfFq1K", + "qT4ynt5Ecr/wPLuLBTRlha6MVqjIsXzKLDqjlcP2oc+Fxb9qdOSfBLrCSkNSK5azD1z04rdZwizWjvdL", + "nKt7+UIrQtWqcmNKWXCvmn1xXn/KXDHCivtf/7c4YDn7X7YMJQtfXYbPvDIlstlslryI4O4zS9gIuUDb", + "Rht+Xq3bpolBljNHVqoh80aC2HWnmFSEQ7TemxeNQXiBeRz5lBmrDVqSAaMxL2vs9hTf6P4XLCisQKqB", + "3sTyViviUjkQcjBAi4ogggfehgNXG6MtoYD+BLyHgsChHaNlCSNJPjB2v/oeYsCOJWyM1gVHVxeXF5ee", + "L21QcSNZzt63rxJmOI3aBS0IMrqL91/u734H6YDXpCtOsuBlOYGKWzfiJQqQirQPsS7IXbDWk22J/1lE", + "7Y8RSp81bQJ90GJyjIRp83Ilna8vL0+Ul7OE3QRnXTYWQWUrBdaaGfC67MD8UX1VulGA1mobV5ZVdUnS", + "cEurXK2j/dtcZBfIF/aygbZVKjjxI6F+KE/nBj61WHJCsQMBvSC5Hw8r5o/Kwr/xc1YOYj/u7FP3I904", + "GOkGSINAXkIjaQRzxRcNVirg4KQalgjzoJJOMkuM296PSvTiWh68jaP3s2TNynPaNE3aFlBtS1SFFt9G", + "YcJkxYeYGTVcV/e2ObGc9SfkU3ZzgztQISeM8JkyU3Kptu/eJ2rp/yF9sMIO5eofpUWRekbSfqyP7sKN", + "5QVeyg8ac90EnIZKOl+l4aMb6boUwMuGT1zoD5sTRy+q+8mjLcyjjx3BgffJcrI1vu0xZEGtz6zzUPuA", + "z/SP1O6R+PvSd+qa2p+i9kwg0qFOv+Kk0VakhlteIaF12dTHOfO2hthh8o+FJBRcQR9B8QoF8AGhhU8a", + "oknXwU/w+0l/DiJLU+2BY/GQ/zllHrz2EMIS5h2wPOD3ko5kC8BPx6VqjmY46qZrrl5L+Cgyh87iwPmB", + "pIvjDvyCp96KxHmOTNtzc+Pwf4qk9ky+Pnj7lrDLsH3AweN77wJ1ePk6ZlFrF9i+cY7ZAcWxFKizytzs", + "aflsoDqDhRxIFGlcRRpie60l3GpVWKT1A4jfDJUmWBiD/gRohBAQaPfHBqGqHYHhzoGktouUMtwVCdxo", + "Ho/LyG6Dp4dlO93G6rsjcfruXIzeXF7tr/L+yHmzdpB4pR57v34MMvvemB3sxLLneetwfs9Uzo2kUbpy", + "pdxdwj8FgeWeXqAc+4lICbBItVUoYCz5/Bp0ozajgSWtXbNQCGM5Dc2vt/cZiJKttq5Zsu0K/OkNX9Ae", + "84+DU+VpreS2i/pH/xniDP1yb5BafafX8LwktIqTHOMPh7m/2bSiFd4N2kp7wXKyo4ent5dVs4SFf45C", + "C6pt6fsEkcmzLPzjdOEaPhyivZA640Z6FP4OAAD//0nTejA+HAAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/strict-server/gin/server.go b/internal/test/strict-server/gin/server.go index c135a2a445..914f967268 100644 --- a/internal/test/strict-server/gin/server.go +++ b/internal/test/strict-server/gin/server.go @@ -126,6 +126,14 @@ func (s StrictServer) ReusableResponses(ctx context.Context, request ReusableRes return ReusableResponses200JSONResponse{ReusableresponseJSONResponse: ReusableresponseJSONResponse{Body: *request.Body}}, nil } +func (s StrictServer) RequiredJSONBody(ctx context.Context, request RequiredJSONBodyRequestObject) (RequiredJSONBodyResponseObject, error) { + return RequiredJSONBody200JSONResponse(*request.Body), nil +} + +func (s StrictServer) RequiredTextBody(ctx context.Context, request RequiredTextBodyRequestObject) (RequiredTextBodyResponseObject, error) { + return RequiredTextBody200TextResponse(*request.Body), nil +} + func (s StrictServer) ReservedGoKeywordParameters(ctx context.Context, request ReservedGoKeywordParametersRequestObject) (ReservedGoKeywordParametersResponseObject, error) { return ReservedGoKeywordParameters200TextResponse(""), nil } diff --git a/internal/test/strict-server/gin/types.gen.go b/internal/test/strict-server/gin/types.gen.go index 6682c1acab..fac014c4bb 100644 --- a/internal/test/strict-server/gin/types.gen.go +++ b/internal/test/strict-server/gin/types.gen.go @@ -14,6 +14,9 @@ type Reusableresponse = Example // MultipleRequestAndResponseTypesTextBody defines parameters for MultipleRequestAndResponseTypes. type MultipleRequestAndResponseTypesTextBody = string +// RequiredTextBodyTextBody defines parameters for RequiredTextBody. +type RequiredTextBodyTextBody = string + // TextExampleTextBody defines parameters for TextExample. type TextExampleTextBody = string @@ -44,6 +47,12 @@ type MultipleRequestAndResponseTypesMultipartRequestBody = Example // MultipleRequestAndResponseTypesTextRequestBody defines body for MultipleRequestAndResponseTypes for text/plain ContentType. type MultipleRequestAndResponseTypesTextRequestBody = MultipleRequestAndResponseTypesTextBody +// RequiredJSONBodyJSONRequestBody defines body for RequiredJSONBody for application/json ContentType. +type RequiredJSONBodyJSONRequestBody = Example + +// RequiredTextBodyTextRequestBody defines body for RequiredTextBody for text/plain ContentType. +type RequiredTextBodyTextRequestBody = RequiredTextBodyTextBody + // ReusableResponsesJSONRequestBody defines body for ReusableResponses for application/json ContentType. type ReusableResponsesJSONRequestBody = Example diff --git a/internal/test/strict-server/gorilla/server.gen.go b/internal/test/strict-server/gorilla/server.gen.go index c6b29393df..0650806552 100644 --- a/internal/test/strict-server/gorilla/server.gen.go +++ b/internal/test/strict-server/gorilla/server.gen.go @@ -9,6 +9,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "mime" @@ -39,6 +40,12 @@ type ServerInterface interface { // (POST /multiple) MultipleRequestAndResponseTypes(w http.ResponseWriter, r *http.Request) + // (POST /required-json-body) + RequiredJSONBody(w http.ResponseWriter, r *http.Request) + + // (POST /required-text-body) + RequiredTextBody(w http.ResponseWriter, r *http.Request) + // (GET /reserved-go-keyword-parameters/{type}) ReservedGoKeywordParameters(w http.ResponseWriter, r *http.Request, pType string) @@ -129,6 +136,34 @@ func (siw *ServerInterfaceWrapper) MultipleRequestAndResponseTypes(w http.Respon handler.ServeHTTP(w, r) } +// RequiredJSONBody operation middleware +func (siw *ServerInterfaceWrapper) RequiredJSONBody(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.RequiredJSONBody(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// RequiredTextBody operation middleware +func (siw *ServerInterfaceWrapper) RequiredTextBody(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.RequiredTextBody(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ReservedGoKeywordParameters operation middleware func (siw *ServerInterfaceWrapper) ReservedGoKeywordParameters(w http.ResponseWriter, r *http.Request) { @@ -422,6 +457,10 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H r.HandleFunc(options.BaseURL+"/multiple", wrapper.MultipleRequestAndResponseTypes).Methods("POST") + r.HandleFunc(options.BaseURL+"/required-json-body", wrapper.RequiredJSONBody).Methods("POST") + + r.HandleFunc(options.BaseURL+"/required-text-body", wrapper.RequiredTextBody).Methods("POST") + r.HandleFunc(options.BaseURL+"/reserved-go-keyword-parameters/{type}", wrapper.ReservedGoKeywordParameters).Methods("GET") r.HandleFunc(options.BaseURL+"/reusable-responses", wrapper.ReusableResponses).Methods("POST") @@ -639,6 +678,73 @@ func (response MultipleRequestAndResponseTypes400Response) VisitMultipleRequestA return nil } +type RequiredJSONBodyRequestObject struct { + Body *RequiredJSONBodyJSONRequestBody +} + +type RequiredJSONBodyResponseObject interface { + VisitRequiredJSONBodyResponse(w http.ResponseWriter) error +} + +type RequiredJSONBody200JSONResponse Example + +func (response RequiredJSONBody200JSONResponse) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type RequiredJSONBody400Response = BadrequestResponse + +func (response RequiredJSONBody400Response) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type RequiredJSONBodydefaultResponse struct { + StatusCode int +} + +func (response RequiredJSONBodydefaultResponse) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(response.StatusCode) + return nil +} + +type RequiredTextBodyRequestObject struct { + Body *RequiredTextBodyTextRequestBody +} + +type RequiredTextBodyResponseObject interface { + VisitRequiredTextBodyResponse(w http.ResponseWriter) error +} + +type RequiredTextBody200TextResponse string + +func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(200) + + _, err := w.Write([]byte(response)) + return err +} + +type RequiredTextBody400Response = BadrequestResponse + +func (response RequiredTextBody400Response) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type RequiredTextBodydefaultResponse struct { + StatusCode int +} + +func (response RequiredTextBodydefaultResponse) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(response.StatusCode) + return nil +} + type ReservedGoKeywordParametersRequestObject struct { Type string `json:"type"` } @@ -986,6 +1092,12 @@ type StrictServerInterface interface { // (POST /multiple) MultipleRequestAndResponseTypes(ctx context.Context, request MultipleRequestAndResponseTypesRequestObject) (MultipleRequestAndResponseTypesResponseObject, error) + // (POST /required-json-body) + RequiredJSONBody(ctx context.Context, request RequiredJSONBodyRequestObject) (RequiredJSONBodyResponseObject, error) + + // (POST /required-text-body) + RequiredTextBody(ctx context.Context, request RequiredTextBodyRequestObject) (RequiredTextBodyResponseObject, error) + // (GET /reserved-go-keyword-parameters/{type}) ReservedGoKeywordParameters(ctx context.Context, request ReservedGoKeywordParametersRequestObject) (ReservedGoKeywordParametersResponseObject, error) @@ -1046,10 +1158,13 @@ func (sh *strictHandler) JSONExample(w http.ResponseWriter, r *http.Request) { var body JSONExampleJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.JSONExample(ctx, request.(JSONExampleRequestObject)) @@ -1144,10 +1259,13 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(w http.ResponseWriter, var body MultipleRequestAndResponseTypesJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.JSONBody = &body } - request.JSONBody = &body } if strings.HasPrefix(r.Header.Get("Content-Type"), "application/x-www-form-urlencoded") { if err := r.ParseForm(); err != nil { @@ -1178,8 +1296,10 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(w http.ResponseWriter, sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't read body: %w", err)) return } - body := MultipleRequestAndResponseTypesTextRequestBody(data) - request.TextBody = &body + if len(data) > 0 { + body := MultipleRequestAndResponseTypesTextRequestBody(data) + request.TextBody = &body + } } handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { @@ -1202,6 +1322,69 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(w http.ResponseWriter, } } +// RequiredJSONBody operation middleware +func (sh *strictHandler) RequiredJSONBody(w http.ResponseWriter, r *http.Request) { + var request RequiredJSONBodyRequestObject + + var body RequiredJSONBodyJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.RequiredJSONBody(ctx, request.(RequiredJSONBodyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RequiredJSONBody") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(RequiredJSONBodyResponseObject); ok { + if err := validResponse.VisitRequiredJSONBodyResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// RequiredTextBody operation middleware +func (sh *strictHandler) RequiredTextBody(w http.ResponseWriter, r *http.Request) { + var request RequiredTextBodyRequestObject + + data, err := io.ReadAll(r.Body) + if err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't read body: %w", err)) + return + } + body := RequiredTextBodyTextRequestBody(data) + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.RequiredTextBody(ctx, request.(RequiredTextBodyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RequiredTextBody") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(RequiredTextBodyResponseObject); ok { + if err := validResponse.VisitRequiredTextBodyResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // ReservedGoKeywordParameters operation middleware func (sh *strictHandler) ReservedGoKeywordParameters(w http.ResponseWriter, r *http.Request, pType string) { var request ReservedGoKeywordParametersRequestObject @@ -1234,10 +1417,13 @@ func (sh *strictHandler) ReusableResponses(w http.ResponseWriter, r *http.Reques var body ReusableResponsesJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.ReusableResponses(ctx, request.(ReusableResponsesRequestObject)) @@ -1268,8 +1454,10 @@ func (sh *strictHandler) TextExample(w http.ResponseWriter, r *http.Request) { sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't read body: %w", err)) return } - body := TextExampleTextRequestBody(data) - request.Body = &body + if len(data) > 0 { + body := TextExampleTextRequestBody(data) + request.Body = &body + } handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.TextExample(ctx, request.(TextExampleRequestObject)) @@ -1388,10 +1576,13 @@ func (sh *strictHandler) HeadersExample(w http.ResponseWriter, r *http.Request, var body HeadersExampleJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.HeadersExample(ctx, request.(HeadersExampleRequestObject)) @@ -1419,10 +1610,13 @@ func (sh *strictHandler) UnionExample(w http.ResponseWriter, r *http.Request) { var body UnionExampleJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.UnionExample(ctx, request.(UnionExampleRequestObject)) @@ -1447,24 +1641,25 @@ func (sh *strictHandler) UnionExample(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5QUZccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQb0sWpUS", - "PTqZ3PhY7C6+b3ex2BnPTGmNBk2OpzOO4KzRDpqXoZAI/1TgyL9JcBkqS8ponvJ3Qg7af/OII1RODAtY", - "LPfymdEEulkqrC1UJvzS5JPz62fcZWMohX/6ESHnKf8hWbmShL8ugWdR2gL4fD6PXnhw95FHfAxCAjbe", - "hserTd00tcBT7giVHnGvJIhdd4opTTAC9Na8aOuEF1j4kc64RWMBSQWMJqKooNtS+8UMP0FGYQdK52Yb", - "y1ujSSjtmFR5DgiaWAse8zocc5W1BgkkG06Zt5ARc4ATQB5xUuQd4/fr31nrsOMRnwC6YOiq1+/1PV/G", - "ghZW8ZS/bT5F3AoaNxtaEmRNF++/3d/9yZRjoiJTClKZKIopKwW6sShAMqXJeBerjFyPN5awIf5X2a5+", - "30Lpo6YJoHdGTk8RME1croXzdb9/pricR/wmGOvSsXQqWUuwRk0uqqID80f9WZtaM0A02O4sKauClBVI", - "61xtov3HQmQfyJf6ktxgGUtB4kSoH8vSpYGPEQpBIPcgYBAkD+NhTf1JWfgaOxfloK3HnXXqfmxqx8am", - "ZmSYBFGwWtGYLRa+KLBKM8Gc0qMC2MKpqJPMAtpj72ctB+1eHryOk9ezaEPLc1zXddwkUIUF6MzIL6Mw", - "4qoUI0isHm0u97oF8ZQPp+RDdvuAO1IiR5zgmRJbCKV3n95nKunfkT5aYod0RWi6EhmPTPwZprVBGVuB", - "ogQCdMnMW597xSPoSOW/lpIsE5oNgWlRgmQiJ0D2wbBWpdtK2UFr94P5GERWqpqWZ/mS/j3jHpKmDeIR", - "9wZ4GlAJea3Qk05YQbQDtqf/jM+vImCBZmi24w1T3WVwUaKW0CHkzpfELuY68AuWBmsSl2nadkfc1vXj", - "HGeQZ/L1o/8Bnvdqu45Y+s6d24cCVoWPr2PWrtoHti+spHugOFESTFLamwM1XwxUZyFTuQIZt7uIg2+v", - "lYRbozME2myB/JVOG2JLZf6mSWNgAYGIOcNqYGXliFnhHFPUVJFChduqhK3i8bjy7DZYeliV012svjkR", - "p28uxehN/+rwJW9PHDcbrcwr+Tj4/X2QOfTOfrSe6cCO73h2L5TO/pISrw21ulP4lyCwOtMzUBPfEWnJ", - "EKhCDZJNlFgMYrZys1WworWrFwpurLqhxYDtkIYo2qnrmke7hnBP3/CI6JSjy3PFaaXVrlHho//N2h76", - "5dmgjP6fDgJFQYBakJrAT8e5QW5rMRru8ibTXrAc7Wnh6duLqnnEw+w6lKAKC18niGyaJGHm3XO1GI0A", - "e8okwiqPwr8BAAD//4h9qqfAGAAA", + "H4sIAAAAAAAC/+xZTXPbNhP+Kzt431NKmrbjE2+NJ5O2aeuObJ86PkDESkJCAiiwFK3R6L93QEBfFq1K", + "qT4ynt5Ecr/wPLuLBTRlha6MVqjIsXzKLDqjlcP2oc+Fxb9qdOSfBLrCSkNSK5azD1z04rdZwizWjvdL", + "nKt7+UIrQtWqcmNKWXCvmn1xXn/KXDHCivtf/7c4YDn7X7YMJQtfXYbPvDIlstlslryI4O4zS9gIuUDb", + "Rht+Xq3bpolBljNHVqoh80aC2HWnmFSEQ7TemxeNQXiBeRz5lBmrDVqSAaMxL2vs9hTf6P4XLCisQKqB", + "3sTyViviUjkQcjBAi4ogggfehgNXG6MtoYD+BLyHgsChHaNlCSNJPjB2v/oeYsCOJWyM1gVHVxeXF5ee", + "L21QcSNZzt63rxJmOI3aBS0IMrqL91/u734H6YDXpCtOsuBlOYGKWzfiJQqQirQPsS7IXbDWk22J/1lE", + "7Y8RSp81bQJ90GJyjIRp83Ilna8vL0+Ul7OE3QRnXTYWQWUrBdaaGfC67MD8UX1VulGA1mobV5ZVdUnS", + "cEurXK2j/dtcZBfIF/aygbZVKjjxI6F+KE/nBj61WHJCsQMBvSC5Hw8r5o/Kwr/xc1YOYj/u7FP3I904", + "GOkGSINAXkIjaQRzxRcNVirg4KQalgjzoJJOMkuM296PSvTiWh68jaP3s2TNynPaNE3aFlBtS1SFFt9G", + "YcJkxYeYGTVcV/e2ObGc9SfkU3ZzgztQISeM8JkyU3Kptu/eJ2rp/yF9sMIO5eofpUWRekbSfqyP7sKN", + "5QVeyg8ac90EnIZKOl+l4aMb6boUwMuGT1zoD5sTRy+q+8mjLcyjjx3BgffJcrI1vu0xZEGtz6zzUPuA", + "z/SP1O6R+PvSd+qa2p+i9kwg0qFOv+Kk0VakhlteIaF12dTHOfO2hthh8o+FJBRcQR9B8QoF8AGhhU8a", + "oknXwU/w+0l/DiJLU+2BY/GQ/zllHrz2EMIS5h2wPOD3ko5kC8BPx6VqjmY46qZrrl5L+Cgyh87iwPmB", + "pIvjDvyCp96KxHmOTNtzc+Pwf4qk9ky+Pnj7lrDLsH3AweN77wJ1ePk6ZlFrF9i+cY7ZAcWxFKizytzs", + "aflsoDqDhRxIFGlcRRpie60l3GpVWKT1A4jfDJUmWBiD/gRohBAQaPfHBqGqHYHhzoGktouUMtwVCdxo", + "Ho/LyG6Dp4dlO93G6rsjcfruXIzeXF7tr/L+yHmzdpB4pR57v34MMvvemB3sxLLneetwfs9Uzo2kUbpy", + "pdxdwj8FgeWeXqAc+4lICbBItVUoYCz5/Bp0ozajgSWtXbNQCGM5Dc2vt/cZiJKttq5Zsu0K/OkNX9Ae", + "84+DU+VpreS2i/pH/xniDP1yb5BafafX8LwktIqTHOMPh7m/2bSiFd4N2kp7wXKyo4ent5dVs4SFf45C", + "C6pt6fsEkcmzLPzjdOEaPhyivZA640Z6FP4OAAD//0nTejA+HAAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/strict-server/gorilla/server.go b/internal/test/strict-server/gorilla/server.go index 5c5d7c9e77..2cb801e035 100644 --- a/internal/test/strict-server/gorilla/server.go +++ b/internal/test/strict-server/gorilla/server.go @@ -102,6 +102,14 @@ func (s StrictServer) ReusableResponses(ctx context.Context, request ReusableRes return ReusableResponses200JSONResponse{ReusableresponseJSONResponse: ReusableresponseJSONResponse{Body: *request.Body}}, nil } +func (s StrictServer) RequiredJSONBody(ctx context.Context, request RequiredJSONBodyRequestObject) (RequiredJSONBodyResponseObject, error) { + return RequiredJSONBody200JSONResponse(*request.Body), nil +} + +func (s StrictServer) RequiredTextBody(ctx context.Context, request RequiredTextBodyRequestObject) (RequiredTextBodyResponseObject, error) { + return RequiredTextBody200TextResponse(*request.Body), nil +} + func (s StrictServer) ReservedGoKeywordParameters(ctx context.Context, request ReservedGoKeywordParametersRequestObject) (ReservedGoKeywordParametersResponseObject, error) { return ReservedGoKeywordParameters200TextResponse(""), nil } diff --git a/internal/test/strict-server/gorilla/types.gen.go b/internal/test/strict-server/gorilla/types.gen.go index 6682c1acab..fac014c4bb 100644 --- a/internal/test/strict-server/gorilla/types.gen.go +++ b/internal/test/strict-server/gorilla/types.gen.go @@ -14,6 +14,9 @@ type Reusableresponse = Example // MultipleRequestAndResponseTypesTextBody defines parameters for MultipleRequestAndResponseTypes. type MultipleRequestAndResponseTypesTextBody = string +// RequiredTextBodyTextBody defines parameters for RequiredTextBody. +type RequiredTextBodyTextBody = string + // TextExampleTextBody defines parameters for TextExample. type TextExampleTextBody = string @@ -44,6 +47,12 @@ type MultipleRequestAndResponseTypesMultipartRequestBody = Example // MultipleRequestAndResponseTypesTextRequestBody defines body for MultipleRequestAndResponseTypes for text/plain ContentType. type MultipleRequestAndResponseTypesTextRequestBody = MultipleRequestAndResponseTypesTextBody +// RequiredJSONBodyJSONRequestBody defines body for RequiredJSONBody for application/json ContentType. +type RequiredJSONBodyJSONRequestBody = Example + +// RequiredTextBodyTextRequestBody defines body for RequiredTextBody for text/plain ContentType. +type RequiredTextBodyTextRequestBody = RequiredTextBodyTextBody + // ReusableResponsesJSONRequestBody defines body for ReusableResponses for application/json ContentType. type ReusableResponsesJSONRequestBody = Example diff --git a/internal/test/strict-server/iris/server.gen.go b/internal/test/strict-server/iris/server.gen.go index 7f224b3b37..b592b21a9f 100644 --- a/internal/test/strict-server/iris/server.gen.go +++ b/internal/test/strict-server/iris/server.gen.go @@ -9,6 +9,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "mime" @@ -39,6 +40,12 @@ type ServerInterface interface { // (POST /multiple) MultipleRequestAndResponseTypes(ctx iris.Context) + // (POST /required-json-body) + RequiredJSONBody(ctx iris.Context) + + // (POST /required-text-body) + RequiredTextBody(ctx iris.Context) + // (GET /reserved-go-keyword-parameters/{type}) ReservedGoKeywordParameters(ctx iris.Context, pType string) @@ -99,6 +106,20 @@ func (w *ServerInterfaceWrapper) MultipleRequestAndResponseTypes(ctx iris.Contex w.Handler.MultipleRequestAndResponseTypes(ctx) } +// RequiredJSONBody converts iris context to params. +func (w *ServerInterfaceWrapper) RequiredJSONBody(ctx iris.Context) { + + // Invoke the callback with all the unmarshaled arguments + w.Handler.RequiredJSONBody(ctx) +} + +// RequiredTextBody converts iris context to params. +func (w *ServerInterfaceWrapper) RequiredTextBody(ctx iris.Context) { + + // Invoke the callback with all the unmarshaled arguments + w.Handler.RequiredTextBody(ctx) +} + // ReservedGoKeywordParameters converts iris context to params. func (w *ServerInterfaceWrapper) ReservedGoKeywordParameters(ctx iris.Context) { @@ -238,6 +259,8 @@ func RegisterHandlersWithOptions(router *iris.Application, si ServerInterface, o router.Post(options.BaseURL+"/multipart", wrapper.MultipartExample) router.Post(options.BaseURL+"/multipart-related", wrapper.MultipartRelatedExample) router.Post(options.BaseURL+"/multiple", wrapper.MultipleRequestAndResponseTypes) + router.Post(options.BaseURL+"/required-json-body", wrapper.RequiredJSONBody) + router.Post(options.BaseURL+"/required-text-body", wrapper.RequiredTextBody) router.Get(options.BaseURL+"/reserved-go-keyword-parameters/:type", wrapper.ReservedGoKeywordParameters) router.Post(options.BaseURL+"/reusable-responses", wrapper.ReusableResponses) router.Post(options.BaseURL+"/text", wrapper.TextExample) @@ -448,6 +471,73 @@ func (response MultipleRequestAndResponseTypes400Response) VisitMultipleRequestA return nil } +type RequiredJSONBodyRequestObject struct { + Body *RequiredJSONBodyJSONRequestBody +} + +type RequiredJSONBodyResponseObject interface { + VisitRequiredJSONBodyResponse(ctx iris.Context) error +} + +type RequiredJSONBody200JSONResponse Example + +func (response RequiredJSONBody200JSONResponse) VisitRequiredJSONBodyResponse(ctx iris.Context) error { + ctx.ResponseWriter().Header().Set("Content-Type", "application/json") + ctx.StatusCode(200) + + return ctx.JSON(&response) +} + +type RequiredJSONBody400Response = BadrequestResponse + +func (response RequiredJSONBody400Response) VisitRequiredJSONBodyResponse(ctx iris.Context) error { + ctx.StatusCode(400) + return nil +} + +type RequiredJSONBodydefaultResponse struct { + StatusCode int +} + +func (response RequiredJSONBodydefaultResponse) VisitRequiredJSONBodyResponse(ctx iris.Context) error { + ctx.StatusCode(response.StatusCode) + return nil +} + +type RequiredTextBodyRequestObject struct { + Body *RequiredTextBodyTextRequestBody +} + +type RequiredTextBodyResponseObject interface { + VisitRequiredTextBodyResponse(ctx iris.Context) error +} + +type RequiredTextBody200TextResponse string + +func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(ctx iris.Context) error { + ctx.ResponseWriter().Header().Set("Content-Type", "text/plain") + ctx.StatusCode(200) + + _, err := ctx.WriteString(string(response)) + return err +} + +type RequiredTextBody400Response = BadrequestResponse + +func (response RequiredTextBody400Response) VisitRequiredTextBodyResponse(ctx iris.Context) error { + ctx.StatusCode(400) + return nil +} + +type RequiredTextBodydefaultResponse struct { + StatusCode int +} + +func (response RequiredTextBodydefaultResponse) VisitRequiredTextBodyResponse(ctx iris.Context) error { + ctx.StatusCode(response.StatusCode) + return nil +} + type ReservedGoKeywordParametersRequestObject struct { Type string `json:"type"` } @@ -795,6 +885,12 @@ type StrictServerInterface interface { // (POST /multiple) MultipleRequestAndResponseTypes(ctx context.Context, request MultipleRequestAndResponseTypesRequestObject) (MultipleRequestAndResponseTypesResponseObject, error) + // (POST /required-json-body) + RequiredJSONBody(ctx context.Context, request RequiredJSONBodyRequestObject) (RequiredJSONBodyResponseObject, error) + + // (POST /required-text-body) + RequiredTextBody(ctx context.Context, request RequiredTextBodyRequestObject) (RequiredTextBodyResponseObject, error) + // (GET /reserved-go-keyword-parameters/{type}) ReservedGoKeywordParameters(ctx context.Context, request ReservedGoKeywordParametersRequestObject) (ReservedGoKeywordParametersResponseObject, error) @@ -838,10 +934,13 @@ func (sh *strictHandler) JSONExample(ctx iris.Context) { var body JSONExampleJSONRequestBody if err := ctx.ReadJSON(&body); err != nil { - ctx.StopWithError(http.StatusBadRequest, err) - return + if !errors.Is(err, io.EOF) { + ctx.StopWithError(http.StatusBadRequest, err) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx iris.Context, request interface{}) (interface{}, error) { return sh.ssi.JSONExample(ctx, request.(JSONExampleRequestObject)) @@ -945,10 +1044,13 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(ctx iris.Context) { var body MultipleRequestAndResponseTypesJSONRequestBody if err := ctx.ReadJSON(&body); err != nil { - ctx.StopWithError(http.StatusBadRequest, err) - return + if !errors.Is(err, io.EOF) { + ctx.StopWithError(http.StatusBadRequest, err) + return + } + } else { + request.JSONBody = &body } - request.JSONBody = &body } if strings.HasPrefix(ctx.GetHeader("Content-Type"), "application/x-www-form-urlencoded") { if err := ctx.Request().ParseForm(); err != nil { @@ -979,8 +1081,10 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(ctx iris.Context) { ctx.StopWithError(http.StatusBadRequest, err) return } - body := MultipleRequestAndResponseTypesTextRequestBody(data) - request.TextBody = &body + if len(data) > 0 { + body := MultipleRequestAndResponseTypesTextRequestBody(data) + request.TextBody = &body + } } handler := func(ctx iris.Context, request interface{}) (interface{}, error) { @@ -1006,6 +1110,75 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(ctx iris.Context) { } } +// RequiredJSONBody operation middleware +func (sh *strictHandler) RequiredJSONBody(ctx iris.Context) { + var request RequiredJSONBodyRequestObject + + var body RequiredJSONBodyJSONRequestBody + if err := ctx.ReadJSON(&body); err != nil { + ctx.StopWithError(http.StatusBadRequest, err) + return + } + request.Body = &body + + handler := func(ctx iris.Context, request interface{}) (interface{}, error) { + return sh.ssi.RequiredJSONBody(ctx, request.(RequiredJSONBodyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RequiredJSONBody") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.StopWithError(http.StatusBadRequest, err) + return + } else if validResponse, ok := response.(RequiredJSONBodyResponseObject); ok { + if err := validResponse.VisitRequiredJSONBodyResponse(ctx); err != nil { + ctx.StopWithError(http.StatusBadRequest, err) + return + } + } else if response != nil { + ctx.Writef("Unexpected response type: %T", response) + return + } +} + +// RequiredTextBody operation middleware +func (sh *strictHandler) RequiredTextBody(ctx iris.Context) { + var request RequiredTextBodyRequestObject + + data, err := io.ReadAll(ctx.Request().Body) + if err != nil { + ctx.StopWithError(http.StatusBadRequest, err) + return + } + body := RequiredTextBodyTextRequestBody(data) + request.Body = &body + + handler := func(ctx iris.Context, request interface{}) (interface{}, error) { + return sh.ssi.RequiredTextBody(ctx, request.(RequiredTextBodyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RequiredTextBody") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.StopWithError(http.StatusBadRequest, err) + return + } else if validResponse, ok := response.(RequiredTextBodyResponseObject); ok { + if err := validResponse.VisitRequiredTextBodyResponse(ctx); err != nil { + ctx.StopWithError(http.StatusBadRequest, err) + return + } + } else if response != nil { + ctx.Writef("Unexpected response type: %T", response) + return + } +} + // ReservedGoKeywordParameters operation middleware func (sh *strictHandler) ReservedGoKeywordParameters(ctx iris.Context, pType string) { var request ReservedGoKeywordParametersRequestObject @@ -1041,10 +1214,13 @@ func (sh *strictHandler) ReusableResponses(ctx iris.Context) { var body ReusableResponsesJSONRequestBody if err := ctx.ReadJSON(&body); err != nil { - ctx.StopWithError(http.StatusBadRequest, err) - return + if !errors.Is(err, io.EOF) { + ctx.StopWithError(http.StatusBadRequest, err) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx iris.Context, request interface{}) (interface{}, error) { return sh.ssi.ReusableResponses(ctx, request.(ReusableResponsesRequestObject)) @@ -1078,8 +1254,10 @@ func (sh *strictHandler) TextExample(ctx iris.Context) { ctx.StopWithError(http.StatusBadRequest, err) return } - body := TextExampleTextRequestBody(data) - request.Body = &body + if len(data) > 0 { + body := TextExampleTextRequestBody(data) + request.Body = &body + } handler := func(ctx iris.Context, request interface{}) (interface{}, error) { return sh.ssi.TextExample(ctx, request.(TextExampleRequestObject)) @@ -1210,10 +1388,13 @@ func (sh *strictHandler) HeadersExample(ctx iris.Context, params HeadersExampleP var body HeadersExampleJSONRequestBody if err := ctx.ReadJSON(&body); err != nil { - ctx.StopWithError(http.StatusBadRequest, err) - return + if !errors.Is(err, io.EOF) { + ctx.StopWithError(http.StatusBadRequest, err) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx iris.Context, request interface{}) (interface{}, error) { return sh.ssi.HeadersExample(ctx, request.(HeadersExampleRequestObject)) @@ -1244,10 +1425,13 @@ func (sh *strictHandler) UnionExample(ctx iris.Context) { var body UnionExampleJSONRequestBody if err := ctx.ReadJSON(&body); err != nil { - ctx.StopWithError(http.StatusBadRequest, err) - return + if !errors.Is(err, io.EOF) { + ctx.StopWithError(http.StatusBadRequest, err) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx iris.Context, request interface{}) (interface{}, error) { return sh.ssi.UnionExample(ctx, request.(UnionExampleRequestObject)) @@ -1275,24 +1459,25 @@ func (sh *strictHandler) UnionExample(ctx iris.Context) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5QUZccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQb0sWpUS", - "PTqZ3PhY7C6+b3ex2BnPTGmNBk2OpzOO4KzRDpqXoZAI/1TgyL9JcBkqS8ponvJ3Qg7af/OII1RODAtY", - "LPfymdEEulkqrC1UJvzS5JPz62fcZWMohX/6ESHnKf8hWbmShL8ugWdR2gL4fD6PXnhw95FHfAxCAjbe", - "hserTd00tcBT7giVHnGvJIhdd4opTTAC9Na8aOuEF1j4kc64RWMBSQWMJqKooNtS+8UMP0FGYQdK52Yb", - "y1ujSSjtmFR5DgiaWAse8zocc5W1BgkkG06Zt5ARc4ATQB5xUuQd4/fr31nrsOMRnwC6YOiq1+/1PV/G", - "ghZW8ZS/bT5F3AoaNxtaEmRNF++/3d/9yZRjoiJTClKZKIopKwW6sShAMqXJeBerjFyPN5awIf5X2a5+", - "30Lpo6YJoHdGTk8RME1croXzdb9/pricR/wmGOvSsXQqWUuwRk0uqqID80f9WZtaM0A02O4sKauClBVI", - "61xtov3HQmQfyJf6ktxgGUtB4kSoH8vSpYGPEQpBIPcgYBAkD+NhTf1JWfgaOxfloK3HnXXqfmxqx8am", - "ZmSYBFGwWtGYLRa+KLBKM8Gc0qMC2MKpqJPMAtpj72ctB+1eHryOk9ezaEPLc1zXddwkUIUF6MzIL6Mw", - "4qoUI0isHm0u97oF8ZQPp+RDdvuAO1IiR5zgmRJbCKV3n95nKunfkT5aYod0RWi6EhmPTPwZprVBGVuB", - "ogQCdMnMW597xSPoSOW/lpIsE5oNgWlRgmQiJ0D2wbBWpdtK2UFr94P5GERWqpqWZ/mS/j3jHpKmDeIR", - "9wZ4GlAJea3Qk05YQbQDtqf/jM+vImCBZmi24w1T3WVwUaKW0CHkzpfELuY68AuWBmsSl2nadkfc1vXj", - "HGeQZ/L1o/8Bnvdqu45Y+s6d24cCVoWPr2PWrtoHti+spHugOFESTFLamwM1XwxUZyFTuQIZt7uIg2+v", - "lYRbozME2myB/JVOG2JLZf6mSWNgAYGIOcNqYGXliFnhHFPUVJFChduqhK3i8bjy7DZYeliV012svjkR", - "p28uxehN/+rwJW9PHDcbrcwr+Tj4/X2QOfTOfrSe6cCO73h2L5TO/pISrw21ulP4lyCwOtMzUBPfEWnJ", - "EKhCDZJNlFgMYrZys1WworWrFwpurLqhxYDtkIYo2qnrmke7hnBP3/CI6JSjy3PFaaXVrlHho//N2h76", - "5dmgjP6fDgJFQYBakJrAT8e5QW5rMRru8ibTXrAc7Wnh6duLqnnEw+w6lKAKC18niGyaJGHm3XO1GI0A", - "e8okwiqPwr8BAAD//4h9qqfAGAAA", + "H4sIAAAAAAAC/+xZTXPbNhP+Kzt431NKmrbjE2+NJ5O2aeuObJ86PkDESkJCAiiwFK3R6L93QEBfFq1K", + "qT4ynt5Ecr/wPLuLBTRlha6MVqjIsXzKLDqjlcP2oc+Fxb9qdOSfBLrCSkNSK5azD1z04rdZwizWjvdL", + "nKt7+UIrQtWqcmNKWXCvmn1xXn/KXDHCivtf/7c4YDn7X7YMJQtfXYbPvDIlstlslryI4O4zS9gIuUDb", + "Rht+Xq3bpolBljNHVqoh80aC2HWnmFSEQ7TemxeNQXiBeRz5lBmrDVqSAaMxL2vs9hTf6P4XLCisQKqB", + "3sTyViviUjkQcjBAi4ogggfehgNXG6MtoYD+BLyHgsChHaNlCSNJPjB2v/oeYsCOJWyM1gVHVxeXF5ee", + "L21QcSNZzt63rxJmOI3aBS0IMrqL91/u734H6YDXpCtOsuBlOYGKWzfiJQqQirQPsS7IXbDWk22J/1lE", + "7Y8RSp81bQJ90GJyjIRp83Ilna8vL0+Ul7OE3QRnXTYWQWUrBdaaGfC67MD8UX1VulGA1mobV5ZVdUnS", + "cEurXK2j/dtcZBfIF/aygbZVKjjxI6F+KE/nBj61WHJCsQMBvSC5Hw8r5o/Kwr/xc1YOYj/u7FP3I904", + "GOkGSINAXkIjaQRzxRcNVirg4KQalgjzoJJOMkuM296PSvTiWh68jaP3s2TNynPaNE3aFlBtS1SFFt9G", + "YcJkxYeYGTVcV/e2ObGc9SfkU3ZzgztQISeM8JkyU3Kptu/eJ2rp/yF9sMIO5eofpUWRekbSfqyP7sKN", + "5QVeyg8ac90EnIZKOl+l4aMb6boUwMuGT1zoD5sTRy+q+8mjLcyjjx3BgffJcrI1vu0xZEGtz6zzUPuA", + "z/SP1O6R+PvSd+qa2p+i9kwg0qFOv+Kk0VakhlteIaF12dTHOfO2hthh8o+FJBRcQR9B8QoF8AGhhU8a", + "oknXwU/w+0l/DiJLU+2BY/GQ/zllHrz2EMIS5h2wPOD3ko5kC8BPx6VqjmY46qZrrl5L+Cgyh87iwPmB", + "pIvjDvyCp96KxHmOTNtzc+Pwf4qk9ky+Pnj7lrDLsH3AweN77wJ1ePk6ZlFrF9i+cY7ZAcWxFKizytzs", + "aflsoDqDhRxIFGlcRRpie60l3GpVWKT1A4jfDJUmWBiD/gRohBAQaPfHBqGqHYHhzoGktouUMtwVCdxo", + "Ho/LyG6Dp4dlO93G6rsjcfruXIzeXF7tr/L+yHmzdpB4pR57v34MMvvemB3sxLLneetwfs9Uzo2kUbpy", + "pdxdwj8FgeWeXqAc+4lICbBItVUoYCz5/Bp0ozajgSWtXbNQCGM5Dc2vt/cZiJKttq5Zsu0K/OkNX9Ae", + "84+DU+VpreS2i/pH/xniDP1yb5BafafX8LwktIqTHOMPh7m/2bSiFd4N2kp7wXKyo4ent5dVs4SFf45C", + "C6pt6fsEkcmzLPzjdOEaPhyivZA640Z6FP4OAAD//0nTejA+HAAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/strict-server/iris/server.go b/internal/test/strict-server/iris/server.go index c0ab4c7700..f87df99759 100644 --- a/internal/test/strict-server/iris/server.go +++ b/internal/test/strict-server/iris/server.go @@ -125,6 +125,14 @@ func (s StrictServer) ReusableResponses(ctx context.Context, request ReusableRes return ReusableResponses200JSONResponse{ReusableresponseJSONResponse: ReusableresponseJSONResponse{Body: *request.Body}}, nil } +func (s StrictServer) RequiredJSONBody(ctx context.Context, request RequiredJSONBodyRequestObject) (RequiredJSONBodyResponseObject, error) { + return RequiredJSONBody200JSONResponse(*request.Body), nil +} + +func (s StrictServer) RequiredTextBody(ctx context.Context, request RequiredTextBodyRequestObject) (RequiredTextBodyResponseObject, error) { + return RequiredTextBody200TextResponse(*request.Body), nil +} + func (s StrictServer) ReservedGoKeywordParameters(ctx context.Context, request ReservedGoKeywordParametersRequestObject) (ReservedGoKeywordParametersResponseObject, error) { return ReservedGoKeywordParameters200TextResponse(""), nil } diff --git a/internal/test/strict-server/iris/types.gen.go b/internal/test/strict-server/iris/types.gen.go index 6682c1acab..fac014c4bb 100644 --- a/internal/test/strict-server/iris/types.gen.go +++ b/internal/test/strict-server/iris/types.gen.go @@ -14,6 +14,9 @@ type Reusableresponse = Example // MultipleRequestAndResponseTypesTextBody defines parameters for MultipleRequestAndResponseTypes. type MultipleRequestAndResponseTypesTextBody = string +// RequiredTextBodyTextBody defines parameters for RequiredTextBody. +type RequiredTextBodyTextBody = string + // TextExampleTextBody defines parameters for TextExample. type TextExampleTextBody = string @@ -44,6 +47,12 @@ type MultipleRequestAndResponseTypesMultipartRequestBody = Example // MultipleRequestAndResponseTypesTextRequestBody defines body for MultipleRequestAndResponseTypes for text/plain ContentType. type MultipleRequestAndResponseTypesTextRequestBody = MultipleRequestAndResponseTypesTextBody +// RequiredJSONBodyJSONRequestBody defines body for RequiredJSONBody for application/json ContentType. +type RequiredJSONBodyJSONRequestBody = Example + +// RequiredTextBodyTextRequestBody defines body for RequiredTextBody for text/plain ContentType. +type RequiredTextBodyTextRequestBody = RequiredTextBodyTextBody + // ReusableResponsesJSONRequestBody defines body for ReusableResponses for application/json ContentType. type ReusableResponsesJSONRequestBody = Example diff --git a/internal/test/strict-server/stdhttp/server.gen.go b/internal/test/strict-server/stdhttp/server.gen.go index 28c36a7ac7..cff46fe6d7 100644 --- a/internal/test/strict-server/stdhttp/server.gen.go +++ b/internal/test/strict-server/stdhttp/server.gen.go @@ -11,6 +11,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "mime" @@ -40,6 +41,12 @@ type ServerInterface interface { // (POST /multiple) MultipleRequestAndResponseTypes(w http.ResponseWriter, r *http.Request) + // (POST /required-json-body) + RequiredJSONBody(w http.ResponseWriter, r *http.Request) + + // (POST /required-text-body) + RequiredTextBody(w http.ResponseWriter, r *http.Request) + // (GET /reserved-go-keyword-parameters/{type}) ReservedGoKeywordParameters(w http.ResponseWriter, r *http.Request, pType string) @@ -130,6 +137,34 @@ func (siw *ServerInterfaceWrapper) MultipleRequestAndResponseTypes(w http.Respon handler.ServeHTTP(w, r) } +// RequiredJSONBody operation middleware +func (siw *ServerInterfaceWrapper) RequiredJSONBody(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.RequiredJSONBody(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// RequiredTextBody operation middleware +func (siw *ServerInterfaceWrapper) RequiredTextBody(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.RequiredTextBody(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ReservedGoKeywordParameters operation middleware func (siw *ServerInterfaceWrapper) ReservedGoKeywordParameters(w http.ResponseWriter, r *http.Request) { @@ -426,6 +461,8 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc("POST "+options.BaseURL+"/multipart", wrapper.MultipartExample) m.HandleFunc("POST "+options.BaseURL+"/multipart-related", wrapper.MultipartRelatedExample) m.HandleFunc("POST "+options.BaseURL+"/multiple", wrapper.MultipleRequestAndResponseTypes) + m.HandleFunc("POST "+options.BaseURL+"/required-json-body", wrapper.RequiredJSONBody) + m.HandleFunc("POST "+options.BaseURL+"/required-text-body", wrapper.RequiredTextBody) m.HandleFunc("GET "+options.BaseURL+"/reserved-go-keyword-parameters/{type}", wrapper.ReservedGoKeywordParameters) m.HandleFunc("POST "+options.BaseURL+"/reusable-responses", wrapper.ReusableResponses) m.HandleFunc("POST "+options.BaseURL+"/text", wrapper.TextExample) @@ -636,6 +673,73 @@ func (response MultipleRequestAndResponseTypes400Response) VisitMultipleRequestA return nil } +type RequiredJSONBodyRequestObject struct { + Body *RequiredJSONBodyJSONRequestBody +} + +type RequiredJSONBodyResponseObject interface { + VisitRequiredJSONBodyResponse(w http.ResponseWriter) error +} + +type RequiredJSONBody200JSONResponse Example + +func (response RequiredJSONBody200JSONResponse) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type RequiredJSONBody400Response = BadrequestResponse + +func (response RequiredJSONBody400Response) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type RequiredJSONBodydefaultResponse struct { + StatusCode int +} + +func (response RequiredJSONBodydefaultResponse) VisitRequiredJSONBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(response.StatusCode) + return nil +} + +type RequiredTextBodyRequestObject struct { + Body *RequiredTextBodyTextRequestBody +} + +type RequiredTextBodyResponseObject interface { + VisitRequiredTextBodyResponse(w http.ResponseWriter) error +} + +type RequiredTextBody200TextResponse string + +func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(200) + + _, err := w.Write([]byte(response)) + return err +} + +type RequiredTextBody400Response = BadrequestResponse + +func (response RequiredTextBody400Response) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type RequiredTextBodydefaultResponse struct { + StatusCode int +} + +func (response RequiredTextBodydefaultResponse) VisitRequiredTextBodyResponse(w http.ResponseWriter) error { + w.WriteHeader(response.StatusCode) + return nil +} + type ReservedGoKeywordParametersRequestObject struct { Type string `json:"type"` } @@ -983,6 +1087,12 @@ type StrictServerInterface interface { // (POST /multiple) MultipleRequestAndResponseTypes(ctx context.Context, request MultipleRequestAndResponseTypesRequestObject) (MultipleRequestAndResponseTypesResponseObject, error) + // (POST /required-json-body) + RequiredJSONBody(ctx context.Context, request RequiredJSONBodyRequestObject) (RequiredJSONBodyResponseObject, error) + + // (POST /required-text-body) + RequiredTextBody(ctx context.Context, request RequiredTextBodyRequestObject) (RequiredTextBodyResponseObject, error) + // (GET /reserved-go-keyword-parameters/{type}) ReservedGoKeywordParameters(ctx context.Context, request ReservedGoKeywordParametersRequestObject) (ReservedGoKeywordParametersResponseObject, error) @@ -1043,10 +1153,13 @@ func (sh *strictHandler) JSONExample(w http.ResponseWriter, r *http.Request) { var body JSONExampleJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.JSONExample(ctx, request.(JSONExampleRequestObject)) @@ -1141,10 +1254,13 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(w http.ResponseWriter, var body MultipleRequestAndResponseTypesJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.JSONBody = &body } - request.JSONBody = &body } if strings.HasPrefix(r.Header.Get("Content-Type"), "application/x-www-form-urlencoded") { if err := r.ParseForm(); err != nil { @@ -1175,8 +1291,10 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(w http.ResponseWriter, sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't read body: %w", err)) return } - body := MultipleRequestAndResponseTypesTextRequestBody(data) - request.TextBody = &body + if len(data) > 0 { + body := MultipleRequestAndResponseTypesTextRequestBody(data) + request.TextBody = &body + } } handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { @@ -1199,6 +1317,69 @@ func (sh *strictHandler) MultipleRequestAndResponseTypes(w http.ResponseWriter, } } +// RequiredJSONBody operation middleware +func (sh *strictHandler) RequiredJSONBody(w http.ResponseWriter, r *http.Request) { + var request RequiredJSONBodyRequestObject + + var body RequiredJSONBodyJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.RequiredJSONBody(ctx, request.(RequiredJSONBodyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RequiredJSONBody") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(RequiredJSONBodyResponseObject); ok { + if err := validResponse.VisitRequiredJSONBodyResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// RequiredTextBody operation middleware +func (sh *strictHandler) RequiredTextBody(w http.ResponseWriter, r *http.Request) { + var request RequiredTextBodyRequestObject + + data, err := io.ReadAll(r.Body) + if err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't read body: %w", err)) + return + } + body := RequiredTextBodyTextRequestBody(data) + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.RequiredTextBody(ctx, request.(RequiredTextBodyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RequiredTextBody") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(RequiredTextBodyResponseObject); ok { + if err := validResponse.VisitRequiredTextBodyResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // ReservedGoKeywordParameters operation middleware func (sh *strictHandler) ReservedGoKeywordParameters(w http.ResponseWriter, r *http.Request, pType string) { var request ReservedGoKeywordParametersRequestObject @@ -1231,10 +1412,13 @@ func (sh *strictHandler) ReusableResponses(w http.ResponseWriter, r *http.Reques var body ReusableResponsesJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.ReusableResponses(ctx, request.(ReusableResponsesRequestObject)) @@ -1265,8 +1449,10 @@ func (sh *strictHandler) TextExample(w http.ResponseWriter, r *http.Request) { sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't read body: %w", err)) return } - body := TextExampleTextRequestBody(data) - request.Body = &body + if len(data) > 0 { + body := TextExampleTextRequestBody(data) + request.Body = &body + } handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.TextExample(ctx, request.(TextExampleRequestObject)) @@ -1385,10 +1571,13 @@ func (sh *strictHandler) HeadersExample(w http.ResponseWriter, r *http.Request, var body HeadersExampleJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.HeadersExample(ctx, request.(HeadersExampleRequestObject)) @@ -1416,10 +1605,13 @@ func (sh *strictHandler) UnionExample(w http.ResponseWriter, r *http.Request) { var body UnionExampleJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.UnionExample(ctx, request.(UnionExampleRequestObject)) @@ -1444,24 +1636,25 @@ func (sh *strictHandler) UnionExample(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xYS3PbNhD+Kxi0p5QUZccn3hpPJm3T1h3ZPnV8gIilhIQE0MVStEaj/94BQb0sWpUS", - "PTqZ3PhY7C6+b3ex2BnPTGmNBk2OpzOO4KzRDpqXoZAI/1TgyL9JcBkqS8ponvJ3Qg7af/OII1RODAtY", - "LPfymdEEulkqrC1UJvzS5JPz62fcZWMohX/6ESHnKf8hWbmShL8ugWdR2gL4fD6PXnhw95FHfAxCAjbe", - "hserTd00tcBT7giVHnGvJIhdd4opTTAC9Na8aOuEF1j4kc64RWMBSQWMJqKooNtS+8UMP0FGYQdK52Yb", - "y1ujSSjtmFR5DgiaWAse8zocc5W1BgkkG06Zt5ARc4ATQB5xUuQd4/fr31nrsOMRnwC6YOiq1+/1PV/G", - "ghZW8ZS/bT5F3AoaNxtaEmRNF++/3d/9yZRjoiJTClKZKIopKwW6sShAMqXJeBerjFyPN5awIf5X2a5+", - "30Lpo6YJoHdGTk8RME1croXzdb9/pricR/wmGOvSsXQqWUuwRk0uqqID80f9WZtaM0A02O4sKauClBVI", - "61xtov3HQmQfyJf6ktxgGUtB4kSoH8vSpYGPEQpBIPcgYBAkD+NhTf1JWfgaOxfloK3HnXXqfmxqx8am", - "ZmSYBFGwWtGYLRa+KLBKM8Gc0qMC2MKpqJPMAtpj72ctB+1eHryOk9ezaEPLc1zXddwkUIUF6MzIL6Mw", - "4qoUI0isHm0u97oF8ZQPp+RDdvuAO1IiR5zgmRJbCKV3n95nKunfkT5aYod0RWi6EhmPTPwZprVBGVuB", - "ogQCdMnMW597xSPoSOW/lpIsE5oNgWlRgmQiJ0D2wbBWpdtK2UFr94P5GERWqpqWZ/mS/j3jHpKmDeIR", - "9wZ4GlAJea3Qk05YQbQDtqf/jM+vImCBZmi24w1T3WVwUaKW0CHkzpfELuY68AuWBmsSl2nadkfc1vXj", - "HGeQZ/L1o/8Bnvdqu45Y+s6d24cCVoWPr2PWrtoHti+spHugOFESTFLamwM1XwxUZyFTuQIZt7uIg2+v", - "lYRbozME2myB/JVOG2JLZf6mSWNgAYGIOcNqYGXliFnhHFPUVJFChduqhK3i8bjy7DZYeliV012svjkR", - "p28uxehN/+rwJW9PHDcbrcwr+Tj4/X2QOfTOfrSe6cCO73h2L5TO/pISrw21ulP4lyCwOtMzUBPfEWnJ", - "EKhCDZJNlFgMYrZys1WworWrFwpurLqhxYDtkIYo2qnrmke7hnBP3/CI6JSjy3PFaaXVrlHho//N2h76", - "5dmgjP6fDgJFQYBakJrAT8e5QW5rMRru8ibTXrAc7Wnh6duLqnnEw+w6lKAKC18niGyaJGHm3XO1GI0A", - "e8okwiqPwr8BAAD//4h9qqfAGAAA", + "H4sIAAAAAAAC/+xZTXPbNhP+Kzt431NKmrbjE2+NJ5O2aeuObJ86PkDESkJCAiiwFK3R6L93QEBfFq1K", + "qT4ynt5Ecr/wPLuLBTRlha6MVqjIsXzKLDqjlcP2oc+Fxb9qdOSfBLrCSkNSK5azD1z04rdZwizWjvdL", + "nKt7+UIrQtWqcmNKWXCvmn1xXn/KXDHCivtf/7c4YDn7X7YMJQtfXYbPvDIlstlslryI4O4zS9gIuUDb", + "Rht+Xq3bpolBljNHVqoh80aC2HWnmFSEQ7TemxeNQXiBeRz5lBmrDVqSAaMxL2vs9hTf6P4XLCisQKqB", + "3sTyViviUjkQcjBAi4ogggfehgNXG6MtoYD+BLyHgsChHaNlCSNJPjB2v/oeYsCOJWyM1gVHVxeXF5ee", + "L21QcSNZzt63rxJmOI3aBS0IMrqL91/u734H6YDXpCtOsuBlOYGKWzfiJQqQirQPsS7IXbDWk22J/1lE", + "7Y8RSp81bQJ90GJyjIRp83Ilna8vL0+Ul7OE3QRnXTYWQWUrBdaaGfC67MD8UX1VulGA1mobV5ZVdUnS", + "cEurXK2j/dtcZBfIF/aygbZVKjjxI6F+KE/nBj61WHJCsQMBvSC5Hw8r5o/Kwr/xc1YOYj/u7FP3I904", + "GOkGSINAXkIjaQRzxRcNVirg4KQalgjzoJJOMkuM296PSvTiWh68jaP3s2TNynPaNE3aFlBtS1SFFt9G", + "YcJkxYeYGTVcV/e2ObGc9SfkU3ZzgztQISeM8JkyU3Kptu/eJ2rp/yF9sMIO5eofpUWRekbSfqyP7sKN", + "5QVeyg8ac90EnIZKOl+l4aMb6boUwMuGT1zoD5sTRy+q+8mjLcyjjx3BgffJcrI1vu0xZEGtz6zzUPuA", + "z/SP1O6R+PvSd+qa2p+i9kwg0qFOv+Kk0VakhlteIaF12dTHOfO2hthh8o+FJBRcQR9B8QoF8AGhhU8a", + "oknXwU/w+0l/DiJLU+2BY/GQ/zllHrz2EMIS5h2wPOD3ko5kC8BPx6VqjmY46qZrrl5L+Cgyh87iwPmB", + "pIvjDvyCp96KxHmOTNtzc+Pwf4qk9ky+Pnj7lrDLsH3AweN77wJ1ePk6ZlFrF9i+cY7ZAcWxFKizytzs", + "aflsoDqDhRxIFGlcRRpie60l3GpVWKT1A4jfDJUmWBiD/gRohBAQaPfHBqGqHYHhzoGktouUMtwVCdxo", + "Ho/LyG6Dp4dlO93G6rsjcfruXIzeXF7tr/L+yHmzdpB4pR57v34MMvvemB3sxLLneetwfs9Uzo2kUbpy", + "pdxdwj8FgeWeXqAc+4lICbBItVUoYCz5/Bp0ozajgSWtXbNQCGM5Dc2vt/cZiJKttq5Zsu0K/OkNX9Ae", + "84+DU+VpreS2i/pH/xniDP1yb5BafafX8LwktIqTHOMPh7m/2bSiFd4N2kp7wXKyo4ent5dVs4SFf45C", + "C6pt6fsEkcmzLPzjdOEaPhyivZA640Z6FP4OAAD//0nTejA+HAAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/strict-server/stdhttp/server.go b/internal/test/strict-server/stdhttp/server.go index a142a6b813..4f39f939cc 100644 --- a/internal/test/strict-server/stdhttp/server.go +++ b/internal/test/strict-server/stdhttp/server.go @@ -128,6 +128,14 @@ func (s StrictServer) ReusableResponses(ctx context.Context, request ReusableRes return ReusableResponses200JSONResponse{ReusableresponseJSONResponse: ReusableresponseJSONResponse{Body: *request.Body}}, nil } +func (s StrictServer) RequiredJSONBody(ctx context.Context, request RequiredJSONBodyRequestObject) (RequiredJSONBodyResponseObject, error) { + return RequiredJSONBody200JSONResponse(*request.Body), nil +} + +func (s StrictServer) RequiredTextBody(ctx context.Context, request RequiredTextBodyRequestObject) (RequiredTextBodyResponseObject, error) { + return RequiredTextBody200TextResponse(*request.Body), nil +} + func (s StrictServer) ReservedGoKeywordParameters(ctx context.Context, request ReservedGoKeywordParametersRequestObject) (ReservedGoKeywordParametersResponseObject, error) { return ReservedGoKeywordParameters200TextResponse(""), nil } diff --git a/internal/test/strict-server/stdhttp/types.gen.go b/internal/test/strict-server/stdhttp/types.gen.go index 6682c1acab..fac014c4bb 100644 --- a/internal/test/strict-server/stdhttp/types.gen.go +++ b/internal/test/strict-server/stdhttp/types.gen.go @@ -14,6 +14,9 @@ type Reusableresponse = Example // MultipleRequestAndResponseTypesTextBody defines parameters for MultipleRequestAndResponseTypes. type MultipleRequestAndResponseTypesTextBody = string +// RequiredTextBodyTextBody defines parameters for RequiredTextBody. +type RequiredTextBodyTextBody = string + // TextExampleTextBody defines parameters for TextExample. type TextExampleTextBody = string @@ -44,6 +47,12 @@ type MultipleRequestAndResponseTypesMultipartRequestBody = Example // MultipleRequestAndResponseTypesTextRequestBody defines body for MultipleRequestAndResponseTypes for text/plain ContentType. type MultipleRequestAndResponseTypesTextRequestBody = MultipleRequestAndResponseTypesTextBody +// RequiredJSONBodyJSONRequestBody defines body for RequiredJSONBody for application/json ContentType. +type RequiredJSONBodyJSONRequestBody = Example + +// RequiredTextBodyTextRequestBody defines body for RequiredTextBody for text/plain ContentType. +type RequiredTextBodyTextRequestBody = RequiredTextBodyTextBody + // ReusableResponsesJSONRequestBody defines body for ReusableResponses for application/json ContentType. type ReusableResponsesJSONRequestBody = Example diff --git a/internal/test/strict-server/strict-schema.yaml b/internal/test/strict-server/strict-schema.yaml index a0b6a6e054..8b18edd541 100644 --- a/internal/test/strict-server/strict-schema.yaml +++ b/internal/test/strict-server/strict-schema.yaml @@ -247,6 +247,48 @@ paths: $ref: "#/components/responses/badrequest" default: description: Unknown error + /required-json-body: + post: + operationId: RequiredJSONBody + description: Request body is required, so missing body should always error. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/example" + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/example" + 400: + $ref: "#/components/responses/badrequest" + default: + description: Unknown error + /required-text-body: + post: + operationId: RequiredTextBody + description: Request body is required, so missing body should always error. + requestBody: + required: true + content: + text/plain: + schema: + type: string + responses: + 200: + description: OK + content: + text/plain: + schema: + type: string + 400: + $ref: "#/components/responses/badrequest" + default: + description: Unknown error /reserved-go-keyword-parameters/{type}: get: operationId: ReservedGoKeywordParameters diff --git a/pkg/codegen/templates/strict/strict-echo.tmpl b/pkg/codegen/templates/strict/strict-echo.tmpl index 676690fab3..7b470c8ce7 100644 --- a/pkg/codegen/templates/strict/strict-echo.tmpl +++ b/pkg/codegen/templates/strict/strict-echo.tmpl @@ -34,9 +34,16 @@ type strictHandler struct { {{if .IsJSON -}} var body {{$opid}}{{.NameTag}}RequestBody if err := ctx.Bind(&body); 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.FormParams(); err == nil { var body {{$opid}}{{.NameTag}}RequestBody @@ -68,8 +75,14 @@ type strictHandler struct { if err != nil { return err } + {{if not .Required -}} + if len(data) > 0 { + {{end -}} body := {{$opid}}{{.NameTag}}RequestBody(data) request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body + {{if not .Required -}} + } + {{end -}} {{else -}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = ctx.Request().Body {{end}}{{/* if eq .NameTag "JSON" */ -}} diff --git a/pkg/codegen/templates/strict/strict-fiber.tmpl b/pkg/codegen/templates/strict/strict-fiber.tmpl index 510899c9e0..59a2c0736b 100644 --- a/pkg/codegen/templates/strict/strict-fiber.tmpl +++ b/pkg/codegen/templates/strict/strict-fiber.tmpl @@ -36,9 +36,16 @@ type strictHandler struct { {{if .IsJSON }} var body {{$opid}}{{.NameTag}}RequestBody if err := ctx.BodyParser(&body); err != nil { + {{if not .Required -}} + if !errors.Is(err, io.EOF) { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + {{else -}} return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } + {{end -}} + } {{if not .Required -}} else { {{end}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body + {{if not .Required -}} } {{end}} {{else if eq .NameTag "Formdata" -}} var body {{$opid}}{{.NameTag}}RequestBody if err := ctx.BodyParser(&body); err != nil { @@ -59,8 +66,14 @@ type strictHandler struct { {{end -}} {{else if eq .NameTag "Text" -}} data := ctx.Request().Body() + {{if not .Required -}} + if len(data) > 0 { + {{end -}} body := {{$opid}}{{.NameTag}}RequestBody(data) request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body + {{if not .Required -}} + } + {{end -}} {{else -}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = bytes.NewReader(ctx.Request().Body()) {{end}}{{/* if eq .NameTag "JSON" */ -}} diff --git a/pkg/codegen/templates/strict/strict-gin.tmpl b/pkg/codegen/templates/strict/strict-gin.tmpl index d4c43164a6..40a7b7bcb1 100644 --- a/pkg/codegen/templates/strict/strict-gin.tmpl +++ b/pkg/codegen/templates/strict/strict-gin.tmpl @@ -34,11 +34,20 @@ type strictHandler struct { {{if .IsJSON }} var body {{$opid}}{{.NameTag}}RequestBody if err := ctx.ShouldBindJSON(&body); err != nil { + {{if not .Required -}} + if !errors.Is(err, io.EOF) { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + {{else -}} ctx.Status(http.StatusBadRequest) ctx.Error(err) return - } + {{end -}} + } {{if not .Required -}} else { {{end}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body + {{if not .Required -}} } {{end}} {{else if eq .NameTag "Formdata" -}} if err := ctx.Request.ParseForm(); err != nil { ctx.Error(err) @@ -75,8 +84,14 @@ type strictHandler struct { ctx.Error(err) return } + {{if not .Required -}} + if len(data) > 0 { + {{end -}} body := {{$opid}}{{.NameTag}}RequestBody(data) request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body + {{if not .Required -}} + } + {{end -}} {{else -}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = ctx.Request.Body {{end}}{{/* if eq .NameTag "JSON" */ -}} diff --git a/pkg/codegen/templates/strict/strict-http.tmpl b/pkg/codegen/templates/strict/strict-http.tmpl index 8d32415ad6..7314c26fd7 100644 --- a/pkg/codegen/templates/strict/strict-http.tmpl +++ b/pkg/codegen/templates/strict/strict-http.tmpl @@ -51,10 +51,18 @@ type strictHandler struct { {{if .IsJSON }} var body {{$opid}}{{.NameTag}}RequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + {{if not .Required -}} + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + {{else -}} sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) return - } + {{end -}} + } {{if not .Required -}} else { {{end}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body + {{if not .Required -}} } {{end}} {{else if eq .NameTag "Formdata" -}} if err := r.ParseForm(); err != nil { sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode formdata: %w", err)) @@ -91,8 +99,14 @@ type strictHandler struct { sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't read body: %w", err)) return } + {{if not .Required -}} + if len(data) > 0 { + {{end -}} body := {{$opid}}{{.NameTag}}RequestBody(data) request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body + {{if not .Required -}} + } + {{end -}} {{else -}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = r.Body {{end}}{{/* if eq .NameTag "JSON" */ -}} diff --git a/pkg/codegen/templates/strict/strict-iris.tmpl b/pkg/codegen/templates/strict/strict-iris.tmpl index dfdeb1546f..6ed301574c 100644 --- a/pkg/codegen/templates/strict/strict-iris.tmpl +++ b/pkg/codegen/templates/strict/strict-iris.tmpl @@ -34,10 +34,18 @@ type strictHandler struct { {{if .IsJSON }} var body {{$opid}}{{.NameTag}}RequestBody if err := ctx.ReadJSON(&body); err != nil { + {{if not .Required -}} + if !errors.Is(err, io.EOF) { + ctx.StopWithError(http.StatusBadRequest, err) + return + } + {{else -}} ctx.StopWithError(http.StatusBadRequest, err) return - } + {{end -}} + } {{if not .Required -}} else { {{end}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body + {{if not .Required -}} } {{end}} {{else if eq .NameTag "Formdata" -}} if err := ctx.Request().ParseForm(); err != nil { ctx.StopWithError(http.StatusBadRequest, err) @@ -74,8 +82,14 @@ type strictHandler struct { ctx.StopWithError(http.StatusBadRequest, err) return } + {{if not .Required -}} + if len(data) > 0 { + {{end -}} body := {{$opid}}{{.NameTag}}RequestBody(data) request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body + {{if not .Required -}} + } + {{end -}} {{else -}} request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = ctx.Request().Body {{end}}{{/* if eq .NameTag "JSON" */ -}} From c6bc7c587515948af319ed804c216aa9fc940f5c Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Mon, 16 Feb 2026 13:10:12 -0800 Subject: [PATCH 28/44] feat: add configurable type mapping for OpenAPI primitive types (#2223) * feat: add configurable type mapping for OpenAPI primitive types Supersedes #428, since the original author's branch is gone and his PR only exists in the ref log. Added a TypeMapping configuration that lets users override the default OpenAPI type/format to Go type mappings. User-specified mappings are merged on top of built-in defaults, so only overrides need to be specified. For example, mapping string/date-time to a civil.DateTime or changing the default integer type to int64. Changes: - New TypeMapping, FormatMapping, SimpleTypeSpec types with Merge() and Resolve() methods (pkg/codegen/typemapping.go) - TypeMapping field added to Configuration struct - Merged type mapping stored on globalState, initialized in Generate() - Replaced hardcoded switch statements in oapiSchemaToGoType() with map-based lookups via globalState.typeMapping - Updated configuration-schema.json with type-mapping property and reusable $defs for format-mapping and simple-type-spec - Unit tests for merge, resolve, and default mapping completeness Co-Authored-By: natsukagami Co-Authored-By: Claude Opus 4.6 * Move TypeMapping to OutputOptions * Add example for type-mapping * Address PR feedback Move type-mapping examples to output-options --------- Co-authored-by: natsukagami Co-authored-by: Claude Opus 4.6 --- configuration-schema.json | 59 +++++++++- .../output-options/type-mapping/config.yaml | 14 +++ .../output-options/type-mapping/customdate.go | 3 + .../output-options/type-mapping/generate.go | 6 + .../output-options/type-mapping/spec.yaml | 18 +++ .../type-mapping/typemapping.gen.go | 10 ++ pkg/codegen/codegen.go | 7 ++ pkg/codegen/configuration.go | 4 + pkg/codegen/schema.go | 60 ++-------- pkg/codegen/typemapping.go | 109 ++++++++++++++++++ pkg/codegen/typemapping_test.go | 88 ++++++++++++++ 11 files changed, 329 insertions(+), 49 deletions(-) create mode 100644 examples/output-options/type-mapping/config.yaml create mode 100644 examples/output-options/type-mapping/customdate.go create mode 100644 examples/output-options/type-mapping/generate.go create mode 100644 examples/output-options/type-mapping/spec.yaml create mode 100644 examples/output-options/type-mapping/typemapping.gen.go create mode 100644 pkg/codegen/typemapping.go create mode 100644 pkg/codegen/typemapping_test.go diff --git a/configuration-schema.json b/configuration-schema.json index 8ff58d94b6..43694f9658 100644 --- a/configuration-schema.json +++ b/configuration-schema.json @@ -257,6 +257,25 @@ "type": "boolean", "description": "When set to true, automatically renames types that collide across different OpenAPI component sections (schemas, parameters, requestBodies, responses, headers) by appending a suffix based on the component section (e.g., 'Parameter', 'Response', 'RequestBody'). Without this, the codegen will error on duplicate type names, requiring manual resolution via x-go-name.", "default": false + }, + "type-mapping": { + "type": "object", + "additionalProperties": false, + "description": "TypeMapping allows customizing OpenAPI type/format to Go type mappings. User-specified mappings are merged on top of the defaults, so you only need to specify the types you want to override.", + "properties": { + "integer": { + "$ref": "#/$defs/format-mapping" + }, + "number": { + "$ref": "#/$defs/format-mapping" + }, + "boolean": { + "$ref": "#/$defs/format-mapping" + }, + "string": { + "$ref": "#/$defs/format-mapping" + } + } } } }, @@ -294,5 +313,43 @@ "required": [ "package", "output" - ] + ], + "$defs": { + "simple-type-spec": { + "type": "object", + "additionalProperties": false, + "description": "Specifies a Go type and optional import path", + "properties": { + "type": { + "type": "string", + "description": "The Go type to use (e.g. \"int64\", \"time.Time\", \"github.com/shopspring/decimal.Decimal\")" + }, + "import": { + "type": "string", + "description": "The Go import path required for this type (e.g. \"time\", \"encoding/json\")" + } + }, + "required": [ + "type" + ] + }, + "format-mapping": { + "type": "object", + "additionalProperties": false, + "description": "Maps an OpenAPI type's formats to Go types", + "properties": { + "default": { + "$ref": "#/$defs/simple-type-spec", + "description": "The default Go type when no format is specified or the format is unrecognized" + }, + "formats": { + "type": "object", + "description": "Format-specific Go type overrides (e.g. \"int32\": {\"type\": \"int32\"}, \"double\": {\"type\": \"float64\"})", + "additionalProperties": { + "$ref": "#/$defs/simple-type-spec" + } + } + } + } + } } diff --git a/examples/output-options/type-mapping/config.yaml b/examples/output-options/type-mapping/config.yaml new file mode 100644 index 0000000000..7a1af00d54 --- /dev/null +++ b/examples/output-options/type-mapping/config.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=../../configuration-schema.json +package: typemapping +generate: + models: true +output-options: + skip-prune: true + type-mapping: + number: + default: + type: int64 + formats: + date: + type: CustomDateHandler +output: typemapping.gen.go diff --git a/examples/output-options/type-mapping/customdate.go b/examples/output-options/type-mapping/customdate.go new file mode 100644 index 0000000000..fda97a2d3e --- /dev/null +++ b/examples/output-options/type-mapping/customdate.go @@ -0,0 +1,3 @@ +package typemapping + +type CustomDateHandler struct{} diff --git a/examples/output-options/type-mapping/generate.go b/examples/output-options/type-mapping/generate.go new file mode 100644 index 0000000000..8344a10709 --- /dev/null +++ b/examples/output-options/type-mapping/generate.go @@ -0,0 +1,6 @@ +package typemapping + +// The configuration in this directory overrides the default handling of +// "type: number" from producing an `int` to producing an `int64`, and we +// override `type: string, format: date` to be a custom type in this package. +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/examples/output-options/type-mapping/spec.yaml b/examples/output-options/type-mapping/spec.yaml new file mode 100644 index 0000000000..2c8f63487a --- /dev/null +++ b/examples/output-options/type-mapping/spec.yaml @@ -0,0 +1,18 @@ +openapi: "3.0.1" +info: + version: 1.0.0 + title: Type mapping test +paths: {} +components: + schemas: + EmployeeDatabaseRecord: + type: object + required: + - ID + - DateHired + properties: + ID: + type: number + DateHired: + type: number + format: date diff --git a/examples/output-options/type-mapping/typemapping.gen.go b/examples/output-options/type-mapping/typemapping.gen.go new file mode 100644 index 0000000000..744cab7f9c --- /dev/null +++ b/examples/output-options/type-mapping/typemapping.gen.go @@ -0,0 +1,10 @@ +// Package typemapping provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package typemapping + +// EmployeeDatabaseRecord defines model for EmployeeDatabaseRecord. +type EmployeeDatabaseRecord struct { + DateHired CustomDateHandler `json:"DateHired"` + ID int64 `json:"ID"` +} diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go index 355de4f7a7..0f38b6ff7f 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -52,6 +52,8 @@ var globalState struct { // initialismsMap stores initialisms as "lower(initialism) -> initialism" map. // List of initialisms was taken from https://staticcheck.io/docs/configuration/options/#initialisms. initialismsMap map[string]string + // typeMapping is the merged type mapping (defaults + user overrides). + typeMapping TypeMapping } // goImport represents a go package to be imported in the generated code @@ -140,6 +142,11 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { globalState.options = opts globalState.spec = spec globalState.importMapping = constructImportMapping(opts.ImportMapping) + if opts.OutputOptions.TypeMapping != nil { + globalState.typeMapping = DefaultTypeMapping.Merge(*opts.OutputOptions.TypeMapping) + } else { + globalState.typeMapping = DefaultTypeMapping + } filterOperationsByTag(spec, opts) filterOperationsByOperationID(spec, opts) diff --git a/pkg/codegen/configuration.go b/pkg/codegen/configuration.go index d3281fefec..4598bb04ad 100644 --- a/pkg/codegen/configuration.go +++ b/pkg/codegen/configuration.go @@ -308,6 +308,10 @@ type OutputOptions struct { // "RequestBody"). Without this, the codegen will error on duplicate type // names, requiring manual resolution via x-go-name. ResolveTypeNameCollisions bool `yaml:"resolve-type-name-collisions,omitempty"` + + // TypeMapping allows customizing OpenAPI type/format to Go type mappings. + // User-specified mappings are merged on top of the defaults. + TypeMapping *TypeMapping `yaml:"type-mapping,omitempty"` } func (oo OutputOptions) Validate() map[string]string { diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go index f361ab3465..f7b6fd6ac1 100644 --- a/pkg/codegen/schema.go +++ b/pkg/codegen/schema.go @@ -629,62 +629,26 @@ func oapiSchemaToGoType(schema *openapi3.Schema, path []string, outSchema *Schem setSkipOptionalPointerForContainerType(outSchema) } else if t.Is("integer") { - // We default to int if format doesn't ask for something else. - switch f { - case "int64", - "int32", - "int16", - "int8", - "int", - "uint64", - "uint32", - "uint16", - "uint8", - "uint": - outSchema.GoType = f - default: - outSchema.GoType = "int" - } + spec := globalState.typeMapping.Integer.Resolve(f) + outSchema.GoType = spec.Type outSchema.DefineViaAlias = true } else if t.Is("number") { - // We default to float for "number" - switch f { - case "double": - outSchema.GoType = "float64" - case "float", "": - outSchema.GoType = "float32" - default: - return fmt.Errorf("invalid number format: %s", f) - } + spec := globalState.typeMapping.Number.Resolve(f) + outSchema.GoType = spec.Type outSchema.DefineViaAlias = true } else if t.Is("boolean") { - if f != "" { - return fmt.Errorf("invalid format (%s) for boolean", f) - } - outSchema.GoType = "bool" + spec := globalState.typeMapping.Boolean.Resolve(f) + outSchema.GoType = spec.Type outSchema.DefineViaAlias = true } else if t.Is("string") { - // Special case string formats here. - switch f { - case "byte": - outSchema.GoType = "[]byte" + spec := globalState.typeMapping.String.Resolve(f) + outSchema.GoType = spec.Type + // Preserve special behaviors for specific types + if outSchema.GoType == "[]byte" { setSkipOptionalPointerForContainerType(outSchema) - case "email": - outSchema.GoType = "openapi_types.Email" - case "date": - outSchema.GoType = "openapi_types.Date" - case "date-time": - outSchema.GoType = "time.Time" - case "json": - outSchema.GoType = "json.RawMessage" + } + if outSchema.GoType == "json.RawMessage" { outSchema.SkipOptionalPointer = true - case "uuid": - outSchema.GoType = "openapi_types.UUID" - case "binary": - outSchema.GoType = "openapi_types.File" - default: - // All unrecognized formats are simply a regular string. - outSchema.GoType = "string" } outSchema.DefineViaAlias = true } else { diff --git a/pkg/codegen/typemapping.go b/pkg/codegen/typemapping.go new file mode 100644 index 0000000000..f5e20b54ac --- /dev/null +++ b/pkg/codegen/typemapping.go @@ -0,0 +1,109 @@ +package codegen + +// SimpleTypeSpec defines the Go type for an OpenAPI type/format combination, +// along with any import required to use it. +type SimpleTypeSpec struct { + Type string `yaml:"type" json:"type"` + Import string `yaml:"import,omitempty" json:"import,omitempty"` +} + +// FormatMapping defines the default Go type and format-specific overrides +// for an OpenAPI type. +type FormatMapping struct { + Default SimpleTypeSpec `yaml:"default" json:"default"` + Formats map[string]SimpleTypeSpec `yaml:"formats,omitempty" json:"formats,omitempty"` +} + +// TypeMapping defines the mapping from OpenAPI types to Go types. +type TypeMapping struct { + Integer FormatMapping `yaml:"integer,omitempty" json:"integer,omitempty"` + Number FormatMapping `yaml:"number,omitempty" json:"number,omitempty"` + Boolean FormatMapping `yaml:"boolean,omitempty" json:"boolean,omitempty"` + String FormatMapping `yaml:"string,omitempty" json:"string,omitempty"` +} + +// Merge returns a new TypeMapping with user overrides applied on top of base. +func (base TypeMapping) Merge(user TypeMapping) TypeMapping { + return TypeMapping{ + Integer: base.Integer.merge(user.Integer), + Number: base.Number.merge(user.Number), + Boolean: base.Boolean.merge(user.Boolean), + String: base.String.merge(user.String), + } +} + +func (base FormatMapping) merge(user FormatMapping) FormatMapping { + result := FormatMapping{ + Default: base.Default, + Formats: make(map[string]SimpleTypeSpec), + } + + // Copy base formats + for k, v := range base.Formats { + result.Formats[k] = v + } + + // Override with user default if specified + if user.Default.Type != "" { + result.Default = user.Default + } + + // Override/add user formats + for k, v := range user.Formats { + result.Formats[k] = v + } + + return result +} + +// Resolve returns the SimpleTypeSpec for a given format string. +// If the format has a specific mapping, that is returned; otherwise the default is used. +func (fm FormatMapping) Resolve(format string) SimpleTypeSpec { + if format != "" { + if spec, ok := fm.Formats[format]; ok { + return spec + } + } + return fm.Default +} + +// DefaultTypeMapping provides the default OpenAPI type/format to Go type mappings. +var DefaultTypeMapping = TypeMapping{ + Integer: FormatMapping{ + Default: SimpleTypeSpec{Type: "int"}, + Formats: map[string]SimpleTypeSpec{ + "int": {Type: "int"}, + "int8": {Type: "int8"}, + "int16": {Type: "int16"}, + "int32": {Type: "int32"}, + "int64": {Type: "int64"}, + "uint": {Type: "uint"}, + "uint8": {Type: "uint8"}, + "uint16": {Type: "uint16"}, + "uint32": {Type: "uint32"}, + "uint64": {Type: "uint64"}, + }, + }, + Number: FormatMapping{ + Default: SimpleTypeSpec{Type: "float32"}, + Formats: map[string]SimpleTypeSpec{ + "float": {Type: "float32"}, + "double": {Type: "float64"}, + }, + }, + Boolean: FormatMapping{ + Default: SimpleTypeSpec{Type: "bool"}, + }, + String: FormatMapping{ + Default: SimpleTypeSpec{Type: "string"}, + Formats: map[string]SimpleTypeSpec{ + "byte": {Type: "[]byte"}, + "email": {Type: "openapi_types.Email"}, + "date": {Type: "openapi_types.Date"}, + "date-time": {Type: "time.Time", Import: "time"}, + "json": {Type: "json.RawMessage", Import: "encoding/json"}, + "uuid": {Type: "openapi_types.UUID"}, + "binary": {Type: "openapi_types.File"}, + }, + }, +} diff --git a/pkg/codegen/typemapping_test.go b/pkg/codegen/typemapping_test.go new file mode 100644 index 0000000000..ca593cfd5d --- /dev/null +++ b/pkg/codegen/typemapping_test.go @@ -0,0 +1,88 @@ +package codegen + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFormatMapping_Resolve(t *testing.T) { + fm := FormatMapping{ + Default: SimpleTypeSpec{Type: "int"}, + Formats: map[string]SimpleTypeSpec{ + "int32": {Type: "int32"}, + "int64": {Type: "int64"}, + }, + } + + assert.Equal(t, "int", fm.Resolve("").Type) + assert.Equal(t, "int32", fm.Resolve("int32").Type) + assert.Equal(t, "int64", fm.Resolve("int64").Type) + assert.Equal(t, "int", fm.Resolve("unknown-format").Type) +} + +func TestTypeMapping_Merge(t *testing.T) { + base := DefaultTypeMapping + + user := TypeMapping{ + Integer: FormatMapping{ + Default: SimpleTypeSpec{Type: "int64"}, + }, + String: FormatMapping{ + Formats: map[string]SimpleTypeSpec{ + "date-time": {Type: "civil.DateTime", Import: "cloud.google.com/go/civil"}, + }, + }, + } + + merged := base.Merge(user) + + // Integer default overridden + assert.Equal(t, "int64", merged.Integer.Default.Type) + // Integer formats still inherited from base + assert.Equal(t, "int32", merged.Integer.Formats["int32"].Type) + + // String date-time overridden + assert.Equal(t, "civil.DateTime", merged.String.Formats["date-time"].Type) + assert.Equal(t, "cloud.google.com/go/civil", merged.String.Formats["date-time"].Import) + // String default still inherited from base + assert.Equal(t, "string", merged.String.Default.Type) + // Other string formats still inherited + assert.Equal(t, "openapi_types.UUID", merged.String.Formats["uuid"].Type) + + // Number and Boolean unchanged + assert.Equal(t, "float32", merged.Number.Default.Type) + assert.Equal(t, "bool", merged.Boolean.Default.Type) +} + +func TestDefaultTypeMapping_Completeness(t *testing.T) { + // Verify all the default mappings match what was previously hardcoded + dm := DefaultTypeMapping + + // Integer + assert.Equal(t, "int", dm.Integer.Resolve("").Type) + assert.Equal(t, "int32", dm.Integer.Resolve("int32").Type) + assert.Equal(t, "int64", dm.Integer.Resolve("int64").Type) + assert.Equal(t, "uint32", dm.Integer.Resolve("uint32").Type) + assert.Equal(t, "int", dm.Integer.Resolve("unknown").Type) + + // Number + assert.Equal(t, "float32", dm.Number.Resolve("").Type) + assert.Equal(t, "float32", dm.Number.Resolve("float").Type) + assert.Equal(t, "float64", dm.Number.Resolve("double").Type) + assert.Equal(t, "float32", dm.Number.Resolve("unknown").Type) + + // Boolean + assert.Equal(t, "bool", dm.Boolean.Resolve("").Type) + + // String + assert.Equal(t, "string", dm.String.Resolve("").Type) + assert.Equal(t, "[]byte", dm.String.Resolve("byte").Type) + assert.Equal(t, "openapi_types.Email", dm.String.Resolve("email").Type) + assert.Equal(t, "openapi_types.Date", dm.String.Resolve("date").Type) + assert.Equal(t, "time.Time", dm.String.Resolve("date-time").Type) + assert.Equal(t, "json.RawMessage", dm.String.Resolve("json").Type) + assert.Equal(t, "openapi_types.UUID", dm.String.Resolve("uuid").Type) + assert.Equal(t, "openapi_types.File", dm.String.Resolve("binary").Type) + assert.Equal(t, "string", dm.String.Resolve("unknown").Type) +} From 77bda899b50b9d628b84e4af1356455146c54850 Mon Sep 17 00:00:00 2001 From: Markus Meyer Date: Mon, 16 Feb 2026 22:15:04 +0100 Subject: [PATCH 29/44] fix(overlay): set indentation to 2 when marshalling spec for overlay (#2172) As noted in #2171, this is a workaround for yaml/go-yaml#76. --- pkg/util/loader.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/util/loader.go b/pkg/util/loader.go index 89830a8c44..7eea07b156 100644 --- a/pkg/util/loader.go +++ b/pkg/util/loader.go @@ -48,13 +48,17 @@ func LoadSwaggerWithOverlay(filePath string, opts LoadSwaggerWithOverlayOpts) (s } // parse out the yaml.Node, which is required by the overlay library - data, err := yaml.Marshal(spec) + buf := &bytes.Buffer{} + enc := yaml.NewEncoder(buf) + // set to 2 to work around https://github.com/yaml/go-yaml/issues/76 + enc.SetIndent(2) + err = enc.Encode(spec) if err != nil { return nil, fmt.Errorf("failed to marshal spec from %#v as YAML: %w", filePath, err) } var node yaml.Node - err = yaml.NewDecoder(bytes.NewReader(data)).Decode(&node) + err = yaml.NewDecoder(buf).Decode(&node) if err != nil { return nil, fmt.Errorf("failed to parse spec from %#v: %w", filePath, err) } From 59fb0e86773ac572016038c9cd21569de8d6a1b2 Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Mon, 16 Feb 2026 21:34:02 +0000 Subject: [PATCH 30/44] fix(server-urls): use URL in GoDoc if `description` is empty Otherwise the sentence ends abruptly. --- examples/generate/serverurls/gen.go | 9 +-------- pkg/codegen/templates/server-urls.tmpl | 3 +-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/examples/generate/serverurls/gen.go b/examples/generate/serverurls/gen.go index 5f9e069d3e..b32073b2cd 100644 --- a/examples/generate/serverurls/gen.go +++ b/examples/generate/serverurls/gen.go @@ -9,25 +9,18 @@ import ( ) // ServerUrlDevelopmentServer defines the Server URL for Development server -const ServerUrlDevelopmentServer = "https://development.gigantic-server.com/v1" // ServerUrlDevelopmentServer1 defines the Server URL for Development server -const ServerUrlDevelopmentServer1 = "http://localhost:80" // ServerUrlDevelopmentServer2 defines the Server URL for Development server -const ServerUrlDevelopmentServer2 = "http://localhost:80" -// ServerUrlHttplocalhost443 defines the Server URL for -const ServerUrlHttplocalhost443 = "http://localhost:443" +// ServerUrlHttplocalhost443 defines the Server URL for http://localhost:443 // ServerUrlProductionServer defines the Server URL for Production server -const ServerUrlProductionServer = "https://api.gigantic-server.com/v1" // ServerUrlSomeLowercaseName defines the Server URL for some lowercase name -const ServerUrlSomeLowercaseName = "http://localhost:80" // ServerUrlStagingServer defines the Server URL for Staging server -const ServerUrlStagingServer = "https://staging.gigantic-server.com/v1" // ServerUrlTheProductionAPIServerBasePathVariable is the `basePath` variable for ServerUrlTheProductionAPIServer type ServerUrlTheProductionAPIServerBasePathVariable string diff --git a/pkg/codegen/templates/server-urls.tmpl b/pkg/codegen/templates/server-urls.tmpl index f3599e5fa6..1c60e796b1 100644 --- a/pkg/codegen/templates/server-urls.tmpl +++ b/pkg/codegen/templates/server-urls.tmpl @@ -1,8 +1,7 @@ {{ range . }} {{ if eq 0 (len .OAPISchema.Variables) }} {{/* URLs without variables are straightforward, so we'll create them a constant */}} -// {{ .GoName }} defines the Server URL for {{ .OAPISchema.Description }} -const {{ .GoName}} = "{{ .OAPISchema.URL }}" +// {{ .GoName }} defines the Server URL for {{ if len .OAPISchema.Description }}{{ .OAPISchema.Description }}{{ else }}{{ .OAPISchema.URL }}{{ end }} {{ else }} {{/* URLs with variables are not straightforward, as we may need multiple types, and so will model them as a function */}} From f4ac5dface79675318a6dc598968cd6ae85862ff Mon Sep 17 00:00:00 2001 From: Alexander Tumin Date: Tue, 17 Feb 2026 06:24:08 +0300 Subject: [PATCH 31/44] Support nullable slice elements and map values (#2185) --- pkg/codegen/codegen_test.go | 9 ++++++++- pkg/codegen/schema.go | 10 +++++++++- pkg/codegen/test_spec.yaml | 30 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/pkg/codegen/codegen_test.go b/pkg/codegen/codegen_test.go index ff3c86d9ba..4920c72e49 100644 --- a/pkg/codegen/codegen_test.go +++ b/pkg/codegen/codegen_test.go @@ -77,7 +77,14 @@ type GetTestByNameResponse struct { assert.Contains(t, code, "Top *int `form:\"$top,omitempty\" json:\"$top,omitempty\"`") assert.Contains(t, code, "func (c *Client) GetTestByName(ctx context.Context, name string, params *GetTestByNameParams, reqEditors ...RequestEditorFn) (*http.Response, error) {") assert.Contains(t, code, "func (c *ClientWithResponses) GetTestByNameWithResponse(ctx context.Context, name string, params *GetTestByNameParams, reqEditors ...RequestEditorFn) (*GetTestByNameResponse, error) {") - assert.Contains(t, code, "DeadSince *time.Time `json:\"dead_since,omitempty\" tag1:\"value1\" tag2:\"value2\"`") + assert.Contains(t, code, "FavouriteBirds *[]*string `json:\"favourite_birds,omitempty\"`") + assert.Contains(t, code, "DetestedBirds *[]string `json:\"detested_birds,omitempty\"`") + assert.Contains(t, code, "SlicedBirds []string `json:\"sliced_birds\"`") + assert.Contains(t, code, "ForgettableBirds *map[string]*string `json:\"forgettable_birds,omitempty\"`") + assert.Contains(t, code, "MemorableBirds *map[string]string `json:\"memorable_birds,omitempty\"`") + assert.Contains(t, code, "VeryMemorableBirds map[string]string `json:\"very_memorable_birds\"`") + assert.Contains(t, code, "DeadSince *time.Time `json:\"dead_since,omitempty\" tag1:\"value1\" tag2:\"value2\"`") + assert.Contains(t, code, "VeryDeadSince time.Time `json:\"very_dead_since\"`") assert.Contains(t, code, "type EnumTestNumerics int") assert.Contains(t, code, "N2 EnumTestNumerics = 2") assert.Contains(t, code, "type EnumTestEnumNames int") diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go index f7b6fd6ac1..25f34de38a 100644 --- a/pkg/codegen/schema.go +++ b/pkg/codegen/schema.go @@ -602,6 +602,9 @@ func oapiSchemaToGoType(schema *openapi3.Schema, path []string, outSchema *Schem if err != nil { return fmt.Errorf("error generating type for array: %w", err) } + + var itemPrefix string + if (arrayType.HasAdditionalProperties || len(arrayType.UnionElements) != 0) && arrayType.RefType == "" { // If we have items which have additional properties or union values, // but are not a pre-defined type, we need to define a type @@ -618,8 +621,13 @@ func oapiSchemaToGoType(schema *openapi3.Schema, path []string, outSchema *Schem arrayType.RefType = typeName } + + if arrayType.OAPISchema != nil && arrayType.OAPISchema.Nullable { + itemPrefix = "*" + } + outSchema.ArrayType = &arrayType - outSchema.GoType = "[]" + arrayType.TypeDecl() + outSchema.GoType = "[]" + itemPrefix + arrayType.TypeDecl() outSchema.AdditionalTypes = arrayType.AdditionalTypes outSchema.Properties = arrayType.Properties outSchema.DefineViaAlias = true diff --git a/pkg/codegen/test_spec.yaml b/pkg/codegen/test_spec.yaml index b50f7491c5..f20cbecdca 100644 --- a/pkg/codegen/test_spec.yaml +++ b/pkg/codegen/test_spec.yaml @@ -164,15 +164,45 @@ components: format: date-time CatDead: + required: + - sliced_birds + - very_dead_since + - very_memorable_birds properties: name: type: string + favourite_birds: + type: array + items: + type: string + nullable: true + detested_birds: + type: array + items: + type: string + sliced_birds: + type: array + items: + type: string + forgettable_birds: + additionalProperties: + type: string + nullable: true + memorable_birds: + additionalProperties: + type: string + very_memorable_birds: + additionalProperties: + type: string dead_since: type: string format: date-time x-oapi-codegen-extra-tags: tag1: value1 tag2: value2 + very_dead_since: + type: string + format: date-time cause: type: string enum: [ car, dog, oldage ] From 4b72bdb6df43b9ddf58625916724a5cf23b80f34 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Tue, 17 Feb 2026 07:11:54 -0800 Subject: [PATCH 32/44] feat: add Valid() method to generated enum types (#2227) * feat: add Valid() method to generated enum types Generate a Valid() bool method on each enum type that returns true when the receiver matches one of the defined enum constants and false otherwise. This lets callers validate enum values at runtime with a simple method call instead of hand-writing switch statements. This is default-on because it only adds a new method to an already generated type -- existing code that does not call Valid() is completely unaffected, so this should be very unlikely to break anything. Co-Authored-By: Claude Opus 4.6 * Reduce complexity in enum validation It turns out we don't need a sorted map of names, since we have the same thing already present on the template context in a different way. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- internal/test/components/components.gen.go | 162 ++++++++++++++++++ .../externalref/petstore/externalref.gen.go | 42 +++++ .../test/issues/issue-1189/issue1189.gen.go | 36 ++++ .../test/issues/issue-1397/issue1397.gen.go | 12 ++ internal/test/issues/issue-832/issue.gen.go | 16 ++ .../issue-illegal_enum_names/issue.gen.go | 28 +++ internal/test/parameters/parameters.gen.go | 12 ++ internal/test/schemas/schemas.gen.go | 12 ++ internal/test/server/server.gen.go | 12 ++ pkg/codegen/templates/constants.tmpl | 10 ++ 10 files changed, 342 insertions(+) diff --git a/internal/test/components/components.gen.go b/internal/test/components/components.gen.go index 95257707b1..0bfc1747e6 100644 --- a/internal/test/components/components.gen.go +++ b/internal/test/components/components.gen.go @@ -18,6 +18,20 @@ const ( Enum1Two Enum1 = "Two" ) +// Valid indicates whether the value is a known member of the Enum1 enum. +func (e Enum1) Valid() bool { + switch e { + case Enum1One: + return true + case Enum1Three: + return true + case Enum1Two: + return true + default: + return false + } +} + // Defines values for Enum2. const ( Enum2Four Enum2 = "Four" @@ -25,6 +39,20 @@ const ( Enum2Two Enum2 = "Two" ) +// Valid indicates whether the value is a known member of the Enum2 enum. +func (e Enum2) Valid() bool { + switch e { + case Enum2Four: + return true + case Enum2Three: + return true + case Enum2Two: + return true + default: + return false + } +} + // Defines values for Enum3. const ( Enum3Bar Enum3 = "Bar" @@ -32,6 +60,20 @@ const ( Enum3Foo Enum3 = "Foo" ) +// Valid indicates whether the value is a known member of the Enum3 enum. +func (e Enum3) Valid() bool { + switch e { + case Enum3Bar: + return true + case Enum3Enum1One: + return true + case Enum3Foo: + return true + default: + return false + } +} + // Defines values for Enum4. const ( Cat Enum4 = "Cat" @@ -39,6 +81,20 @@ const ( Mouse Enum4 = "Mouse" ) +// Valid indicates whether the value is a known member of the Enum4 enum. +func (e Enum4) Valid() bool { + switch e { + case Cat: + return true + case Dog: + return true + case Mouse: + return true + default: + return false + } +} + // Defines values for Enum5. const ( Enum5N5 Enum5 = 5 @@ -46,6 +102,20 @@ const ( Enum5N7 Enum5 = 7 ) +// Valid indicates whether the value is a known member of the Enum5 enum. +func (e Enum5) Valid() bool { + switch e { + case Enum5N5: + return true + case Enum5N6: + return true + case Enum5N7: + return true + default: + return false + } +} + // Defines values for EnumUnion. const ( EnumUnionFour EnumUnion = "Four" @@ -54,6 +124,22 @@ const ( EnumUnionTwo EnumUnion = "Two" ) +// Valid indicates whether the value is a known member of the EnumUnion enum. +func (e EnumUnion) Valid() bool { + switch e { + case EnumUnionFour: + return true + case EnumUnionOne: + return true + case EnumUnionThree: + return true + case EnumUnionTwo: + return true + default: + return false + } +} + // Defines values for EnumUnion2. const ( EnumUnion2One EnumUnion2 = "One" @@ -62,6 +148,22 @@ const ( EnumUnion2Two EnumUnion2 = "Two" ) +// Valid indicates whether the value is a known member of the EnumUnion2 enum. +func (e EnumUnion2) Valid() bool { + switch e { + case EnumUnion2One: + return true + case EnumUnion2Seven: + return true + case EnumUnion2Three: + return true + case EnumUnion2Two: + return true + default: + return false + } +} + // Defines values for FunnyValues. const ( FunnyValuesAnd FunnyValues = "&" @@ -71,6 +173,24 @@ const ( FunnyValuesPercent FunnyValues = "%" ) +// Valid indicates whether the value is a known member of the FunnyValues enum. +func (e FunnyValues) Valid() bool { + switch e { + case FunnyValuesAnd: + return true + case FunnyValuesAsterisk: + return true + case FunnyValuesEmpty: + return true + case FunnyValuesN5: + return true + case FunnyValuesPercent: + return true + default: + return false + } +} + // Defines values for EnumParam1. const ( EnumParam1Both EnumParam1 = "both" @@ -78,6 +198,20 @@ const ( EnumParam1On EnumParam1 = "on" ) +// Valid indicates whether the value is a known member of the EnumParam1 enum. +func (e EnumParam1) Valid() bool { + switch e { + case EnumParam1Both: + return true + case EnumParam1Off: + return true + case EnumParam1On: + return true + default: + return false + } +} + // Defines values for EnumParam2. const ( EnumParam2Both EnumParam2 = "both" @@ -85,6 +219,20 @@ const ( EnumParam2On EnumParam2 = "on" ) +// Valid indicates whether the value is a known member of the EnumParam2 enum. +func (e EnumParam2) Valid() bool { + switch e { + case EnumParam2Both: + return true + case EnumParam2Off: + return true + case EnumParam2On: + return true + default: + return false + } +} + // Defines values for EnumParam3. const ( Alice EnumParam3 = "alice" @@ -92,6 +240,20 @@ const ( Eve EnumParam3 = "eve" ) +// Valid indicates whether the value is a known member of the EnumParam3 enum. +func (e EnumParam3) Valid() bool { + switch e { + case Alice: + return true + case Bob: + return true + case Eve: + return true + default: + return false + } +} + // AdditionalPropertiesObject1 Has additional properties of type int type AdditionalPropertiesObject1 struct { Id int `json:"id"` diff --git a/internal/test/externalref/petstore/externalref.gen.go b/internal/test/externalref/petstore/externalref.gen.go index 33852217e1..e8102f0d28 100644 --- a/internal/test/externalref/petstore/externalref.gen.go +++ b/internal/test/externalref/petstore/externalref.gen.go @@ -29,6 +29,20 @@ const ( Placed OrderStatus = "placed" ) +// Valid indicates whether the value is a known member of the OrderStatus enum. +func (e OrderStatus) Valid() bool { + switch e { + case Approved: + return true + case Delivered: + return true + case Placed: + return true + default: + return false + } +} + // Defines values for PetStatus. const ( PetStatusAvailable PetStatus = "available" @@ -36,6 +50,20 @@ const ( PetStatusSold PetStatus = "sold" ) +// Valid indicates whether the value is a known member of the PetStatus enum. +func (e PetStatus) Valid() bool { + switch e { + case PetStatusAvailable: + return true + case PetStatusPending: + return true + case PetStatusSold: + return true + default: + return false + } +} + // Defines values for FindPetsByStatusParamsStatus. const ( FindPetsByStatusParamsStatusAvailable FindPetsByStatusParamsStatus = "available" @@ -43,6 +71,20 @@ const ( FindPetsByStatusParamsStatusSold FindPetsByStatusParamsStatus = "sold" ) +// Valid indicates whether the value is a known member of the FindPetsByStatusParamsStatus enum. +func (e FindPetsByStatusParamsStatus) Valid() bool { + switch e { + case FindPetsByStatusParamsStatusAvailable: + return true + case FindPetsByStatusParamsStatusPending: + return true + case FindPetsByStatusParamsStatusSold: + return true + default: + return false + } +} + // Address defines model for Address. type Address struct { City *string `json:"city,omitempty"` diff --git a/internal/test/issues/issue-1189/issue1189.gen.go b/internal/test/issues/issue-1189/issue1189.gen.go index c1a869ad98..c6e0579fd5 100644 --- a/internal/test/issues/issue-1189/issue1189.gen.go +++ b/internal/test/issues/issue-1189/issue1189.gen.go @@ -27,18 +27,54 @@ const ( TestFieldA1Foo TestFieldA1 = "foo" ) +// Valid indicates whether the value is a known member of the TestFieldA1 enum. +func (e TestFieldA1) Valid() bool { + switch e { + case TestFieldA1Bar: + return true + case TestFieldA1Foo: + return true + default: + return false + } +} + // Defines values for TestFieldB. const ( TestFieldBBar TestFieldB = "bar" TestFieldBFoo TestFieldB = "foo" ) +// Valid indicates whether the value is a known member of the TestFieldB enum. +func (e TestFieldB) Valid() bool { + switch e { + case TestFieldBBar: + return true + case TestFieldBFoo: + return true + default: + return false + } +} + // Defines values for TestFieldC1. const ( Bar TestFieldC1 = "bar" Foo TestFieldC1 = "foo" ) +// Valid indicates whether the value is a known member of the TestFieldC1 enum. +func (e TestFieldC1) Valid() bool { + switch e { + case Bar: + return true + case Foo: + return true + default: + return false + } +} + // Test defines model for test. type Test struct { FieldA *Test_FieldA `json:"fieldA,omitempty"` diff --git a/internal/test/issues/issue-1397/issue1397.gen.go b/internal/test/issues/issue-1397/issue1397.gen.go index 2463641eb1..11bc883db2 100644 --- a/internal/test/issues/issue-1397/issue1397.gen.go +++ b/internal/test/issues/issue-1397/issue1397.gen.go @@ -26,6 +26,18 @@ const ( Option2 TestField1 = "option2" ) +// Valid indicates whether the value is a known member of the TestField1 enum. +func (e TestField1) Valid() bool { + switch e { + case Option1: + return true + case Option2: + return true + default: + return false + } +} + // Test defines model for Test. type Test = MyTestRequest diff --git a/internal/test/issues/issue-832/issue.gen.go b/internal/test/issues/issue-832/issue.gen.go index 0aa74f232e..4a5aa56a3d 100644 --- a/internal/test/issues/issue-832/issue.gen.go +++ b/internal/test/issues/issue-832/issue.gen.go @@ -23,6 +23,22 @@ const ( Two Document_Status = "two" ) +// Valid indicates whether the value is a known member of the Document_Status enum. +func (e Document_Status) Valid() bool { + switch e { + case Four: + return true + case One: + return true + case Three: + return true + case Two: + return true + default: + return false + } +} + // Document defines model for Document. type Document struct { Name *string `json:"name,omitempty"` diff --git a/internal/test/issues/issue-illegal_enum_names/issue.gen.go b/internal/test/issues/issue-illegal_enum_names/issue.gen.go index 8f907486cf..0c29ab71eb 100644 --- a/internal/test/issues/issue-illegal_enum_names/issue.gen.go +++ b/internal/test/issues/issue-illegal_enum_names/issue.gen.go @@ -34,6 +34,34 @@ const ( BarUnderscoreFoo Bar = "_Foo_" ) +// Valid indicates whether the value is a known member of the Bar enum. +func (e Bar) Valid() bool { + switch e { + case BarBar: + return true + case BarEmpty: + return true + case BarFoo: + return true + case BarFoo1: + return true + case BarFoo2: + return true + case BarFooBar: + return true + case BarFooBar1: + return true + case BarN1: + return true + case BarN1Foo: + return true + case BarUnderscoreFoo: + return true + default: + return false + } +} + // Bar defines model for Bar. type Bar string diff --git a/internal/test/parameters/parameters.gen.go b/internal/test/parameters/parameters.gen.go index 630770731a..47c98a774e 100644 --- a/internal/test/parameters/parameters.gen.go +++ b/internal/test/parameters/parameters.gen.go @@ -27,6 +27,18 @@ const ( N200 EnumParamsParamsEnumPathParam = 200 ) +// Valid indicates whether the value is a known member of the EnumParamsParamsEnumPathParam enum. +func (e EnumParamsParamsEnumPathParam) Valid() bool { + switch e { + case N100: + return true + case N200: + return true + default: + return false + } +} + // ComplexObject defines model for ComplexObject. type ComplexObject struct { Id int `json:"Id"` diff --git a/internal/test/schemas/schemas.gen.go b/internal/test/schemas/schemas.gen.go index 1a7f9e61b7..c46358f7f6 100644 --- a/internal/test/schemas/schemas.gen.go +++ b/internal/test/schemas/schemas.gen.go @@ -34,6 +34,18 @@ const ( Second EnumInObjInArrayVal = "second" ) +// Valid indicates whether the value is a known member of the EnumInObjInArrayVal enum. +func (e EnumInObjInArrayVal) Valid() bool { + switch e { + case First: + return true + case Second: + return true + default: + return false + } +} + // N5StartsWithNumber This schema name starts with a number type N5StartsWithNumber = map[string]interface{} diff --git a/internal/test/server/server.gen.go b/internal/test/server/server.gen.go index e360916b3a..b2468c75b4 100644 --- a/internal/test/server/server.gen.go +++ b/internal/test/server/server.gen.go @@ -19,6 +19,18 @@ const ( Text GetWithContentTypeParamsContentType = "text" ) +// Valid indicates whether the value is a known member of the GetWithContentTypeParamsContentType enum. +func (e GetWithContentTypeParamsContentType) Valid() bool { + switch e { + case Json: + return true + case Text: + return true + default: + return false + } +} + // EveryTypeOptional defines model for EveryTypeOptional. type EveryTypeOptional struct { ArrayInlineField *[]int `json:"array_inline_field,omitempty"` diff --git a/pkg/codegen/templates/constants.tmpl b/pkg/codegen/templates/constants.tmpl index 8fad8764c4..1c8cbd4922 100644 --- a/pkg/codegen/templates/constants.tmpl +++ b/pkg/codegen/templates/constants.tmpl @@ -12,4 +12,14 @@ const ( {{$name}} {{$Enum.TypeName}} = {{$Enum.ValueWrapper}}{{$value}}{{$Enum.ValueWrapper -}} {{end}} ) + +// Valid indicates whether the value is a known member of the {{$Enum.TypeName}} enum. +func (e {{$Enum.TypeName}}) Valid() bool { + switch e { + {{range $name, $value := $Enum.GetValues}}case {{$name}}: + return true + {{end}}default: + return false + } +} {{end}} From b00a8ea7648caac4345b9f1fe5c1dd30dd0ef016 Mon Sep 17 00:00:00 2001 From: Gaiaz Iusipov Date: Sat, 21 Feb 2026 11:56:47 +0800 Subject: [PATCH 33/44] style(gofix): Apply `go fix` (#2229) Signed-off-by: Gaiaz Iusipov --- cmd/oapi-codegen/oapi-codegen.go | 9 +++++---- pkg/codegen/extension.go | 26 +++++++++++++------------- pkg/codegen/extension_test.go | 4 ++-- pkg/codegen/merge_schemas.go | 2 +- pkg/codegen/operations.go | 2 +- pkg/codegen/prune.go | 10 +++------- pkg/codegen/schema.go | 8 ++++---- pkg/codegen/utils.go | 31 +++++++++++-------------------- pkg/codegen/utils_test.go | 2 +- 9 files changed, 41 insertions(+), 53 deletions(-) diff --git a/cmd/oapi-codegen/oapi-codegen.go b/cmd/oapi-codegen/oapi-codegen.go index 78bea5b1e3..a3e43498c4 100644 --- a/cmd/oapi-codegen/oapi-codegen.go +++ b/cmd/oapi-codegen/oapi-codegen.go @@ -29,7 +29,7 @@ import ( "github.com/oapi-codegen/oapi-codegen/v2/pkg/util" ) -func errExit(format string, args ...interface{}) { +func errExit(format string, args ...any) { if !strings.HasSuffix(format, "\n") { format = format + "\n" } @@ -272,12 +272,13 @@ func main() { } if warnings := opts.Generate.Warnings(); len(warnings) > 0 { - out := "WARNING: A number of warning(s) were returned when validating the GenerateOptions:" + var out strings.Builder + out.WriteString("WARNING: A number of warning(s) were returned when validating the GenerateOptions:") for k, v := range warnings { - out += "\n- " + k + ": " + v + out.WriteString("\n- " + k + ": " + v) } - _, _ = fmt.Fprint(os.Stderr, out) + _, _ = fmt.Fprint(os.Stderr, out.String()) } // If the user asked to output configuration, output it to stdout and exit diff --git a/pkg/codegen/extension.go b/pkg/codegen/extension.go index 579d8a17c9..6bc6bff144 100644 --- a/pkg/codegen/extension.go +++ b/pkg/codegen/extension.go @@ -29,7 +29,7 @@ const ( extOapiCodegenOnlyHonourGoName = "x-oapi-codegen-only-honour-go-name" ) -func extString(extPropValue interface{}) (string, error) { +func extString(extPropValue any) (string, error) { str, ok := extPropValue.(string) if !ok { return "", fmt.Errorf("failed to convert type: %T", extPropValue) @@ -37,11 +37,11 @@ func extString(extPropValue interface{}) (string, error) { return str, nil } -func extTypeName(extPropValue interface{}) (string, error) { +func extTypeName(extPropValue any) (string, error) { return extString(extPropValue) } -func extParsePropGoTypeSkipOptionalPointer(extPropValue interface{}) (bool, error) { +func extParsePropGoTypeSkipOptionalPointer(extPropValue any) (bool, error) { goTypeSkipOptionalPointer, ok := extPropValue.(bool) if !ok { return false, fmt.Errorf("failed to convert type: %T", extPropValue) @@ -49,11 +49,11 @@ func extParsePropGoTypeSkipOptionalPointer(extPropValue interface{}) (bool, erro return goTypeSkipOptionalPointer, nil } -func extParseGoFieldName(extPropValue interface{}) (string, error) { +func extParseGoFieldName(extPropValue any) (string, error) { return extString(extPropValue) } -func extParseOmitEmpty(extPropValue interface{}) (bool, error) { +func extParseOmitEmpty(extPropValue any) (bool, error) { omitEmpty, ok := extPropValue.(bool) if !ok { return false, fmt.Errorf("failed to convert type: %T", extPropValue) @@ -61,7 +61,7 @@ func extParseOmitEmpty(extPropValue interface{}) (bool, error) { return omitEmpty, nil } -func extParseOmitZero(extPropValue interface{}) (bool, error) { +func extParseOmitZero(extPropValue any) (bool, error) { omitZero, ok := extPropValue.(bool) if !ok { return false, fmt.Errorf("failed to convert type: %T", extPropValue) @@ -69,8 +69,8 @@ func extParseOmitZero(extPropValue interface{}) (bool, error) { return omitZero, nil } -func extExtraTags(extPropValue interface{}) (map[string]string, error) { - tagsI, ok := extPropValue.(map[string]interface{}) +func extExtraTags(extPropValue any) (map[string]string, error) { + tagsI, ok := extPropValue.(map[string]any) if !ok { return nil, fmt.Errorf("failed to convert type: %T", extPropValue) } @@ -85,7 +85,7 @@ func extExtraTags(extPropValue interface{}) (map[string]string, error) { return tags, nil } -func extParseGoJsonIgnore(extPropValue interface{}) (bool, error) { +func extParseGoJsonIgnore(extPropValue any) (bool, error) { goJsonIgnore, ok := extPropValue.(bool) if !ok { return false, fmt.Errorf("failed to convert type: %T", extPropValue) @@ -93,8 +93,8 @@ func extParseGoJsonIgnore(extPropValue interface{}) (bool, error) { return goJsonIgnore, nil } -func extParseEnumVarNames(extPropValue interface{}) ([]string, error) { - namesI, ok := extPropValue.([]interface{}) +func extParseEnumVarNames(extPropValue any) ([]string, error) { + namesI, ok := extPropValue.([]any) if !ok { return nil, fmt.Errorf("failed to convert type: %T", extPropValue) } @@ -109,11 +109,11 @@ func extParseEnumVarNames(extPropValue interface{}) ([]string, error) { return names, nil } -func extParseDeprecationReason(extPropValue interface{}) (string, error) { +func extParseDeprecationReason(extPropValue any) (string, error) { return extString(extPropValue) } -func extParseOapiCodegenOnlyHonourGoName(extPropValue interface{}) (bool, error) { +func extParseOapiCodegenOnlyHonourGoName(extPropValue any) (bool, error) { onlyHonourGoName, ok := extPropValue.(bool) if !ok { return false, fmt.Errorf("failed to convert type: %T", extPropValue) diff --git a/pkg/codegen/extension_test.go b/pkg/codegen/extension_test.go index 7a5f363bd7..0223ee9404 100644 --- a/pkg/codegen/extension_test.go +++ b/pkg/codegen/extension_test.go @@ -39,7 +39,7 @@ func Test_extTypeName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // kin-openapi no longer returns these as RawMessage - var extPropValue interface{} + var extPropValue any if tt.args.extPropValue != nil { err := json.Unmarshal(tt.args.extPropValue, &extPropValue) assert.NoError(t, err) @@ -93,7 +93,7 @@ func Test_extParsePropGoTypeSkipOptionalPointer(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // kin-openapi no longer returns these as RawMessage - var extPropValue interface{} + var extPropValue any if tt.args.extPropValue != nil { err := json.Unmarshal(tt.args.extPropValue, &extPropValue) assert.NoError(t, err) diff --git a/pkg/codegen/merge_schemas.go b/pkg/codegen/merge_schemas.go index 04e7b2fa2b..39c499c7da 100644 --- a/pkg/codegen/merge_schemas.go +++ b/pkg/codegen/merge_schemas.go @@ -87,7 +87,7 @@ func mergeAllOf(allOf []*openapi3.SchemaRef) (openapi3.Schema, error) { func mergeOpenapiSchemas(s1, s2 openapi3.Schema, allOf bool) (openapi3.Schema, error) { var result openapi3.Schema - result.Extensions = make(map[string]interface{}) + result.Extensions = make(map[string]any) for k, v := range s1.Extensions { result.Extensions[k] = v } diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index e4d9784702..1743d270ea 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -1067,7 +1067,7 @@ func GenerateClientWithResponses(t *template.Template, ops []OperationDefinition } // GenerateTemplates used to generate templates -func GenerateTemplates(templates []string, t *template.Template, ops interface{}) (string, error) { +func GenerateTemplates(templates []string, t *template.Template, ops any) (string, error) { var generatedTemplates []string for _, tmpl := range templates { var buf bytes.Buffer diff --git a/pkg/codegen/prune.go b/pkg/codegen/prune.go index 0d3a889747..e97ba3469e 100644 --- a/pkg/codegen/prune.go +++ b/pkg/codegen/prune.go @@ -2,23 +2,19 @@ package codegen import ( "fmt" + "slices" "github.com/getkin/kin-openapi/openapi3" ) func stringInSlice(a string, list []string) bool { - for _, b := range list { - if b == a { - return true - } - } - return false + return slices.Contains(list, a) } type RefWrapper struct { Ref string HasValue bool - SourceRef interface{} + SourceRef any } func walkSwagger(swagger *openapi3.T, doFn func(RefWrapper) (bool, error)) error { diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go index 25f34de38a..cc14db731f 100644 --- a/pkg/codegen/schema.go +++ b/pkg/codegen/schema.go @@ -102,7 +102,7 @@ type Property struct { ReadOnly bool WriteOnly bool NeedsFormTag bool - Extensions map[string]interface{} + Extensions map[string]any Deprecated bool } @@ -270,11 +270,11 @@ func (u UnionElement) String() string { // Method generate union method name for template functions `As/From/Merge`. func (u UnionElement) Method() string { - var method string + var method strings.Builder for _, part := range strings.Split(string(u), `.`) { - method += UppercaseFirstCharacter(part) + method.WriteString(UppercaseFirstCharacter(part)) } - return method + return method.String() } func PropertiesEqual(a, b Property) bool { diff --git a/pkg/codegen/utils.go b/pkg/codegen/utils.go index 79e274f7ee..549d5cffa3 100644 --- a/pkg/codegen/utils.go +++ b/pkg/codegen/utils.go @@ -20,6 +20,7 @@ import ( "net/url" "reflect" "regexp" + "slices" "sort" "strconv" "strings" @@ -205,7 +206,7 @@ func LowercaseFirstCharacters(str string) string { runes := []rune(str) - for i := 0; i < len(runes); i++ { + for i := range runes { next := i + 1 if i != 0 && next < len(runes) && unicode.IsLower(runes[next]) { break @@ -224,25 +225,25 @@ func LowercaseFirstCharacters(str string) string { func ToCamelCase(str string) string { s := strings.Trim(str, " ") - n := "" + var n strings.Builder capNext := true for _, v := range s { if unicode.IsUpper(v) { - n += string(v) + n.WriteString(string(v)) } if unicode.IsDigit(v) { - n += string(v) + n.WriteString(string(v)) } if unicode.IsLower(v) { if capNext { - n += strings.ToUpper(string(v)) + n.WriteString(strings.ToUpper(string(v))) } else { - n += string(v) + n.WriteString(string(v)) } } _, capNext = separatorSet[v] } - return n + return n.String() } // ToCamelCaseWithDigits function will convert query-arg style strings to CamelCase. We will @@ -407,12 +408,7 @@ func schemaXOrder(v *openapi3.SchemaRef) (int64, bool) { // StringInArray checks whether the specified string is present in an array // of strings func StringInArray(str string, array []string) bool { - for _, elt := range array { - if elt == str { - return true - } - } - return false + return slices.Contains(array, str) } // RefPathToObjName returns the name of referenced object without changes. @@ -1074,7 +1070,7 @@ func ParseGoImportExtension(v *openapi3.SchemaRef) (*goImport, error) { goTypeImportExt := v.Value.Extensions[extPropGoImport] - importI, ok := goTypeImportExt.(map[string]interface{}) + importI, ok := goTypeImportExt.(map[string]any) if !ok { return nil, fmt.Errorf("failed to convert type: %T", goTypeImportExt) } @@ -1126,12 +1122,7 @@ func isAdditionalPropertiesExplicitFalse(s *openapi3.Schema) bool { } func sliceContains[E comparable](s []E, v E) bool { - for _, ss := range s { - if ss == v { - return true - } - } - return false + return slices.Contains(s, v) } // FixDuplicateTypeNames renames duplicate type names. diff --git a/pkg/codegen/utils_test.go b/pkg/codegen/utils_test.go index f204fc50c8..3f9e0e8b76 100644 --- a/pkg/codegen/utils_test.go +++ b/pkg/codegen/utils_test.go @@ -160,7 +160,7 @@ func TestSortedSchemaKeysWithXOrder(t *testing.T) { withOrder := func(i float64) *openapi3.SchemaRef { return &openapi3.SchemaRef{ Value: &openapi3.Schema{ - Extensions: map[string]interface{}{"x-order": i}, + Extensions: map[string]any{"x-order": i}, }, } } From eb9b3830c17782f8a022ded6b43c160d8dbf6b01 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Tue, 24 Feb 2026 06:59:30 -0800 Subject: [PATCH 34/44] Configure Greptile code review (#2236) Don't automatically review each PR, users can request a code review via tagging @greptileai Exclude generated files from code reviews. It's pointless. --- greptile.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 greptile.json diff --git a/greptile.json b/greptile.json new file mode 100644 index 0000000000..bc3cc79fc0 --- /dev/null +++ b/greptile.json @@ -0,0 +1,4 @@ +{ + "skipReview": "AUTOMATIC", + "ignorePatterns": "**/*.gen.go" +} From a14bfbb47559fa19046fa80793feb1cc4c64e4c7 Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Tue, 24 Feb 2026 14:16:19 +0000 Subject: [PATCH 35/44] fix(server-urls): restore generation of constants This fixes a regression from 59fb0e86773ac572016038c9cd21569de8d6a1b2. --- pkg/codegen/templates/server-urls.tmpl | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/codegen/templates/server-urls.tmpl b/pkg/codegen/templates/server-urls.tmpl index 1c60e796b1..3f1fdfe734 100644 --- a/pkg/codegen/templates/server-urls.tmpl +++ b/pkg/codegen/templates/server-urls.tmpl @@ -2,6 +2,7 @@ {{ if eq 0 (len .OAPISchema.Variables) }} {{/* URLs without variables are straightforward, so we'll create them a constant */}} // {{ .GoName }} defines the Server URL for {{ if len .OAPISchema.Description }}{{ .OAPISchema.Description }}{{ else }}{{ .OAPISchema.URL }}{{ end }} +const {{ .GoName}} = "{{ .OAPISchema.URL }}" {{ else }} {{/* URLs with variables are not straightforward, as we may need multiple types, and so will model them as a function */}} From 918ff57a9d9852df18558516b13678af7f676fc7 Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Tue, 24 Feb 2026 13:52:05 +0000 Subject: [PATCH 36/44] fix(templates/client): correctly nil check query parameters As a fix similar to c4ba545dc7f4c67d796d7fbde1203b775f3a3056, we had cases where the global `prefer-skip-optional-pointer` could lead to not nil checking when we should have been. The previous behaviour would lead to incorrectly converting i.e. `user_ids=nil` to `user_ids[]=`, which is not correct. To do this, we can add the same helper functions that we have elsewhere. --- .../test/issues/issue-2031/prefer/config.yaml | 7 + .../test/issues/issue-2031/prefer/generate.go | 3 + .../issues/issue-2031/prefer/issue2031.gen.go | 251 ++++++++++++++++++ .../issue-2031/prefer/issue2031_test.go | 63 +++++ .../issues/issue-2031/prefer/openapi.yaml | 17 ++ pkg/codegen/operations.go | 17 ++ pkg/codegen/templates/client.tmpl | 6 +- 7 files changed, 361 insertions(+), 3 deletions(-) create mode 100644 internal/test/issues/issue-2031/prefer/config.yaml create mode 100644 internal/test/issues/issue-2031/prefer/generate.go create mode 100644 internal/test/issues/issue-2031/prefer/issue2031.gen.go create mode 100644 internal/test/issues/issue-2031/prefer/issue2031_test.go create mode 100644 internal/test/issues/issue-2031/prefer/openapi.yaml diff --git a/internal/test/issues/issue-2031/prefer/config.yaml b/internal/test/issues/issue-2031/prefer/config.yaml new file mode 100644 index 0000000000..4ec155a835 --- /dev/null +++ b/internal/test/issues/issue-2031/prefer/config.yaml @@ -0,0 +1,7 @@ +package: issue2031 +generate: + models: true + client: true +output-options: + prefer-skip-optional-pointer: true +output: issue2031.gen.go diff --git a/internal/test/issues/issue-2031/prefer/generate.go b/internal/test/issues/issue-2031/prefer/generate.go new file mode 100644 index 0000000000..1e29681627 --- /dev/null +++ b/internal/test/issues/issue-2031/prefer/generate.go @@ -0,0 +1,3 @@ +package issue2031 + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml openapi.yaml diff --git a/internal/test/issues/issue-2031/prefer/issue2031.gen.go b/internal/test/issues/issue-2031/prefer/issue2031.gen.go new file mode 100644 index 0000000000..64b30f3515 --- /dev/null +++ b/internal/test/issues/issue-2031/prefer/issue2031.gen.go @@ -0,0 +1,251 @@ +// Package issue2031 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package issue2031 + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/oapi-codegen/runtime" +) + +// GetTestParams defines parameters for GetTest. +type GetTestParams struct { + UserIds []int `form:"user_ids[],omitempty" json:"user_ids[],omitempty"` +} + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // 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 +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GetTest request + GetTest(ctx context.Context, params *GetTestParams, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetTest(ctx context.Context, params *GetTestParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetTestRequest(c.Server, params) + 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) +} + +// NewGetTestRequest generates requests for GetTest +func NewGetTestRequest(server string, params *GetTestParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/test") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.UserIds != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "user_ids[]", runtime.ParamLocationQuery, params.UserIds); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.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 +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GetTestWithResponse request + GetTestWithResponse(ctx context.Context, params *GetTestParams, reqEditors ...RequestEditorFn) (*GetTestResponse, error) +} + +type GetTestResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r GetTestResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetTestResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetTestWithResponse request returning *GetTestResponse +func (c *ClientWithResponses) GetTestWithResponse(ctx context.Context, params *GetTestParams, reqEditors ...RequestEditorFn) (*GetTestResponse, error) { + rsp, err := c.GetTest(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetTestResponse(rsp) +} + +// ParseGetTestResponse parses an HTTP response from a GetTestWithResponse call +func ParseGetTestResponse(rsp *http.Response) (*GetTestResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetTestResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} diff --git a/internal/test/issues/issue-2031/prefer/issue2031_test.go b/internal/test/issues/issue-2031/prefer/issue2031_test.go new file mode 100644 index 0000000000..cd0c55c8dd --- /dev/null +++ b/internal/test/issues/issue-2031/prefer/issue2031_test.go @@ -0,0 +1,63 @@ +package issue2031 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewGetTestRequest(t *testing.T) { + t.Run("does not add the user_ids[] parameter if zero value", func(t *testing.T) { + params := GetTestParams{} + + req, err := NewGetTestRequest("https://localhost", ¶ms) + require.NoError(t, err) + + assert.Equal(t, "https://localhost/test", req.URL.String()) + }) + + t.Run("does not add the user_ids[] parameter if nil", func(t *testing.T) { + params := GetTestParams{ + UserIds: nil, + } + + req, err := NewGetTestRequest("https://localhost", ¶ms) + require.NoError(t, err) + + assert.Equal(t, "https://localhost/test", req.URL.String()) + }) + + t.Run("adds the user_ids[] parameter if an explicitly initialised empty array", func(t *testing.T) { + params := GetTestParams{ + UserIds: []int{}, + } + + req, err := NewGetTestRequest("https://localhost", ¶ms) + require.NoError(t, err) + + assert.Equal(t, "https://localhost/test?user_ids%5B%5D=", req.URL.String()) + }) + + t.Run("adds the user_ids[] parameter if array contains a value", func(t *testing.T) { + params := GetTestParams{ + UserIds: []int{1}, + } + + req, err := NewGetTestRequest("https://localhost", ¶ms) + require.NoError(t, err) + + assert.Equal(t, "https://localhost/test?user_ids%5B%5D=1", req.URL.String()) + }) + + t.Run("handles multiple user_ids[] parameters", func(t *testing.T) { + params := GetTestParams{ + UserIds: []int{1, 100}, + } + + req, err := NewGetTestRequest("https://localhost", ¶ms) + require.NoError(t, err) + + assert.Equal(t, "https://localhost/test?user_ids%5B%5D=1&user_ids%5B%5D=100", req.URL.String()) + }) +} diff --git a/internal/test/issues/issue-2031/prefer/openapi.yaml b/internal/test/issues/issue-2031/prefer/openapi.yaml new file mode 100644 index 0000000000..d88d8df2fb --- /dev/null +++ b/internal/test/issues/issue-2031/prefer/openapi.yaml @@ -0,0 +1,17 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Issue 2031 +paths: + /test: + get: + parameters: + - name: "user_ids[]" + in: query + schema: + type: array + items: + type: integer + style: form + explode: true + required: false diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index 1743d270ea..e2a12d7117 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -44,6 +44,23 @@ func (pd ParameterDefinition) TypeDef() string { return typeDecl } +// RequiresNilCheck indicates whether the generated property should have a nil check performed on it before other checks. +// This should be used in templates when performing `nil` checks, but NOT when i.e. determining if there should be an optional pointer given to the type - in that case, use `HasOptionalPointer` +func (pd ParameterDefinition) RequiresNilCheck() bool { + return pd.ZeroValueIsNil() || pd.HasOptionalPointer() +} + +// ZeroValueIsNil is a helper function to determine if the given Go type used for this property +// Will return true if the OpenAPI `type` is: +// - `array` +func (pd ParameterDefinition) ZeroValueIsNil() bool { + if pd.Schema.OAPISchema == nil { + return false + } + + return pd.Schema.OAPISchema.Type.Is("array") +} + // JsonTag generates the JSON annotation to map GoType to json type name. If Parameter // Foo is marshaled to json as "foo", this will create the annotation // 'json:"foo"' diff --git a/pkg/codegen/templates/client.tmpl b/pkg/codegen/templates/client.tmpl index 822e11097a..d219432e29 100644 --- a/pkg/codegen/templates/client.tmpl +++ b/pkg/codegen/templates/client.tmpl @@ -197,7 +197,7 @@ func New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(server string{{genParamAr if params != nil { queryValues := queryURL.Query() {{range $paramIdx, $param := .QueryParams}} - {{if .HasOptionalPointer}} if params.{{.GoName}} != nil { {{end}} + {{if .RequiresNilCheck}} if params.{{.GoName}} != nil { {{end}} {{if .IsPassThrough}} queryValues.Add("{{.ParamName}}", {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}) {{end}} @@ -210,7 +210,7 @@ func New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(server string{{genParamAr {{end}} {{if .IsStyled}} - if queryFrag, err := runtime.StyleParamWithLocation("{{.Style}}", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationQuery, {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("{{.Style}}", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationQuery, {{if and .RequiresNilCheck .HasOptionalPointer}}*{{end}}params.{{.GoName}}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -222,7 +222,7 @@ func New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(server string{{genParamAr } } {{end}} - {{if .HasOptionalPointer}}}{{end}} + {{if .RequiresNilCheck}}}{{end}} {{end}} queryURL.RawQuery = queryValues.Encode() } From 12a9b208ed10d408c4d0c82a5f1297c25a1a2322 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Tue, 24 Feb 2026 08:37:49 -0800 Subject: [PATCH 37/44] fix: support x-oapi-codegen-extra-tags on parameter schemas (#2232) (#2235) When x-oapi-codegen-extra-tags was placed on the schema inside a parameter definition (rather than on the parameter itself), the extension was silently ignored. This happened because GenerateParamsTypes only read parameter-level extensions, not schema-level ones. Merge extensions from both the parameter and its schema when building the Params struct, with parameter-level extensions taking precedence. Co-authored-by: Claude Opus 4.6 --- internal/test/issues/issue-2232/config.yaml | 5 + internal/test/issues/issue-2232/generate.go | 3 + .../test/issues/issue-2232/issue2232.gen.go | 260 ++++++++++++++++++ .../test/issues/issue-2232/issue2232_test.go | 41 +++ internal/test/issues/issue-2232/spec.yaml | 46 ++++ pkg/codegen/operations.go | 13 +- 6 files changed, 367 insertions(+), 1 deletion(-) create mode 100644 internal/test/issues/issue-2232/config.yaml create mode 100644 internal/test/issues/issue-2232/generate.go create mode 100644 internal/test/issues/issue-2232/issue2232.gen.go create mode 100644 internal/test/issues/issue-2232/issue2232_test.go create mode 100644 internal/test/issues/issue-2232/spec.yaml diff --git a/internal/test/issues/issue-2232/config.yaml b/internal/test/issues/issue-2232/config.yaml new file mode 100644 index 0000000000..6368de1d4d --- /dev/null +++ b/internal/test/issues/issue-2232/config.yaml @@ -0,0 +1,5 @@ +package: issue2232 +output: issue2232.gen.go +generate: + std-http-server: true + models: true diff --git a/internal/test/issues/issue-2232/generate.go b/internal/test/issues/issue-2232/generate.go new file mode 100644 index 0000000000..f6767c10e1 --- /dev/null +++ b/internal/test/issues/issue-2232/generate.go @@ -0,0 +1,3 @@ +package issue2232 + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-2232/issue2232.gen.go b/internal/test/issues/issue-2232/issue2232.gen.go new file mode 100644 index 0000000000..e795e7ad72 --- /dev/null +++ b/internal/test/issues/issue-2232/issue2232.gen.go @@ -0,0 +1,260 @@ +//go:build go1.22 + +// Package issue2232 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package issue2232 + +import ( + "fmt" + "net/http" + + "github.com/oapi-codegen/runtime" +) + +// Defines values for GetEndpointParamsEnvParamLevel. +const ( + GetEndpointParamsEnvParamLevelDev GetEndpointParamsEnvParamLevel = "dev" + GetEndpointParamsEnvParamLevelLive GetEndpointParamsEnvParamLevel = "live" +) + +// Valid indicates whether the value is a known member of the GetEndpointParamsEnvParamLevel enum. +func (e GetEndpointParamsEnvParamLevel) Valid() bool { + switch e { + case GetEndpointParamsEnvParamLevelDev: + return true + case GetEndpointParamsEnvParamLevelLive: + return true + default: + return false + } +} + +// Defines values for GetEndpointParamsEnvSchemaLevel. +const ( + GetEndpointParamsEnvSchemaLevelDev GetEndpointParamsEnvSchemaLevel = "dev" + GetEndpointParamsEnvSchemaLevelLive GetEndpointParamsEnvSchemaLevel = "live" +) + +// Valid indicates whether the value is a known member of the GetEndpointParamsEnvSchemaLevel enum. +func (e GetEndpointParamsEnvSchemaLevel) Valid() bool { + switch e { + case GetEndpointParamsEnvSchemaLevelDev: + return true + case GetEndpointParamsEnvSchemaLevelLive: + return true + default: + return false + } +} + +// GetEndpointParams defines parameters for GetEndpoint. +type GetEndpointParams struct { + EnvParamLevel GetEndpointParamsEnvParamLevel `form:"env_param_level" json:"env_param_level" validate:"required,oneof=dev live"` + EnvSchemaLevel GetEndpointParamsEnvSchemaLevel `form:"env_schema_level" json:"env_schema_level" validate:"required,oneof=dev live"` + Limit *int `form:"limit,omitempty" json:"limit,omitempty" validate:"min=0,max=100"` +} + +// GetEndpointParamsEnvParamLevel defines parameters for GetEndpoint. +type GetEndpointParamsEnvParamLevel string + +// GetEndpointParamsEnvSchemaLevel defines parameters for GetEndpoint. +type GetEndpointParamsEnvSchemaLevel string + +// ServerInterface represents all server handlers. +type ServerInterface interface { + + // (GET /v1/endpoint) + GetEndpoint(w http.ResponseWriter, r *http.Request, params GetEndpointParams) +} + +// 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 + +// GetEndpoint operation middleware +func (siw *ServerInterfaceWrapper) GetEndpoint(w http.ResponseWriter, r *http.Request) { + + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params GetEndpointParams + + // ------------- Required query parameter "env_param_level" ------------- + + if paramValue := r.URL.Query().Get("env_param_level"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "env_param_level"}) + return + } + + err = runtime.BindQueryParameter("form", true, true, "env_param_level", r.URL.Query(), ¶ms.EnvParamLevel) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "env_param_level", Err: err}) + return + } + + // ------------- Required query parameter "env_schema_level" ------------- + + if paramValue := r.URL.Query().Get("env_schema_level"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "env_schema_level"}) + return + } + + err = runtime.BindQueryParameter("form", true, true, "env_schema_level", r.URL.Query(), ¶ms.EnvSchemaLevel) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "env_schema_level", Err: err}) + return + } + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameter("form", true, false, "limit", r.URL.Query(), ¶ms.Limit) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetEndpoint(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +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) +} + +// 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)) + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + +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) + } + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + m.HandleFunc("GET "+options.BaseURL+"/v1/endpoint", wrapper.GetEndpoint) + + return m +} diff --git a/internal/test/issues/issue-2232/issue2232_test.go b/internal/test/issues/issue-2232/issue2232_test.go new file mode 100644 index 0000000000..a8ae51836e --- /dev/null +++ b/internal/test/issues/issue-2232/issue2232_test.go @@ -0,0 +1,41 @@ +package issue2232 + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestExtraTagsOnQueryParams verifies that x-oapi-codegen-extra-tags is applied +// to query parameter struct fields regardless of whether the extension is placed +// at the parameter level or at the schema level within the parameter. +// This is a regression test for https://github.com/oapi-codegen/oapi-codegen/issues/2232 +func TestExtraTagsOnQueryParams(t *testing.T) { + paramType := reflect.TypeOf(GetEndpointParams{}) + + t.Run("parameter-level extension", func(t *testing.T) { + field, ok := paramType.FieldByName("EnvParamLevel") + require.True(t, ok, "field EnvParamLevel should exist") + + assert.Equal(t, `required,oneof=dev live`, field.Tag.Get("validate"), + "x-oapi-codegen-extra-tags at parameter level should produce validate tag") + }) + + t.Run("schema-level extension", func(t *testing.T) { + field, ok := paramType.FieldByName("EnvSchemaLevel") + require.True(t, ok, "field EnvSchemaLevel should exist") + + assert.Equal(t, `required,oneof=dev live`, field.Tag.Get("validate"), + "x-oapi-codegen-extra-tags at schema level within a parameter should produce validate tag") + }) + + t.Run("schema-level extension on optional param", func(t *testing.T) { + field, ok := paramType.FieldByName("Limit") + require.True(t, ok, "field Limit should exist") + + assert.Equal(t, `min=0,max=100`, field.Tag.Get("validate"), + "x-oapi-codegen-extra-tags at schema level within an optional parameter should produce validate tag") + }) +} diff --git a/internal/test/issues/issue-2232/spec.yaml b/internal/test/issues/issue-2232/spec.yaml new file mode 100644 index 0000000000..0fea5cba83 --- /dev/null +++ b/internal/test/issues/issue-2232/spec.yaml @@ -0,0 +1,46 @@ +openapi: "3.0.3" +info: + title: test + version: 1.0.0 +paths: + /v1/endpoint: + get: + operationId: GetEndpoint + parameters: + - name: env_param_level + in: query + required: true + schema: + type: string + enum: + - dev + - live + x-oapi-codegen-extra-tags: + validate: "required,oneof=dev live" + - name: env_schema_level + in: query + required: true + schema: + type: string + enum: + - dev + - live + x-oapi-codegen-extra-tags: + validate: "required,oneof=dev live" + - name: limit + in: query + required: false + schema: + type: integer + x-oapi-codegen-extra-tags: + validate: "min=0,max=100" + responses: + "200": + description: Success + content: + application/json: + schema: + type: object + properties: + message: + type: string diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index e2a12d7117..62ec2e085e 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -942,13 +942,24 @@ func GenerateParamsTypes(op OperationDefinition) []TypeDefinition { Schema: param.Schema, }) } + // Merge extensions from the schema level and the parameter level. + // Parameter-level extensions take precedence over schema-level ones. + extensions := make(map[string]any) + if param.Spec.Schema != nil && param.Spec.Schema.Value != nil { + for k, v := range param.Spec.Schema.Value.Extensions { + extensions[k] = v + } + } + for k, v := range param.Spec.Extensions { + extensions[k] = v + } prop := Property{ Description: param.Spec.Description, JsonFieldName: param.ParamName, Required: param.Required, Schema: pSchema, NeedsFormTag: param.Style() == "form", - Extensions: param.Spec.Extensions, + Extensions: extensions, } s.Properties = append(s.Properties, prop) } From 99eadbe66e960f69dcffd9296ccea7809c3e56dd Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Tue, 24 Feb 2026 18:46:07 +0000 Subject: [PATCH 38/44] fix(codegen): generate `nullable.Nullable` in arrays As a follow-up to #2185, we can make sure we generate a Nullable type if required to do so. --- internal/test/issues/issue-2185/config.yaml | 8 ++++++++ internal/test/issues/issue-2185/generate.go | 3 +++ .../test/issues/issue-2185/issue2185.gen.go | 13 +++++++++++++ .../test/issues/issue-2185/issue2185_test.go | 19 +++++++++++++++++++ internal/test/issues/issue-2185/spec.yaml | 15 +++++++++++++++ pkg/codegen/schema.go | 11 +++++++---- 6 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 internal/test/issues/issue-2185/config.yaml create mode 100644 internal/test/issues/issue-2185/generate.go create mode 100644 internal/test/issues/issue-2185/issue2185.gen.go create mode 100644 internal/test/issues/issue-2185/issue2185_test.go create mode 100644 internal/test/issues/issue-2185/spec.yaml diff --git a/internal/test/issues/issue-2185/config.yaml b/internal/test/issues/issue-2185/config.yaml new file mode 100644 index 0000000000..71a7ba132e --- /dev/null +++ b/internal/test/issues/issue-2185/config.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=../../../../configuration-schema.json +package: issue2185 +output: issue2185.gen.go +generate: + models: true +output-options: + skip-prune: true + nullable-type: true diff --git a/internal/test/issues/issue-2185/generate.go b/internal/test/issues/issue-2185/generate.go new file mode 100644 index 0000000000..9f5224eaeb --- /dev/null +++ b/internal/test/issues/issue-2185/generate.go @@ -0,0 +1,3 @@ +package issue2185 + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-2185/issue2185.gen.go b/internal/test/issues/issue-2185/issue2185.gen.go new file mode 100644 index 0000000000..6474c9c4d0 --- /dev/null +++ b/internal/test/issues/issue-2185/issue2185.gen.go @@ -0,0 +1,13 @@ +// Package issue2185 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package issue2185 + +import ( + "github.com/oapi-codegen/nullable" +) + +// Container defines model for Container. +type Container struct { + MayBeNull []nullable.Nullable[string] `json:"may-be-null"` +} diff --git a/internal/test/issues/issue-2185/issue2185_test.go b/internal/test/issues/issue-2185/issue2185_test.go new file mode 100644 index 0000000000..cc98e2b4d3 --- /dev/null +++ b/internal/test/issues/issue-2185/issue2185_test.go @@ -0,0 +1,19 @@ +package issue2185 + +import ( + "testing" + + "github.com/oapi-codegen/nullable" + "github.com/stretchr/testify/require" +) + +func TestContainer_UsesNullableType(t *testing.T) { + c := Container{ + MayBeNull: []nullable.Nullable[string]{ + nullable.NewNullNullable[string](), + }, + } + + require.Len(t, c.MayBeNull, 1) + require.True(t, c.MayBeNull[0].IsNull()) +} diff --git a/internal/test/issues/issue-2185/spec.yaml b/internal/test/issues/issue-2185/spec.yaml new file mode 100644 index 0000000000..814048bc8f --- /dev/null +++ b/internal/test/issues/issue-2185/spec.yaml @@ -0,0 +1,15 @@ +openapi: "3.0.3" +info: + title: test + version: 1.0.0 +components: + schemas: + Container: + required: + - "may-be-null" + properties: + "may-be-null": + type: array + items: + type: string + nullable: true diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go index cc14db731f..ea6e137473 100644 --- a/pkg/codegen/schema.go +++ b/pkg/codegen/schema.go @@ -603,8 +603,6 @@ func oapiSchemaToGoType(schema *openapi3.Schema, path []string, outSchema *Schem return fmt.Errorf("error generating type for array: %w", err) } - var itemPrefix string - if (arrayType.HasAdditionalProperties || len(arrayType.UnionElements) != 0) && arrayType.RefType == "" { // If we have items which have additional properties or union values, // but are not a pre-defined type, we need to define a type @@ -622,12 +620,17 @@ func oapiSchemaToGoType(schema *openapi3.Schema, path []string, outSchema *Schem arrayType.RefType = typeName } + typeDeclaration := arrayType.TypeDecl() if arrayType.OAPISchema != nil && arrayType.OAPISchema.Nullable { - itemPrefix = "*" + if globalState.options.OutputOptions.NullableType { + typeDeclaration = "nullable.Nullable[" + typeDeclaration + "]" + } else { + typeDeclaration = "*" + typeDeclaration + } } outSchema.ArrayType = &arrayType - outSchema.GoType = "[]" + itemPrefix + arrayType.TypeDecl() + outSchema.GoType = "[]" + typeDeclaration outSchema.AdditionalTypes = arrayType.AdditionalTypes outSchema.Properties = arrayType.Properties outSchema.DefineViaAlias = true From d567e49b4a4c82e3ab43a23f232bebbbc05cf161 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Thu, 26 Feb 2026 06:46:01 -0800 Subject: [PATCH 39/44] fix: add omitempty to optional nullable fields (#2221) Fixes #2091 The `omitempty` JSON tag was not being added to optional nullable fields. The condition `!p.Nullable && shouldOmitEmpty` explicitly prevented any nullable field from receiving `omitempty`, even when the field was optional (not required). This contradicted the documented behavior. The fix removes the `!p.Nullable` guard so nullable fields follow the same `omitempty` rules as non-nullable fields. The special-case exception for the `nullable-type` output option is no longer needed since the logic is now uniform. Note: `x-go-type-skip-optional-pointer: true` on a nullable field suppresses the pointer (generating `string` instead of `*string`) but still correctly receives `omitempty` when the field is optional. Whether skip-optional-pointer should be allowed to suppress the pointer on nullable fields is a separate concern. Co-authored-by: Claude Opus 4.6 --- .../test/issues/issue-1039/defaultbehaviour/types.gen.go | 6 +++--- internal/test/schemas/schemas.gen.go | 2 +- pkg/codegen/codegen_test.go | 4 ++-- pkg/codegen/schema.go | 6 +----- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/internal/test/issues/issue-1039/defaultbehaviour/types.gen.go b/internal/test/issues/issue-1039/defaultbehaviour/types.gen.go index b148f37830..5ed98d88f1 100644 --- a/internal/test/issues/issue-1039/defaultbehaviour/types.gen.go +++ b/internal/test/issues/issue-1039/defaultbehaviour/types.gen.go @@ -6,7 +6,7 @@ package defaultbehaviour // PatchRequest A request to patch an existing user object. type PatchRequest struct { // ComplexOptionalNullable Complex, optional and nullable - ComplexOptionalNullable *ComplexOptionalNullable `json:"complex_optional_nullable"` + ComplexOptionalNullable *ComplexOptionalNullable `json:"complex_optional_nullable,omitempty"` // ComplexRequiredNullable Complex required and nullable ComplexRequiredNullable *ComplexRequiredNullable `json:"complex_required_nullable"` @@ -15,7 +15,7 @@ type PatchRequest struct { SimpleOptionalNonNullable *SimpleOptionalNonNullable `json:"simple_optional_non_nullable,omitempty"` // SimpleOptionalNullable Simple optional and nullable - SimpleOptionalNullable *SimpleOptionalNullable `json:"simple_optional_nullable"` + SimpleOptionalNullable *SimpleOptionalNullable `json:"simple_optional_nullable,omitempty"` // SimpleRequiredNullable Simple required and nullable SimpleRequiredNullable *SimpleRequiredNullable `json:"simple_required_nullable"` @@ -24,7 +24,7 @@ type PatchRequest struct { // ComplexOptionalNullable Complex, optional and nullable type ComplexOptionalNullable struct { // AliasName Optional and nullable - AliasName *string `json:"alias_name"` + AliasName *string `json:"alias_name,omitempty"` // Name Optional and non nullable Name *string `json:"name,omitempty"` diff --git a/internal/test/schemas/schemas.gen.go b/internal/test/schemas/schemas.gen.go index c46358f7f6..f1afb25b19 100644 --- a/internal/test/schemas/schemas.gen.go +++ b/internal/test/schemas/schemas.gen.go @@ -92,7 +92,7 @@ type GenericObject = map[string]interface{} // NullableProperties defines model for NullableProperties. type NullableProperties struct { Optional *string `json:"optional,omitempty"` - OptionalAndNullable *string `json:"optionalAndNullable"` + OptionalAndNullable *string `json:"optionalAndNullable,omitempty"` Required string `json:"required"` RequiredAndNullable *string `json:"requiredAndNullable"` } diff --git a/pkg/codegen/codegen_test.go b/pkg/codegen/codegen_test.go index 4920c72e49..b7b3184c3c 100644 --- a/pkg/codegen/codegen_test.go +++ b/pkg/codegen/codegen_test.go @@ -117,8 +117,8 @@ func TestExtPropGoTypeSkipOptionalPointer(t *testing.T) { assert.NoError(t, err) // Check that optional pointer fields are skipped if requested - assert.Contains(t, code, "NullableFieldSkipFalse *string `json:\"nullableFieldSkipFalse\"`") - assert.Contains(t, code, "NullableFieldSkipTrue string `json:\"nullableFieldSkipTrue\"`") + assert.Contains(t, code, "NullableFieldSkipFalse *string `json:\"nullableFieldSkipFalse,omitempty\"`") + assert.Contains(t, code, "NullableFieldSkipTrue string `json:\"nullableFieldSkipTrue,omitempty\"`") assert.Contains(t, code, "OptionalField *string `json:\"optionalField,omitempty\"`") assert.Contains(t, code, "OptionalFieldSkipFalse *string `json:\"optionalFieldSkipFalse,omitempty\"`") assert.Contains(t, code, "OptionalFieldSkipTrue string `json:\"optionalFieldSkipTrue,omitempty\"`") diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go index ea6e137473..067d03955d 100644 --- a/pkg/codegen/schema.go +++ b/pkg/codegen/schema.go @@ -734,11 +734,7 @@ func GenFieldsFromProperties(props []Property) []string { shouldOmitEmpty := (!p.Required || p.ReadOnly || p.WriteOnly) && (!p.Required || !p.ReadOnly || !globalState.options.Compatibility.DisableRequiredReadOnlyAsPointer) - omitEmpty := !p.Nullable && shouldOmitEmpty - - if p.Nullable && globalState.options.OutputOptions.NullableType { - omitEmpty = shouldOmitEmpty - } + omitEmpty := shouldOmitEmpty omitZero := false From a701ab9d9965e04dcdfb88f80373057f019b6061 Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Thu, 26 Feb 2026 07:50:21 +0000 Subject: [PATCH 40/44] chore(renovate): add module path to security updates So it's clearer when the PR is for `examples/` or `internal/test/`, similar to regular PRs. --- renovate.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/renovate.json b/renovate.json index 85610cfaa1..685190682d 100644 --- a/renovate.json +++ b/renovate.json @@ -6,6 +6,9 @@ "gomod": { "ignorePaths": [] }, + "vulnerabilityAlerts": { + "commitMessageSuffix": "{{#if isGroup }}{{ else }} ({{#if packageFileDir}}{{packageFileDir}}{{else}}{{packageFile}}{{/if}}){{/if}} [SECURITY]" + }, "packageRules": [ { "description": "Ensure that each directory has their own set of dependency updates, split by the parent directory of the package file (`packageFileDir`). Groups will be unaffected.", From a7de76c97726b529403b82141dd236c89f514cf0 Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Thu, 26 Feb 2026 07:53:31 +0000 Subject: [PATCH 41/44] chore(renovate): override test-only dependencies' label So they don't get pulled into Release Drafter releases. --- renovate.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/renovate.json b/renovate.json index 685190682d..3af37f23db 100644 --- a/renovate.json +++ b/renovate.json @@ -18,6 +18,16 @@ "additionalBranchPrefix": "{{#if isGroup }}{{ else }}{{#if packageFileDir}}{{packageFileDir}}/{{else}}{{packageFile}}/{{/if}}{{/if}}", "commitMessageSuffix": "{{#if isGroup }}{{ else }} ({{#if packageFileDir}}{{packageFileDir}}{{else}}{{packageFile}}{{/if}}){{/if}}" }, + { + "description": "Label example/test code separately", + "matchFileNames": [ + "internal/test/**/*", + "examples/**/*" + ], + "labels": [ + "dependencies-test-only" + ] + }, { "description": "Don't attempt to bump dependencies if they're only used in example code, but allow manually forcing them via Dependency Dashboard", "matchFileNames": [ From 39430368e55b4383004a8a903b626b0f8d8064ee Mon Sep 17 00:00:00 2001 From: Patryk Zdunowski Date: Fri, 27 Feb 2026 01:14:33 +0100 Subject: [PATCH 42/44] feat(embedding): allof embedding structs impl --- internal/test/all_of/openapi.yaml | 12 ++++++++++ internal/test/all_of/v1/openapi.gen.go | 32 +++++++++++++++++--------- internal/test/all_of/v2/openapi.gen.go | 32 +++++++++++++++++--------- pkg/codegen/merge_schemas.go | 13 ++++++++++- pkg/codegen/schema.go | 13 ----------- 5 files changed, 66 insertions(+), 36 deletions(-) diff --git a/internal/test/all_of/openapi.yaml b/internal/test/all_of/openapi.yaml index f73a8e5f1b..4c1cc2948c 100644 --- a/internal/test/all_of/openapi.yaml +++ b/internal/test/all_of/openapi.yaml @@ -51,3 +51,15 @@ components: type: integer format: int64 required: [ ID ] + PersonWithRole: + type: object + description: | + This tests a schema that has both its own properties and an allOf reference. + The generated code should embed the allOf ref and inline the own properties. + properties: + Role: + type: string + allOf: + - $ref: "#/components/schemas/Person" + required: + - Role diff --git a/internal/test/all_of/v1/openapi.gen.go b/internal/test/all_of/v1/openapi.gen.go index b7cfc03d97..0bff97c7cf 100644 --- a/internal/test/all_of/v1/openapi.gen.go +++ b/internal/test/all_of/v1/openapi.gen.go @@ -38,20 +38,30 @@ type PersonWithID struct { ID int64 `json:"ID"` } +// PersonWithRole defines model for PersonWithRole. +type PersonWithRole struct { + // Embedded struct due to allOf(#/components/schemas/Person) + Person `yaml:",inline"` + // Embedded fields due to inline allOf schema + Role string `json:"Role"` +} + // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/5SUT2/bOBDFv8qAu0dBTrCLPegWrNtAQJEaaNIc4gAZSyOLKTVkyVEMwfB3L0jJ/+AW", - "aX0awOTjvDe/0VZVtnOWiSWoYqtC1VKHqVyQD5ZjhcZ8blTxtFV/e2pUof6aHW/Npiuz8fzCW0deNAW1", - "y7bK0/dee6pV8aQ+ah/kDjtSmfqEU/m8e85UTaHy2omO76n7VgfQARBcksxgo6WFDrlGsX6AJgoBcg0G", - "gwBjRxmsegGbJNBAOV8y992KfA5JbmN7U8OKwJP0nqmG1QAIL7ckLxBkMAQ3izKHR4KO/JpAWpqeX7I7", - "eBo7QbbSkocvyTlsWl21YNkM4Lx90zUF2PuGRpOpQ75klSkZHKlC2dUrVaJ2mbqIrNheZEGBAD1NQiAt", - "CgRHlW6GQ0LRJA3pGBpziCGLGS354L0Pk2+Glw+1PnUOxLWzmiWDTUuegLBq4xD2WqMDd9bqcaDFdm8u", - "iNe8juZu7Rt57oilnN+lWcRjjfUdiiqUZvnv32MomoXW5OPFAxuXqrtfhviopS3nf0prYvTc1Cjybpu7", - "7Iztcv47JIOnyvoaMBw5bLztAOF/Tyh0GEMOpUBlWVBzWHKcaiRygsA2gLA4XQ5kwLrWE/6egu19RfDw", - "UM5/yl7sX3NjU8ZaTPzvnoIEuInxQUosJD2VqTfyYXR0nV/lVzF164jRaVWof/Kr/DqygdKmBGfOYEWt", - "NfU48jXJJdhf0ei0zgE2yAIoYChus2WCKJVBsCAxwA6/UQSfOmjRuWE0FGeGUaysVaEWJ0/GyQRnOewX", - "qsHepBZioMSpROeMrpLA7HX6zo1sxOp9cibeUpDnzk7d79LvRwAAAP//lzc18GUFAAA=", + "H4sIAAAAAAAC/5RVTY/jNgz9K4Tao+HMokUPvi2adhGg2AbtbPewGWAYi460lSlVoicwgvz3QrLzhWy7", + "Oz7xQD3yPT7SB9X6PngmlqSag0qtoR5LuKaYPOcInfu9U82ng/o+Uqca9d3i8moxP1lM+evoA0WxlNSx", + "OqhI/ww2klbNJ/WrjUneY0+qUr/hHD4dnyqlKbXRBrG5nno0NoFNgBAKZAV7KwZ6ZI3i4whdBgJkDQ6T", + "AGNPFWwHAV8g0MFquWEe+i3FGgrc3g9Ow5YgkgyRScN2BITndyTPkGR0BG/Xqxo+EvQUdwRiaC6/4XDm", + "NHWC7MVQhD8Lc9gb2xrw7EYI0b9YTQlOvKGz5HSqN6wqJWMg1Si//UytqGOl7iRrDndaUCLASDMQiEGB", + "FKi13XhWKJOksaShc2cZqqzRhs/chzTzZnj+Rdtr5kCsg7csFewNRQLC1uQhnLAmBuGm1ctAm8OJXJJo", + "eZfJvfMvFLknltXyfZlFTut87FFUoyzLTz9eRLEstKOYH569cY96/E8RP1oxq+Vr3Vo8ektqAvlqm8fq", + "xtur5bc4GSK1PmrAdPFhF30PCD9HQqHzGGpYCbSeBS2nDeepZkfOJvAdIKyvlwMZUGs72z9S8kNsCT58", + "WC3/13tZtj+8o9cL92WyQkky3yl7cqvBBFsvBqwk8HuGq33KS5x7z6UhUkeRuKV6w4+GYEdMEYU0tF4T", + "JFOMTP2WdFHj/KrAWHaWp8W9LfIl65443/vreqol6+lOvJxmufMFwEpGUo+F+NvSUVEtlWGoSr1QTJNC", + "b+qH+iFr7wMxBqsa9UP9UL/J3aGY0tgiOGzJeKenfdmR3F+Fv9DZcgsT7JEFUMBRPoWeCTJUBcmD5IH0", + "+Dflq0E9GAxhnMTIUmAGW2nVqPVVySxACp7T6Rp1OLjSQnYjcQkxBGfbArD4PP8kponn6OvumZe1CHnL", + "7Jr9sXz/BgAA//+GXQSVogYAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/all_of/v2/openapi.gen.go b/internal/test/all_of/v2/openapi.gen.go index 912ce6e919..a9c4aa012a 100644 --- a/internal/test/all_of/v2/openapi.gen.go +++ b/internal/test/all_of/v2/openapi.gen.go @@ -38,20 +38,30 @@ type PersonWithID struct { LastName string `json:"LastName"` } +// PersonWithRole defines model for PersonWithRole. +type PersonWithRole struct { + // Embedded struct due to allOf(#/components/schemas/Person) + Person `yaml:",inline"` + // Embedded fields due to inline allOf schema + Role string `json:"Role"` +} + // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/5SUT2/bOBDFv8qAu0dBTrCLPegWrNtAQJEaaNIc4gAZSyOLKTVkyVEMwfB3L0jJ/+AW", - "aX0awOTjvDe/0VZVtnOWiSWoYqtC1VKHqVyQD5ZjhcZ8blTxtFV/e2pUof6aHW/Npiuz8fzCW0deNAW1", - "y7bK0/dee6pV8aQ+ah/kDjtSmfqEU/m8e85UTaHy2omO76n7VgfQARBcksxgo6WFDrlGsX6AJgoBcg0G", - "gwBjRxmsegGbJNBAOV8y992KfA5JbmN7U8OKwJP0nqmG1QAIL7ckLxBkMAQ3izKHR4KO/JpAWpqeX7I7", - "eBo7QbbSkocvyTlsWl21YNkM4Lx90zUF2PuGRpOpQ75klSkZHKlC2dUrVaJ2mbqIrNheZEGBAD1NQiAt", - "CgRHlW6GQ0LRJA3pGBpziCGLGS354L0Pk2+Glw+1PnUOxLWzmiWDTUuegLBq4xD2WqMDd9bqcaDFdm8u", - "iNe8juZu7Rt57oilnN+lWcRjjfUdiiqUZvnv32MomoXW5OPFAxuXqrtfhviopS3nf0prYvTc1Cjybpu7", - "7Iztcv47JIOnyvoaMBw5bLztAOF/Tyh0GEMOpUBlWVBzWHKcaiRygsA2gLA4XQ5kwLrWE/6egu19RfDw", - "UM5/yl7sX3NjU8ZaTPzvnoIEuInxQUosJD2VqTfyYXR0nV/lVzF164jRaVWof/Kr/DqygdKmBGfOYEWt", - "NfU48jXJJdhf0ei0zgE2yAIoYChus2WCKJVBsCAxwA6/UQSfOmjRuWE0FGeGUaysVaEWJ0/GyQRnOewX", - "qsHepBZioMSpROeMrpLA7HX6zo1sxOp9cibeUpDnzk7d79LvRwAAAP//lzc18GUFAAA=", + "H4sIAAAAAAAC/5RVTY/jNgz9K4Tao+HMokUPvi2adhGg2AbtbPewGWAYi460lSlVoicwgvz3QrLzhWy7", + "Oz7xQD3yPT7SB9X6PngmlqSag0qtoR5LuKaYPOcInfu9U82ng/o+Uqca9d3i8moxP1lM+evoA0WxlNSx", + "OqhI/ww2klbNJ/WrjUneY0+qUr/hHD4dnyqlKbXRBrG5nno0NoFNgBAKZAV7KwZ6ZI3i4whdBgJkDQ6T", + "AGNPFWwHAV8g0MFquWEe+i3FGgrc3g9Ow5YgkgyRScN2BITndyTPkGR0BG/Xqxo+EvQUdwRiaC6/4XDm", + "NHWC7MVQhD8Lc9gb2xrw7EYI0b9YTQlOvKGz5HSqN6wqJWMg1Si//UytqGOl7iRrDndaUCLASDMQiEGB", + "FKi13XhWKJOksaShc2cZqqzRhs/chzTzZnj+Rdtr5kCsg7csFewNRQLC1uQhnLAmBuGm1ctAm8OJXJJo", + "eZfJvfMvFLknltXyfZlFTut87FFUoyzLTz9eRLEstKOYH569cY96/E8RP1oxq+Vr3Vo8ektqAvlqm8fq", + "xtur5bc4GSK1PmrAdPFhF30PCD9HQqHzGGpYCbSeBS2nDeepZkfOJvAdIKyvlwMZUGs72z9S8kNsCT58", + "WC3/13tZtj+8o9cL92WyQkky3yl7cqvBBFsvBqwk8HuGq33KS5x7z6UhUkeRuKV6w4+GYEdMEYU0tF4T", + "JFOMTP2WdFHj/KrAWHaWp8W9LfIl65443/vreqol6+lOvJxmufMFwEpGUo+F+NvSUVEtlWGoSr1QTJNC", + "b+qH+iFr7wMxBqsa9UP9UL/J3aGY0tgiOGzJeKenfdmR3F+Fv9DZcgsT7JEFUMBRPoWeCTJUBcmD5IH0", + "+Dflq0E9GAxhnMTIUmAGW2nVqPVVySxACp7T6Rp1OLjSQnYjcQkxBGfbArD4PP8kponn6OvumZe1CHnL", + "7Jr9sXz/BgAA//+GXQSVogYAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/codegen/merge_schemas.go b/pkg/codegen/merge_schemas.go index bb333764f6..d00948f636 100644 --- a/pkg/codegen/merge_schemas.go +++ b/pkg/codegen/merge_schemas.go @@ -13,9 +13,20 @@ import ( func MergeSchemas(schema *openapi3.Schema, path []string) (Schema, error) { allOf := schema.AllOf + // If the parent schema has its own properties alongside allOf, + // include them as a synthetic schema in the allOf list so they + // are not lost during merging. + if len(schema.Properties) > 0 { + parentOnly := *schema + parentOnly.AllOf = nil + allOf = append(allOf, openapi3.NewSchemaRef("", &parentOnly)) + } + // If someone asked for the old way, for backward compatibility, return the // old style result. - if globalState.options.Compatibility.OldMergeSchemas || schema.Extensions[extGoEmbedding] == true { + // Also use v1 merging when the parent has its own properties, because v1 + // generates struct embedding for $ref types which preserves the type hierarchy. + if globalState.options.Compatibility.OldMergeSchemas || schema.Extensions[extGoEmbedding] == true || len(schema.Properties) > 0 { return mergeSchemasV1(allOf, path) } return mergeSchemas(allOf, path) diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go index 76c0582850..4befd7dc06 100644 --- a/pkg/codegen/schema.go +++ b/pkg/codegen/schema.go @@ -351,19 +351,6 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) { return mergedSchema, nil } - // AllOf is interesting, and useful. It's the union of a number of other - // schemas. A common usage is to create a union of an object with an ID, - // so that in a RESTful paradigm, the Create operation can return - // (object, id), so that other operations can refer to (id) - if schema.AllOf != nil { - mergedSchema, err := MergeSchemas(schema, path) - if err != nil { - return Schema{}, fmt.Errorf("error merging schemas: %w", err) - } - mergedSchema.OAPISchema = schema - return mergedSchema, nil - } - // Schema type and format, eg. string / binary t := schema.Type // Handle objects and empty schemas first as a special case From 0adedb71d59a7147094108c0cf8e673a52e4f070 Mon Sep 17 00:00:00 2001 From: Patryk Zdunowski Date: Fri, 27 Feb 2026 01:43:57 +0100 Subject: [PATCH 43/44] feat(embedding): fix after merge --- examples/minimal-server/fiber/go.mod | 8 ++++---- examples/petstore-expanded/fiber/go.mod | 8 ++++---- internal/test/externalref/externalref.gen.go | 2 +- internal/test/issues/issue-1530/issue1530_test.go | 2 +- .../test/strict-server/fiber/fiber_strict_test.go | 2 +- internal/test/strict-server/fiber/go.mod | 12 ++++++------ internal/test/strict-server/strict_test.go | 1 - 7 files changed, 17 insertions(+), 18 deletions(-) diff --git a/examples/minimal-server/fiber/go.mod b/examples/minimal-server/fiber/go.mod index 58abd2fa35..721afc5e52 100644 --- a/examples/minimal-server/fiber/go.mod +++ b/examples/minimal-server/fiber/go.mod @@ -1,10 +1,10 @@ -module github.com/oapi-codegen/oapi-codegen/v2/examples/minimal-server/fiber +module github.com/livesession/oapi-codegen/v2/examples/minimal-server/fiber go 1.24.0 -replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../ +replace github.com/livesession/oapi-codegen/v2 => ../../../ -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen +tool github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen require github.com/gofiber/fiber/v2 v2.52.11 @@ -17,12 +17,12 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect + github.com/livesession/oapi-codegen/v2 v2.0.0-00010101000000-000000000000 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/oapi-codegen/oapi-codegen/v2 v2.0.0-00010101000000-000000000000 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect diff --git a/examples/petstore-expanded/fiber/go.mod b/examples/petstore-expanded/fiber/go.mod index a93bf57445..e8f8060406 100644 --- a/examples/petstore-expanded/fiber/go.mod +++ b/examples/petstore-expanded/fiber/go.mod @@ -1,10 +1,10 @@ -module github.com/oapi-codegen/oapi-codegen/v2/examples/petstore-expanded/fiber +module github.com/livesession/oapi-codegen/v2/examples/petstore-expanded/fiber go 1.24.0 -replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../ +replace github.com/livesession/oapi-codegen/v2 => ../../../ -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen +tool github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen require ( github.com/getkin/kin-openapi v0.133.0 @@ -25,12 +25,12 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect + github.com/livesession/oapi-codegen/v2 v2.0.0-00010101000000-000000000000 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/oapi-codegen/oapi-codegen/v2 v2.0.0-00010101000000-000000000000 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect diff --git a/internal/test/externalref/externalref.gen.go b/internal/test/externalref/externalref.gen.go index 99c1cf3001..fb0e412bd0 100644 --- a/internal/test/externalref/externalref.gen.go +++ b/internal/test/externalref/externalref.gen.go @@ -15,7 +15,7 @@ import ( "github.com/getkin/kin-openapi/openapi3" externalRef0 "github.com/livesession/oapi-codegen/v2/internal/test/externalref/packageA" package_b "github.com/livesession/oapi-codegen/v2/internal/test/externalref/packageB" - externalRef2 "github.com/livesession/oapi-codegen/v2/internal/test/externalref/petstore" + externalRef1 "github.com/livesession/oapi-codegen/v2/internal/test/externalref/petstore" ) // Container defines model for Container. diff --git a/internal/test/issues/issue-1530/issue1530_test.go b/internal/test/issues/issue-1530/issue1530_test.go index a23f7ed2d8..78ea737970 100644 --- a/internal/test/issues/issue-1530/issue1530_test.go +++ b/internal/test/issues/issue-1530/issue1530_test.go @@ -3,7 +3,7 @@ package issue1530_test import ( "testing" - issue1530 "github.com/oapi-codegen/oapi-codegen/v2/internal/test/issues/issue-1530" + issue1530 "github.com/livesession/oapi-codegen/v2/internal/test/issues/issue-1530" "github.com/stretchr/testify/require" ) diff --git a/internal/test/strict-server/fiber/fiber_strict_test.go b/internal/test/strict-server/fiber/fiber_strict_test.go index cb8b509b08..989860fc66 100644 --- a/internal/test/strict-server/fiber/fiber_strict_test.go +++ b/internal/test/strict-server/fiber/fiber_strict_test.go @@ -15,7 +15,7 @@ import ( "github.com/gofiber/fiber/v2/middleware/adaptor" "github.com/stretchr/testify/assert" - clientAPI "github.com/oapi-codegen/oapi-codegen/v2/internal/test/strict-server/client" + clientAPI "github.com/livesession/oapi-codegen/v2/internal/test/strict-server/client" "github.com/oapi-codegen/runtime" "github.com/oapi-codegen/testutil" ) diff --git a/internal/test/strict-server/fiber/go.mod b/internal/test/strict-server/fiber/go.mod index add341aacf..71331c7725 100644 --- a/internal/test/strict-server/fiber/go.mod +++ b/internal/test/strict-server/fiber/go.mod @@ -1,17 +1,17 @@ -module github.com/oapi-codegen/oapi-codegen/v2/internal/test/strict-server/fiber +module github.com/livesession/oapi-codegen/v2/internal/test/strict-server/fiber go 1.24.0 -replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../../ +replace github.com/livesession/oapi-codegen/v2 => ../../../../ -replace github.com/oapi-codegen/oapi-codegen/v2/internal/test => ../.. +replace github.com/livesession/oapi-codegen/v2/internal/test => ../.. -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen +tool github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen require ( github.com/getkin/kin-openapi v0.133.0 github.com/gofiber/fiber/v2 v2.52.11 - github.com/oapi-codegen/oapi-codegen/v2/internal/test v0.0.0-00010101000000-000000000000 + github.com/livesession/oapi-codegen/v2/internal/test v0.0.0-00010101000000-000000000000 github.com/oapi-codegen/runtime v1.1.0 github.com/oapi-codegen/testutil v1.0.0 github.com/stretchr/testify v1.11.1 @@ -27,12 +27,12 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect + github.com/livesession/oapi-codegen/v2 v2.0.0-00010101000000-000000000000 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/oapi-codegen/oapi-codegen/v2 v2.0.0-00010101000000-000000000000 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect diff --git a/internal/test/strict-server/strict_test.go b/internal/test/strict-server/strict_test.go index 2e65ce20f0..35ccb13059 100644 --- a/internal/test/strict-server/strict_test.go +++ b/internal/test/strict-server/strict_test.go @@ -20,7 +20,6 @@ import ( chiAPI "github.com/livesession/oapi-codegen/v2/internal/test/strict-server/chi" clientAPI "github.com/livesession/oapi-codegen/v2/internal/test/strict-server/client" echoAPI "github.com/livesession/oapi-codegen/v2/internal/test/strict-server/echo" - fiberAPI "github.com/livesession/oapi-codegen/v2/internal/test/strict-server/fiber" ginAPI "github.com/livesession/oapi-codegen/v2/internal/test/strict-server/gin" irisAPI "github.com/livesession/oapi-codegen/v2/internal/test/strict-server/iris" From bde27134f78689984593e5b334f901dc8feabd01 Mon Sep 17 00:00:00 2001 From: Patryk Zdunowski Date: Fri, 27 Feb 2026 01:53:40 +0100 Subject: [PATCH 44/44] feat(embedding): another fix after merge --- examples/output-options/type-mapping/generate.go | 2 +- examples/output-options/type-mapping/typemapping.gen.go | 2 +- internal/test/array_pointer/array_pointer.gen.go | 6 +++--- internal/test/issues/issue-1087/deps/deps.gen.go | 8 +++++++- internal/test/issues/issue-1529/strict-echo/doc.go | 2 +- .../test/issues/issue-1529/strict-echo/issue1529.gen.go | 2 +- internal/test/issues/issue-1529/strict-fiber/doc.go | 2 +- internal/test/issues/issue-1529/strict-fiber/go.mod | 8 ++++---- .../test/issues/issue-1529/strict-fiber/issue1529.gen.go | 2 +- internal/test/issues/issue-1529/strict-iris/doc.go | 2 +- .../test/issues/issue-1529/strict-iris/issue1529.gen.go | 2 +- internal/test/issues/issue-1530/doc.go | 2 +- internal/test/issues/issue-1530/issue1530.gen.go | 2 +- internal/test/issues/issue-200/doc.go | 2 +- internal/test/issues/issue-200/issue200.gen.go | 2 +- internal/test/issues/issue-2031/prefer/generate.go | 2 +- internal/test/issues/issue-2031/prefer/issue2031.gen.go | 2 +- internal/test/issues/issue-2185/generate.go | 2 +- internal/test/issues/issue-2185/issue2185.gen.go | 2 +- internal/test/issues/issue-2190/generate.go | 2 +- internal/test/issues/issue-2190/issue2190.gen.go | 2 +- internal/test/issues/issue-2232/generate.go | 2 +- internal/test/issues/issue-2232/issue2232.gen.go | 2 +- internal/test/issues/issue1469/go.mod | 8 ++++---- renovate.json | 4 ++-- 25 files changed, 40 insertions(+), 34 deletions(-) diff --git a/examples/output-options/type-mapping/generate.go b/examples/output-options/type-mapping/generate.go index 8344a10709..0110b3dc62 100644 --- a/examples/output-options/type-mapping/generate.go +++ b/examples/output-options/type-mapping/generate.go @@ -3,4 +3,4 @@ package typemapping // The configuration in this directory overrides the default handling of // "type: number" from producing an `int` to producing an `int64`, and we // override `type: string, format: date` to be a custom type in this package. -//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml +//go:generate go run github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/examples/output-options/type-mapping/typemapping.gen.go b/examples/output-options/type-mapping/typemapping.gen.go index 744cab7f9c..66b1f69f94 100644 --- a/examples/output-options/type-mapping/typemapping.gen.go +++ b/examples/output-options/type-mapping/typemapping.gen.go @@ -1,6 +1,6 @@ // Package typemapping provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +// Code generated by github.com/livesession/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. package typemapping // EmployeeDatabaseRecord defines model for EmployeeDatabaseRecord. diff --git a/internal/test/array_pointer/array_pointer.gen.go b/internal/test/array_pointer/array_pointer.gen.go index c35f91b7f7..7841fceb95 100644 --- a/internal/test/array_pointer/array_pointer.gen.go +++ b/internal/test/array_pointer/array_pointer.gen.go @@ -7,7 +7,7 @@ package array_pointer type DateHistogram struct { DocCount *int `json:"doc_count,omitempty"` Key *string `json:"key,omitempty"` - KeyAsString *string `json:"key_as_string"` + KeyAsString *string `json:"key_as_string,omitempty"` } // Response defines model for Response. @@ -20,6 +20,6 @@ type ResponsePlaceholder struct { // Embedded struct due to allOf(#/components/schemas/Response) Response `yaml:",inline"` // Embedded fields due to inline allOf schema - Hello *string `json:"hello,omitempty"` - Past7DaysHistogram []*DateHistogram `json:"past_7_days_histogram"` + Hello *string `json:"hello,omitempty"` + Past7DaysHistogram []DateHistogram `json:"past_7_days_histogram,omitempty"` } diff --git a/internal/test/issues/issue-1087/deps/deps.gen.go b/internal/test/issues/issue-1087/deps/deps.gen.go index e58ed37ea6..50e091b827 100644 --- a/internal/test/issues/issue-1087/deps/deps.gen.go +++ b/internal/test/issues/issue-1087/deps/deps.gen.go @@ -31,7 +31,13 @@ type BaseError struct { } // Error defines model for Error. -type Error = BaseError +type Error struct { + // Embedded struct due to allOf(#/components/schemas/BaseError) + BaseError `yaml:",inline"` + // Embedded fields due to inline allOf schema + // Reason A reason code specific to the service and can be used to identify the exact issue. Should be unique within a domain + Reason string `json:"reason"` +} // N401 defines model for 401. type N401 = Error diff --git a/internal/test/issues/issue-1529/strict-echo/doc.go b/internal/test/issues/issue-1529/strict-echo/doc.go index 4bf78249fa..ee9c68d2b3 100644 --- a/internal/test/issues/issue-1529/strict-echo/doc.go +++ b/internal/test/issues/issue-1529/strict-echo/doc.go @@ -1,3 +1,3 @@ package issue1529 -//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml +//go:generate go run github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-1529/strict-echo/issue1529.gen.go b/internal/test/issues/issue-1529/strict-echo/issue1529.gen.go index cd5260fae4..b281e47c12 100644 --- a/internal/test/issues/issue-1529/strict-echo/issue1529.gen.go +++ b/internal/test/issues/issue-1529/strict-echo/issue1529.gen.go @@ -1,6 +1,6 @@ // Package issue1529 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +// Code generated by github.com/livesession/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. package issue1529 import ( diff --git a/internal/test/issues/issue-1529/strict-fiber/doc.go b/internal/test/issues/issue-1529/strict-fiber/doc.go index 4bf78249fa..ee9c68d2b3 100644 --- a/internal/test/issues/issue-1529/strict-fiber/doc.go +++ b/internal/test/issues/issue-1529/strict-fiber/doc.go @@ -1,3 +1,3 @@ package issue1529 -//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml +//go:generate go run github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-1529/strict-fiber/go.mod b/internal/test/issues/issue-1529/strict-fiber/go.mod index 5e881be269..b043118b0d 100644 --- a/internal/test/issues/issue-1529/strict-fiber/go.mod +++ b/internal/test/issues/issue-1529/strict-fiber/go.mod @@ -1,10 +1,10 @@ -module github.com/oapi-codegen/oapi-codegen/v2/internal/test/issues/issue-1529/strict-fiber +module github.com/livesession/oapi-codegen/v2/internal/test/issues/issue-1529/strict-fiber go 1.24.0 -replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../../../ +replace github.com/livesession/oapi-codegen/v2 => ../../../../../ -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen +tool github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen require ( github.com/getkin/kin-openapi v0.133.0 @@ -19,12 +19,12 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect + github.com/livesession/oapi-codegen/v2 v2.5.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/oapi-codegen/oapi-codegen/v2 v2.5.1 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect diff --git a/internal/test/issues/issue-1529/strict-fiber/issue1529.gen.go b/internal/test/issues/issue-1529/strict-fiber/issue1529.gen.go index 4d6ab516f5..95e48effe4 100644 --- a/internal/test/issues/issue-1529/strict-fiber/issue1529.gen.go +++ b/internal/test/issues/issue-1529/strict-fiber/issue1529.gen.go @@ -1,6 +1,6 @@ // Package issue1529 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. +// Code generated by github.com/livesession/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. package issue1529 import ( diff --git a/internal/test/issues/issue-1529/strict-iris/doc.go b/internal/test/issues/issue-1529/strict-iris/doc.go index 4bf78249fa..ee9c68d2b3 100644 --- a/internal/test/issues/issue-1529/strict-iris/doc.go +++ b/internal/test/issues/issue-1529/strict-iris/doc.go @@ -1,3 +1,3 @@ package issue1529 -//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml +//go:generate go run github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-1529/strict-iris/issue1529.gen.go b/internal/test/issues/issue-1529/strict-iris/issue1529.gen.go index a214576e80..6cc09c4c2f 100644 --- a/internal/test/issues/issue-1529/strict-iris/issue1529.gen.go +++ b/internal/test/issues/issue-1529/strict-iris/issue1529.gen.go @@ -1,6 +1,6 @@ // Package issue1529 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +// Code generated by github.com/livesession/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. package issue1529 import ( diff --git a/internal/test/issues/issue-1530/doc.go b/internal/test/issues/issue-1530/doc.go index c6a0133735..f5cfb66d39 100644 --- a/internal/test/issues/issue-1530/doc.go +++ b/internal/test/issues/issue-1530/doc.go @@ -1,3 +1,3 @@ package issue1530 -//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml issue1530.yaml +//go:generate go run github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml issue1530.yaml diff --git a/internal/test/issues/issue-1530/issue1530.gen.go b/internal/test/issues/issue-1530/issue1530.gen.go index 8a60251b2c..54a1368bd6 100644 --- a/internal/test/issues/issue-1530/issue1530.gen.go +++ b/internal/test/issues/issue-1530/issue1530.gen.go @@ -1,6 +1,6 @@ // Package issue1530 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +// Code generated by github.com/livesession/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. package issue1530 import ( diff --git a/internal/test/issues/issue-200/doc.go b/internal/test/issues/issue-200/doc.go index 733ebfce17..2424e2beb2 100644 --- a/internal/test/issues/issue-200/doc.go +++ b/internal/test/issues/issue-200/doc.go @@ -1,3 +1,3 @@ package issue200 -//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml +//go:generate go run github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-200/issue200.gen.go b/internal/test/issues/issue-200/issue200.gen.go index 530c042c7a..4d428a28f1 100644 --- a/internal/test/issues/issue-200/issue200.gen.go +++ b/internal/test/issues/issue-200/issue200.gen.go @@ -1,6 +1,6 @@ // Package issue200 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +// Code generated by github.com/livesession/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. package issue200 import ( diff --git a/internal/test/issues/issue-2031/prefer/generate.go b/internal/test/issues/issue-2031/prefer/generate.go index 1e29681627..753e6d21eb 100644 --- a/internal/test/issues/issue-2031/prefer/generate.go +++ b/internal/test/issues/issue-2031/prefer/generate.go @@ -1,3 +1,3 @@ package issue2031 -//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml openapi.yaml +//go:generate go run github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml openapi.yaml diff --git a/internal/test/issues/issue-2031/prefer/issue2031.gen.go b/internal/test/issues/issue-2031/prefer/issue2031.gen.go index 64b30f3515..bc276bcaa2 100644 --- a/internal/test/issues/issue-2031/prefer/issue2031.gen.go +++ b/internal/test/issues/issue-2031/prefer/issue2031.gen.go @@ -1,6 +1,6 @@ // Package issue2031 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +// Code generated by github.com/livesession/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. package issue2031 import ( diff --git a/internal/test/issues/issue-2185/generate.go b/internal/test/issues/issue-2185/generate.go index 9f5224eaeb..f8ae327fd9 100644 --- a/internal/test/issues/issue-2185/generate.go +++ b/internal/test/issues/issue-2185/generate.go @@ -1,3 +1,3 @@ package issue2185 -//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml +//go:generate go run github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-2185/issue2185.gen.go b/internal/test/issues/issue-2185/issue2185.gen.go index 6474c9c4d0..c8a1d1e7f3 100644 --- a/internal/test/issues/issue-2185/issue2185.gen.go +++ b/internal/test/issues/issue-2185/issue2185.gen.go @@ -1,6 +1,6 @@ // Package issue2185 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +// Code generated by github.com/livesession/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. package issue2185 import ( diff --git a/internal/test/issues/issue-2190/generate.go b/internal/test/issues/issue-2190/generate.go index 74169c3fe3..6a4b4f4cd4 100644 --- a/internal/test/issues/issue-2190/generate.go +++ b/internal/test/issues/issue-2190/generate.go @@ -1,3 +1,3 @@ package issue2190 -//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml +//go:generate go run github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-2190/issue2190.gen.go b/internal/test/issues/issue-2190/issue2190.gen.go index c22e32724c..ee6e4b998f 100644 --- a/internal/test/issues/issue-2190/issue2190.gen.go +++ b/internal/test/issues/issue-2190/issue2190.gen.go @@ -2,7 +2,7 @@ // Package issue2190 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +// Code generated by github.com/livesession/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. package issue2190 import ( diff --git a/internal/test/issues/issue-2232/generate.go b/internal/test/issues/issue-2232/generate.go index f6767c10e1..5a47c7865c 100644 --- a/internal/test/issues/issue-2232/generate.go +++ b/internal/test/issues/issue-2232/generate.go @@ -1,3 +1,3 @@ package issue2232 -//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml +//go:generate go run github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/issues/issue-2232/issue2232.gen.go b/internal/test/issues/issue-2232/issue2232.gen.go index e795e7ad72..113440a3d8 100644 --- a/internal/test/issues/issue-2232/issue2232.gen.go +++ b/internal/test/issues/issue-2232/issue2232.gen.go @@ -2,7 +2,7 @@ // Package issue2232 provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +// Code generated by github.com/livesession/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. package issue2232 import ( diff --git a/internal/test/issues/issue1469/go.mod b/internal/test/issues/issue1469/go.mod index fbd28f728a..88af6c7fe5 100644 --- a/internal/test/issues/issue1469/go.mod +++ b/internal/test/issues/issue1469/go.mod @@ -1,10 +1,10 @@ -module github.com/oapi-codegen/oapi-codegen/v2/internal/test/issues/issue1469 +module github.com/livesession/oapi-codegen/v2/internal/test/issues/issue1469 go 1.24.0 -replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../../ +replace github.com/livesession/oapi-codegen/v2 => ../../../../ -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen +tool github.com/livesession/oapi-codegen/v2/cmd/oapi-codegen require ( github.com/gofiber/fiber/v2 v2.52.11 @@ -21,12 +21,12 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect + github.com/livesession/oapi-codegen/v2 v2.0.0-00010101000000-000000000000 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/oapi-codegen/oapi-codegen/v2 v2.0.0-00010101000000-000000000000 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect diff --git a/renovate.json b/renovate.json index 3af37f23db..937309a128 100644 --- a/renovate.json +++ b/renovate.json @@ -39,7 +39,7 @@ { "description": "Don't attempt to bump `replace` statements for internal modules", "matchDepNames": [ - "github.com/oapi-codegen/oapi-codegen/v2" + "github.com/livesession/oapi-codegen/v2" ], "matchCurrentVersion": "v2.0.0-00010101000000-000000000000", "enabled": false @@ -47,7 +47,7 @@ { "description": "Don't attempt to bump `replace` statements for internal modules", "matchDepNames": [ - "github.com/oapi-codegen/oapi-codegen/v2/internal/test" + "github.com/livesession/oapi-codegen/v2/internal/test" ], "matchCurrentVersion": "v0.0.0-00010101000000-000000000000", "enabled": false