feat: remove Gin and standardize on nethttp
This commit is contained in:
@@ -12,7 +12,7 @@ application := omni.NewWithRouter(netrouter.New())
|
||||
registerRoutes(application) // 与 omni.New() 复用同一份业务注册代码
|
||||
```
|
||||
|
||||
`transport/gin` 默认使用 Gin 分发请求;`transport/nethttp` 使用独立的标准库路由实现。两者共用路由声明校验和路径匹配规则,Gin 的正常请求仍由 Gin 路由树处理;它的 404/405 回退由适配层统一处理。
|
||||
`transport/nethttp` 是当前内置的标准库路由实现。它提供一套可移植的路径、方法和错误回退规则;自研网络层可以直接实现同一个 `httpx.Router` 契约。
|
||||
|
||||
## 可移植路由规则(v0.3.0)
|
||||
|
||||
@@ -35,8 +35,6 @@ HTTP 回退响应属于路由层,不进入应用路由中间件,不携带统
|
||||
|
||||
业务层通过 `application.Handle("HEAD", path, handler)` 或 `group.Handle("OPTIONS", path, handler)` 显式注册其他方法,同样经过框架中间件和错误处理。
|
||||
|
||||
`gin.NewWithEngine` 保留原生路由和中间件,但覆盖重定向、路径清理和 404/405 策略。可移植契约与 `Allow` 集合只覆盖经 GoOmni Router 注册的路由;直接操作 `Engine()` 的原生扩展不属于该保证。
|
||||
|
||||
## 业务行为
|
||||
|
||||
- 同一套 Handler、Service、中间件用于两种适配器;应用、分组、嵌套分组、路由中间件依次进入、反序退出。
|
||||
@@ -69,9 +67,9 @@ func TestContract(t *testing.T) {
|
||||
|
||||
```sh
|
||||
go test -race ./transport/... ./examples/reference
|
||||
python3 scripts/verifyTransports.py --version v0.3.0
|
||||
python3 scripts/verifyTransports.py --version v0.4.0
|
||||
```
|
||||
|
||||
第一条同时验证完整用户 CRUD 在两种适配器上复用相同 Handler / Service / Repository。第二条在源码目录外创建无 replace 的消费模块,从远程版本导入同一套验收包。
|
||||
第一条验证标准库适配器复用完整 Handler / Service / Repository。第二条在源码目录外创建无 replace 的消费模块,从远程版本导入同一套验收包;自研适配器也应使用同一个公共测试包。
|
||||
|
||||
当前替换边界是路由与 HTTP 适配层,`App.Run` 仍使用标准库 `net/http.Server`。未来自研监听、连接管理或协议解析时,可让自研服务调用 `App.ServeHTTP`(提供标准 HTTP 请求/响应桥接),由自研启动层管理监听与关闭,并在退出时调用 `App.Close`;本阶段没有实现新的网络内核。
|
||||
|
||||
48
transport/benchmark_test.go
Normal file
48
transport/benchmark_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package transport_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp"
|
||||
)
|
||||
|
||||
func BenchmarkTransportRouteDispatch(b *testing.B) {
|
||||
b.Run("nethttp", func(b *testing.B) {
|
||||
router := netrouter.New()
|
||||
router.Handle(http.MethodGet, "/users/:id", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
request := httptest.NewRequest(http.MethodGet, "/users/42", nil)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
writer := httptest.NewRecorder()
|
||||
router.ServeHTTP(writer, request)
|
||||
if writer.Code != http.StatusNoContent {
|
||||
b.Fatalf("unexpected status %d", writer.Code)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkTransportApplicationRequest(b *testing.B) {
|
||||
b.Run("nethttp", func(b *testing.B) {
|
||||
application := omni.NewWithRouter(netrouter.New())
|
||||
application.GET("/health", func(c *omni.Context) error {
|
||||
return c.OK(map[string]string{"status": "ok"})
|
||||
})
|
||||
request := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
writer := httptest.NewRecorder()
|
||||
application.ServeHTTP(writer, request)
|
||||
if writer.Code != http.StatusOK {
|
||||
b.Fatalf("unexpected status %d", writer.Code)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,19 +4,12 @@ import (
|
||||
"testing"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
ginrouter "git.yuntuo.hk/k2safe/go-omni/transport/gin"
|
||||
netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp"
|
||||
"git.yuntuo.hk/k2safe/go-omni/transport/transporttest"
|
||||
)
|
||||
|
||||
func TestAdapterContract(t *testing.T) {
|
||||
for _, adapter := range []struct {
|
||||
name string
|
||||
newRouter func() httpx.Router
|
||||
}{
|
||||
{"gin", func() httpx.Router { return ginrouter.New("test") }},
|
||||
{"nethttp", func() httpx.Router { return netrouter.New() }},
|
||||
} {
|
||||
t.Run(adapter.name, func(t *testing.T) { transporttest.Run(t, adapter.newRouter) })
|
||||
}
|
||||
t.Run("nethttp", func(t *testing.T) {
|
||||
transporttest.Run(t, func() httpx.Router { return netrouter.New() })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func Parse(method, path string, handler http.Handler) []Segment {
|
||||
return segments
|
||||
}
|
||||
|
||||
// CheckConflict enforces Gin-compatible ambiguity rules for one method.
|
||||
// CheckConflict enforces the portable ambiguity rules for one method.
|
||||
func CheckConflict(existing, next []Segment) {
|
||||
for i := 0; i < len(existing) && i < len(next); i++ {
|
||||
a, b := existing[i], next[i]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package nethttp provides a standard-library router for GoOmni, independent
|
||||
// of Gin. Register routes before serving requests.
|
||||
// Package nethttp provides GoOmni's standard-library router. Register routes
|
||||
// before serving requests.
|
||||
package nethttp
|
||||
|
||||
import (
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp"
|
||||
)
|
||||
|
||||
func TestRouterWorksWithoutGin(t *testing.T) {
|
||||
func TestRouterWorks(t *testing.T) {
|
||||
router := netrouter.New()
|
||||
app := omni.NewWithRouter(router)
|
||||
app.GET("/users/:id", func(ctx *omni.Context) error {
|
||||
|
||||
Reference in New Issue
Block a user