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
59 lines
1.8 KiB
Go
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
|
|
}
|