8000 feat: Support echo/v5 by jinuthankachan · Pull Request #2188 · oapi-codegen/oapi-codegen · GitHub
[go: up one dir, main page]

Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/oapi-codegen/oapi-codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,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":
Expand Down
15 changes: 15 additions & 0 deletions pkg/codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,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)
Expand Down Expand Up @@ -372,6 +380,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 {
Expand Down
5 changes: 5 additions & 0 deletions pkg/codegen/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ func (o Configuration) Validate() error {
if o.Generate.EchoServer {
nServers++
}
if o.Generate.Echo5Server {
nServers++
}
if o.Generate.GorillaServer {
nServers++
}
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions pkg/codegen/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions pkg/codegen/templates/echo/v5/echo-interface.tmpl
Original file line number Diff line number Diff line change
@@ -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}}
}
33 changes: 33 additions & 0 deletions pkg/codegen/templates/echo/v5/echo-register.tmpl
Original file line number Diff line number Diff line change
@@ -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}}
}
131 changes: 131 additions & 0 deletions pkg/codegen/templates/echo/v5/echo-wrappers.tmpl
Original file line number Diff line number Diff line change
@@ -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(), &params.{{.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}}
16 changes: 16 additions & 0 deletions pkg/codegen/templates/imports.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
97 changes: 97 additions & 0 deletions pkg/codegen/templates/strict/strict-echo5.tmpl
D2FD
Original file line number Diff line number Diff line change
@@ -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}}
0