219 lines
5.3 KiB
Go
219 lines
5.3 KiB
Go
// Package nethttp provides a small standard-library router for GoOmni.
|
|
// It is intentionally independent of Gin and serves as the reference for a
|
|
// future project-owned network layer.
|
|
package nethttp
|
|
|
|
import (
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
|
)
|
|
|
|
// Router implements httpx.Router with net/http only. Register routes before
|
|
// serving requests.
|
|
type Router struct {
|
|
mu sync.RWMutex
|
|
routes []route
|
|
}
|
|
|
|
type route struct {
|
|
method string
|
|
pattern string
|
|
segments []segment
|
|
handler http.Handler
|
|
score int
|
|
}
|
|
|
|
type segment struct {
|
|
literal string
|
|
name string
|
|
wildcard bool
|
|
}
|
|
|
|
// New creates an empty standard-library router.
|
|
func New() *Router { return &Router{} }
|
|
|
|
// ServeHTTP dispatches a request and injects route params and the matched
|
|
// pattern into its context for the transport-neutral handler chain.
|
|
func (r *Router) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|
r.mu.RLock()
|
|
routes := append([]route(nil), r.routes...)
|
|
r.mu.RUnlock()
|
|
|
|
var selected *route
|
|
var selectedParams map[string]string
|
|
pathMatched := false
|
|
allow := make(map[string]struct{})
|
|
for index := range routes {
|
|
candidate := &routes[index]
|
|
params, matched := match(candidate.segments, request.URL.Path)
|
|
if !matched {
|
|
continue
|
|
}
|
|
pathMatched = true
|
|
if candidate.method != "*" && candidate.method != request.Method {
|
|
allow[candidate.method] = struct{}{}
|
|
continue
|
|
}
|
|
if selected == nil || candidate.score > selected.score {
|
|
selected = candidate
|
|
selectedParams = params
|
|
}
|
|
}
|
|
|
|
if selected == nil {
|
|
if pathMatched {
|
|
methods := make([]string, 0, len(allow))
|
|
for method := range allow {
|
|
methods = append(methods, method)
|
|
}
|
|
sort.Strings(methods)
|
|
writer.Header().Set("Allow", strings.Join(methods, ", "))
|
|
http.Error(writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
http.NotFound(writer, request)
|
|
return
|
|
}
|
|
|
|
requestContext := httpx.WithRouteParams(request.Context(), selectedParams)
|
|
requestContext = httpx.WithRoutePattern(requestContext, selected.pattern)
|
|
selected.handler.ServeHTTP(writer, request.WithContext(requestContext))
|
|
}
|
|
|
|
func (r *Router) Handle(method, path string, handler http.Handler) {
|
|
if handler == nil {
|
|
panic("go-omni/nethttp: nil route handler")
|
|
}
|
|
segments := parsePattern(path)
|
|
r.mu.Lock()
|
|
r.routes = append(r.routes, route{
|
|
method: method,
|
|
pattern: cleanPattern(path),
|
|
segments: segments,
|
|
handler: handler,
|
|
score: routeScore(method, segments),
|
|
})
|
|
r.mu.Unlock()
|
|
}
|
|
|
|
func (r *Router) Any(path string, handler http.Handler) { r.Handle("*", path, handler) }
|
|
|
|
func (r *Router) Group(path string) httpx.RouterGroup {
|
|
return &group{router: r, prefix: cleanPattern(path)}
|
|
}
|
|
|
|
type group struct {
|
|
router *Router
|
|
prefix string
|
|
}
|
|
|
|
func (g *group) Handle(method, path string, handler http.Handler) {
|
|
g.router.Handle(method, joinPattern(g.prefix, path), handler)
|
|
}
|
|
|
|
func (g *group) Any(path string, handler http.Handler) { g.Handle("*", path, handler) }
|
|
|
|
func (g *group) Group(path string) httpx.RouterGroup {
|
|
return &group{router: g.router, prefix: joinPattern(g.prefix, path)}
|
|
}
|
|
|
|
func cleanPattern(pattern string) string {
|
|
if pattern == "" {
|
|
return "/"
|
|
}
|
|
if !strings.HasPrefix(pattern, "/") {
|
|
pattern = "/" + pattern
|
|
}
|
|
pattern = strings.TrimRight(pattern, "/")
|
|
if pattern == "" {
|
|
return "/"
|
|
}
|
|
return pattern
|
|
}
|
|
|
|
func joinPattern(prefix, path string) string {
|
|
if prefix == "" || prefix == "/" {
|
|
return cleanPattern(path)
|
|
}
|
|
if path == "" || path == "/" {
|
|
return cleanPattern(prefix)
|
|
}
|
|
return cleanPattern(strings.TrimRight(prefix, "/") + "/" + strings.Trim(path, "/"))
|
|
}
|
|
|
|
func parsePattern(pattern string) []segment {
|
|
pattern = cleanPattern(pattern)
|
|
if pattern == "/" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(strings.Trim(pattern, "/"), "/")
|
|
segments := make([]segment, 0, len(parts))
|
|
for _, part := range parts {
|
|
switch {
|
|
case strings.HasPrefix(part, ":") && len(part) > 1:
|
|
segments = append(segments, segment{name: part[1:]})
|
|
case strings.HasPrefix(part, "*") && len(part) > 1:
|
|
segments = append(segments, segment{name: part[1:], wildcard: true})
|
|
default:
|
|
segments = append(segments, segment{literal: part})
|
|
}
|
|
}
|
|
return segments
|
|
}
|
|
|
|
func match(segments []segment, path string) (map[string]string, bool) {
|
|
parts := splitPath(path)
|
|
params := make(map[string]string)
|
|
partIndex := 0
|
|
for segmentIndex, current := range segments {
|
|
if current.wildcard {
|
|
params[current.name] = strings.Join(parts[partIndex:], "/")
|
|
return params, true
|
|
}
|
|
if partIndex >= len(parts) {
|
|
return nil, false
|
|
}
|
|
if current.name != "" {
|
|
params[current.name] = parts[partIndex]
|
|
} else if current.literal != parts[partIndex] {
|
|
return nil, false
|
|
}
|
|
partIndex++
|
|
if segmentIndex == len(segments)-1 && partIndex != len(parts) {
|
|
return nil, false
|
|
}
|
|
}
|
|
if partIndex != len(parts) {
|
|
return nil, false
|
|
}
|
|
return params, true
|
|
}
|
|
|
|
func splitPath(path string) []string {
|
|
path = strings.Trim(path, "/")
|
|
if path == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(path, "/")
|
|
}
|
|
|
|
func routeScore(method string, segments []segment) int {
|
|
score := len(segments)
|
|
if method != "*" {
|
|
score += 1000
|
|
}
|
|
for _, current := range segments {
|
|
if current.literal != "" {
|
|
score += 10
|
|
}
|
|
if current.wildcard {
|
|
score -= 5
|
|
}
|
|
}
|
|
return score
|
|
}
|