feat(06-03): add raw groups, house middleware, and route:list
- GroupRaw plus sticky raw inheritance and registration-time house-envelope refusal via pact.HasHouseMiddleware - Recover on raw routes writes a bare 500; non-raw keeps the house JSON body - Router.Routes() and surf.RouteListCommand; generated main registers route:list
This commit is contained in:
124
surf/router.go
124
surf/router.go
@@ -18,13 +18,15 @@ func Use(names ...string) []string {
|
||||
}
|
||||
|
||||
type namedMiddleware struct {
|
||||
pluginID string
|
||||
fn pact.Middleware
|
||||
pluginID string
|
||||
fn pact.Middleware
|
||||
houseTagged bool
|
||||
}
|
||||
|
||||
type namedMiddlewareFactory struct {
|
||||
pluginID string
|
||||
fn func(param string) pact.Middleware
|
||||
pluginID string
|
||||
fn func(param string) pact.Middleware
|
||||
houseTagged bool
|
||||
}
|
||||
|
||||
type route struct {
|
||||
@@ -34,6 +36,7 @@ type route struct {
|
||||
handler http.Handler
|
||||
middleware []string
|
||||
constraints []Constraint
|
||||
raw bool
|
||||
}
|
||||
|
||||
// Router compiles group declarations onto net/http ServeMux.
|
||||
@@ -61,6 +64,7 @@ type Group struct {
|
||||
pluginID string
|
||||
prefix string
|
||||
middleware []string
|
||||
raw bool
|
||||
}
|
||||
|
||||
// New returns an empty router.
|
||||
@@ -75,6 +79,17 @@ func New(origins []string) *Router {
|
||||
|
||||
// RegisterMiddleware stores a named wrapper. Duplicate names fail.
|
||||
func (r *Router) RegisterMiddleware(pluginID, name string, fn pact.Middleware) error {
|
||||
return r.registerNamed(pluginID, name, false, fn)
|
||||
}
|
||||
|
||||
// RegisterHouseMiddleware stores a named wrapper tagged as house-envelope/error
|
||||
// handling. Raw groups refuse these names at wrap/Assemble time. Called only
|
||||
// from BuildRouter's HasHouseMiddleware plugin loop.
|
||||
func (r *Router) RegisterHouseMiddleware(pluginID, name string, fn pact.Middleware) error {
|
||||
return r.registerNamed(pluginID, name, true, fn)
|
||||
}
|
||||
|
||||
func (r *Router) registerNamed(pluginID, name string, tagged bool, fn pact.Middleware) error {
|
||||
if r == nil {
|
||||
return fmt.Errorf("surf: router is nil")
|
||||
}
|
||||
@@ -84,7 +99,7 @@ func (r *Router) RegisterMiddleware(pluginID, name string, fn pact.Middleware) e
|
||||
if existing, ok := r.named[name]; ok {
|
||||
return fmt.Errorf("surf: middleware %q already registered by %s", name, existing.pluginID)
|
||||
}
|
||||
r.named[name] = namedMiddleware{pluginID: pluginID, fn: fn}
|
||||
r.named[name] = namedMiddleware{pluginID: pluginID, fn: fn, houseTagged: tagged}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -92,6 +107,17 @@ func (r *Router) RegisterMiddleware(pluginID, name string, fn pact.Middleware) e
|
||||
// At wrap time a name not found in r.named is split on the first ':' and
|
||||
// the base is looked up here. Duplicate factory names fail.
|
||||
func (r *Router) RegisterMiddlewareFactory(pluginID, name string, fn func(param string) pact.Middleware) error {
|
||||
return r.registerNamedFactory(pluginID, name, false, fn)
|
||||
}
|
||||
|
||||
// RegisterHouseMiddlewareFactory stores a parameterized house-envelope/error
|
||||
// factory. No plugin-facing capability wires this yet; it exists as
|
||||
// infrastructure for a future house-tagged parameterized consumer.
|
||||
func (r *Router) RegisterHouseMiddlewareFactory(pluginID, name string, fn func(param string) pact.Middleware) error {
|
||||
return r.registerNamedFactory(pluginID, name, true, fn)
|
||||
}
|
||||
|
||||
func (r *Router) registerNamedFactory(pluginID, name string, tagged bool, fn func(param string) pact.Middleware) error {
|
||||
if r == nil {
|
||||
return fmt.Errorf("surf: router is nil")
|
||||
}
|
||||
@@ -101,7 +127,7 @@ func (r *Router) RegisterMiddlewareFactory(pluginID, name string, fn func(param
|
||||
if existing, ok := r.factories[name]; ok {
|
||||
return fmt.Errorf("surf: middleware factory %q already registered by %s", name, existing.pluginID)
|
||||
}
|
||||
r.factories[name] = namedMiddlewareFactory{pluginID: pluginID, fn: fn}
|
||||
r.factories[name] = namedMiddlewareFactory{pluginID: pluginID, fn: fn, houseTagged: tagged}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -113,6 +139,14 @@ func (r *Router) BindPlugin(id string) {
|
||||
}
|
||||
|
||||
func (r *Router) Group(prefix string, middleware []string, fn func(pact.Router)) {
|
||||
r.openGroup(prefix, middleware, false, fn)
|
||||
}
|
||||
|
||||
func (r *Router) GroupRaw(prefix string, middleware []string, fn func(pact.Router)) {
|
||||
r.openGroup(prefix, middleware, true, fn)
|
||||
}
|
||||
|
||||
func (r *Router) openGroup(prefix string, middleware []string, raw bool, fn func(pact.Router)) {
|
||||
if r == nil || fn == nil {
|
||||
return
|
||||
}
|
||||
@@ -121,6 +155,7 @@ func (r *Router) Group(prefix string, middleware []string, fn func(pact.Router))
|
||||
pluginID: r.pluginID,
|
||||
prefix: joinPath(r.prefix, prefix),
|
||||
middleware: append([]string{}, r.middleware...),
|
||||
raw: raw,
|
||||
}
|
||||
g.middleware = append(g.middleware, middleware...)
|
||||
fn(g)
|
||||
@@ -130,38 +165,46 @@ func (r *Router) Get(path string, handler http.HandlerFunc, middleware ...string
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.add(r.pluginID, r.prefix, r.middleware, http.MethodGet, path, handler, middleware)
|
||||
r.add(r.pluginID, r.prefix, r.middleware, http.MethodGet, path, handler, middleware, false)
|
||||
}
|
||||
|
||||
func (r *Router) Post(path string, handler http.HandlerFunc, middleware ...string) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.add(r.pluginID, r.prefix, r.middleware, http.MethodPost, path, handler, middleware)
|
||||
r.add(r.pluginID, r.prefix, r.middleware, http.MethodPost, path, handler, middleware, false)
|
||||
}
|
||||
|
||||
func (r *Router) Put(path string, handler http.HandlerFunc, middleware ...string) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.add(r.pluginID, r.prefix, r.middleware, http.MethodPut, path, handler, middleware)
|
||||
r.add(r.pluginID, r.prefix, r.middleware, http.MethodPut, path, handler, middleware, false)
|
||||
}
|
||||
|
||||
func (r *Router) Patch(path string, handler http.HandlerFunc, middleware ...string) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.add(r.pluginID, r.prefix, r.middleware, http.MethodPatch, path, handler, middleware)
|
||||
r.add(r.pluginID, r.prefix, r.middleware, http.MethodPatch, path, handler, middleware, false)
|
||||
}
|
||||
|
||||
func (r *Router) Delete(path string, handler http.HandlerFunc, middleware ...string) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.add(r.pluginID, r.prefix, r.middleware, http.MethodDelete, path, handler, middleware)
|
||||
r.add(r.pluginID, r.prefix, r.middleware, http.MethodDelete, path, handler, middleware, false)
|
||||
}
|
||||
|
||||
func (g *Group) Group(prefix string, middleware []string, fn func(pact.Router)) {
|
||||
g.openGroup(prefix, middleware, g.raw, fn)
|
||||
}
|
||||
|
||||
func (g *Group) GroupRaw(prefix string, middleware []string, fn func(pact.Router)) {
|
||||
g.openGroup(prefix, middleware, true, fn)
|
||||
}
|
||||
|
||||
func (g *Group) openGroup(prefix string, middleware []string, raw bool, fn func(pact.Router)) {
|
||||
if g == nil || g.router == nil || fn == nil {
|
||||
return
|
||||
}
|
||||
@@ -170,6 +213,7 @@ func (g *Group) Group(prefix string, middleware []string, fn func(pact.Router))
|
||||
pluginID: g.pluginID,
|
||||
prefix: joinPath(g.prefix, prefix),
|
||||
middleware: append([]string{}, g.middleware...),
|
||||
raw: raw,
|
||||
}
|
||||
next.middleware = append(next.middleware, middleware...)
|
||||
fn(next)
|
||||
@@ -179,35 +223,35 @@ func (g *Group) Get(path string, handler http.HandlerFunc, middleware ...string)
|
||||
if g == nil || g.router == nil {
|
||||
return
|
||||
}
|
||||
g.router.add(g.pluginID, g.prefix, g.middleware, http.MethodGet, path, handler, middleware)
|
||||
g.router.add(g.pluginID, g.prefix, g.middleware, http.MethodGet, path, handler, middleware, g.raw)
|
||||
}
|
||||
|
||||
func (g *Group) Post(path string, handler http.HandlerFunc, middleware ...string) {
|
||||
if g == nil || g.router == nil {
|
||||
return
|
||||
}
|
||||
g.router.add(g.pluginID, g.prefix, g.middleware, http.MethodPost, path, handler, middleware)
|
||||
g.router.add(g.pluginID, g.prefix, g.middleware, http.MethodPost, path, handler, middleware, g.raw)
|
||||
}
|
||||
|
||||
func (g *Group) Put(path string, handler http.HandlerFunc, middleware ...string) {
|
||||
if g == nil || g.router == nil {
|
||||
return
|
||||
}
|
||||
g.router.add(g.pluginID, g.prefix, g.middleware, http.MethodPut, path, handler, middleware)
|
||||
g.router.add(g.pluginID, g.prefix, g.middleware, http.MethodPut, path, handler, middleware, g.raw)
|
||||
}
|
||||
|
||||
func (g *Group) Patch(path string, handler http.HandlerFunc, middleware ...string) {
|
||||
if g == nil || g.router == nil {
|
||||
return
|
||||
}
|
||||
g.router.add(g.pluginID, g.prefix, g.middleware, http.MethodPatch, path, handler, middleware)
|
||||
g.router.add(g.pluginID, g.prefix, g.middleware, http.MethodPatch, path, handler, middleware, g.raw)
|
||||
}
|
||||
|
||||
func (g *Group) Delete(path string, handler http.HandlerFunc, middleware ...string) {
|
||||
if g == nil || g.router == nil {
|
||||
return
|
||||
}
|
||||
g.router.add(g.pluginID, g.prefix, g.middleware, http.MethodDelete, path, handler, middleware)
|
||||
g.router.add(g.pluginID, g.prefix, g.middleware, http.MethodDelete, path, handler, middleware, g.raw)
|
||||
}
|
||||
|
||||
// Where attaches a compiled regex constraint to the last route, matching PHP ->where().
|
||||
@@ -266,7 +310,7 @@ func (r *Router) addConstraint(c Constraint) {
|
||||
last.constraints = append(last.constraints, c)
|
||||
}
|
||||
|
||||
func (r *Router) add(pluginID, prefix string, groupMW []string, method, path string, handler http.HandlerFunc, extra []string) {
|
||||
func (r *Router) add(pluginID, prefix string, groupMW []string, method, path string, handler http.HandlerFunc, extra []string, raw bool) {
|
||||
full := joinPath(prefix, path)
|
||||
key := method + " " + full
|
||||
if prev, ok := r.seen[key]; ok {
|
||||
@@ -282,6 +326,7 @@ func (r *Router) add(pluginID, prefix string, groupMW []string, method, path str
|
||||
path: full,
|
||||
handler: handler,
|
||||
middleware: mw,
|
||||
raw: raw,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -297,7 +342,7 @@ func (r *Router) compile() (http.Handler, error) {
|
||||
}
|
||||
mux.Handle(rt.method+" "+rt.path, h)
|
||||
}
|
||||
return recoverJSON(cors(r.origins, mux)), nil
|
||||
return cors(r.origins, mux), nil
|
||||
}
|
||||
|
||||
func (r *Router) wrap(rt route) (http.Handler, error) {
|
||||
@@ -306,12 +351,18 @@ func (r *Router) wrap(rt route) (http.Handler, error) {
|
||||
for i := len(rt.middleware) - 1; i >= 0; i-- {
|
||||
name := rt.middleware[i]
|
||||
if named, ok := r.named[name]; ok {
|
||||
if rt.raw && named.houseTagged {
|
||||
return nil, fmt.Errorf("surf: raw group cannot use house-envelope middleware %q (plugin %q)", name, rt.pluginID)
|
||||
}
|
||||
h = named.fn(h)
|
||||
continue
|
||||
}
|
||||
base, param, hasParam := strings.Cut(name, ":")
|
||||
if hasParam {
|
||||
if factory, ok := r.factories[base]; ok {
|
||||
if rt.raw && factory.houseTagged {
|
||||
return nil, fmt.Errorf("surf: raw group cannot use house-envelope middleware %q (plugin %q)", name, rt.pluginID)
|
||||
}
|
||||
if base == "throttle" && r.limiter != nil {
|
||||
if err := r.limiter.ValidateThrottle(param); err != nil {
|
||||
return nil, fmt.Errorf("surf: plugin %q: %w", rt.pluginID, err)
|
||||
@@ -324,11 +375,26 @@ func (r *Router) wrap(rt route) (http.Handler, error) {
|
||||
return nil, fmt.Errorf("surf: plugin %q references unknown middleware %q", rt.pluginID, name)
|
||||
}
|
||||
h = locale(h)
|
||||
if rt.raw {
|
||||
h = recoverBare(h)
|
||||
} else {
|
||||
h = recoverJSON(h)
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// Assemble registers plugin middleware and routes, then compiles ServeMux.
|
||||
func Assemble(app *backpack.App, plugins []party.Plugin) (http.Handler, error) {
|
||||
r, err := BuildRouter(app, plugins)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.compile()
|
||||
}
|
||||
|
||||
// BuildRouter registers plugin middleware and routes without compiling ServeMux,
|
||||
// so callers (route:list) can inspect Routes() after a successful boot.
|
||||
func BuildRouter(app *backpack.App, plugins []party.Plugin) (*Router, error) {
|
||||
r := New(corsOrigins(app))
|
||||
trusted := TrustedProxies(nil)
|
||||
if app != nil {
|
||||
@@ -360,6 +426,15 @@ func Assemble(app *backpack.App, plugins []party.Plugin) (http.Handler, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range plugins {
|
||||
if hh, ok := p.(pact.HasHouseMiddleware); ok {
|
||||
for name, fn := range hh.HouseMiddlewares() {
|
||||
if err := r.RegisterHouseMiddleware(p.ID(), name, fn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range plugins {
|
||||
if bp, ok := p.(BucketProvider); ok {
|
||||
for name, b := range bp.Buckets() {
|
||||
@@ -382,7 +457,7 @@ func Assemble(app *backpack.App, plugins []party.Plugin) (http.Handler, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return r.compile()
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func corsOrigins(app *backpack.App) []string {
|
||||
@@ -423,6 +498,17 @@ func recoverJSON(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func recoverBare(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func cors(origins []string, next http.Handler) http.Handler {
|
||||
allowed := make(map[string]struct{}, len(origins))
|
||||
for _, o := range origins {
|
||||
|
||||
Reference in New Issue
Block a user