diff --git a/documentation/docs/guides/alternative-routers-support/gin.md b/documentation/docs/guides/alternative-routers-support/gin.md new file mode 100644 index 00000000..50f206a3 --- /dev/null +++ b/documentation/docs/guides/alternative-routers-support/gin.md @@ -0,0 +1,18 @@ +# Using Fuego with Gin + +Fuego can be used with Gin by using the `fuegogin` adaptor. + +Instead of using the **Server** (`fuego.NewServer()`), you will use the **Engine** (`fuego.NewEngine()`) along with your router. + +The usage is similar to the default server, but you will need to declare the routes with `fuegogin.Get`, `fuegogin.Post`... instead of `fuego.Get`, `fuego.Post`... + +## Migrate incrementally + +1. Spawn an engine with `fuego.NewEngine()`. +2. Use `fuegogin.GetGin` instead of `gin.GET` to wrap the routes with OpenAPI declaration of the route, **without even touching the existing controllers**!!! +3. Replace the controllers **one by one** with Fuego controllers. You'll get complete OpenAPI documentation, validation, Content-Negotiation for each controller you replace! +4. Enjoy the benefits of Fuego with your existing Gin application! + +## Example + +Please refer to the [Gin example](https://github.com/go-fuego/fuego/tree/main/examples/gin-compat) for a complete and up-to-date example. diff --git a/documentation/docs/guides/01-data-flow.mdx b/documentation/docs/internals/01-data-flow.mdx similarity index 100% rename from documentation/docs/guides/01-data-flow.mdx rename to documentation/docs/internals/01-data-flow.mdx diff --git a/documentation/docs/internals/02-architecture.md b/documentation/docs/internals/02-architecture.md new file mode 100644 index 00000000..ebfb8f66 --- /dev/null +++ b/documentation/docs/internals/02-architecture.md @@ -0,0 +1,13 @@ +# Architecture + +Fuego's architecture rely on the following components: + +- **Engine**: The engine is responsible for handling the request and response. It is the core of Fuego. + - It contains the **OpenAPI** struct with the Description and OpenAPI-related utilities. + - It also contains the centralized Error Handler. +- **Server**: The default `net/http` server that Fuego uses to listen for incoming requests. + - Responsible for routes, groups and middlewares. +- **Adaptors**: If you use Gin, Echo, or any other web framework, you can use an adaptor to use Fuego with them. +- **Context**: The context is a generic typed interface that represents the state that the user can access & modify in the controller. + +![Fuego Architecture](./architecture.png) diff --git a/documentation/docs/internals/_category_.json b/documentation/docs/internals/_category_.json new file mode 100644 index 00000000..b82b021f --- /dev/null +++ b/documentation/docs/internals/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "⚙ Internals & Contributing", + "position": 4, + "link": { + "type": "generated-index" + } +} diff --git a/documentation/docs/internals/architecture.png b/documentation/docs/internals/architecture.png new file mode 100644 index 00000000..e0e0b7ee Binary files /dev/null and b/documentation/docs/internals/architecture.png differ diff --git a/extra/fuegogin/adaptor.go b/extra/fuegogin/adaptor.go index c1693e64..4d3a4c11 100644 --- a/extra/fuegogin/adaptor.go +++ b/extra/fuegogin/adaptor.go @@ -1,7 +1,6 @@ package fuegogin import ( - "log/slog" "net/http" "github.com/gin-gonic/gin" @@ -28,27 +27,36 @@ func Post[T, B any](engine *fuego.Engine, ginRouter gin.IRouter, path string, ha func handleFuego[T, B any](engine *fuego.Engine, ginRouter gin.IRouter, 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 handle(engine, ginRouter, &fuego.Route[T, B]{BaseRoute: baseRoute}, GinHandler(engine, fuegoHandler, baseRoute)) + return fuego.Registers(engine, ginRouteRegisterer[T, B]{ + ginRouter: ginRouter, + route: fuego.Route[T, B]{BaseRoute: baseRoute}, + ginHandler: GinHandler(engine, fuegoHandler, baseRoute), + }) } func handleGin(engine *fuego.Engine, ginRouter gin.IRouter, method, path string, ginHandler gin.HandlerFunc, options ...func(*fuego.BaseRoute)) *fuego.Route[any, any] { baseRoute := fuego.NewBaseRoute(method, path, ginHandler, engine.OpenAPI, options...) - return handle(engine, ginRouter, &fuego.Route[any, any]{BaseRoute: baseRoute}, ginHandler) + return fuego.Registers(engine, ginRouteRegisterer[any, any]{ + ginRouter: ginRouter, + route: fuego.Route[any, any]{BaseRoute: baseRoute}, + ginHandler: ginHandler, + }) } -func handle[T, B any](engine *fuego.Engine, ginRouter gin.IRouter, route *fuego.Route[T, B], ginHandler gin.HandlerFunc) *fuego.Route[T, B] { - if _, ok := ginRouter.(*gin.RouterGroup); ok { - route.Path = ginRouter.(*gin.RouterGroup).BasePath() + route.Path - } - - ginRouter.Handle(route.Method, route.Path, ginHandler) +type ginRouteRegisterer[T, B any] struct { + ginRouter gin.IRouter + ginHandler gin.HandlerFunc + route fuego.Route[T, B] +} - err := route.RegisterOpenAPIOperation(engine.OpenAPI) - if err != nil { - slog.Warn("error documenting openapi operation", "error", err) +func (a ginRouteRegisterer[T, B]) Register() fuego.Route[T, B] { + if _, ok := a.ginRouter.(*gin.RouterGroup); ok { + a.route.Path = a.ginRouter.(*gin.RouterGroup).BasePath() + a.route.Path } - return route + a.ginRouter.Handle(a.route.Method, a.route.Path, a.ginHandler) + + return a.route } // Convert a Fuego handler to a Gin handler. diff --git a/generic_mux.go b/generic_mux.go new file mode 100644 index 00000000..354ac752 --- /dev/null +++ b/generic_mux.go @@ -0,0 +1,19 @@ +package fuego + +import "log/slog" + +// Registerer is an interface that allows registering routes. +// It can be implementable by any router. +type Registerer[T, B any] interface { + Register() Route[T, B] +} + +func Registers[B, T any](engine *Engine, a Registerer[B, T]) *Route[B, T] { + route := a.Register() + + err := route.RegisterOpenAPIOperation(engine.OpenAPI) + if err != nil { + slog.Warn("error documenting openapi operation", "error", err) + } + return &route +} diff --git a/mux.go b/net_http_mux.go similarity index 90% rename from mux.go rename to net_http_mux.go index 854d57ad..7450dd97 100644 --- a/mux.go +++ b/net_http_mux.go @@ -69,7 +69,9 @@ func Patch[T, B any](s *Server, path string, controller func(ContextWithBody[B]) return registerFuegoController(s, http.MethodPatch, path, controller, options...) } -// Register registers a controller into the default mux and documents it in the OpenAPI spec. +// Register registers a controller into the default net/http mux. +// +// Deprecated: Used internally. Please satisfy the [Registerer] interface instead and pass to [Registers]. func Register[T, B any](s *Server, route Route[T, B], controller http.Handler, options ...func(*BaseRoute)) *Route[T, B] { for _, o := range options { o(&route.BaseRoute) @@ -86,11 +88,6 @@ func Register[T, B any](s *Server, route Route[T, B], controller http.Handler, o route.Middlewares = append(s.middlewares, route.Middlewares...) s.Mux.Handle(fullPath, withMiddlewares(controller, route.Middlewares...)) - err := route.RegisterOpenAPIOperation(s.OpenAPI) - if err != nil { - slog.Warn("error documenting openapi operation", "error", err) - } - return &route } @@ -144,13 +141,21 @@ func registerFuegoController[T, B any](s *Server, method, path string, controlle acceptHeaderParameter.Schema = openapi3.NewStringSchema().NewRef() route.Operation.AddParameter(acceptHeaderParameter) - return Register(s, route, HTTPHandler(s, controller, route.BaseRoute)) + return Registers(s.Engine, netHttpRouteRegisterer[T, B]{ + s: s, + route: route, + controller: HTTPHandler(s, controller, route.BaseRoute), + }) } func registerStdController(s *Server, method, path string, controller func(http.ResponseWriter, *http.Request), options ...func(*BaseRoute)) *Route[any, any] { route := NewRoute[any, any](method, path, controller, s.OpenAPI, append(s.routeOptions, options...)...) - return Register(s, route, http.HandlerFunc(controller)) + return Registers(s.Engine, netHttpRouteRegisterer[any, any]{ + s: s, + route: route, + controller: http.HandlerFunc(controller), + }) } func withMiddlewares(controller http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler { @@ -216,3 +221,15 @@ func DefaultDescription[T any](handler string, middlewares []T) string { return description + "\n\n---\n\n" } + +type netHttpRouteRegisterer[T, B any] struct { + s *Server + controller http.Handler + route Route[T, B] +} + +var _ Registerer[string, any] = netHttpRouteRegisterer[string, any]{} + +func (a netHttpRouteRegisterer[T, B]) Register() Route[T, B] { + return *Register(a.s, a.route, a.controller) +} diff --git a/mux_test.go b/net_http_mux_test.go similarity index 100% rename from mux_test.go rename to net_http_mux_test.go