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:
Achim Rohn
2025-08-28 01:36:53 +02:00
parent 90955267d2
commit 644f3dcf31
+140 -1
View File
@@ -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) {