Files
member-console/internal/web/route_url.go
T
cgalo5758 1a1649ba53 Add operatorURL helper and template linter
Introduce cmd/lint to statically verify operator templates vs routes
(internal/lint). Replace interpolated hx-* URL strings with operatorURL
calls in partials, register operatorURL in the template FuncMap,
add server/operator_url.go with unit tests, and update go.mod.
Add routeURL helper and template linter
2026-05-18 01:36:18 -05:00

59 lines
1.8 KiB
Go

package web
import (
"fmt"
"net/url"
"strings"
)
// RouteURL substitutes {placeholder} markers in a route pattern with the
// given args in order. The first arg replaces the first placeholder, and
// so on. Path components are escaped via url.PathEscape so values
// containing slashes or reserved characters do not break route matching.
//
// Pattern strings must match the path portion of a registered route (the
// pattern as written in mux.HandleFunc, minus the method), e.g.
// "/partials/operator/products/{productID}/prices". Templates use this
// helper instead of literal `hx-get="/partials/operator/products/{{.ID}}/prices"`
// so cmd/lint can statically verify every URL reference resolves to a
// registered handler. See docs/operator-ux-conventions.md §9.
//
// Template registration name is "routeURL". Handlers wire it in with:
//
// template.FuncMap{
// "routeURL": web.RouteURL,
// ...
// }
//
// Returns an error if placeholder count and arg count disagree — surfaced
// as a template execution error in dev and to the slog handler in prod.
func RouteURL(pattern string, args ...any) (string, error) {
var out strings.Builder
out.Grow(len(pattern))
argIdx := 0
i := 0
for i < len(pattern) {
if pattern[i] != '{' {
out.WriteByte(pattern[i])
i++
continue
}
end := strings.IndexByte(pattern[i:], '}')
if end == -1 {
return "", fmt.Errorf("routeURL: unclosed placeholder in %q", pattern)
}
if argIdx >= len(args) {
return "", fmt.Errorf("routeURL: not enough args for %q (placeholders > %d args)", pattern, len(args))
}
out.WriteString(url.PathEscape(fmt.Sprint(args[argIdx])))
argIdx++
i += end + 1
}
if argIdx != len(args) {
return "", fmt.Errorf("routeURL: too many args for %q (got %d, want %d)", pattern, len(args), argIdx)
}
return out.String(), nil
}