Skip to content

Commit

Permalink
Generic route registration (#304)
Browse files Browse the repository at this point in the history
* Generic route registration

* Adds documentation for the architecture of Fuego and how to use it with Gin.
  • Loading branch information
EwenQuim authored Dec 24, 2024
1 parent c93b8d1 commit 1c8071b
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 21 deletions.
18 changes: 18 additions & 0 deletions documentation/docs/guides/alternative-routers-support/gin.md
Original file line number Diff line number Diff line change
@@ -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.
File renamed without changes.
13 changes: 13 additions & 0 deletions documentation/docs/internals/02-architecture.md
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions documentation/docs/internals/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"label": "⚙ Internals & Contributing",
"position": 4,
"link": {
"type": "generated-index"
}
}
Binary file added documentation/docs/internals/architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 21 additions & 13 deletions extra/fuegogin/adaptor.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package fuegogin

import (
"log/slog"
"net/http"

"github.com/gin-gonic/gin"
Expand All @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions generic_mux.go
Original file line number Diff line number Diff line change
@@ -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
}
33 changes: 25 additions & 8 deletions mux.go → net_http_mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
File renamed without changes.

0 comments on commit 1c8071b

Please sign in to comment.