package surf import ( "fmt" "net/http" "regexp" "strconv" "strings" ) // Constraint is a path-parameter restriction matching PHP ->where() / // ->whereIn(). Patterns and enum sets are compiled at route registration; // request text is only matched, never interpolated into SQL or regex. type Constraint struct { param string re *regexp.Regexp enum map[string]struct{} } // 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 } // Regex compiles a PHP-style where() pattern for param at registration. func Regex(param, pattern string) (Constraint, error) { if param == "" { return Constraint{}, fmt.Errorf("surf: constraint param is empty") } if pattern == "" { return Constraint{}, fmt.Errorf("surf: regex for %q is empty", param) } re, err := regexp.Compile("^(?:" + pattern + ")$") if err != nil { return Constraint{}, fmt.Errorf("surf: invalid regex for %q: %w", param, err) } return Constraint{param: param, re: re}, nil } // Enum allow-lists exact path values for param, matching PHP whereIn(). func Enum(param string, values ...string) (Constraint, error) { if param == "" { return Constraint{}, fmt.Errorf("surf: constraint param is empty") } if len(values) == 0 { return Constraint{}, fmt.Errorf("surf: enum for %q is empty", param) } set := make(map[string]struct{}, len(values)) for _, v := range values { if v == "" { return Constraint{}, fmt.Errorf("surf: enum for %q contains an empty value", param) } set[v] = struct{}{} } return Constraint{param: param, enum: set}, nil } func (c Constraint) match(value string) bool { if c.enum != nil { _, ok := c.enum[value] return ok } if c.re != nil { return c.re.MatchString(value) } return false } func constrain(next http.Handler, cs []Constraint) http.Handler { if len(cs) == 0 { return next } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for _, c := range cs { if !c.match(r.PathValue(c.param)) { http.NotFound(w, r) return } } next.ServeHTTP(w, r) }) } func pathHasParam(path, param string) bool { return strings.Contains(path, "{"+param+"}") || strings.Contains(path, "{"+param+":") }