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:
@@ -111,6 +111,7 @@ func generateMain(m Manifest) ([]byte, error) {
|
|||||||
b.WriteString("\t}\n")
|
b.WriteString("\t}\n")
|
||||||
b.WriteString("\tcommands := lagoon.RuntimeCommands(app, plugins)\n")
|
b.WriteString("\tcommands := lagoon.RuntimeCommands(app, plugins)\n")
|
||||||
b.WriteString("\tcommands = append(commands, surf.ServeCommand(app, plugins))\n")
|
b.WriteString("\tcommands = append(commands, surf.ServeCommand(app, plugins))\n")
|
||||||
|
b.WriteString("\tcommands = append(commands, surf.RouteListCommand(app, plugins))\n")
|
||||||
b.WriteString("\tfor _, plugin := range plugins {\n")
|
b.WriteString("\tfor _, plugin := range plugins {\n")
|
||||||
b.WriteString("\t\tif hasCommands, ok := plugin.(pact.HasCommands); ok {\n")
|
b.WriteString("\t\tif hasCommands, ok := plugin.(pact.HasCommands); ok {\n")
|
||||||
b.WriteString("\t\t\tcommands = append(commands, hasCommands.Commands()...)\n")
|
b.WriteString("\t\t\tcommands = append(commands, hasCommands.Commands()...)\n")
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ func TestGenerateStableQuotedImportsInManifestOrder(t *testing.T) {
|
|||||||
if !bytes.Contains(mainSrc, []byte("surf.ServeCommand")) {
|
if !bytes.Contains(mainSrc, []byte("surf.ServeCommand")) {
|
||||||
t.Fatalf("main does not register serve:\n%s", mainSrc)
|
t.Fatalf("main does not register serve:\n%s", mainSrc)
|
||||||
}
|
}
|
||||||
|
if !bytes.Contains(mainSrc, []byte("surf.RouteListCommand")) {
|
||||||
|
t.Fatalf("main does not register route:list:\n%s", mainSrc)
|
||||||
|
}
|
||||||
if bytes.Contains(mainSrc, []byte("examples/hello")) {
|
if bytes.Contains(mainSrc, []byte("examples/hello")) {
|
||||||
t.Fatal("generated main hard-codes examples/hello")
|
t.Fatal("generated main hard-codes examples/hello")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,9 +42,25 @@ type HasMiddlewareFactories interface {
|
|||||||
MiddlewareFactories() map[string]func(param string) Middleware
|
MiddlewareFactories() map[string]func(param string) Middleware
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasHouseMiddleware is implemented by plugins that register middleware
|
||||||
|
// tagged as house-envelope/error handling -- refused inside a raw group
|
||||||
|
// (D-16). This is the ONLY way a plugin declares a house-tagged name:
|
||||||
|
// plugins never call a Router.Register* method directly (there is no such
|
||||||
|
// call site anywhere in this codebase -- RegisterMiddleware/
|
||||||
|
// RegisterMiddlewareFactory/RegisterHouseMiddleware are all called
|
||||||
|
// exclusively from surf.Assemble/BuildRouter's plugin loop, the same way
|
||||||
|
// HasMiddleware's Middlewares() map is today). A name present in both
|
||||||
|
// Middlewares() and HouseMiddlewares() (from the same or a different
|
||||||
|
// plugin) fails boot with the existing duplicate-name error, since both are
|
||||||
|
// registered into the same underlying name table.
|
||||||
|
type HasHouseMiddleware interface {
|
||||||
|
HouseMiddlewares() map[string]Middleware
|
||||||
|
}
|
||||||
|
|
||||||
// Router is the Laravel-like group builder implemented by surf.
|
// Router is the Laravel-like group builder implemented by surf.
|
||||||
type Router interface {
|
type Router interface {
|
||||||
Group(prefix string, middleware []string, fn func(Router))
|
Group(prefix string, middleware []string, fn func(Router))
|
||||||
|
GroupRaw(prefix string, middleware []string, fn func(Router))
|
||||||
Get(path string, handler http.HandlerFunc, middleware ...string)
|
Get(path string, handler http.HandlerFunc, middleware ...string)
|
||||||
Post(path string, handler http.HandlerFunc, middleware ...string)
|
Post(path string, handler http.HandlerFunc, middleware ...string)
|
||||||
Put(path string, handler http.HandlerFunc, middleware ...string)
|
Put(path string, handler http.HandlerFunc, middleware ...string)
|
||||||
@@ -127,6 +143,6 @@ type OptionalMessage interface {
|
|||||||
//
|
//
|
||||||
// The kernel type-asserts HasConfig (party, before Register), HasCommands
|
// The kernel type-asserts HasConfig (party, before Register), HasCommands
|
||||||
// (generated app main, after Boot), HasMigrations (lagoon migrate), and
|
// (generated app main, after Boot), HasMigrations (lagoon migrate), and
|
||||||
// HasMiddleware/HasMiddlewareFactories/HasRoutes (surf assemble).
|
// HasMiddleware/HasMiddlewareFactories/HasHouseMiddleware/HasRoutes
|
||||||
// surf.BucketProvider is type-asserted in Assemble (not a pact interface:
|
// (surf assemble). surf.BucketProvider is type-asserted in Assemble/
|
||||||
// pact cannot import surf without a cycle).
|
// BuildRouter (not a pact interface: pact cannot import surf without a cycle).
|
||||||
|
|||||||
39
surf/routelist_command.go
Normal file
39
surf/routelist_command.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package surf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.golem15.com/golem15/summercms/backpack"
|
||||||
|
"git.golem15.com/golem15/summercms/bonfire"
|
||||||
|
"git.golem15.com/golem15/summercms/party"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RouteListCommand builds the router the same way ServeCommand does, then
|
||||||
|
// renders Routes() as a table instead of serving.
|
||||||
|
func RouteListCommand(app *backpack.App, plugins []party.Plugin) bonfire.Command {
|
||||||
|
return bonfire.Command{
|
||||||
|
Name: "route:list",
|
||||||
|
Description: "List registered HTTP routes",
|
||||||
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||||
|
r, err := BuildRouter(app, plugins)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
infos := r.Routes()
|
||||||
|
rows := make([][]string, 0, len(infos))
|
||||||
|
for _, rt := range infos {
|
||||||
|
rows = append(rows, []string{
|
||||||
|
rt.Method,
|
||||||
|
rt.Pattern,
|
||||||
|
rt.PluginID,
|
||||||
|
strings.Join(rt.Middleware, ","),
|
||||||
|
strconv.FormatBool(rt.Raw),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
out.Table([]string{"Method", "Pattern", "Plugin", "Middleware", "Raw"}, rows)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
124
surf/router.go
124
surf/router.go
@@ -18,13 +18,15 @@ func Use(names ...string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type namedMiddleware struct {
|
type namedMiddleware struct {
|
||||||
pluginID string
|
pluginID string
|
||||||
fn pact.Middleware
|
fn pact.Middleware
|
||||||
|
houseTagged bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type namedMiddlewareFactory struct {
|
type namedMiddlewareFactory struct {
|
||||||
pluginID string
|
pluginID string
|
||||||
fn func(param string) pact.Middleware
|
fn func(param string) pact.Middleware
|
||||||
|
houseTagged bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type route struct {
|
type route struct {
|
||||||
@@ -34,6 +36,7 @@ type route struct {
|
|||||||
handler http.Handler
|
handler http.Handler
|
||||||
middleware []string
|
middleware []string
|
||||||
constraints []Constraint
|
constraints []Constraint
|
||||||
|
raw bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Router compiles group declarations onto net/http ServeMux.
|
// Router compiles group declarations onto net/http ServeMux.
|
||||||
@@ -61,6 +64,7 @@ type Group struct {
|
|||||||
pluginID string
|
pluginID string
|
||||||
prefix string
|
prefix string
|
||||||
middleware []string
|
middleware []string
|
||||||
|
raw bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns an empty router.
|
// New returns an empty router.
|
||||||
@@ -75,6 +79,17 @@ func New(origins []string) *Router {
|
|||||||
|
|
||||||
// RegisterMiddleware stores a named wrapper. Duplicate names fail.
|
// RegisterMiddleware stores a named wrapper. Duplicate names fail.
|
||||||
func (r *Router) RegisterMiddleware(pluginID, name string, fn pact.Middleware) error {
|
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 {
|
if r == nil {
|
||||||
return fmt.Errorf("surf: router is 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 {
|
if existing, ok := r.named[name]; ok {
|
||||||
return fmt.Errorf("surf: middleware %q already registered by %s", name, existing.pluginID)
|
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
|
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
|
// 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.
|
// the base is looked up here. Duplicate factory names fail.
|
||||||
func (r *Router) RegisterMiddlewareFactory(pluginID, name string, fn func(param string) pact.Middleware) error {
|
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 {
|
if r == nil {
|
||||||
return fmt.Errorf("surf: router is 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 {
|
if existing, ok := r.factories[name]; ok {
|
||||||
return fmt.Errorf("surf: middleware factory %q already registered by %s", name, existing.pluginID)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +139,14 @@ func (r *Router) BindPlugin(id string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) Group(prefix string, middleware []string, fn func(pact.Router)) {
|
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 {
|
if r == nil || fn == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -121,6 +155,7 @@ func (r *Router) Group(prefix string, middleware []string, fn func(pact.Router))
|
|||||||
pluginID: r.pluginID,
|
pluginID: r.pluginID,
|
||||||
prefix: joinPath(r.prefix, prefix),
|
prefix: joinPath(r.prefix, prefix),
|
||||||
middleware: append([]string{}, r.middleware...),
|
middleware: append([]string{}, r.middleware...),
|
||||||
|
raw: raw,
|
||||||
}
|
}
|
||||||
g.middleware = append(g.middleware, middleware...)
|
g.middleware = append(g.middleware, middleware...)
|
||||||
fn(g)
|
fn(g)
|
||||||
@@ -130,38 +165,46 @@ func (r *Router) Get(path string, handler http.HandlerFunc, middleware ...string
|
|||||||
if r == nil {
|
if r == nil {
|
||||||
return
|
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) {
|
func (r *Router) Post(path string, handler http.HandlerFunc, middleware ...string) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return
|
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) {
|
func (r *Router) Put(path string, handler http.HandlerFunc, middleware ...string) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return
|
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) {
|
func (r *Router) Patch(path string, handler http.HandlerFunc, middleware ...string) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return
|
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) {
|
func (r *Router) Delete(path string, handler http.HandlerFunc, middleware ...string) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return
|
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)) {
|
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 {
|
if g == nil || g.router == nil || fn == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -170,6 +213,7 @@ func (g *Group) Group(prefix string, middleware []string, fn func(pact.Router))
|
|||||||
pluginID: g.pluginID,
|
pluginID: g.pluginID,
|
||||||
prefix: joinPath(g.prefix, prefix),
|
prefix: joinPath(g.prefix, prefix),
|
||||||
middleware: append([]string{}, g.middleware...),
|
middleware: append([]string{}, g.middleware...),
|
||||||
|
raw: raw,
|
||||||
}
|
}
|
||||||
next.middleware = append(next.middleware, middleware...)
|
next.middleware = append(next.middleware, middleware...)
|
||||||
fn(next)
|
fn(next)
|
||||||
@@ -179,35 +223,35 @@ func (g *Group) Get(path string, handler http.HandlerFunc, middleware ...string)
|
|||||||
if g == nil || g.router == nil {
|
if g == nil || g.router == nil {
|
||||||
return
|
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) {
|
func (g *Group) Post(path string, handler http.HandlerFunc, middleware ...string) {
|
||||||
if g == nil || g.router == nil {
|
if g == nil || g.router == nil {
|
||||||
return
|
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) {
|
func (g *Group) Put(path string, handler http.HandlerFunc, middleware ...string) {
|
||||||
if g == nil || g.router == nil {
|
if g == nil || g.router == nil {
|
||||||
return
|
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) {
|
func (g *Group) Patch(path string, handler http.HandlerFunc, middleware ...string) {
|
||||||
if g == nil || g.router == nil {
|
if g == nil || g.router == nil {
|
||||||
return
|
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) {
|
func (g *Group) Delete(path string, handler http.HandlerFunc, middleware ...string) {
|
||||||
if g == nil || g.router == nil {
|
if g == nil || g.router == nil {
|
||||||
return
|
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().
|
// 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)
|
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)
|
full := joinPath(prefix, path)
|
||||||
key := method + " " + full
|
key := method + " " + full
|
||||||
if prev, ok := r.seen[key]; ok {
|
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,
|
path: full,
|
||||||
handler: handler,
|
handler: handler,
|
||||||
middleware: mw,
|
middleware: mw,
|
||||||
|
raw: raw,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,7 +342,7 @@ func (r *Router) compile() (http.Handler, error) {
|
|||||||
}
|
}
|
||||||
mux.Handle(rt.method+" "+rt.path, h)
|
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) {
|
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-- {
|
for i := len(rt.middleware) - 1; i >= 0; i-- {
|
||||||
name := rt.middleware[i]
|
name := rt.middleware[i]
|
||||||
if named, ok := r.named[name]; ok {
|
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)
|
h = named.fn(h)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
base, param, hasParam := strings.Cut(name, ":")
|
base, param, hasParam := strings.Cut(name, ":")
|
||||||
if hasParam {
|
if hasParam {
|
||||||
if factory, ok := r.factories[base]; ok {
|
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 base == "throttle" && r.limiter != nil {
|
||||||
if err := r.limiter.ValidateThrottle(param); err != nil {
|
if err := r.limiter.ValidateThrottle(param); err != nil {
|
||||||
return nil, fmt.Errorf("surf: plugin %q: %w", rt.pluginID, err)
|
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)
|
return nil, fmt.Errorf("surf: plugin %q references unknown middleware %q", rt.pluginID, name)
|
||||||
}
|
}
|
||||||
h = locale(h)
|
h = locale(h)
|
||||||
|
if rt.raw {
|
||||||
|
h = recoverBare(h)
|
||||||
|
} else {
|
||||||
|
h = recoverJSON(h)
|
||||||
|
}
|
||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assemble registers plugin middleware and routes, then compiles ServeMux.
|
// Assemble registers plugin middleware and routes, then compiles ServeMux.
|
||||||
func Assemble(app *backpack.App, plugins []party.Plugin) (http.Handler, error) {
|
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))
|
r := New(corsOrigins(app))
|
||||||
trusted := TrustedProxies(nil)
|
trusted := TrustedProxies(nil)
|
||||||
if app != 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 {
|
for _, p := range plugins {
|
||||||
if bp, ok := p.(BucketProvider); ok {
|
if bp, ok := p.(BucketProvider); ok {
|
||||||
for name, b := range bp.Buckets() {
|
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 nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return r.compile()
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func corsOrigins(app *backpack.App) []string {
|
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 {
|
func cors(origins []string, next http.Handler) http.Handler {
|
||||||
allowed := make(map[string]struct{}, len(origins))
|
allowed := make(map[string]struct{}, len(origins))
|
||||||
for _, o := range origins {
|
for _, o := range origins {
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.golem15.com/golem15/summercms/backpack"
|
||||||
"git.golem15.com/golem15/summercms/pact"
|
"git.golem15.com/golem15/summercms/pact"
|
||||||
|
"git.golem15.com/golem15/summercms/party"
|
||||||
)
|
)
|
||||||
|
|
||||||
type routePlugin struct {
|
type routePlugin struct {
|
||||||
@@ -46,6 +48,72 @@ func TestMissingMiddlewareNamesPluginAndName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRawGroupPanicBare500(t *testing.T) {
|
||||||
|
r := New(nil)
|
||||||
|
r.GroupRaw("/oauth", nil, func(g pact.Router) {
|
||||||
|
g.Get("/panic", func(http.ResponseWriter, *http.Request) {
|
||||||
|
panic("secret internals")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
r.Get("/panic", func(http.ResponseWriter, *http.Request) {
|
||||||
|
panic("secret internals")
|
||||||
|
})
|
||||||
|
h, err := r.compile()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/oauth/panic", nil))
|
||||||
|
if rec.Code != http.StatusInternalServerError {
|
||||||
|
t.Fatalf("raw status = %d", rec.Code)
|
||||||
|
}
|
||||||
|
if rec.Body.Len() != 0 {
|
||||||
|
t.Fatalf("raw body = %q", rec.Body.String())
|
||||||
|
}
|
||||||
|
if ct := rec.Header().Get("Content-Type"); ct != "" {
|
||||||
|
t.Fatalf("raw Content-Type = %q", ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/panic", nil))
|
||||||
|
if rec.Code != http.StatusInternalServerError {
|
||||||
|
t.Fatalf("house status = %d", rec.Code)
|
||||||
|
}
|
||||||
|
if rec.Header().Get("Content-Type") != "application/json" {
|
||||||
|
t.Fatalf("house Content-Type = %q", rec.Header().Get("Content-Type"))
|
||||||
|
}
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if payload["error"] != true || payload["message"] != "Internal server error" {
|
||||||
|
t.Fatalf("payload = %v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHouseMiddlewareDuplicateNameFailsBoot(t *testing.T) {
|
||||||
|
identity := func(next http.Handler) http.Handler { return next }
|
||||||
|
named := assemblePlugin{
|
||||||
|
id: "golem15.one",
|
||||||
|
mw: map[string]pact.Middleware{"shared.mw": identity},
|
||||||
|
use: []string{},
|
||||||
|
}
|
||||||
|
house := houseRoutePlugin{
|
||||||
|
id: "golem15.two",
|
||||||
|
house: map[string]pact.Middleware{
|
||||||
|
"shared.mw": identity,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := BuildRouter(backpack.New(nil), []party.Plugin{named, house})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("want duplicate-name error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "shared.mw") || !strings.Contains(err.Error(), "golem15.one") {
|
||||||
|
t.Fatalf("want existing duplicate-name error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRecoverReturnsOpaqueJSON500(t *testing.T) {
|
func TestRecoverReturnsOpaqueJSON500(t *testing.T) {
|
||||||
r := New(nil)
|
r := New(nil)
|
||||||
r.Get("/panic", func(http.ResponseWriter, *http.Request) {
|
r.Get("/panic", func(http.ResponseWriter, *http.Request) {
|
||||||
|
|||||||
30
surf/routetable.go
Normal file
30
surf/routetable.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package surf
|
||||||
|
|
||||||
|
// RouteInfo is a read-only snapshot of one registered route after Assemble
|
||||||
|
// or BuildRouter.
|
||||||
|
type RouteInfo struct {
|
||||||
|
Method string
|
||||||
|
Pattern string
|
||||||
|
PluginID string
|
||||||
|
Middleware []string
|
||||||
|
Raw bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routes returns a defensive copy of every registered route.
|
||||||
|
func (r *Router) Routes() []RouteInfo {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]RouteInfo, len(r.routes))
|
||||||
|
for i, rt := range r.routes {
|
||||||
|
mw := append([]string{}, rt.middleware...)
|
||||||
|
out[i] = RouteInfo{
|
||||||
|
Method: rt.method,
|
||||||
|
Pattern: rt.path,
|
||||||
|
PluginID: rt.pluginID,
|
||||||
|
Middleware: mw,
|
||||||
|
Raw: rt.raw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
126
surf/routetable_test.go
Normal file
126
surf/routetable_test.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package surf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.golem15.com/golem15/summercms/backpack"
|
||||||
|
"git.golem15.com/golem15/summercms/pact"
|
||||||
|
"git.golem15.com/golem15/summercms/party"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRouteTableRawFlagAndStickyInheritance(t *testing.T) {
|
||||||
|
r := New(nil)
|
||||||
|
r.BindPlugin("golem15.demo")
|
||||||
|
r.Get("/plain", func(http.ResponseWriter, *http.Request) {})
|
||||||
|
r.Group("/g", nil, func(g pact.Router) {
|
||||||
|
g.Get("/nested", func(http.ResponseWriter, *http.Request) {})
|
||||||
|
})
|
||||||
|
r.GroupRaw("/raw", nil, func(g pact.Router) {
|
||||||
|
g.Get("/a", func(http.ResponseWriter, *http.Request) {})
|
||||||
|
g.Group("/child", nil, func(c pact.Router) {
|
||||||
|
c.Get("/b", func(http.ResponseWriter, *http.Request) {})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
byPattern := map[string]RouteInfo{}
|
||||||
|
for _, rt := range r.Routes() {
|
||||||
|
byPattern[rt.Pattern] = rt
|
||||||
|
}
|
||||||
|
|
||||||
|
want := map[string]bool{
|
||||||
|
"/plain": false,
|
||||||
|
"/g/nested": false,
|
||||||
|
"/raw/a": true,
|
||||||
|
"/raw/child/b": true,
|
||||||
|
}
|
||||||
|
for pattern, raw := range want {
|
||||||
|
got, ok := byPattern[pattern]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("missing route %s in %v", pattern, keys(byPattern))
|
||||||
|
}
|
||||||
|
if got.Raw != raw {
|
||||||
|
t.Fatalf("%s Raw = %v, want %v", pattern, got.Raw, raw)
|
||||||
|
}
|
||||||
|
if got.Method != http.MethodGet {
|
||||||
|
t.Fatalf("%s Method = %s", pattern, got.Method)
|
||||||
|
}
|
||||||
|
if got.PluginID != "golem15.demo" {
|
||||||
|
t.Fatalf("%s PluginID = %s", pattern, got.PluginID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func keys(m map[string]RouteInfo) []string {
|
||||||
|
out := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
out = append(out, k)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouteTableDoesNotAliasInternalSlice(t *testing.T) {
|
||||||
|
r := New(nil)
|
||||||
|
r.BindPlugin("golem15.demo")
|
||||||
|
r.Group("/api", Use("jwt.auth"), func(g pact.Router) {
|
||||||
|
g.Get("/items", func(http.ResponseWriter, *http.Request) {})
|
||||||
|
})
|
||||||
|
first := r.Routes()
|
||||||
|
if len(first) != 1 || len(first[0].Middleware) != 1 {
|
||||||
|
t.Fatalf("got %+v", first)
|
||||||
|
}
|
||||||
|
first[0].Middleware[0] = "mutated"
|
||||||
|
first[0].Pattern = "/changed"
|
||||||
|
second := r.Routes()
|
||||||
|
if second[0].Middleware[0] != "jwt.auth" {
|
||||||
|
t.Fatalf("internal middleware aliased: %v", second[0].Middleware)
|
||||||
|
}
|
||||||
|
if second[0].Pattern != "/api/items" {
|
||||||
|
t.Fatalf("internal pattern aliased: %s", second[0].Pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRawGroupHouseMiddlewareRefusedAtBuild(t *testing.T) {
|
||||||
|
identity := func(next http.Handler) http.Handler { return next }
|
||||||
|
p := houseRoutePlugin{
|
||||||
|
id: "golem15.demo",
|
||||||
|
house: map[string]pact.Middleware{
|
||||||
|
"house.err": identity,
|
||||||
|
},
|
||||||
|
rawUse: []string{"house.err"},
|
||||||
|
}
|
||||||
|
_, err := BuildRouter(backpack.New(nil), []party.Plugin{p})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("want BuildRouter error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "house.err") {
|
||||||
|
t.Fatalf("want middleware name in error, got %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "golem15.demo") {
|
||||||
|
t.Fatalf("want plugin in error, got %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "raw group cannot use house-envelope middleware") {
|
||||||
|
t.Fatalf("want raw-group refusal, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type houseRoutePlugin struct {
|
||||||
|
id string
|
||||||
|
house map[string]pact.Middleware
|
||||||
|
mw map[string]pact.Middleware
|
||||||
|
rawUse []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p houseRoutePlugin) ID() string { return p.id }
|
||||||
|
func (p houseRoutePlugin) Requires() []string { return nil }
|
||||||
|
func (p houseRoutePlugin) Register(*backpack.App) error { return nil }
|
||||||
|
func (p houseRoutePlugin) Boot(*backpack.App) error { return nil }
|
||||||
|
func (p houseRoutePlugin) Middlewares() map[string]pact.Middleware { return p.mw }
|
||||||
|
func (p houseRoutePlugin) HouseMiddlewares() map[string]pact.Middleware { return p.house }
|
||||||
|
func (p houseRoutePlugin) Routes(r pact.Router) error {
|
||||||
|
r.GroupRaw("/oauth", Use(p.rawUse...), func(g pact.Router) {
|
||||||
|
g.Get("/x", func(http.ResponseWriter, *http.Request) {})
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user