Skip to content

Commit

Permalink
Echo compatibility (#327)
Browse files Browse the repository at this point in the history
feat: add fuegoecho alpha for #260 #304 parity
  • Loading branch information
EwenQuim authored Jan 15, 2025
2 parents 6752f86 + dedd2d1 commit a92f1eb
Show file tree
Hide file tree
Showing 6 changed files with 400 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

.vscode
.idea
coverage.out
.DS_Store
**/openapi.json
Expand Down
120 changes: 120 additions & 0 deletions extra/fuegoecho/adaptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package fuegoecho

import (
"net/http"

"github.com/labstack/echo/v4"

"github.com/go-fuego/fuego"
"github.com/go-fuego/fuego/internal"
)

type echoIRouter interface {
Add(method, path string, handler echo.HandlerFunc, middleware ...echo.MiddlewareFunc) *echo.Route
}

func AddEcho(engine *fuego.Engine, echoRouter *echo.Echo,
method, path string, handler echo.HandlerFunc,
options ...func(*fuego.BaseRoute)) *fuego.Route[any, any] {
return handleEcho(engine, echoRouter, method, path, handler, options...)
}

func GetEcho(engine *fuego.Engine, echoRouter *echo.Echo,
path string, handler echo.HandlerFunc,
options ...func(*fuego.BaseRoute)) *fuego.Route[any, any] {
return handleEcho(engine, echoRouter, http.MethodGet, path, handler, options...)
}

func PostEcho(engine *fuego.Engine, echoRouter *echo.Echo,
path string, handler echo.HandlerFunc,
options ...func(*fuego.BaseRoute)) *fuego.Route[any, any] {
return handleEcho(engine, echoRouter, http.MethodPost, path, handler, options...)
}

func PutEcho(engine *fuego.Engine, echoRouter *echo.Echo,
path string, handler echo.HandlerFunc,
options ...func(*fuego.BaseRoute)) *fuego.Route[any, any] {
return handleEcho(engine, echoRouter, http.MethodPut, path, handler, options...)
}

func PatchEcho(engine *fuego.Engine, echoRouter *echo.Echo,
path string, handler echo.HandlerFunc,
options ...func(*fuego.BaseRoute)) *fuego.Route[any, any] {
return handleEcho(engine, echoRouter, http.MethodPatch, path, handler, options...)
}

func DeleteEcho(engine *fuego.Engine, echoRouter *echo.Echo,
path string, handler echo.HandlerFunc,
options ...func(*fuego.BaseRoute)) *fuego.Route[any, any] {
return handleEcho(engine, echoRouter, http.MethodDelete, path, handler, options...)
}

func Add[T, B any](engine *fuego.Engine, echoRouter *echo.Echo, method, path string, handler func(c fuego.ContextWithBody[B]) (T, error), options ...func(*fuego.BaseRoute)) *fuego.Route[T, B] {
return handleFuego(engine, echoRouter, method, path, handler, options...)
}

func Get[T, B any](engine *fuego.Engine, echoRouter *echo.Echo, path string, handler func(c fuego.ContextWithBody[B]) (T, error), options ...func(*fuego.BaseRoute)) *fuego.Route[T, B] {
return handleFuego(engine, echoRouter, http.MethodGet, path, handler, options...)
}

func Post[T, B any](engine *fuego.Engine, echoRouter *echo.Echo, path string, handler func(c fuego.ContextWithBody[B]) (T, error), options ...func(*fuego.BaseRoute)) *fuego.Route[T, B] {
return handleFuego(engine, echoRouter, http.MethodPost, path, handler, options...)
}

func Put[T, B any](engine *fuego.Engine, echoRouter *echo.Echo, path string, handler func(c fuego.ContextWithBody[B]) (T, error), options ...func(*fuego.BaseRoute)) *fuego.Route[T, B] {
return handleFuego(engine, echoRouter, http.MethodPut, path, handler, options...)
}

func Patch[T, B any](engine *fuego.Engine, echoRouter *echo.Echo, path string, handler func(c fuego.ContextWithBody[B]) (T, error), options ...func(*fuego.BaseRoute)) *fuego.Route[T, B] {
return handleFuego(engine, echoRouter, http.MethodPatch, path, handler, options...)
}

func Delete[T, B any](engine *fuego.Engine, echoRouter *echo.Echo, path string, handler func(c fuego.ContextWithBody[B]) (T, error), options ...func(*fuego.BaseRoute)) *fuego.Route[T, B] {
return handleFuego(engine, echoRouter, http.MethodDelete, path, handler, options...)
}

func handleFuego[T, B any](engine *fuego.Engine, echoRouter *echo.Echo, method, path string, fuegoHandler func(c fuego.ContextWithBody[B]) (T, error), options ...func(*fuego.BaseRoute)) *fuego.Route[T, B] {
baseRoute := fuego.NewBaseRoute(method, path, fuegoHandler, engine.OpenAPI, options...)
return fuego.Registers(engine, echoRouteRegisterer[T, B]{
echoRouter: echoRouter,
route: fuego.Route[T, B]{BaseRoute: baseRoute},
echoHandler: EchoHandler(engine, fuegoHandler, baseRoute),
})
}

func handleEcho(engine *fuego.Engine, echoRouter *echo.Echo, method, path string, echoHandler echo.HandlerFunc, options ...func(*fuego.BaseRoute)) *fuego.Route[any, any] {
baseRoute := fuego.NewBaseRoute(method, path, echoHandler, engine.OpenAPI, options...)
return fuego.Registers(engine, echoRouteRegisterer[any, any]{
echoRouter: echoRouter,
route: fuego.Route[any, any]{BaseRoute: baseRoute},
echoHandler: echoHandler,
})
}

type echoRouteRegisterer[T, B any] struct {
echoRouter echoIRouter
echoHandler echo.HandlerFunc
route fuego.Route[T, B]
}

func (a echoRouteRegisterer[T, B]) Register() fuego.Route[T, B] {
route := a.echoRouter.Add(a.route.Method, a.route.Path, a.echoHandler)
a.route.Path = route.Path
return a.route
}

// Convert a Fuego handler to a Gin handler.
func EchoHandler[B, T any](engine *fuego.Engine, handler func(c fuego.ContextWithBody[B]) (T, error), route fuego.BaseRoute) echo.HandlerFunc {
return func(c echo.Context) error {
context := &echoContext[B]{
CommonContext: internal.CommonContext[B]{
CommonCtx: c.Request().Context(),
UrlValues: c.Request().URL.Query(),
OpenAPIParams: route.Params,
},
echoCtx: c,
}
fuego.Flow(engine, context, handler)
return nil
}
}
131 changes: 131 additions & 0 deletions extra/fuegoecho/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package fuegoecho

import (
"context"
"errors"
"net/http"
"strings"

"github.com/go-fuego/fuego"
"github.com/go-fuego/fuego/internal"
"github.com/labstack/echo/v4"
)

type echoContext[B any] struct {
internal.CommonContext[B]
echoCtx echo.Context
}

var (
_ fuego.ContextWithBody[any] = &echoContext[any]{}
_ fuego.ContextFlowable[any] = &echoContext[any]{}
)

func (c echoContext[B]) Body() (B, error) {
var body B
err := c.echoCtx.Bind(&body)
if err != nil {
return body, err
}

return fuego.TransformAndValidate(c, body)
}

func (c echoContext[B]) Context() context.Context {
return c.echoCtx.Request().Context()
}

func (c echoContext[B]) Cookie(name string) (*http.Cookie, error) {
return c.echoCtx.Request().Cookie(name)
}

func (c echoContext[B]) Header(key string) string {
return c.echoCtx.Request().Header.Get(key)
}

func (c echoContext[B]) MustBody() B {
body, err := c.Body()
if err != nil {
panic(err)
}
return body
}

func (c echoContext[B]) PathParam(name string) string {
return c.echoCtx.Param(name)
}

func (c echoContext[B]) MainLang() string {
return strings.Split(c.MainLocale(), "-")[0]
}

func (c echoContext[B]) MainLocale() string {
return strings.Split(c.Request().Header.Get("Accept-Language"), ",")[0]
}

func (c echoContext[B]) Redirect(code int, url string) (any, error) {
c.echoCtx.Redirect(code, url)
return nil, nil
}

func (c echoContext[B]) Render(templateToExecute string, data any, templateGlobsToOverride ...string) (fuego.CtxRenderer, error) {
panic("unimplemented")
}

func (c echoContext[B]) Request() *http.Request {
return c.echoCtx.Request()
}

func (c echoContext[B]) Response() http.ResponseWriter {
return c.echoCtx.Response()
}

func (c echoContext[B]) SetCookie(cookie http.Cookie) {
c.echoCtx.SetCookie(&cookie)
}

func (c echoContext[B]) HasCookie(name string) bool {
_, err := c.Cookie(name)
return err == nil
}

func (c echoContext[B]) HasHeader(key string) bool {
_, ok := c.echoCtx.Request().Header[key]
return ok
}

func (c echoContext[B]) SetHeader(key, value string) {
c.echoCtx.Response().Header().Add(key, value)
}

func (c echoContext[B]) SetStatus(code int) {
c.echoCtx.Response().WriteHeader(code)
}

func (c echoContext[B]) Serialize(data any) error {
status := c.echoCtx.Response().Status
if status == 0 {
status = c.DefaultStatusCode
}
if status == 0 {
status = http.StatusOK
}
c.echoCtx.JSON(status, data)
return nil
}

func (c echoContext[B]) SerializeError(err error) {
statusCode := http.StatusInternalServerError
var errorWithStatusCode fuego.ErrorWithStatus
if errors.As(err, &errorWithStatusCode) {
statusCode = errorWithStatusCode.StatusCode()
}
c.echoCtx.JSON(statusCode, err)
}

func (c echoContext[B]) SetDefaultStatusCode() {
if c.DefaultStatusCode == 0 {
c.DefaultStatusCode = http.StatusOK
}
c.SetStatus(c.DefaultStatusCode)
}
31 changes: 31 additions & 0 deletions extra/fuegoecho/context_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package fuegoecho

import (
"net/http"
"net/url"
)

type ContextTest[B any] struct {
echoContext[B]
BodyInjected B
ErrorInjected error

Params url.Values
}

func (c *ContextTest[B]) Body() (B, error) {
return c.BodyInjected, c.ErrorInjected
}

func (c *ContextTest[B]) Request() *http.Request {
return c.echoCtx.Request()
}

func (c *ContextTest[B]) Response() http.ResponseWriter {
return c.echoCtx.Response().Writer
}

// QueryParam implements fuego.Ctx
func (c *ContextTest[B]) QueryParam(key string) string {
return c.Params.Get(key)
}
37 changes: 37 additions & 0 deletions extra/fuegoecho/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module github.com/go-fuego/fuego/extra/fuegoecho

go 1.23.3

require (
github.com/go-fuego/fuego v0.17.0
github.com/labstack/echo/v4 v4.13.3
)

require (
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
github.com/getkin/kin-openapi v0.128.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.23.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/gorilla/schema v1.4.1 // indirect
github.com/invopop/yaml v0.3.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit a92f1eb

Please sign in to comment.