feat(03-03): add typed path params and per-plugin rollback
Compile regex and enum constraints at route registration so malformed and unknown IDs share a 404, and named rollback errors isolate one plugin's history. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,7 +3,6 @@ package surf
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.golem15.com/golem15/summercms/backpack"
|
||||
@@ -23,11 +22,12 @@ type namedMiddleware struct {
|
||||
}
|
||||
|
||||
type route struct {
|
||||
pluginID string
|
||||
method string
|
||||
path string
|
||||
handler http.Handler
|
||||
middleware []string
|
||||
pluginID string
|
||||
method string
|
||||
path string
|
||||
handler http.Handler
|
||||
middleware []string
|
||||
constraints []Constraint
|
||||
}
|
||||
|
||||
// Router compiles group declarations onto net/http ServeMux.
|
||||
@@ -128,6 +128,62 @@ func (g *Group) Get(path string, handler http.HandlerFunc, middleware ...string)
|
||||
g.router.add(g.pluginID, g.prefix, g.middleware, path, handler, middleware)
|
||||
}
|
||||
|
||||
// Where attaches a compiled regex constraint to the last route, matching PHP ->where().
|
||||
func (r *Router) Where(param, pattern string) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
c, err := Regex(param, pattern)
|
||||
if err != nil {
|
||||
r.compileErr = err
|
||||
return
|
||||
}
|
||||
r.addConstraint(c)
|
||||
}
|
||||
|
||||
func (g *Group) Where(param, pattern string) {
|
||||
if g == nil || g.router == nil {
|
||||
return
|
||||
}
|
||||
g.router.Where(param, pattern)
|
||||
}
|
||||
|
||||
// WhereIn attaches an allow-listed enum constraint to the last route.
|
||||
func (r *Router) WhereIn(param string, values ...string) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
c, err := Enum(param, values...)
|
||||
if err != nil {
|
||||
r.compileErr = err
|
||||
return
|
||||
}
|
||||
r.addConstraint(c)
|
||||
}
|
||||
|
||||
func (g *Group) WhereIn(param string, values ...string) {
|
||||
if g == nil || g.router == nil {
|
||||
return
|
||||
}
|
||||
g.router.WhereIn(param, values...)
|
||||
}
|
||||
|
||||
func (r *Router) addConstraint(c Constraint) {
|
||||
if r.compileErr != nil {
|
||||
return
|
||||
}
|
||||
if len(r.routes) == 0 {
|
||||
r.compileErr = fmt.Errorf("surf: Where(%q) with no route", c.param)
|
||||
return
|
||||
}
|
||||
last := &r.routes[len(r.routes)-1]
|
||||
if !pathHasParam(last.path, c.param) {
|
||||
r.compileErr = fmt.Errorf("surf: Where(%q) is not a path parameter of %s", c.param, last.path)
|
||||
return
|
||||
}
|
||||
last.constraints = append(last.constraints, c)
|
||||
}
|
||||
|
||||
func (r *Router) add(pluginID, prefix string, groupMW []string, path string, handler http.HandlerFunc, extra []string) {
|
||||
full := joinPath(prefix, path)
|
||||
key := "GET " + full
|
||||
@@ -163,7 +219,7 @@ func (r *Router) compile() (http.Handler, error) {
|
||||
}
|
||||
|
||||
func (r *Router) wrap(rt route) (http.Handler, error) {
|
||||
h := rt.handler
|
||||
h := constrain(rt.handler, rt.constraints)
|
||||
h = noOpLimit(h)
|
||||
h = orgSlot(h)
|
||||
for i := len(rt.middleware) - 1; i >= 0; i-- {
|
||||
@@ -290,23 +346,6 @@ func noOpLimit(next http.Handler) http.Handler {
|
||||
return noopLimiter{}.Wrap(next)
|
||||
}
|
||||
|
||||
// IntParam returns a positive integer path value. Missing or malformed ids
|
||||
// are false so callers can 404 both unknown and non-integer values.
|
||||
func IntParam(r *http.Request, name string) (int64, bool) {
|
||||
if r == nil {
|
||||
return 0, false
|
||||
}
|
||||
raw := r.PathValue(name)
|
||||
if raw == "" {
|
||||
return 0, false
|
||||
}
|
||||
n, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil || n < 1 {
|
||||
return 0, false
|
||||
}
|
||||
return n, true
|
||||
}
|
||||
|
||||
func joinPath(prefix, path string) string {
|
||||
prefix = strings.TrimSuffix(prefix, "/")
|
||||
path = strings.TrimSpace(path)
|
||||
|
||||
Reference in New Issue
Block a user