feat: standardize transport contracts across Gin and nethttp
This commit is contained in:
119
transport/internal/routing/pattern.go
Normal file
119
transport/internal/routing/pattern.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// Package routing defines registration rules shared by the built-in adapters.
|
||||
// Request dispatch remains the responsibility of each adapter.
|
||||
package routing
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var Methods = [...]string{
|
||||
http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
|
||||
http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect, http.MethodTrace,
|
||||
}
|
||||
|
||||
type Segment struct {
|
||||
Literal string
|
||||
Name string
|
||||
Wildcard bool
|
||||
}
|
||||
|
||||
// Parse accepts absolute paths, whole-segment parameters and a terminal
|
||||
// catch-all. It rejects ambiguous patterns before an adapter registers them.
|
||||
func Parse(method, path string, handler http.Handler) []Segment {
|
||||
if handler == nil {
|
||||
panic("go-omni: nil route handler")
|
||||
}
|
||||
if method == "" || strings.IndexFunc(method, func(r rune) bool { return r < 'A' || r > 'Z' }) >= 0 {
|
||||
panic("go-omni: route method must contain uppercase letters")
|
||||
}
|
||||
if path == "" || path[0] != '/' {
|
||||
panic("go-omni: route path must start with /")
|
||||
}
|
||||
parts := strings.Split(path[1:], "/")
|
||||
segments := make([]Segment, len(parts))
|
||||
names := make(map[string]bool)
|
||||
for i, part := range parts {
|
||||
if part == "." || part == ".." || strings.ContainsAny(part, "?#") || (part == "" && i != len(parts)-1) {
|
||||
panic("go-omni: invalid route path " + path)
|
||||
}
|
||||
if strings.HasPrefix(part, ":") || strings.HasPrefix(part, "*") {
|
||||
name := part[1:]
|
||||
if name == "" || strings.ContainsAny(name, ":*") || names[name] {
|
||||
panic("go-omni: route parameters must have unique, non-empty names")
|
||||
}
|
||||
wildcard := part[0] == '*'
|
||||
if wildcard && i != len(parts)-1 {
|
||||
panic("go-omni: catch-all must be the final segment")
|
||||
}
|
||||
names[name] = true
|
||||
segments[i] = Segment{Name: name, Wildcard: wildcard}
|
||||
} else {
|
||||
if strings.ContainsAny(part, ":*") {
|
||||
panic("go-omni: parameters must occupy a whole path segment")
|
||||
}
|
||||
segments[i] = Segment{Literal: part}
|
||||
}
|
||||
}
|
||||
return segments
|
||||
}
|
||||
|
||||
// CheckConflict enforces Gin-compatible 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]
|
||||
if a.Wildcard || b.Wildcard {
|
||||
panic("go-omni: catch-all conflicts with an existing route")
|
||||
}
|
||||
if a.Name != "" && b.Name != "" {
|
||||
if a.Name != b.Name {
|
||||
panic("go-omni: conflicting parameter names at the same route prefix")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if a != b {
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(existing) == len(next) {
|
||||
panic("go-omni: duplicate route")
|
||||
}
|
||||
}
|
||||
|
||||
// Join supports relative group paths while preserving a final slash.
|
||||
func Join(prefix, path string) string {
|
||||
if path == "" {
|
||||
if prefix == "" {
|
||||
return "/"
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
return strings.TrimSuffix(prefix, "/") + "/" + strings.TrimPrefix(path, "/")
|
||||
}
|
||||
|
||||
// Match uses the decoded URL.Path without slash normalization or redirects.
|
||||
func Match(segments []Segment, path string) (map[string]string, bool) {
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
return nil, false
|
||||
}
|
||||
parts := strings.Split(path[1:], "/")
|
||||
params := make(map[string]string)
|
||||
for i, current := range segments {
|
||||
if i >= len(parts) {
|
||||
return nil, false
|
||||
}
|
||||
if current.Wildcard {
|
||||
params[current.Name] = "/" + strings.Join(parts[i:], "/")
|
||||
return params, true
|
||||
}
|
||||
if current.Name != "" {
|
||||
if parts[i] == "" {
|
||||
return nil, false
|
||||
}
|
||||
params[current.Name] = parts[i]
|
||||
} else if current.Literal != parts[i] {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return params, len(segments) == len(parts)
|
||||
}
|
||||
Reference in New Issue
Block a user