From a4861bbcbe2dd1708f9829792e63840a82cba2ba Mon Sep 17 00:00:00 2001 From: EwenQuim Date: Mon, 16 Dec 2024 15:48:20 +0100 Subject: [PATCH] Tried using interface for context in fuegogin --- ctx.go | 14 +++-- extra/fuegogin/adaptor.go | 97 +++++++++++++++++++--------------- extra/fuegogin/adaptor_test.go | 3 +- extra/fuegogin/context.go | 55 +++++++++++-------- extra/fuegogin/context_mock.go | 33 ++++++++++++ extra/fuegogin/lib/lib.go | 15 +++--- extra/fuegogin/lib/lib_test.go | 21 ++++++++ html.go | 2 +- mux.go | 2 +- 9 files changed, 163 insertions(+), 79 deletions(-) create mode 100644 extra/fuegogin/context_mock.go create mode 100644 extra/fuegogin/lib/lib_test.go diff --git a/ctx.go b/ctx.go index 40b7860d..c6c64bc5 100644 --- a/ctx.go +++ b/ctx.go @@ -22,6 +22,15 @@ const ( // It contains the request body, the path parameters, the query parameters, and the HTTP request. // Please do not use a pointer type as parameter. type Ctx[B any] interface { + CommonCtx[B] + + Context() context.Context + + Request() *http.Request // Request returns the underlying HTTP request. + Response() http.ResponseWriter // Response returns the underlying HTTP response writer. +} + +type CommonCtx[B any] interface { // Body returns the body of the request. // If (*B) implements [InTransformer], it will be transformed after deserialization. // It caches the result, so it can be called multiple times. @@ -70,11 +79,6 @@ type Ctx[B any] interface { Header(key string) string // Get request header SetHeader(key, value string) // Sets response header - Context() context.Context - - Request() *http.Request // Request returns the underlying HTTP request. - Response() http.ResponseWriter // Response returns the underlying HTTP response writer. - // SetStatus sets the status code of the response. // Alias to http.ResponseWriter.WriteHeader. SetStatus(code int) diff --git a/extra/fuegogin/adaptor.go b/extra/fuegogin/adaptor.go index 4ab8ad3d..dec120b7 100644 --- a/extra/fuegogin/adaptor.go +++ b/extra/fuegogin/adaptor.go @@ -1,59 +1,76 @@ package fuegogin import ( + "log/slog" + "github.com/getkin/kin-openapi/openapi3" "github.com/gin-gonic/gin" "github.com/go-fuego/fuego" ) -func Get[T, B any]( - s *fuego.OpenAPI, - e *gin.Engine, - path string, - handler func(c *ContextWithBody[B]) (T, error), - options ...func(*fuego.BaseRoute), -) *fuego.Route[T, B] { - return Handle(s, e, "GET", path, handler, options...) +func GetGin(s *fuego.OpenAPI, e gin.IRouter, path string, handler gin.HandlerFunc, options ...func(*fuego.BaseRoute)) *fuego.Route[any, any] { + return handleGin(s, e, "GET", path, handler, options...) +} + +func PostGin(s *fuego.OpenAPI, e gin.IRouter, path string, handler gin.HandlerFunc, options ...func(*fuego.BaseRoute)) *fuego.Route[any, any] { + return handleGin(s, e, "POST", path, handler, options...) +} + +func Get[T, B any](s *fuego.OpenAPI, e gin.IRouter, path string, handler func(c ContextWithBody[B]) (T, error), options ...func(*fuego.BaseRoute)) *fuego.Route[T, B] { + return handleFuego(s, e, "GET", path, handler, options...) +} + +func Post[T, B any](s *fuego.OpenAPI, e gin.IRouter, path string, handler func(c ContextWithBody[B]) (T, error), options ...func(*fuego.BaseRoute)) *fuego.Route[T, B] { + return handleFuego(s, e, "POST", path, handler, options...) +} + +func handleFuego[T, B any](openapi *fuego.OpenAPI, e gin.IRouter, method, path string, fuegoHandler func(c ContextWithBody[B]) (T, error), options ...func(*fuego.BaseRoute)) *fuego.Route[T, B] { + baseRoute := NewBaseRoute(method, path, fuegoHandler, openapi, options...) + return handle(openapi, e, &fuego.Route[T, B]{BaseRoute: baseRoute}, GinHandler(fuegoHandler)) +} + +func handleGin(openapi *fuego.OpenAPI, e gin.IRouter, method, path string, ginHandler gin.HandlerFunc, options ...func(*fuego.BaseRoute)) *fuego.Route[any, any] { + baseRoute := NewBaseRoute(method, path, ginHandler, openapi, options...) + return handle(openapi, e, &fuego.Route[any, any]{BaseRoute: baseRoute}, ginHandler) } -func Post[T, B any]( - s *fuego.OpenAPI, - e *gin.Engine, - path string, - handler func(c *ContextWithBody[B]) (T, error), - options ...func(*fuego.BaseRoute), -) *fuego.Route[T, B] { - return Handle(s, e, "POST", path, handler, options...) +func handle[T, B any](openapi *fuego.OpenAPI, e gin.IRouter, route *fuego.Route[T, B], fuegoHandler gin.HandlerFunc) *fuego.Route[T, B] { + if _, ok := e.(*gin.RouterGroup); ok { + route.Path = e.(*gin.RouterGroup).BasePath() + route.Path + } + + e.Handle(route.Method, route.Path, fuegoHandler) + + err := route.RegisterOpenAPIOperation(openapi) + if err != nil { + slog.Warn("error documenting openapi operation", "error", err) + } + + return route } -func Handle[T, B any]( - openapi *fuego.OpenAPI, - e *gin.Engine, - method, - path string, - handler func(c *ContextWithBody[B]) (T, error), - options ...func(*fuego.BaseRoute), -) *fuego.Route[T, B] { - route := &fuego.Route[T, B]{ - BaseRoute: fuego.BaseRoute{ - Method: method, - Path: path, - Params: make(map[string]fuego.OpenAPIParam), - FullName: fuego.FuncName(handler), - Operation: openapi3.NewOperation(), - OpenAPI: openapi, - }, +func NewBaseRoute(method, path string, handler any, openapi *fuego.OpenAPI, options ...func(*fuego.BaseRoute)) fuego.BaseRoute { + baseRoute := fuego.BaseRoute{ + Method: method, + Path: path, + Params: make(map[string]fuego.OpenAPIParam), + FullName: fuego.FuncName(handler), + Operation: openapi3.NewOperation(), + OpenAPI: openapi, } for _, o := range options { - o(&route.BaseRoute) + o(&baseRoute) } - route.BaseRoute.GenerateDefaultDescription() + return baseRoute +} - e.Handle(method, path, func(c *gin.Context) { - context := &ContextWithBody[B]{ +// Convert a Fuego handler to a Gin handler. +func GinHandler[B, T any](handler func(c ContextWithBody[B]) (T, error)) gin.HandlerFunc { + return func(c *gin.Context) { + context := &contextWithBody[B]{ ginCtx: c, } @@ -69,9 +86,5 @@ func Handle[T, B any]( } c.JSON(200, resp) - }) - - route.RegisterOpenAPIOperation(openapi) - - return route + } } diff --git a/extra/fuegogin/adaptor_test.go b/extra/fuegogin/adaptor_test.go index 357804d1..b170e763 100644 --- a/extra/fuegogin/adaptor_test.go +++ b/extra/fuegogin/adaptor_test.go @@ -1,6 +1,7 @@ package fuegogin_test import ( + "net/http" "net/http/httptest" "testing" @@ -28,6 +29,6 @@ func TestFuegoGin(t *testing.T) { e.ServeHTTP(w, r) require.Equal(t, http.StatusOK, w.Code) - require.Equal(t, `{"message":"Hello "}`, w.Body.String()) + require.JSONEq(t, `{"message":"Hello "}`, w.Body.String()) }) } diff --git a/extra/fuegogin/context.go b/extra/fuegogin/context.go index a6abc20a..f4163412 100644 --- a/extra/fuegogin/context.go +++ b/extra/fuegogin/context.go @@ -1,7 +1,6 @@ package fuegogin import ( - "context" "net/http" "net/url" @@ -10,36 +9,46 @@ import ( "github.com/go-fuego/fuego" ) -type ContextWithBody[B any] struct { - ginCtx *gin.Context +type ContextWithBody[B any] interface { + fuego.CommonCtx[B] + + Request() *http.Request + Response() gin.ResponseWriter + + // Original Gin context + Context() *gin.Context } type ContextNoBody = ContextWithBody[any] +type contextWithBody[B any] struct { + ginCtx *gin.Context +} + // Body implements fuego.Ctx. -func (c *ContextWithBody[B]) Body() (B, error) { +func (c *contextWithBody[B]) Body() (B, error) { var body B err := c.ginCtx.Bind(&body) return body, err } // Context implements fuego.Ctx. -func (c *ContextWithBody[B]) Context() context.Context { +func (c *contextWithBody[B]) Context() *gin.Context { return c.ginCtx } // Cookie implements fuego.Ctx. -func (c *ContextWithBody[B]) Cookie(name string) (*http.Cookie, error) { +func (c *contextWithBody[B]) Cookie(name string) (*http.Cookie, error) { panic("unimplemented") } // Header implements fuego.Ctx. -func (c *ContextWithBody[B]) Header(key string) string { +func (c *contextWithBody[B]) Header(key string) string { return c.ginCtx.GetHeader(key) } // MustBody implements fuego.Ctx. -func (c *ContextWithBody[B]) MustBody() B { +func (c *contextWithBody[B]) MustBody() B { body, err := c.Body() if err != nil { panic(err) @@ -48,76 +57,76 @@ func (c *ContextWithBody[B]) MustBody() B { } // PathParam implements fuego.Ctx. -func (c *ContextWithBody[B]) PathParam(name string) string { +func (c *contextWithBody[B]) PathParam(name string) string { return c.ginCtx.Param(name) } // QueryParam implements fuego.Ctx. -func (c *ContextWithBody[B]) QueryParam(name string) string { +func (c *contextWithBody[B]) QueryParam(name string) string { return c.ginCtx.Query(name) } // QueryParamArr implements fuego.Ctx. -func (c *ContextWithBody[B]) QueryParamArr(name string) []string { +func (c *contextWithBody[B]) QueryParamArr(name string) []string { panic("unimplemented") } // QueryParamBool implements fuego.Ctx. -func (c *ContextWithBody[B]) QueryParamBool(name string) bool { +func (c *contextWithBody[B]) QueryParamBool(name string) bool { panic("unimplemented") } // QueryParamBoolErr implements fuego.Ctx. -func (c *ContextWithBody[B]) QueryParamBoolErr(name string) (bool, error) { +func (c *contextWithBody[B]) QueryParamBoolErr(name string) (bool, error) { panic("unimplemented") } // QueryParamInt implements fuego.Ctx. -func (c *ContextWithBody[B]) QueryParamInt(name string) int { +func (c *contextWithBody[B]) QueryParamInt(name string) int { panic("unimplemented") } // QueryParamIntErr implements fuego.Ctx. -func (c *ContextWithBody[B]) QueryParamIntErr(name string) (int, error) { +func (c *contextWithBody[B]) QueryParamIntErr(name string) (int, error) { panic("unimplemented") } // QueryParams implements fuego.Ctx. -func (c *ContextWithBody[B]) QueryParams() url.Values { +func (c *contextWithBody[B]) QueryParams() url.Values { return c.ginCtx.Request.URL.Query() } // Redirect implements fuego.Ctx. -func (c *ContextWithBody[B]) Redirect(code int, url string) (any, error) { +func (c *contextWithBody[B]) Redirect(code int, url string) (any, error) { c.ginCtx.Redirect(code, url) return nil, nil } // Render implements fuego.Ctx. -func (c *ContextWithBody[B]) Render(templateToExecute string, data any, templateGlobsToOverride ...string) (fuego.CtxRenderer, error) { +func (c *contextWithBody[B]) Render(templateToExecute string, data any, templateGlobsToOverride ...string) (fuego.CtxRenderer, error) { panic("unimplemented") } // Request implements fuego.Ctx. -func (c *ContextWithBody[B]) Request() *http.Request { +func (c *contextWithBody[B]) Request() *http.Request { return c.ginCtx.Request } // Response implements fuego.Ctx. -func (c *ContextWithBody[B]) Response() http.ResponseWriter { +func (c *contextWithBody[B]) Response() gin.ResponseWriter { return c.ginCtx.Writer } // SetCookie implements fuego.Ctx. -func (c *ContextWithBody[B]) SetCookie(cookie http.Cookie) { +func (c *contextWithBody[B]) SetCookie(cookie http.Cookie) { } // SetHeader implements fuego.Ctx. -func (c *ContextWithBody[B]) SetHeader(key, value string) { +func (c *contextWithBody[B]) SetHeader(key, value string) { c.ginCtx.Header(key, value) } // SetStatus implements fuego.Ctx. -func (c *ContextWithBody[B]) SetStatus(code int) { +func (c *contextWithBody[B]) SetStatus(code int) { c.ginCtx.Status(code) } diff --git a/extra/fuegogin/context_mock.go b/extra/fuegogin/context_mock.go new file mode 100644 index 00000000..ca926618 --- /dev/null +++ b/extra/fuegogin/context_mock.go @@ -0,0 +1,33 @@ +package fuegogin + +import ( + "net/http" + "net/url" + + "github.com/gin-gonic/gin" +) + +type ContextTest[B any] struct { + contextWithBody[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.ginCtx.Request +} + +func (c *ContextTest[B]) Response() gin.ResponseWriter { + return c.ginCtx.Writer +} + +// QueryParam implements fuego.Ctx +func (c *ContextTest[B]) QueryParam(key string) string { + return c.Params.Get(key) +} diff --git a/extra/fuegogin/lib/lib.go b/extra/fuegogin/lib/lib.go index 969b24c4..8dcb2639 100644 --- a/extra/fuegogin/lib/lib.go +++ b/extra/fuegogin/lib/lib.go @@ -13,7 +13,7 @@ import ( ) type HelloRequest struct { - Name string `json:"name"` + Word string `json:"word"` } type HelloResponse struct { @@ -27,6 +27,9 @@ func SetupGin() (*gin.Engine, *fuego.OpenAPI) { // Register Gin controller e.GET("/gin", ginController) + group := e.Group("/my-group/:id") + fuegogin.Get(openapi, group, "/fuego", fuegoControllerGet) + // Register to Gin router with Fuego wrapper for same OpenAPI spec fuegogin.Get(openapi, e, "/fuego", fuegoControllerGet) fuegogin.Post(openapi, e, "/fuego-with-options", fuegoControllerPost, @@ -43,6 +46,8 @@ func SetupGin() (*gin.Engine, *fuego.OpenAPI) { e.GET("/openapi.json", serveController(openapi)) e.GET("/swagger", DefaultOpenAPIHandler("/openapi.json")) + fmt.Println("OpenAPI at at http://localhost:8980/swagger") + return e, openapi } @@ -50,24 +55,22 @@ func ginController(c *gin.Context) { c.String(200, "pong") } -func fuegoControllerGet(c *fuegogin.ContextNoBody) (HelloResponse, error) { +func fuegoControllerGet(c fuegogin.ContextNoBody) (HelloResponse, error) { return HelloResponse{ Message: "Hello", }, nil } -func fuegoControllerPost(c *fuegogin.ContextWithBody[HelloRequest]) (HelloResponse, error) { +func fuegoControllerPost(c fuegogin.ContextWithBody[HelloRequest]) (HelloResponse, error) { body, err := c.Body() if err != nil { return HelloResponse{}, err } - fmt.Println("body", body) name := c.QueryParam("name") - fmt.Println("name", name) return HelloResponse{ - Message: "Hello " + body.Name + name, + Message: fmt.Sprintf("Hello %s, %s", body.Word, name), }, nil } diff --git a/extra/fuegogin/lib/lib_test.go b/extra/fuegogin/lib/lib_test.go new file mode 100644 index 00000000..5dd9cfdb --- /dev/null +++ b/extra/fuegogin/lib/lib_test.go @@ -0,0 +1,21 @@ +package lib + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/go-fuego/fuego/extra/fuegogin" +) + +func TestFuegoControllerPost(t *testing.T) { + testCtx := &fuegogin.ContextTest[HelloRequest]{ + BodyInjected: HelloRequest{Word: "World"}, + Params: url.Values{"name": []string{"Ewen"}}, + } + + response, err := fuegoControllerPost(testCtx) + require.NoError(t, err) + require.Equal(t, "Hello World, Ewen", response.Message) +} diff --git a/html.go b/html.go index 5ff246b1..4d8cc59b 100644 --- a/html.go +++ b/html.go @@ -49,7 +49,7 @@ type Renderer interface { type Gomponent = Renderer // HTML is a marker type used to differentiate between a string response and an HTML response. -// To use templating, use [ctx.Render]. +// To use templating, use [Ctx.Render]. type HTML string // H is a shortcut for map[string]any diff --git a/mux.go b/mux.go index 0b05f64e..4a350645 100644 --- a/mux.go +++ b/mux.go @@ -141,7 +141,7 @@ func PatchStd(s *Server, path string, controller func(http.ResponseWriter, *http return registerStdController(s, http.MethodPatch, path, controller, options...) } -func registerFuegoController[T, B any, Contexted ctx[B]](s *Server, method, path string, controller func(Contexted) (T, error), options ...func(*BaseRoute)) *Route[T, B] { +func registerFuegoController[T, B any, Contexted Ctx[B]](s *Server, method, path string, controller func(Contexted) (T, error), options ...func(*BaseRoute)) *Route[T, B] { route := NewRoute[T, B](method, path, controller, s.OpenAPI, append(s.routeOptions, options...)...) acceptHeaderParameter := openapi3.NewHeaderParameter("Accept")