// 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) }