120 lines
3.4 KiB
Go
120 lines
3.4 KiB
Go
// 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 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]
|
|
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)
|
|
}
|