Collect all routes globally and add a method to find the one that matches the path
Thereby getting the stack trace from where it was added.
This commit is contained in:
+140
-1
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -173,12 +174,144 @@ type HtmxRoute interface {
|
||||
ToUrlFromContext(c HtmxContext, language Language) string
|
||||
GetHtmx(language Language, queryParams ...HtmxPathParam) gomponents.Node
|
||||
SetActivePath(activePath *ActivePath) HtmxRoute
|
||||
GetPaths() LanguagePaths
|
||||
Add(server HtmxServer)
|
||||
Execute(c HtmxContext)
|
||||
RedirectToThisRoute(c HtmxContext, params LocationRedirectParams)
|
||||
IsCurrentRoute(c HtmxContext) bool
|
||||
}
|
||||
|
||||
type GlobalHtmxRoutes interface {
|
||||
GetRoutes() []HtmxRoute
|
||||
Add(route HtmxRoute)
|
||||
// MatchByPath tries to find a route whose template matches the given path
|
||||
// even if the path contains path parameters. Returns (route, true) if found.
|
||||
MatchByPath(path string) (*HtmxRouteInfo, bool)
|
||||
}
|
||||
|
||||
type HtmxRouteInfo struct {
|
||||
initTrace string
|
||||
route HtmxRoute
|
||||
}
|
||||
|
||||
type GlobalHtmxRoutesImpl struct {
|
||||
routes map[string]*HtmxRouteInfo
|
||||
}
|
||||
|
||||
var GlobalHtmxRoutesInstance GlobalHtmxRoutes = NewGlobalHtmxRoutesImpl()
|
||||
|
||||
func (g *GlobalHtmxRoutesImpl) GetRoutes() []HtmxRoute {
|
||||
routes := make([]HtmxRoute, len(g.routes))
|
||||
index := 0
|
||||
for _, route := range g.routes {
|
||||
Debug("index: ", index, "route: ", route, "")
|
||||
routes[index] = route.route
|
||||
index++
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
func (g *GlobalHtmxRoutesImpl) Add(route HtmxRoute) {
|
||||
paths := route.GetPaths()
|
||||
uniqueKey := ""
|
||||
for language, path := range paths {
|
||||
uniqueKey += string(language) + path
|
||||
}
|
||||
g.routes[uniqueKey] = &HtmxRouteInfo{
|
||||
initTrace: string(debug.Stack()),
|
||||
route: route,
|
||||
}
|
||||
}
|
||||
|
||||
// MatchByPath tries to find a route whose language-specific template matches the given
|
||||
// concrete URL path. It treats path parameters like {id} as wildcards.
|
||||
func (g *GlobalHtmxRoutesImpl) MatchByPath(path string) (*HtmxRouteInfo, bool) {
|
||||
p := normalizePath(path)
|
||||
// Extract language as first segment (if present)
|
||||
trim := strings.Trim(p, "/")
|
||||
segs := []string{}
|
||||
if trim != "" {
|
||||
segs = strings.Split(trim, "/")
|
||||
}
|
||||
if len(segs) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
lang := Language(segs[0])
|
||||
|
||||
for _, info := range g.routes {
|
||||
paths := info.route.GetPaths()
|
||||
tpl, ok := paths[lang]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
candidate := addLanguageToPath(tpl, lang)
|
||||
if matchTemplate(candidate, p) {
|
||||
return info, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func normalizePath(path string) string {
|
||||
if path == "" {
|
||||
return "/"
|
||||
}
|
||||
// strip query string and fragment
|
||||
if i := strings.Index(path, "?"); i >= 0 {
|
||||
path = path[:i]
|
||||
}
|
||||
if i := strings.Index(path, "#"); i >= 0 {
|
||||
path = path[:i]
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
// remove duplicate slashes (basic)
|
||||
for strings.Contains(path, "//") {
|
||||
path = strings.ReplaceAll(path, "//", "/")
|
||||
}
|
||||
// drop trailing slash except root
|
||||
if len(path) > 1 && strings.HasSuffix(path, "/") {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func matchTemplate(templatePath, concretePath string) bool {
|
||||
// Normalize both
|
||||
t := normalizePath(templatePath)
|
||||
p := normalizePath(concretePath)
|
||||
// Quick equality short-circuit
|
||||
if t == p {
|
||||
return true
|
||||
}
|
||||
// Compare segment-wise
|
||||
tSegs := strings.Split(strings.Trim(t, "/"), "/")
|
||||
pSegs := strings.Split(strings.Trim(p, "/"), "/")
|
||||
if len(tSegs) != len(pSegs) {
|
||||
return false
|
||||
}
|
||||
for i := range tSegs {
|
||||
seg := tSegs[i]
|
||||
if isParamSegment(seg) {
|
||||
// wildcard for one segment
|
||||
continue
|
||||
}
|
||||
if seg != pSegs[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isParamSegment(seg string) bool {
|
||||
return strings.HasPrefix(seg, "{") && strings.HasSuffix(seg, "}") && len(seg) > 2
|
||||
}
|
||||
|
||||
func NewGlobalHtmxRoutesImpl() *GlobalHtmxRoutesImpl {
|
||||
return &GlobalHtmxRoutesImpl{routes: make(map[string]*HtmxRouteInfo)}
|
||||
}
|
||||
|
||||
func addLanguageToPath(path string, language Language) string {
|
||||
return "/" + string(language) + path
|
||||
}
|
||||
@@ -192,6 +325,10 @@ type CommonHtmxRoute struct {
|
||||
HtmxMethod HtmxHttpMethodFunction
|
||||
}
|
||||
|
||||
func (h CommonHtmxRoute) GetPaths() LanguagePaths {
|
||||
return h.Paths
|
||||
}
|
||||
|
||||
func (h CommonHtmxRoute) ToUrlFromContext(c HtmxContext, language Language) string {
|
||||
routeWithParams := h.WithPathParams(h.extractPathParams(c)...)
|
||||
queryParams := c.GetQueryParams()
|
||||
@@ -218,7 +355,9 @@ func (h CommonHtmxRoute) RedirectToThisRoute(c HtmxContext, params LocationRedir
|
||||
|
||||
func NewCommonHtmxRoute(routeFunc HtmxRouteFunc, paths LanguagePaths, method string,
|
||||
htmxMethod HtmxHttpMethodFunction) *CommonHtmxRoute {
|
||||
return &CommonHtmxRoute{RouteFunc: routeFunc, Paths: paths, Method: method, HtmxMethod: htmxMethod}
|
||||
route := &CommonHtmxRoute{RouteFunc: routeFunc, Paths: paths, Method: method, HtmxMethod: htmxMethod}
|
||||
GlobalHtmxRoutesInstance.Add(route)
|
||||
return route
|
||||
}
|
||||
|
||||
func (h CommonHtmxRoute) Execute(c HtmxContext) {
|
||||
|
||||
Reference in New Issue
Block a user