From 88b59780fa8d41134080ac675d0f959e70d8c479 Mon Sep 17 00:00:00 2001 From: Jinu Thankachan <7960767+jinuthankachan@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:13:57 +0530 Subject: [PATCH 1/6] feat: server code generation for echo/v5 * feat/echov5-codegen (#6) * server code generation for echo/v5 * does not include: - strict-server for echo/v5 - middlewares for echo/v5 --- cmd/oapi-codegen/oapi-codegen.go | 2 + pkg/codegen/codegen.go | 15 ++ pkg/codegen/configuration.go | 5 + pkg/codegen/operations.go | 6 + .../templates/echo/v5/echo-interface.tmpl | 7 + .../templates/echo/v5/echo-register.tmpl | 33 +++++ .../templates/echo/v5/echo-wrappers.tmpl | 131 ++++++++++++++++++ pkg/codegen/templates/imports.tmpl | 16 +++ .../templates/strict/strict-echo5.tmpl | 97 +++++++++++++ 9 files changed, 312 insertions(+) create mode 100644 pkg/codegen/templates/echo/v5/echo-interface.tmpl create mode 100644 pkg/codegen/templates/echo/v5/echo-register.tmpl create mode 100644 pkg/codegen/templates/echo/v5/echo-wrappers.tmpl create mode 100644 pkg/codegen/templates/strict/strict-echo5.tmpl diff --git a/cmd/oapi-codegen/oapi-codegen.go b/cmd/oapi-codegen/oapi-codegen.go index a3e43498c4..088821f5da 100644 --- a/cmd/oapi-codegen/oapi-codegen.go +++ b/cmd/oapi-codegen/oapi-codegen.go @@ -512,6 +512,8 @@ func generationTargets(cfg *codegen.Configuration, targets []string) error { opts.FiberServer = true case "server", "echo-server", "echo": opts.EchoServer = true + case "echo5", "echo5-server": + opts.Echo5Server = true case "gin", "gin-server": opts.GinServer = true case "gorilla", "gorilla-server": diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go index 0f38b6ff7f..09f47a0c17 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -254,6 +254,14 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { } } + var echo5ServerOut string + if opts.Generate.Echo5Server { + echo5ServerOut, err = GenerateEcho5Server(t, ops) + if err != nil { + return "", fmt.Errorf("error generating Go handlers for Paths: %w", err) + } + } + var chiServerOut string if opts.Generate.ChiServer { chiServerOut, err = GenerateChiServer(t, ops) @@ -398,6 +406,13 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { } } + if opts.Generate.Echo5Server { + _, err = w.WriteString(echo5ServerOut) + if err != nil { + return "", fmt.Errorf("error writing server path handlers: %w", err) + } + } + if opts.Generate.ChiServer { _, err = w.WriteString(chiServerOut) if err != nil { diff --git a/pkg/codegen/configuration.go b/pkg/codegen/configuration.go index 4598bb04ad..e2dc5bd5b3 100644 --- a/pkg/codegen/configuration.go +++ b/pkg/codegen/configuration.go @@ -50,6 +50,9 @@ func (o Configuration) Validate() error { if o.Generate.EchoServer { nServers++ } + if o.Generate.Echo5Server { + nServers++ + } if o.Generate.GorillaServer { nServers++ } @@ -112,6 +115,8 @@ type GenerateOptions struct { FiberServer bool `yaml:"fiber-server,omitempty"` // EchoServer specifies whether to generate echo server boilerplate EchoServer bool `yaml:"echo-server,omitempty"` + // Echo5Server specifies whether to generate echo v5 server boilerplate + Echo5Server bool `yaml:"echo5-server,omitempty"` // GinServer specifies whether to generate gin server boilerplate GinServer bool `yaml:"gin-server,omitempty"` // GorillaServer specifies whether to generate Gorilla server boilerplate diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index 1743d270ea..25bcea3fde 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -1009,6 +1009,12 @@ func GenerateEchoServer(t *template.Template, operations []OperationDefinition) return GenerateTemplates([]string{"echo/echo-interface.tmpl", "echo/echo-wrappers.tmpl", "echo/echo-register.tmpl"}, t, operations) } +// GenerateEcho5Server generates all the go code for the ServerInterface as well as +// all the wrapper functions around our handlers. +func GenerateEcho5Server(t *template.Template, operations []OperationDefinition) (string, error) { + return GenerateTemplates([]string{"echo/v5/echo-interface.tmpl", "echo/v5/echo-wrappers.tmpl", "echo/v5/echo-register.tmpl"}, t, operations) +} + // GenerateGinServer generates all the go code for the ServerInterface as well as // all the wrapper functions around our handlers. func GenerateGinServer(t *template.Template, operations []OperationDefinition) (string, error) { diff --git a/pkg/codegen/templates/echo/v5/echo-interface.tmpl b/pkg/codegen/templates/echo/v5/echo-interface.tmpl new file mode 100644 index 0000000000..1531eef866 --- /dev/null +++ b/pkg/codegen/templates/echo/v5/echo-interface.tmpl @@ -0,0 +1,7 @@ +// ServerInterface represents all server handlers. +type ServerInterface interface { +{{range .}}{{.SummaryAsComment }} +// ({{.Method}} {{.Path}}) +{{.OperationId}}(ctx *echo.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error +{{end}} +} diff --git a/pkg/codegen/templates/echo/v5/echo-register.tmpl b/pkg/codegen/templates/echo/v5/echo-register.tmpl new file mode 100644 index 0000000000..78de21b83e --- /dev/null +++ b/pkg/codegen/templates/echo/v5/echo-register.tmpl @@ -0,0 +1,33 @@ + + +// This is a simple interface which specifies echo.Route addition functions which +// are present on both echo.Echo and echo.Group, since we want to allow using +// either of them for path registration +type EchoRouter interface { + CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo +} + +// 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) { +{{if .}} + wrapper := ServerInterfaceWrapper{ + Handler: si, + } +{{end}} +{{range .}}router.{{.Method}}(baseURL + "{{.Path | swaggerUriToEchoUri}}", wrapper.{{.OperationId}}) +{{end}} +} diff --git a/pkg/codegen/templates/echo/v5/echo-wrappers.tmpl b/pkg/codegen/templates/echo/v5/echo-wrappers.tmpl new file mode 100644 index 0000000000..8e1f6b7c2f --- /dev/null +++ b/pkg/codegen/templates/echo/v5/echo-wrappers.tmpl @@ -0,0 +1,131 @@ +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +{{range .}}{{$opid := .OperationId}}// {{$opid}} converts echo context to params. +func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx *echo.Context) error { + var err error +{{range .PathParams}}// ------------- Path parameter "{{.ParamName}}" ------------- + var {{$varName := .GoVariableName}}{{$varName}} {{.TypeDef}} +{{if .IsPassThrough}} + {{$varName}} = ctx.PathParam("{{.ParamName}}") +{{end}} +{{if .IsJson}} + err = json.Unmarshal([]byte(ctx.PathParam("{{.ParamName}}")), &{{$varName}}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") + } +{{end}} +{{if .IsStyled}} + err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", ctx.PathParam("{{.ParamName}}"), &{{$varName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: {{.Explode}}, Required: {{.Required}}}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) + } +{{end}} +{{end}} + +{{range .SecurityDefinitions}} + ctx.Set({{.ProviderName | sanitizeGoIdentity | ucFirst}}Scopes, {{toStringArray .Scopes}}) +{{end}} + +{{if .RequiresParamObject}} + // Parameter object where we will unmarshal all parameters from the context + var params {{.OperationId}}Params +{{range $paramIdx, $param := .QueryParams}} + {{- if (or (or .Required .IsPassThrough) (or .IsJson .IsStyled)) -}} + // ------------- {{if .Required}}Required{{else}}Optional{{end}} query parameter "{{.ParamName}}" ------------- + {{ end }} + {{if .IsStyled}} + err = runtime.BindQueryParameter("{{.Style}}", {{.Explode}}, {{.Required}}, "{{.ParamName}}", ctx.QueryParams(), ¶ms.{{.GoName}}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) + } + {{else}} + if paramValue := ctx.QueryParam("{{.ParamName}}"); paramValue != "" { + {{if .IsPassThrough}} + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue + {{end}} + {{if .IsJson}} + var value {{.TypeDef}} + err = json.Unmarshal([]byte(paramValue), &value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") + } + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value + {{end}} + }{{if .Required}} else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{.ParamName}} is required, but not found")) + }{{end}} + {{end}} +{{end}} + +{{if .HeaderParams}} + headers := ctx.Request().Header +{{range .HeaderParams}}// ------------- {{if .Required}}Required{{else}}Optional{{end}} header parameter "{{.ParamName}}" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("{{.ParamName}}")]; found { + var {{.GoName}} {{.TypeDef}} + n := len(valueList) + if n != 1 { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for {{.ParamName}}, got %d", n)) + } +{{if .IsPassThrough}} + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}valueList[0] +{{end}} +{{if .IsJson}} + err = json.Unmarshal([]byte(valueList[0]), &{{.GoName}}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") + } +{{end}} +{{if .IsStyled}} + err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", valueList[0], &{{.GoName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: {{.Explode}}, Required: {{.Required}}}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) + } +{{end}} + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} + } {{if .Required}}else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter {{.ParamName}} is required, but not found")) + }{{end}} +{{end}} +{{end}} + +{{range .CookieParams}} + if cookie, err := ctx.Cookie("{{.ParamName}}"); err == nil { + {{if .IsPassThrough}} + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}cookie.Value + {{end}} + {{if .IsJson}} + var value {{.TypeDef}} + var decoded string + decoded, err := url.QueryUnescape(cookie.Value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Error unescaping cookie parameter '{{.ParamName}}'") + } + err = json.Unmarshal([]byte(decoded), &value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") + } + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value + {{end}} + {{if .IsStyled}} + var value {{.TypeDef}} + err = runtime.BindStyledParameterWithOptions("simple", "{{.ParamName}}", cookie.Value, &value, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationCookie, Explode: {{.Explode}}, Required: {{.Required}}}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) + } + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value + {{end}} + }{{if .Required}} else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{.ParamName}} is required, but not found")) + }{{end}} + +{{end}}{{/* .CookieParams */}} + +{{end}}{{/* .RequiresParamObject */}} + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.{{.OperationId}}(ctx{{genParamNames .PathParams}}{{if .RequiresParamObject}}, params{{end}}) + return err +} +{{end}} diff --git a/pkg/codegen/templates/imports.tmpl b/pkg/codegen/templates/imports.tmpl index 4d19422423..aa759751ed 100644 --- a/pkg/codegen/templates/imports.tmpl +++ b/pkg/codegen/templates/imports.tmpl @@ -28,19 +28,35 @@ import ( "github.com/oapi-codegen/runtime" "github.com/oapi-codegen/nullable" + strictecho5 "github.com/oapi-codegen/runtime/strictmiddleware/echo/v5" strictecho "github.com/oapi-codegen/runtime/strictmiddleware/echo" strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin" strictiris "github.com/oapi-codegen/runtime/strictmiddleware/iris" strictnethttp "github.com/oapi-codegen/runtime/strictmiddleware/nethttp" openapi_types "github.com/oapi-codegen/runtime/types" "github.com/getkin/kin-openapi/openapi3" + {{- if opts.Generate.ChiServer }} "github.com/go-chi/chi/v5" + {{- end }} + {{- if opts.Generate.EchoServer }} "github.com/labstack/echo/v4" + {{- end }} + {{- if opts.Generate.Echo5Server }} + "github.com/labstack/echo/v5" + {{- end }} + {{- if opts.Generate.GinServer }} "github.com/gin-gonic/gin" + {{- end }} + {{- if opts.Generate.FiberServer }} "github.com/gofiber/fiber/v2" + {{- end }} + {{- if opts.Generate.IrisServer }} "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/core/router" + {{- end }} + {{- if opts.Generate.GorillaServer }} "github.com/gorilla/mux" + {{- end }} {{- range .ExternalImports}} {{ . }} {{- end}} diff --git a/pkg/codegen/templates/strict/strict-echo5.tmpl b/pkg/codegen/templates/strict/strict-echo5.tmpl new file mode 100644 index 0000000000..1f377bfdab --- /dev/null +++ b/pkg/codegen/templates/strict/strict-echo5.tmpl @@ -0,0 +1,97 @@ +type StrictHandlerFunc = strictecho5.StrictEchoHandlerFunc +type StrictMiddlewareFunc = strictecho5.StrictEchoMiddlewareFunc + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc +} + +{{range .}} + {{$opid := .OperationId}} + // {{$opid}} operation middleware + func (sh *strictHandler) {{.OperationId}}(ctx *echo.Context{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}}) error { + var request {{$opid | ucFirst}}RequestObject + + {{range .PathParams -}} + request.{{.GoName}} = {{.GoVariableName}} + {{end -}} + + {{if .RequiresParamObject -}} + request.Params = params + {{end -}} + + {{ if .HasMaskedRequestContentTypes -}} + request.ContentType = ctx.Request().Header.Get("Content-Type") + {{end -}} + + {{$multipleBodies := gt (len .Bodies) 1 -}} + {{range .Bodies -}} + {{if $multipleBodies}}if strings.HasPrefix(ctx.Request().Header.Get("Content-Type"), "{{.ContentType}}") { {{end}} + {{if .IsJSON -}} + var body {{$opid}}{{.NameTag}}RequestBody + if err := ctx.Bind(&body); err != nil { + return err + } + request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body + {{else if eq .NameTag "Formdata" -}} + if form, err := ctx.FormParams(); err == nil { + var body {{$opid}}{{.NameTag}}RequestBody + if err := runtime.BindForm(&body, form, nil, nil); err != nil { + return err + } + request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body + } else { + return err + } + {{else if eq .NameTag "Multipart" -}} + {{if eq .ContentType "multipart/form-data" -}} + if reader, err := ctx.Request().MultipartReader(); err != nil { + return err + } else { + request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = reader + } + {{else -}} + if _, params, err := mime.ParseMediaType(ctx.Request().Header.Get("Content-Type")); err != nil { + return err + } else if boundary := params["boundary"]; boundary == "" { + return http.ErrMissingBoundary + } else { + request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = multipart.NewReader(ctx.Request().Body, boundary) + } + {{end -}} + {{else if eq .NameTag "Text" -}} + data, err := io.ReadAll(ctx.Request().Body) + if err != nil { + return err + } + body := {{$opid}}{{.NameTag}}RequestBody(data) + request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body + {{else -}} + request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = ctx.Request().Body + {{end}}{{/* if eq .NameTag "JSON" */ -}} + {{if $multipleBodies}}}{{end}} + {{end}}{{/* range .Bodies */}} + + handler := func(ctx *echo.Context, request interface{}) (interface{}, error){ + return sh.ssi.{{.OperationId}}(ctx.Request().Context(), request.({{$opid | ucFirst}}RequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "{{.OperationId}}") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.({{$opid | ucFirst}}ResponseObject); ok { + return validResponse.Visit{{$opid}}Response(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil + } +{{end}} From a59838023280c0490428456d9e44430080c339dc Mon Sep 17 00:00:00 2001 From: DanDagadita Date: Sun, 1 Feb 2026 14:45:33 +0200 Subject: [PATCH 2/6] fix: rename PathParam to Param for echo/v5 --- configuration-schema.json | 4 ++++ pkg/codegen/templates/echo/v5/echo-wrappers.tmpl | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/configuration-schema.json b/configuration-schema.json index 43694f9658..619fc05840 100644 --- a/configuration-schema.json +++ b/configuration-schema.json @@ -29,6 +29,10 @@ "type": "boolean", "description": "EchoServer specifies whether to generate echo server boilerplate" }, + "echo5-server": { + "type": "boolean", + "description": "Echo5Server specifies whether to generate echo v5 server boilerplate" + }, "gin-server": { "type": "boolean", "description": "GinServer specifies whether to generate gin server boilerplate" diff --git a/pkg/codegen/templates/echo/v5/echo-wrappers.tmpl b/pkg/codegen/templates/echo/v5/echo-wrappers.tmpl index 8e1f6b7c2f..9c1856167c 100644 --- a/pkg/codegen/templates/echo/v5/echo-wrappers.tmpl +++ b/pkg/codegen/templates/echo/v5/echo-wrappers.tmpl @@ -9,16 +9,16 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx *echo.Context) error { {{range .PathParams}}// ------------- Path parameter "{{.ParamName}}" ------------- var {{$varName := .GoVariableName}}{{$varName}} {{.TypeDef}} {{if .IsPassThrough}} - {{$varName}} = ctx.PathParam("{{.ParamName}}") + {{$varName}} = ctx.Param("{{.ParamName}}") {{end}} {{if .IsJson}} - err = json.Unmarshal([]byte(ctx.PathParam("{{.ParamName}}")), &{{$varName}}) + err = json.Unmarshal([]byte(ctx.Param("{{.ParamName}}")), &{{$varName}}) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") } {{end}} {{if .IsStyled}} - err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", ctx.PathParam("{{.ParamName}}"), &{{$varName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: {{.Explode}}, Required: {{.Required}}}) + err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", ctx.Param("{{.ParamName}}"), &{{$varName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: {{.Explode}}, Required: {{.Required}}}) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) } From 23e1bba8236298cafbfd473f0944f6a47693d817 Mon Sep 17 00:00:00 2001 From: Jinu Thankachan <7960767+jinuthankachan@users.noreply.github.com> Date: Wed, 4 Mar 2026 00:08:31 +0530 Subject: [PATCH 3/6] fix: missing parts for strict server (#9) * fix: add the missing strict server generation --- configuration-schema.json | 12 +++--------- pkg/codegen/operations.go | 3 +++ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/configuration-schema.json b/configuration-schema.json index 20a63f8eb3..d25061e31b 100644 --- a/configuration-schema.json +++ b/configuration-schema.json @@ -201,9 +201,7 @@ "description": "DisableTypeAliasesForType allows defining which OpenAPI `type`s will explicitly not use type aliases", "items": { "type": "string", - "enum": [ - "array" - ] + "enum": ["array"] } }, "name-normalizer": { @@ -230,9 +228,7 @@ "default": true } }, - "required": [ - "path" - ] + "required": ["path"] }, "yaml-tags": { "type": "boolean", @@ -303,9 +299,7 @@ "type": "string" } }, - "required": [ - "package" - ] + "required": ["package"] }, "description": "AdditionalImports defines any additional Go imports to add to the generated code" }, diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index 05073986fb..5d9f735506 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -1132,6 +1132,9 @@ func GenerateStrictServer(t *template.Template, operations []OperationDefinition if opts.Generate.IrisServer { templates = append(templates, "strict/strict-iris-interface.tmpl", "strict/strict-iris.tmpl") } + if opts.Generate.Echo5Server { + templates = append(templates, "strict/strict-interface.tmpl", "strict/strict-echo5.tmpl") + } return GenerateTemplates(templates, t, operations) } From 0a35ba538eef873dc6bdd9ef7e5b891eb4ccacad Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Tue, 3 Mar 2026 10:03:55 -0800 Subject: [PATCH 4/6] Clean up router imports Instead of hardcoding each router's framework and strict middleware imports directly in imports.tmpl with per-router conditional blocks, compute them in Go code via GenerateOptions.RouterImports() and pass them to the template as RouterImports. The template now uses a single range loop over .RouterImports, making it straightforward to add new routers without touching the template. Co-Authored-By: Claude Opus 4.6 --- pkg/codegen/codegen.go | 2 ++ pkg/codegen/configuration.go | 48 ++++++++++++++++++++++++++++++ pkg/codegen/templates/imports.tmpl | 30 ++----------------- 3 files changed, 53 insertions(+), 27 deletions(-) diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go index 2f9d0dc840..3a77729bff 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -1038,12 +1038,14 @@ func GenerateImports(t *template.Template, externalImports []string, packageName ModuleName string Version string AdditionalImports []AdditionalImport + RouterImports []AdditionalImport }{ ExternalImports: externalImports, PackageName: packageName, ModuleName: modulePath, Version: moduleVersion, AdditionalImports: globalState.options.AdditionalImports, + RouterImports: globalState.options.Generate.RouterImports(), } return GenerateTemplates([]string{"imports.tmpl"}, t, context) diff --git a/pkg/codegen/configuration.go b/pkg/codegen/configuration.go index cdb8170456..0f35d13a90 100644 --- a/pkg/codegen/configuration.go +++ b/pkg/codegen/configuration.go @@ -135,6 +135,54 @@ type GenerateOptions struct { ServerURLs bool `yaml:"server-urls,omitempty"` } +// RouterImports returns the framework-specific and strict middleware imports +// needed based on which server type is selected. +func (g GenerateOptions) RouterImports() []AdditionalImport { + var imports []AdditionalImport + + switch { + case g.EchoServer: + imports = append(imports, AdditionalImport{Package: "github.com/labstack/echo/v4"}) + if g.Strict { + imports = append(imports, AdditionalImport{Alias: "strictecho", Package: "github.com/oapi-codegen/runtime/strictmiddleware/echo"}) + } + case g.Echo5Server: + imports = append(imports, AdditionalImport{Package: "github.com/labstack/echo/v5"}) + if g.Strict { + imports = append(imports, AdditionalImport{Alias: "strictecho5", Package: "github.com/oapi-codegen/runtime/strictmiddleware/echo/v5"}) + } + case g.ChiServer: + imports = append(imports, AdditionalImport{Package: "github.com/go-chi/chi/v5"}) + if g.Strict { + imports = append(imports, AdditionalImport{Alias: "strictnethttp", Package: "github.com/oapi-codegen/runtime/strictmiddleware/nethttp"}) + } + case g.GinServer: + imports = append(imports, AdditionalImport{Package: "github.com/gin-gonic/gin"}) + if g.Strict { + imports = append(imports, AdditionalImport{Alias: "strictgin", Package: "github.com/oapi-codegen/runtime/strictmiddleware/gin"}) + } + case g.GorillaServer: + imports = append(imports, AdditionalImport{Package: "github.com/gorilla/mux"}) + if g.Strict { + imports = append(imports, AdditionalImport{Alias: "strictnethttp", Package: "github.com/oapi-codegen/runtime/strictmiddleware/nethttp"}) + } + case g.FiberServer: + imports = append(imports, AdditionalImport{Package: "github.com/gofiber/fiber/v2"}) + case g.IrisServer: + imports = append(imports, AdditionalImport{Package: "github.com/kataras/iris/v12"}) + imports = append(imports, AdditionalImport{Package: "github.com/kataras/iris/v12/core/router"}) + if g.Strict { + imports = append(imports, AdditionalImport{Alias: "strictiris", Package: "github.com/oapi-codegen/runtime/strictmiddleware/iris"}) + } + case g.StdHTTPServer: + if g.Strict { + imports = append(imports, AdditionalImport{Alias: "strictnethttp", Package: "github.com/oapi-codegen/runtime/strictmiddleware/nethttp"}) + } + } + + return imports +} + func (oo GenerateOptions) Validate() map[string]string { return nil } diff --git a/pkg/codegen/templates/imports.tmpl b/pkg/codegen/templates/imports.tmpl index aa759751ed..8c76dad36d 100644 --- a/pkg/codegen/templates/imports.tmpl +++ b/pkg/codegen/templates/imports.tmpl @@ -28,35 +28,11 @@ import ( "github.com/oapi-codegen/runtime" "github.com/oapi-codegen/nullable" - strictecho5 "github.com/oapi-codegen/runtime/strictmiddleware/echo/v5" - strictecho "github.com/oapi-codegen/runtime/strictmiddleware/echo" - strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin" - strictiris "github.com/oapi-codegen/runtime/strictmiddleware/iris" - strictnethttp "github.com/oapi-codegen/runtime/strictmiddleware/nethttp" openapi_types "github.com/oapi-codegen/runtime/types" "github.com/getkin/kin-openapi/openapi3" - {{- if opts.Generate.ChiServer }} - "github.com/go-chi/chi/v5" - {{- end }} - {{- if opts.Generate.EchoServer }} - "github.com/labstack/echo/v4" - {{- end }} - {{- if opts.Generate.Echo5Server }} - "github.com/labstack/echo/v5" - {{- end }} - {{- if opts.Generate.GinServer }} - "github.com/gin-gonic/gin" - {{- end }} - {{- if opts.Generate.FiberServer }} - "github.com/gofiber/fiber/v2" - {{- end }} - {{- if opts.Generate.IrisServer }} - "github.com/kataras/iris/v12" - "github.com/kataras/iris/v12/core/router" - {{- end }} - {{- if opts.Generate.GorillaServer }} - "github.com/gorilla/mux" - {{- end }} + {{- range .RouterImports}} + {{if .Alias}}{{.Alias}} {{end}}"{{.Package}}" + {{- end}} {{- range .ExternalImports}} {{ . }} {{- end}} From a37f3639ffc1681d9c6650ccc9de10ce4030e30f Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Tue, 3 Mar 2026 10:40:28 -0800 Subject: [PATCH 5/6] Add petstore-expanded/echo-v5 example Echo v5 version of the petstore-expanded example, in its own Go module (go 1.25.0) to avoid bumping the Go version of the shared examples module. Key differences from the echo (v4) example: - *echo.Context (pointer) instead of echo.Context (interface) - echo.NewHTTPError(code, message) takes exactly two args - No HTTPError.Internal field - EchoRouter returns echo.RouteInfo instead of *echo.Route - log.Fatal(e.Start(...)) instead of e.Logger.Fatal(...) Includes an inline OpenAPI request validation middleware (middleware/) adapted from echo-middleware for echo v5, since no published echo-v5 middleware package exists yet. Models and server stubs are placeholders until the codegen circular dependency for go:generate in isolated modules is resolved. Co-Authored-By: Claude Opus 4.6 --- examples/petstore-expanded/echo-v5/Makefile | 17 ++ .../echo-v5/api/models.cfg.yaml | 5 + .../echo-v5/api/models/models.gen.go | 46 ++++ .../echo-v5/api/petstore-server.gen.go | 247 ++++++++++++++++++ .../petstore-expanded/echo-v5/api/petstore.go | 146 +++++++++++ .../echo-v5/api/server.cfg.yaml | 9 + examples/petstore-expanded/echo-v5/go.mod | 42 +++ examples/petstore-expanded/echo-v5/go.sum | 191 ++++++++++++++ .../echo-v5/middleware/middleware.go | 226 ++++++++++++++++ .../petstore-expanded/echo-v5/petstore.go | 48 ++++ .../echo-v5/petstore_test.go | 152 +++++++++++ .../petstore-expanded/echo-v5/tools/tools.go | 8 + 12 files changed, 1137 insertions(+) create mode 100644 examples/petstore-expanded/echo-v5/Makefile create mode 100644 examples/petstore-expanded/echo-v5/api/models.cfg.yaml create mode 100644 examples/petstore-expanded/echo-v5/api/models/models.gen.go create mode 100644 examples/petstore-expanded/echo-v5/api/petstore-server.gen.go create mode 100644 examples/petstore-expanded/echo-v5/api/petstore.go create mode 100644 examples/petstore-expanded/echo-v5/api/server.cfg.yaml create mode 100644 examples/petstore-expanded/echo-v5/go.mod create mode 100644 examples/petstore-expanded/echo-v5/go.sum create mode 100644 examples/petstore-expanded/echo-v5/middleware/middleware.go create mode 100644 examples/petstore-expanded/echo-v5/petstore.go create mode 100644 examples/petstore-expanded/echo-v5/petstore_test.go create mode 100644 examples/petstore-expanded/echo-v5/tools/tools.go diff --git a/examples/petstore-expanded/echo-v5/Makefile b/examples/petstore-expanded/echo-v5/Makefile new file mode 100644 index 0000000000..5ec0edd058 --- /dev/null +++ b/examples/petstore-expanded/echo-v5/Makefile @@ -0,0 +1,17 @@ +lint: + $(GOBIN)/golangci-lint run ./... + +lint-ci: + $(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m + +generate: + go generate ./... + +test: + go test -cover ./... + +tidy: + go mod tidy + +tidy-ci: + tidied -verbose diff --git a/examples/petstore-expanded/echo-v5/api/models.cfg.yaml b/examples/petstore-expanded/echo-v5/api/models.cfg.yaml new file mode 100644 index 0000000000..46b5e629c3 --- /dev/null +++ b/examples/petstore-expanded/echo-v5/api/models.cfg.yaml @@ -0,0 +1,5 @@ +# yaml-language-server: $schema=../../../../configuration-schema.json +package: models +generate: + models: true +output: models/models.gen.go diff --git a/examples/petstore-expanded/echo-v5/api/models/models.gen.go b/examples/petstore-expanded/echo-v5/api/models/models.gen.go new file mode 100644 index 0000000000..0945e02abb --- /dev/null +++ b/examples/petstore-expanded/echo-v5/api/models/models.gen.go @@ -0,0 +1,46 @@ +// Package models 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 models + +// Error defines model for Error. +type Error struct { + // Code Error code + Code int32 `json:"code"` + + // Message Error message + Message string `json:"message"` +} + +// 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"` +} + +// 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"` +} + +// FindPetsParams defines parameters for FindPets. +type FindPetsParams struct { + // Tags tags to filter by + Tags *[]string `form:"tags,omitempty" json:"tags,omitempty"` + + // Limit maximum number of results to return + Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` +} + +// AddPetJSONRequestBody defines body for AddPet for application/json ContentType. +type AddPetJSONRequestBody = NewPet diff --git a/examples/petstore-expanded/echo-v5/api/petstore-server.gen.go b/examples/petstore-expanded/echo-v5/api/petstore-server.gen.go new file mode 100644 index 0000000000..c99d14e424 --- /dev/null +++ b/examples/petstore-expanded/echo-v5/api/petstore-server.gen.go @@ -0,0 +1,247 @@ +// Package api 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 api + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "net/http" + "net/url" + "path" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/labstack/echo/v5" + . "github.com/oapi-codegen/oapi-codegen/v2/examples/petstore-expanded/echo-v5/api/models" + "github.com/oapi-codegen/runtime" +) + +// 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 +} + +// 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 = runtime.BindQueryParameter("form", true, false, "tags", 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 = runtime.BindQueryParameter("form", true, false, "limit", 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 = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + 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 = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + 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 +} + +// This is a simple interface which specifies echo.Route addition functions which +// are present on both echo.Echo and echo.Group, since we want to allow using +// either of them for path registration +type EchoRouter interface { + CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo + TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) echo.RouteInfo +} + +// 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+"/pets", wrapper.FindPets) + router.POST(baseURL+"/pets", wrapper.AddPet) + router.DELETE(baseURL+"/pets/:id", wrapper.DeletePet) + router.GET(baseURL+"/pets/:id", wrapper.FindPetByID) + +} + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "H4sIAAAAAAAC/+RXW48budH9KwV+32OnNbEXedBTvB4vICBrT+LdvKznoYZdkmrBSw9Z1FgY6L8HRbZu", + "I3k2QYIgQV506WY1T51zqlj9bGz0YwwUJJv5s8l2TR7rzw8pxaQ/xhRHSsJUL9s4kH4PlG3iUTgGM2+L", + "od7rzDImj2LmhoO8fWM6I9uR2l9aUTK7znjKGVfffND+9iE0S+KwMrtdZxI9Fk40mPkvZtpwv/x+15mP", + "9HRHcok7oL+y3Uf0BHEJsiYYSS437Izg6jLup+34etwLoHV3hTdhQ+c+Lc38l2fz/4mWZm7+b3YUYjap", + "MJty2XUvk+HhEtLPgR8LAQ/nuE7F+MN3V8R4gZQHc7+73+llDsvYJA+CtuImj+zM3ODIQuj/mJ9wtaLU", + "czTdRLH53K7Bu7sF/EToTWdK0qC1yJjns9lJ0K57kcU7yOhHRzVa1ihQMmVAzSZLTASYAQPQ17ZMIgzk", + "Y8iSUAiWhFISZeBQOfg0UtAnve1vII9keckW61adcWwpZDqaw7wb0a4J3vQ3F5ifnp56rLf7mFazKTbP", + "/rR4/+Hj5w+/e9Pf9GvxrjqGks+flp8pbdjS1cRndc1M5WBxp6zdTXmazmwo5cbK7/ub/kYfHUcKOLKZ", + "m7f1UmdGlHX1xEwZ0h+rZrFzXv9CUlLIgM5VKmGZoq8U5W0W8o1r/V8yJVgry9ZSziDxS/iIHjINYGMY", + "2FOQ4oGy9PAjkqWAGYT8GBNkXLEIZ8g4MoUOAllI6xhsyZDJnyxgAfQkPbyjQBgABVYJNzwgYFkV6gAt", + "MNriuIb28L4kfGApCeLAEVxM5DuIKWAioBUJkKMJXSDbgS0pl6wl4chKyT3cFs7gGaSkkXMHY3EbDph0", + "L0pRk+5AOFgeShDYYOKS4deSJfawCLBGC2sFgTkTjA6FEAa2UrzSsWhFpbngwCNny2EFGESzOebueFUc", + "HjIf15hIEu5J1PXgo6MsTMB+pDSwMvVX3qBvCaHjx4IeBkZlJmGGR81tQ44FQgwgMUlMSgkvKQyH3Xu4", + "S0iZgihMCuyPAEoKCJvoiowosKFAARVwI1c/PJakz1iE45OXlCbWl2jZcT7bpO6gH91RXws5DuhIhR06", + "5dFSQtHE9LuHzyWPFAZWlh2qeYboYurUgZmsqJtrltUqmnUHG1qzLQ5BW1saigfHD5RiDz/G9MBAhbOP", + "w6kMersa26HlwNh/CV/CZxqqEiXDktR8Lj7EVAMoHh2TiqTie9Da8FgfOJHP2XVA5axamuTgivpQ3dnD", + "3RozOdcKY6Q0hVeaq7wksMRi+aE0wnG/j647jd+Qm6TjDaWE3fnWWifAQ3coxMAP6x5+FhjJOQpCWU+O", + "MeZCWkn7IupBqcB9FWjR7bncP2mfVmWyq0AOtgglWJDEWerBtGFB6uGHki0BSe0GQ+FDFWinyJYcJa5w", + "mn/3AV7dUrCaxxafMYDHlaZMblKrhz+XFuqjU92aelSad45QukPzASxWi6StnOzZ0p7MMTWZQzWqWVRg", + "4NAdoUyFGzjzHnBWDJalDKxQc0YosvfZJGTb6Yy0ul8Pd6fCVOYmjGMi4eJPOlczTelO/K2tt/+iZ5wO", + "DfW8Wwxmbn7gMOj5Uo+NpARQynUKOT8sBFfa92HJTijBw9boMGDm5rFQ2h5Pel1numlorHOJkK9n0OUU", + "1S5gSrjV/1m29djT8aQOOOcIPH5lr228+AdKOtEkysVJhZXqWfYNTI49yxmo3xxHd/c6AuVRW0tF/+bm", + "Zj/3UGjz2ji6aXKY/ZoV4vO1tF8b5tok94KI3cUANJLAHkwbj5ZYnPxDeF6D0cb6KxuXQF9Hba3ag9ua", + "zuTiPabtlQFCsY0xXxk13idCqTNboCddux/G6lyjZ3DDrkt0nnMuPtFwYdZ3g3rVtOmUsnwfh+2/jIX9", + "ZH1Jwx2JegyHQb8OsM3plCyp0O6f9MxvWuW/xxoXgtf7dR6dPfOwaxZxJFdewNp1jc0cVq6+tcADapuN", + "zTWLW8hFc7rikdsa3Wzyakdb3GoPGZu2E5apf+gAfWwfPFwo/a1ecv1t6rKXfHeZtQJpKIb/JCFvD2JU", + "FbawuFV4r79QnCt20HFx+63j5/ttvff367Ukset/m1z/s2X8QtGmfl1CabOX6fyteP9S3p+82err6e5+", + "97cAAAD//ykDnxlaEgAA", +} + +// 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/examples/petstore-expanded/echo-v5/api/petstore.go b/examples/petstore-expanded/echo-v5/api/petstore.go new file mode 100644 index 0000000000..6313eec228 --- /dev/null +++ b/examples/petstore-expanded/echo-v5/api/petstore.go @@ -0,0 +1,146 @@ +// Copyright 2019 DeepMap, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=models.cfg.yaml ../../petstore-expanded.yaml +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=server.cfg.yaml ../../petstore-expanded.yaml + +package api + +import ( + "fmt" + "net/http" + "sync" + + "github.com/labstack/echo/v5" + "github.com/oapi-codegen/oapi-codegen/v2/examples/petstore-expanded/echo-v5/api/models" +) + +type PetStore struct { + Pets map[int64]models.Pet + NextId int64 + Lock sync.Mutex +} + +func NewPetStore() *PetStore { + return &PetStore{ + Pets: make(map[int64]models.Pet), + NextId: 1000, + } +} + +// sendPetStoreError wraps sending of an error in the Error format, and +// handling the failure to marshal that. +func sendPetStoreError(ctx *echo.Context, code int, message string) error { + petErr := models.Error{ + Code: int32(code), + Message: message, + } + err := ctx.JSON(code, petErr) + return err +} + +// FindPets implements all the handlers in the ServerInterface +func (p *PetStore) FindPets(ctx *echo.Context, params models.FindPetsParams) error { + p.Lock.Lock() + defer p.Lock.Unlock() + + var result []models.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) +} + +func (p *PetStore) AddPet(ctx *echo.Context) error { + // We expect a NewPet object in the request body. + var newPet models.NewPet + err := ctx.Bind(&newPet) + if err != nil { + return sendPetStoreError(ctx, http.StatusBadRequest, "Invalid format for NewPet") + } + // We now have a pet, let's add it to our "database". + + // We're always asynchronous, so lock unsafe operations below + p.Lock.Lock() + defer p.Lock.Unlock() + + // We handle pets, not NewPets, which have an additional ID field + var pet models.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 NewPet + err = ctx.JSON(http.StatusCreated, pet) + if err != nil { + // Something really bad happened, tell Echo that our handler failed + return err + } + + // Return no error. This refers to the handler. Even if we return an HTTP + // error, but everything else is working properly, tell Echo that we serviced + // the error. We should only return errors from Echo handlers if the actual + // servicing of the error on the infrastructure level failed. Returning an + // HTTP/400 or HTTP/500 from here means Echo/HTTP are still working, so + // return nil. + return nil +} + +func (p *PetStore) FindPetByID(ctx *echo.Context, petId int64) error { + p.Lock.Lock() + defer p.Lock.Unlock() + + pet, found := p.Pets[petId] + if !found { + return sendPetStoreError(ctx, http.StatusNotFound, + fmt.Sprintf("Could not find pet with ID %d", petId)) + } + return ctx.JSON(http.StatusOK, pet) +} + +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, + fmt.Sprintf("Could not find pet with ID %d", id)) + } + delete(p.Pets, id) + return ctx.NoContent(http.StatusNoContent) +} diff --git a/examples/petstore-expanded/echo-v5/api/server.cfg.yaml b/examples/petstore-expanded/echo-v5/api/server.cfg.yaml new file mode 100644 index 0000000000..478dc9fc99 --- /dev/null +++ b/examples/petstore-expanded/echo-v5/api/server.cfg.yaml @@ -0,0 +1,9 @@ +# yaml-language-server: $schema=../../../../configuration-schema.json +package: api +output: petstore-server.gen.go +additional-imports: + - package: github.com/oapi-codegen/oapi-codegen/v2/examples/petstore-expanded/echo-v5/api/models + alias: . +generate: + echo5-server: true + embedded-spec: true diff --git a/examples/petstore-expanded/echo-v5/go.mod b/examples/petstore-expanded/echo-v5/go.mod new file mode 100644 index 0000000000..090320989f --- /dev/null +++ b/examples/petstore-expanded/echo-v5/go.mod @@ -0,0 +1,42 @@ +module github.com/oapi-codegen/oapi-codegen/v2/examples/petstore-expanded/echo-v5 + +go 1.25.0 + +replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../ + +require ( + github.com/getkin/kin-openapi v0.133.0 + github.com/labstack/echo/v5 v5.0.4 + github.com/oapi-codegen/oapi-codegen/v2 v2.0.0-00010101000000-000000000000 + github.com/oapi-codegen/runtime v1.2.0 + github.com/oapi-codegen/testutil v1.1.0 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // 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.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/speakeasy-api/jsonpath v0.6.0 // indirect + 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.4.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.42.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/examples/petstore-expanded/echo-v5/go.sum b/examples/petstore-expanded/echo-v5/go.sum new file mode 100644 index 0000000000..6cb9a23cb9 --- /dev/null +++ b/examples/petstore-expanded/echo-v5/go.sum @@ -0,0 +1,191 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +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/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.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +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/labstack/echo/v5 v5.0.4 h1:ll3I/O8BifjMztj9dD1vx/peZQv8cR2CTUdQK6QxGGc= +github.com/labstack/echo/v5 v5.0.4/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +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.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/oapi-codegen/testutil v1.1.0 h1:EufqpNg43acR3qzr3ObhXmWg3Sl2kwtRnUN5GYY4d5g= +github.com/oapi-codegen/testutil v1.1.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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +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.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= +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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +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.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +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/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.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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +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.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/echo-v5/middleware/middleware.go b/examples/petstore-expanded/echo-v5/middleware/middleware.go new file mode 100644 index 0000000000..0a241fbcf6 --- /dev/null +++ b/examples/petstore-expanded/echo-v5/middleware/middleware.go @@ -0,0 +1,226 @@ +// Package middleware provides OpenAPI request validation middleware for Echo v5. +// +// Adapted from github.com/oapi-codegen/echo-middleware (Echo v4) for use +// with github.com/labstack/echo/v5, which has breaking API changes +// (pointer context, NewHTTPError signature, no HTTPError.Internal field). +// +// This is intentionally inlined in the example because there is no published +// echo-v5 middleware package yet. +// TODO: make an echo-v5 middleware repo +package middleware + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "net/url" + "os" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers" + "github.com/getkin/kin-openapi/routers/gorillamux" + "github.com/labstack/echo/v5" + echomiddleware "github.com/labstack/echo/v5/middleware" +) + +const ( + EchoContextKey = "oapi-codegen/echo-context" + UserDataKey = "oapi-codegen/user-data" +) + +// OapiValidatorFromYamlFile creates validator middleware from a YAML file path. +func OapiValidatorFromYamlFile(path string) (echo.MiddlewareFunc, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("error reading %s: %w", path, err) + } + + spec, err := openapi3.NewLoader().LoadFromData(data) + if err != nil { + return nil, fmt.Errorf("error parsing %s as OpenAPI YAML: %w", path, err) + } + return OapiRequestValidator(spec), nil +} + +// OapiRequestValidator creates middleware to validate incoming requests against +// the given OpenAPI 3.x spec with default configuration. +func OapiRequestValidator(spec *openapi3.T) echo.MiddlewareFunc { + return OapiRequestValidatorWithOptions(spec, nil) +} + +// ErrorHandler is called when there is an error in validation. +type ErrorHandler func(c *echo.Context, err *echo.HTTPError) error + +// MultiErrorHandler is called when the OpenAPI filter returns a MultiError. +type MultiErrorHandler func(openapi3.MultiError) *echo.HTTPError + +// Options to customize request validation. +type Options struct { + ErrorHandler ErrorHandler + Options openapi3filter.Options + ParamDecoder openapi3filter.ContentParameterDecoder + UserData any + Skipper echomiddleware.Skipper + MultiErrorHandler MultiErrorHandler + SilenceServersWarning bool + DoNotValidateServers bool + Prefix string +} + +// OapiRequestValidatorWithOptions creates middleware with explicit configuration. +func OapiRequestValidatorWithOptions(spec *openapi3.T, options *Options) echo.MiddlewareFunc { + if options != nil && options.DoNotValidateServers { + spec.Servers = nil + } + + if spec.Servers != nil && (options == nil || !options.SilenceServersWarning) { + log.Println("WARN: OapiRequestValidatorWithOptions called with an OpenAPI spec that has `Servers` set. This may lead to an HTTP 400 with `no matching operation was found` when sending a valid request, as the validator performs `Host` header validation. If you're expecting `Host` header validation, you can silence this warning by setting `Options.SilenceServersWarning = true`. See https://github.com/oapi-codegen/oapi-codegen/issues/882 for more information.") + } + + router, err := gorillamux.NewRouter(spec) + if err != nil { + panic(err) + } + + skipper := getSkipperFromOptions(options) + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + if skipper(c) { + return next(c) + } + + err := validateRequestFromContext(c, router, options) + if err != nil { + if options != nil && options.ErrorHandler != nil { + return options.ErrorHandler(c, err) + } + return err + } + return next(c) + } + } +} + +// validateRequestFromContext does the work of validating a request. +func validateRequestFromContext(ctx *echo.Context, router routers.Router, options *Options) *echo.HTTPError { + req := ctx.Request() + + if options != nil && options.Prefix != "" { + clone := req.Clone(req.Context()) + clone.URL.Path = strings.TrimPrefix(clone.URL.Path, options.Prefix) + req = clone + } + + route, pathParams, err := router.FindRoute(req) + if err != nil { + if errors.Is(err, routers.ErrMethodNotAllowed) { + return echo.NewHTTPError(http.StatusMethodNotAllowed, http.StatusText(http.StatusMethodNotAllowed)) + } + + switch e := err.(type) { + case *routers.RouteError: + return echo.NewHTTPError(http.StatusNotFound, e.Reason) + default: + return echo.NewHTTPError(http.StatusInternalServerError, + fmt.Sprintf("error validating route: %s", err.Error())) + } + } + + for k, v := range pathParams { + if unescaped, err := url.PathUnescape(v); err == nil { + pathParams[k] = unescaped + } + } + + validationInput := &openapi3filter.RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + } + + requestContext := context.WithValue(context.Background(), EchoContextKey, ctx) //nolint:staticcheck + + if options != nil { + validationInput.Options = &options.Options + validationInput.ParamDecoder = options.ParamDecoder + requestContext = context.WithValue(requestContext, UserDataKey, options.UserData) //nolint:staticcheck + } + + err = openapi3filter.ValidateRequest(requestContext, validationInput) + if err != nil { + me := openapi3.MultiError{} + if errors.As(err, &me) { + errFunc := getMultiErrorHandlerFromOptions(options) + return errFunc(me) + } + + switch e := err.(type) { + case *openapi3filter.RequestError: + errorLines := strings.Split(e.Error(), "\n") + return echo.NewHTTPError(http.StatusBadRequest, errorLines[0]) + case *openapi3filter.SecurityRequirementsError: + for _, err := range e.Errors { + httpErr, ok := err.(*echo.HTTPError) + if ok { + return httpErr + } + } + return echo.NewHTTPError(http.StatusForbidden, e.Error()) + default: + return echo.NewHTTPError(http.StatusInternalServerError, + fmt.Sprintf("error validating request: %s", err)) + } + } + return nil +} + +// GetEchoContext gets the echo context from within requests. It returns +// nil if not found or wrong type. +func GetEchoContext(c context.Context) *echo.Context { + iface := c.Value(EchoContextKey) + if iface == nil { + return nil + } + eCtx, ok := iface.(*echo.Context) + if !ok { + return nil + } + return eCtx +} + +// GetUserData gets the user data from the context. +func GetUserData(c context.Context) any { + return c.Value(UserDataKey) +} + +func getSkipperFromOptions(options *Options) echomiddleware.Skipper { + if options == nil { + return echomiddleware.DefaultSkipper + } + + if options.Skipper == nil { + return echomiddleware.DefaultSkipper + } + + return options.Skipper +} + +func getMultiErrorHandlerFromOptions(options *Options) MultiErrorHandler { + if options == nil { + return defaultMultiErrorHandler + } + + if options.MultiErrorHandler == nil { + return defaultMultiErrorHandler + } + + return options.MultiErrorHandler +} + +func defaultMultiErrorHandler(me openapi3.MultiError) *echo.HTTPError { + return echo.NewHTTPError(http.StatusBadRequest, me.Error()) +} diff --git a/examples/petstore-expanded/echo-v5/petstore.go b/examples/petstore-expanded/echo-v5/petstore.go new file mode 100644 index 0000000000..9742cc1939 --- /dev/null +++ b/examples/petstore-expanded/echo-v5/petstore.go @@ -0,0 +1,48 @@ +// 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 +// +// The code under api/petstore/ has been generated from that specification. +package main + +import ( + "flag" + "fmt" + "log" + "net" + "os" + + "github.com/labstack/echo/v5" + "github.com/oapi-codegen/oapi-codegen/v2/examples/petstore-expanded/echo-v5/api" + mw "github.com/oapi-codegen/oapi-codegen/v2/examples/petstore-expanded/echo-v5/middleware" +) + +func main() { + port := flag.String("port", "8080", "Port for test HTTP server") + flag.Parse() + + swagger, err := api.GetSwagger() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err) + os.Exit(1) + } + + // Clear out the servers array in the swagger spec, that skips validating + // that server names match. We don't know how this thing will be run. + swagger.Servers = nil + + // Create an instance of our handler which satisfies the generated interface + petStore := api.NewPetStore() + + // This is how you set up a basic Echo router + e := echo.New() + // Use our validation middleware to check all requests against the + // OpenAPI schema. + e.Use(mw.OapiRequestValidator(swagger)) + + // We now register our petStore above as the handler for the interface + api.RegisterHandlers(e, petStore) + + // And we serve HTTP until the world ends. + log.Fatal(e.Start(net.JoinHostPort("0.0.0.0", *port))) +} diff --git a/examples/petstore-expanded/echo-v5/petstore_test.go b/examples/petstore-expanded/echo-v5/petstore_test.go new file mode 100644 index 0000000000..61d0530a23 --- /dev/null +++ b/examples/petstore-expanded/echo-v5/petstore_test.go @@ -0,0 +1,152 @@ +// Copyright 2019 DeepMap, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "net/http" + "testing" + + "github.com/labstack/echo/v5" + "github.com/oapi-codegen/oapi-codegen/v2/examples/petstore-expanded/echo-v5/api" + "github.com/oapi-codegen/oapi-codegen/v2/examples/petstore-expanded/echo-v5/api/models" + mw "github.com/oapi-codegen/oapi-codegen/v2/examples/petstore-expanded/echo-v5/middleware" + "github.com/oapi-codegen/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPetStore(t *testing.T) { + var err error + // Here, we Initialize echo + e := echo.New() + + // Now, we create our empty pet store + store := api.NewPetStore() + + // Get the swagger description of our API + swagger, err := api.GetSwagger() + require.NoError(t, err) + + // This disables swagger server name validation. It seems to work poorly, + // and requires our test server to be in that list. + swagger.Servers = nil + + // Validate requests against the OpenAPI spec + e.Use(mw.OapiRequestValidator(swagger)) + + // We register the autogenerated boilerplate and bind our PetStore to this + // echo router. + api.RegisterHandlers(e, store) + + // At this point, we can start sending simulated Http requests, and record + // the HTTP responses to check for validity. This exercises every part of + // the stack except the well-tested HTTP system in Go, which there is no + // point for us to test. + tag := "TagOfSpot" + newPet := models.NewPet{ + Name: "Spot", + Tag: &tag, + } + result := testutil.NewRequest().Post("/pets").WithJsonBody(newPet).GoWithHTTPHandler(t, e) + // We expect 201 code on successful pet insertion + assert.Equal(t, http.StatusCreated, result.Code()) + + // We should have gotten a response from the server with the new pet. Make + // sure that its fields match. + var resultPet models.Pet + err = result.UnmarshalBodyToObject(&resultPet) + assert.NoError(t, err, "error unmarshaling response") + assert.Equal(t, newPet.Name, resultPet.Name) + assert.Equal(t, *newPet.Tag, *resultPet.Tag) + + // This is the Id of the pet we inserted. + petId := resultPet.Id + + // Test the getter function. + result = testutil.NewRequest().Get(fmt.Sprintf("/pets/%d", petId)).WithAcceptJson().GoWithHTTPHandler(t, e) + var resultPet2 models.Pet + err = result.UnmarshalBodyToObject(&resultPet2) + assert.NoError(t, err, "error getting pet") + assert.Equal(t, resultPet, resultPet2) + + // We should get a 404 on invalid ID + result = testutil.NewRequest().Get("/pets/27179095781").WithAcceptJson().GoWithHTTPHandler(t, e) + assert.Equal(t, http.StatusNotFound, result.Code()) + var petError models.Error + err = result.UnmarshalBodyToObject(&petError) + assert.NoError(t, err, "error getting response", err) + assert.Equal(t, int32(http.StatusNotFound), petError.Code) + + // Let's insert another pet for subsequent tests. + tag = "TagOfFido" + newPet = models.NewPet{ + Name: "Fido", + Tag: &tag, + } + result = testutil.NewRequest().Post("/pets").WithJsonBody(newPet).GoWithHTTPHandler(t, e) + // We expect 201 code on successful pet insertion + assert.Equal(t, http.StatusCreated, result.Code()) + // We should have gotten a response from the server with the new pet. Make + // sure that its fields match. + err = result.UnmarshalBodyToObject(&resultPet) + assert.NoError(t, err, "error unmarshaling response") + petId2 := resultPet.Id + + // Now, list all pets, we should have two + result = testutil.NewRequest().Get("/pets").WithAcceptJson().GoWithHTTPHandler(t, e) + assert.Equal(t, http.StatusOK, result.Code()) + var petList []models.Pet + err = result.UnmarshalBodyToObject(&petList) + assert.NoError(t, err, "error getting response", err) + assert.Equal(t, 2, len(petList)) + + // Filter pets by tag, we should have 1 + petList = nil + result = testutil.NewRequest().Get("/pets?tags=TagOfFido").WithAcceptJson().GoWithHTTPHandler(t, e) + assert.Equal(t, http.StatusOK, result.Code()) + err = result.UnmarshalBodyToObject(&petList) + assert.NoError(t, err, "error getting response", err) + assert.Equal(t, 1, len(petList)) + + // Filter pets by non existent tag, we should have 0 + petList = nil + result = testutil.NewRequest().Get("/pets?tags=NotExists").WithAcceptJson().GoWithHTTPHandler(t, e) + assert.Equal(t, http.StatusOK, result.Code()) + err = result.UnmarshalBodyToObject(&petList) + assert.NoError(t, err, "error getting response", err) + assert.Equal(t, 0, len(petList)) + + // Let's delete non-existent pet + result = testutil.NewRequest().Delete("/pets/7").GoWithHTTPHandler(t, e) + assert.Equal(t, http.StatusNotFound, result.Code()) + err = result.UnmarshalBodyToObject(&petError) + assert.NoError(t, err, "error unmarshaling PetError") + assert.Equal(t, int32(http.StatusNotFound), petError.Code) + + // Now, delete both real pets + result = testutil.NewRequest().Delete(fmt.Sprintf("/pets/%d", petId)).GoWithHTTPHandler(t, e) + assert.Equal(t, http.StatusNoContent, result.Code()) + result = testutil.NewRequest().Delete(fmt.Sprintf("/pets/%d", petId2)).GoWithHTTPHandler(t, e) + assert.Equal(t, http.StatusNoContent, result.Code()) + + // Should have no pets left. + petList = nil + result = testutil.NewRequest().Get("/pets").WithAcceptJson().GoWithHTTPHandler(t, e) + assert.Equal(t, http.StatusOK, result.Code()) + err = result.UnmarshalBodyToObject(&petList) + assert.NoError(t, err, "error getting response", err) + assert.Equal(t, 0, len(petList)) +} diff --git a/examples/petstore-expanded/echo-v5/tools/tools.go b/examples/petstore-expanded/echo-v5/tools/tools.go new file mode 100644 index 0000000000..8615cb4c57 --- /dev/null +++ b/examples/petstore-expanded/echo-v5/tools/tools.go @@ -0,0 +1,8 @@ +//go:build tools +// +build tools + +package tools + +import ( + _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" +) From 8f3e91a400072650d470054a3f595e39ea174e75 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Tue, 3 Mar 2026 11:18:37 -0800 Subject: [PATCH 6/6] fix: skip echo-v5 example on Go < 1.25 The echo-v5 example requires Go 1.25+ (for echo/v5). When CI runs with GOTOOLCHAIN=local on Go 1.24, the module refuses to build. Add a Go version guard to the Makefile so all targets skip gracefully with an informational message on older Go versions. Co-Authored-By: Claude Opus 4.6 --- examples/petstore-expanded/echo-v5/Makefile | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/examples/petstore-expanded/echo-v5/Makefile b/examples/petstore-expanded/echo-v5/Makefile index 5ec0edd058..7eb1896df1 100644 --- a/examples/petstore-expanded/echo-v5/Makefile +++ b/examples/petstore-expanded/echo-v5/Makefile @@ -1,3 +1,9 @@ +# This module requires Go 1.25+ (for echo/v5). Skip gracefully on older versions. +GO_VERSION := $(shell go version | sed 's/.*go\([0-9]*\)\.\([0-9]*\).*/\1\2/') +GO_125_OR_LATER := $(shell [ "$(GO_VERSION)" -ge 125 ] 2>/dev/null && echo yes || echo no) + +ifeq ($(GO_125_OR_LATER),yes) + lint: $(GOBIN)/golangci-lint run ./... @@ -15,3 +21,10 @@ tidy: tidy-ci: tidied -verbose + +else + +lint generate test tidy lint-ci tidy-ci: + @echo "Skipping echo-v5 example: requires Go 1.25+ (found $(shell go version))" + +endif