Files
go-omni/transport/gin/router.go

125 lines
3.7 KiB
Go

// Package gin adapts Gin to GoOmni's transport-neutral router contract.
// Business packages should import httpx and app, not this package.
package gin
import (
"net/http"
"sort"
"strings"
"git.yuntuo.hk/k2safe/go-omni/httpx"
"git.yuntuo.hk/k2safe/go-omni/transport/internal/routing"
ginlib "github.com/gin-gonic/gin"
)
// Router is the default Gin-backed GoOmni router.
type Router struct {
engine *ginlib.Engine
routes []registeredRoute
}
type registeredRoute struct {
method string
segments []routing.Segment
}
// New creates a Gin router without Gin's default logger or recovery middleware.
func New(mode string) *Router {
if mode != "" {
ginlib.SetMode(mode)
}
return NewWithEngine(ginlib.New())
}
// NewWithEngine wraps an existing engine and applies GoOmni's routing policy.
// Native middleware/routes are retained; redirect and fallback settings are
// replaced. Register portable routes through Router, not Engine().
func NewWithEngine(engine *ginlib.Engine) *Router {
if engine == nil {
panic("go-omni/gin: nil engine")
}
engine.RedirectTrailingSlash = false
engine.RedirectFixedPath = false
engine.RemoveExtraSlash = false
engine.UseRawPath = false
// Gin 1.10 can carry backtracking state between method trees during its
// 405 lookup. Derive Allow from registered patterns instead of that lookup.
engine.HandleMethodNotAllowed = false
router := &Router{engine: engine}
engine.NoRoute(router.fallback)
return router
}
func (r *Router) fallback(ctx *ginlib.Context) {
allowed := make(map[string]bool)
for _, route := range r.routes {
if _, matches := routing.Match(route.segments, ctx.Request.URL.Path); matches {
allowed[route.method] = true
}
}
if len(allowed) == 0 {
http.NotFound(ctx.Writer, ctx.Request)
return
}
methods := make([]string, 0, len(allowed))
for method := range allowed {
methods = append(methods, method)
}
sort.Strings(methods)
ctx.Header("Allow", strings.Join(methods, ", "))
http.Error(ctx.Writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
}
// Engine exposes the underlying Gin engine to adapter-specific integrations.
func (r *Router) Engine() *ginlib.Engine { return r.engine }
func (r *Router) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
r.engine.ServeHTTP(writer, request)
}
func (r *Router) Handle(method, path string, handler http.Handler) {
segments := routing.Parse(method, path, handler)
r.engine.Handle(method, path, adapt(handler))
r.routes = append(r.routes, registeredRoute{method: method, segments: segments})
}
func (r *Router) Any(path string, handler http.Handler) {
for _, method := range routing.Methods {
r.Handle(method, path, handler)
}
}
func (r *Router) Group(path string) httpx.RouterGroup {
return &group{router: r, prefix: routing.Join("", path)}
}
type group struct {
router *Router
prefix string
}
func (g *group) Handle(method, path string, handler http.Handler) {
g.router.Handle(method, routing.Join(g.prefix, path), handler)
}
func (g *group) Any(path string, handler http.Handler) {
g.router.Any(routing.Join(g.prefix, path), handler)
}
func (g *group) Group(path string) httpx.RouterGroup {
return &group{router: g.router, prefix: routing.Join(g.prefix, path)}
}
func adapt(handler http.Handler) ginlib.HandlerFunc {
return func(ctx *ginlib.Context) {
params := make(map[string]string, len(ctx.Params))
for _, param := range ctx.Params {
params[param.Key] = param.Value
}
requestContext := httpx.WithRouteParams(ctx.Request.Context(), params)
requestContext = httpx.WithRoutePattern(requestContext, ctx.FullPath())
request := ctx.Request.WithContext(requestContext)
handler.ServeHTTP(ctx.Writer, request)
}
}