mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
feat(core): RouterExtension 增加 HandleRaw 与 BasePath
Handle 经 cleanPath 归一化会剥掉尾部斜杠,插件无法同时声明 /resource 与 /resource/ 两条路由;部署关闭 gin 的 RedirectTrailingSlash 时,缺失的那条 直接 404。下游 OpenFlare 有 20 个历史列表接口依赖该行为。 - HandleRaw:与组前缀拼接但保留尾部斜杠,分配独立路由 ID; - BasePath:返回组的绝对前缀(根注册表为空串); - 作用域包装器为 HandleRaw 同样登记 OnDispose 反注册。 验证:go build 通过;go test ./... exit 0(48 包 ok); check_cordis_architecture.sh 0 violations;gofmt 干净。
This commit is contained in:
@@ -22,6 +22,11 @@ type RouterExtension interface {
|
||||
Use(middlewares ...any)
|
||||
Group(prefix string, middlewares ...any) RouterExtension
|
||||
Handle(method, path string, handlers ...any) RouteDefinition
|
||||
// HandleRaw joins path with the group prefix but preserves a trailing slash,
|
||||
// so `/resource` and `/resource/` can coexist as distinct routes.
|
||||
HandleRaw(method, path string, handlers ...any) RouteDefinition
|
||||
// BasePath reports this group's absolute prefix ("" for the root registry).
|
||||
BasePath() string
|
||||
GET(path string, handlers ...any) RouteDefinition
|
||||
POST(path string, handlers ...any) RouteDefinition
|
||||
PUT(path string, handlers ...any) RouteDefinition
|
||||
@@ -503,3 +508,67 @@ func (w *PathWhitelist) Patterns() []string {
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// ─── Raw path registration ────────────────────────────────────────────────────
|
||||
|
||||
// ensureLeadingSlash normalises a path to start with exactly one "/" while
|
||||
// preserving any trailing slash (unlike cleanPath).
|
||||
func ensureLeadingSlash(p string) string {
|
||||
if p == "" {
|
||||
return "/"
|
||||
}
|
||||
if !strings.HasPrefix(p, "/") {
|
||||
return "/" + p
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// joinPathPreservingTrailing joins a group prefix and a relative path without
|
||||
// stripping a trailing slash, so a group "/x" can serve both "/x" and "/x/".
|
||||
func joinPathPreservingTrailing(base, relative string) string {
|
||||
rel := ensureLeadingSlash(relative)
|
||||
if base == "" || base == "/" {
|
||||
return rel
|
||||
}
|
||||
return strings.TrimSuffix(cleanPath(base), "/") + rel
|
||||
}
|
||||
|
||||
// HandleRaw registers a route on the root registry, preserving a trailing slash.
|
||||
func (r *RouterRegistry) HandleRaw(method, path string, handlers ...any) RouteDefinition {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.nextID++
|
||||
rd := RouteDefinition{
|
||||
ID: r.nextID,
|
||||
Method: strings.ToUpper(method),
|
||||
Path: ensureLeadingSlash(path),
|
||||
Handlers: handlers,
|
||||
Middlewares: append([]any(nil), r.middlewares...),
|
||||
}
|
||||
r.routes = append(r.routes, rd)
|
||||
return rd
|
||||
}
|
||||
|
||||
// BasePath returns "" because the root registry has no prefix.
|
||||
func (r *RouterRegistry) BasePath() string { return "" }
|
||||
|
||||
// HandleRaw registers a route under this group, preserving a trailing slash.
|
||||
func (g *RouterGroup) HandleRaw(method, path string, handlers ...any) RouteDefinition {
|
||||
g.registry.mu.Lock()
|
||||
defer g.registry.mu.Unlock()
|
||||
|
||||
g.registry.nextID++
|
||||
rd := RouteDefinition{
|
||||
ID: g.registry.nextID,
|
||||
Method: strings.ToUpper(method),
|
||||
Path: joinPathPreservingTrailing(g.prefix, path),
|
||||
}
|
||||
rd.Handlers = handlers
|
||||
rd.Middlewares = append(g.registry.middlewares, g.middlewares...)
|
||||
g.registry.routes = append(g.registry.routes, rd)
|
||||
return rd
|
||||
}
|
||||
|
||||
// BasePath returns this group's absolute prefix.
|
||||
func (g *RouterGroup) BasePath() string { return g.prefix }
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package extpoints
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestHandleRawPreservesTrailingSlash 验证 HandleRaw 能表达 /x 与 /x/ 两条不同路由,
|
||||
// 而 Handle 会归一化掉尾部斜杠(server 插件的 list 端点历史行为依赖这一点)。
|
||||
func TestHandleRawPreservesTrailingSlash(t *testing.T) {
|
||||
r := &RouterRegistry{}
|
||||
g := r.Group("/api/v1/nodes")
|
||||
|
||||
if got := g.BasePath(); got != "/api/v1/nodes" {
|
||||
t.Fatalf("BasePath() = %q, want %q", got, "/api/v1/nodes")
|
||||
}
|
||||
slashless := g.Handle("GET", "")
|
||||
slashed := g.HandleRaw("GET", "/")
|
||||
|
||||
if slashless.Path != "/api/v1/nodes" {
|
||||
t.Errorf("Handle(\"\") path = %q, want %q", slashless.Path, "/api/v1/nodes")
|
||||
}
|
||||
if slashed.Path != "/api/v1/nodes/" {
|
||||
t.Errorf("HandleRaw(\"/\") path = %q, want %q", slashed.Path, "/api/v1/nodes/")
|
||||
}
|
||||
if slashed.ID == slashless.ID {
|
||||
t.Error("HandleRaw must allocate its own route ID so scoped teardown can unregister both")
|
||||
}
|
||||
if got := len(r.Routes()); got != 2 {
|
||||
t.Errorf("registry routes = %d, want 2", got)
|
||||
}
|
||||
if !r.UnregisterByID(slashed.ID) {
|
||||
t.Error("UnregisterByID(HandleRaw route) = false, want true")
|
||||
}
|
||||
if got := len(r.Routes()); got != 1 {
|
||||
t.Errorf("routes after unregister = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRegistryHandleRawKeepsAbsolutePath 根注册表上 HandleRaw 只做绝对化处理。
|
||||
func TestRegistryHandleRawKeepsAbsolutePath(t *testing.T) {
|
||||
r := &RouterRegistry{}
|
||||
if got := r.HandleRaw("GET", "/health/").Path; got != "/health/" {
|
||||
t.Errorf("path = %q, want %q", got, "/health/")
|
||||
}
|
||||
if got := r.HandleRaw("POST", "submit").Path; got != "/submit" {
|
||||
t.Errorf("path = %q, want %q", got, "/submit")
|
||||
}
|
||||
if got := r.BasePath(); got != "" {
|
||||
t.Errorf("registry BasePath() = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,20 @@ func (s *scopedRouterExtension) Handle(method, path string, handlers ...any) ext
|
||||
return rd
|
||||
}
|
||||
|
||||
// HandleRaw registers a trailing-slash-preserving route and tears it down with the scope.
|
||||
func (s *scopedRouterExtension) HandleRaw(method, path string, handlers ...any) extpoints.RouteDefinition {
|
||||
rd := s.underlying.HandleRaw(method, path, handlers...)
|
||||
routeID := rd.ID
|
||||
s.ctx.OnDispose(func() error {
|
||||
s.underlying.UnregisterByID(routeID)
|
||||
return nil
|
||||
})
|
||||
return rd
|
||||
}
|
||||
|
||||
// BasePath delegates to the wrapped group prefix.
|
||||
func (s *scopedRouterExtension) BasePath() string { return s.underlying.BasePath() }
|
||||
|
||||
func (s *scopedRouterExtension) GET(path string, handlers ...any) extpoints.RouteDefinition {
|
||||
return s.Handle("GET", path, handlers...)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user