diff --git a/internal/build/build.go b/internal/build/build.go index dbba10d..390c593 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -111,6 +111,7 @@ func generateMain(m Manifest) ([]byte, error) { b.WriteString("\t}\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.RouteListCommand(app, plugins))\n") b.WriteString("\tfor _, plugin := range plugins {\n") b.WriteString("\t\tif hasCommands, ok := plugin.(pact.HasCommands); ok {\n") b.WriteString("\t\t\tcommands = append(commands, hasCommands.Commands()...)\n") diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 3f4e726..136d1f5 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -101,6 +101,9 @@ func TestGenerateStableQuotedImportsInManifestOrder(t *testing.T) { if !bytes.Contains(mainSrc, []byte("surf.ServeCommand")) { 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")) { t.Fatal("generated main hard-codes examples/hello") } diff --git a/pact/capabilities.go b/pact/capabilities.go index 04764cf..9915f27 100644 --- a/pact/capabilities.go +++ b/pact/capabilities.go @@ -42,9 +42,25 @@ type HasMiddlewareFactories interface { 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. type Router interface { Group(prefix string, middleware []string, fn func(Router)) + GroupRaw(prefix string, middleware []string, fn func(Router)) Get(path string, handler http.HandlerFunc, middleware ...string) Post(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 // (generated app main, after Boot), HasMigrations (lagoon migrate), and -// HasMiddleware/HasMiddlewareFactories/HasRoutes (surf assemble). -// surf.BucketProvider is type-asserted in Assemble (not a pact interface: -// pact cannot import surf without a cycle). +// HasMiddleware/HasMiddlewareFactories/HasHouseMiddleware/HasRoutes +// (surf assemble). surf.BucketProvider is type-asserted in Assemble/ +// BuildRouter (not a pact interface: pact cannot import surf without a cycle). diff --git a/surf/routelist_command.go b/surf/routelist_command.go new file mode 100644 index 0000000..7bf4453 --- /dev/null +++ b/surf/routelist_command.go @@ -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 + }, + } +} diff --git a/surf/router.go b/surf/router.go index 46394b8..b274aa7 100644 --- a/surf/router.go +++ b/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 { diff --git a/surf/router_test.go b/surf/router_test.go index ebdb595..daec18b 100644 --- a/surf/router_test.go +++ b/surf/router_test.go @@ -7,7 +7,9 @@ import ( "strings" "testing" + "git.golem15.com/golem15/summercms/backpack" "git.golem15.com/golem15/summercms/pact" + "git.golem15.com/golem15/summercms/party" ) 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) { r := New(nil) r.Get("/panic", func(http.ResponseWriter, *http.Request) { diff --git a/surf/routetable.go b/surf/routetable.go new file mode 100644 index 0000000..0cc1935 --- /dev/null +++ b/surf/routetable.go @@ -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 +} diff --git a/surf/routetable_test.go b/surf/routetable_test.go new file mode 100644 index 0000000..b182045 --- /dev/null +++ b/surf/routetable_test.go @@ -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 +}