feat(06-01): grow router verbs, factories, and guard registry
- Add Post/Put/Patch/Delete on pact.Router and surf Router/Group - Resolve name:param middleware via RegisterMiddlewareFactory - Add bouncer.Registry with Guard, CredentialGuard, UnauthorizedWriter - Re-express jwt as NewJWTGuard without changing Middleware bodies
This commit is contained in:
122
surf/router.go
122
surf/router.go
@@ -21,6 +21,11 @@ type namedMiddleware struct {
|
||||
fn pact.Middleware
|
||||
}
|
||||
|
||||
type namedMiddlewareFactory struct {
|
||||
pluginID string
|
||||
fn func(param string) pact.Middleware
|
||||
}
|
||||
|
||||
type route struct {
|
||||
pluginID string
|
||||
method string
|
||||
@@ -36,6 +41,7 @@ type Router struct {
|
||||
prefix string
|
||||
middleware []string
|
||||
named map[string]namedMiddleware
|
||||
factories map[string]namedMiddlewareFactory
|
||||
routes []route
|
||||
seen map[string]string
|
||||
origins []string
|
||||
@@ -58,9 +64,10 @@ type Group struct {
|
||||
// New returns an empty router.
|
||||
func New(origins []string) *Router {
|
||||
return &Router{
|
||||
named: make(map[string]namedMiddleware),
|
||||
seen: make(map[string]string),
|
||||
origins: origins,
|
||||
named: make(map[string]namedMiddleware),
|
||||
factories: make(map[string]namedMiddlewareFactory),
|
||||
seen: make(map[string]string),
|
||||
origins: origins,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +86,23 @@ func (r *Router) RegisterMiddleware(pluginID, name string, fn pact.Middleware) e
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterMiddlewareFactory stores a parameterized middleware builder.
|
||||
// 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 {
|
||||
if r == nil {
|
||||
return fmt.Errorf("surf: router is nil")
|
||||
}
|
||||
if name == "" || fn == nil {
|
||||
return fmt.Errorf("surf: plugin %q registered empty middleware factory", pluginID)
|
||||
}
|
||||
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}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BindPlugin records the plugin declaring subsequent routes.
|
||||
func (r *Router) BindPlugin(id string) {
|
||||
if r != nil {
|
||||
@@ -104,7 +128,35 @@ func (r *Router) Get(path string, handler http.HandlerFunc, middleware ...string
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.add(r.pluginID, r.prefix, r.middleware, path, handler, middleware)
|
||||
r.add(r.pluginID, r.prefix, r.middleware, http.MethodGet, path, handler, middleware)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (g *Group) Group(prefix string, middleware []string, fn func(pact.Router)) {
|
||||
@@ -125,7 +177,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, path, handler, middleware)
|
||||
g.router.add(g.pluginID, g.prefix, g.middleware, http.MethodGet, path, handler, middleware)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Where attaches a compiled regex constraint to the last route, matching PHP ->where().
|
||||
@@ -184,9 +264,9 @@ func (r *Router) addConstraint(c Constraint) {
|
||||
last.constraints = append(last.constraints, c)
|
||||
}
|
||||
|
||||
func (r *Router) add(pluginID, prefix string, groupMW []string, path string, handler http.HandlerFunc, extra []string) {
|
||||
func (r *Router) add(pluginID, prefix string, groupMW []string, method, path string, handler http.HandlerFunc, extra []string) {
|
||||
full := joinPath(prefix, path)
|
||||
key := "GET " + full
|
||||
key := method + " " + full
|
||||
if prev, ok := r.seen[key]; ok {
|
||||
r.compileErr = fmt.Errorf("surf: duplicate route %s registered by %s and %s", key, prev, pluginID)
|
||||
return
|
||||
@@ -196,7 +276,7 @@ func (r *Router) add(pluginID, prefix string, groupMW []string, path string, han
|
||||
mw = append(mw, extra...)
|
||||
r.routes = append(r.routes, route{
|
||||
pluginID: pluginID,
|
||||
method: "GET",
|
||||
method: method,
|
||||
path: full,
|
||||
handler: handler,
|
||||
middleware: mw,
|
||||
@@ -213,7 +293,7 @@ func (r *Router) compile() (http.Handler, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mux.Handle("GET "+rt.path, h)
|
||||
mux.Handle(rt.method+" "+rt.path, h)
|
||||
}
|
||||
return recoverJSON(cors(r.origins, mux)), nil
|
||||
}
|
||||
@@ -224,11 +304,18 @@ func (r *Router) wrap(rt route) (http.Handler, error) {
|
||||
h = orgSlot(h)
|
||||
for i := len(rt.middleware) - 1; i >= 0; i-- {
|
||||
name := rt.middleware[i]
|
||||
named, ok := r.named[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("surf: plugin %q references unknown middleware %q", rt.pluginID, name)
|
||||
if named, ok := r.named[name]; ok {
|
||||
h = named.fn(h)
|
||||
continue
|
||||
}
|
||||
h = named.fn(h)
|
||||
base, param, hasParam := strings.Cut(name, ":")
|
||||
if hasParam {
|
||||
if factory, ok := r.factories[base]; ok {
|
||||
h = factory.fn(param)(h)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("surf: plugin %q references unknown middleware %q", rt.pluginID, name)
|
||||
}
|
||||
h = locale(h)
|
||||
return h, nil
|
||||
@@ -246,6 +333,15 @@ func Assemble(app *backpack.App, plugins []party.Plugin) (http.Handler, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range plugins {
|
||||
if hf, ok := p.(pact.HasMiddlewareFactories); ok {
|
||||
for name, fn := range hf.MiddlewareFactories() {
|
||||
if err := r.RegisterMiddlewareFactory(p.ID(), name, fn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range plugins {
|
||||
r.BindPlugin(p.ID())
|
||||
if hr, ok := p.(pact.HasRoutes); ok {
|
||||
|
||||
@@ -180,3 +180,115 @@ func TestWhereUnknownParamFailsCompile(t *testing.T) {
|
||||
t.Fatal("want unknown param error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostAndGetSamePathAreIndependent(t *testing.T) {
|
||||
r := New(nil)
|
||||
r.Get("/items", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("get"))
|
||||
})
|
||||
r.Post("/items", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_, _ = w.Write([]byte("post"))
|
||||
})
|
||||
h, err := r.compile()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/items", nil))
|
||||
if rec.Code != http.StatusOK || rec.Body.String() != "get" {
|
||||
t.Fatalf("GET status=%d body=%q", rec.Code, rec.Body.String())
|
||||
}
|
||||
rec = httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/items", nil))
|
||||
if rec.Code != http.StatusCreated || rec.Body.String() != "post" {
|
||||
t.Fatalf("POST status=%d body=%q", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerbsOnRouterAndGroup(t *testing.T) {
|
||||
r := New(nil)
|
||||
r.Put("/r", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("put")) })
|
||||
r.Patch("/r", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("patch")) })
|
||||
r.Delete("/r", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("delete")) })
|
||||
r.Group("/g", nil, func(g pact.Router) {
|
||||
g.Get("/x", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("gget")) })
|
||||
g.Post("/x", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("gpost")) })
|
||||
g.Put("/x", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("gput")) })
|
||||
g.Patch("/x", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("gpatch")) })
|
||||
g.Delete("/x", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("gdelete")) })
|
||||
})
|
||||
h, err := r.compile()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cases := []struct {
|
||||
method, path, want string
|
||||
}{
|
||||
{http.MethodPut, "/r", "put"},
|
||||
{http.MethodPatch, "/r", "patch"},
|
||||
{http.MethodDelete, "/r", "delete"},
|
||||
{http.MethodGet, "/g/x", "gget"},
|
||||
{http.MethodPost, "/g/x", "gpost"},
|
||||
{http.MethodPut, "/g/x", "gput"},
|
||||
{http.MethodPatch, "/g/x", "gpatch"},
|
||||
{http.MethodDelete, "/g/x", "gdelete"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(tc.method, tc.path, nil))
|
||||
if rec.Code != http.StatusOK || rec.Body.String() != tc.want {
|
||||
t.Fatalf("%s %s status=%d body=%q", tc.method, tc.path, rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddlewareFactoryReceivesParam(t *testing.T) {
|
||||
r := New(nil)
|
||||
var gotParam string
|
||||
ran := false
|
||||
if err := r.RegisterMiddlewareFactory("golem15.fonoteka", "inv.scope", func(param string) pact.Middleware {
|
||||
gotParam = param
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ran = true
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Get("/x", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}, "inv.scope:write")
|
||||
h, err := r.compile()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/x", nil))
|
||||
if rec.Code != http.StatusNoContent {
|
||||
t.Fatalf("status = %d", rec.Code)
|
||||
}
|
||||
if gotParam != "write" {
|
||||
t.Fatalf("param = %q", gotParam)
|
||||
}
|
||||
if !ran {
|
||||
t.Fatal("factory middleware did not run")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateMiddlewareFactoryNamesPluginAndName(t *testing.T) {
|
||||
r := New(nil)
|
||||
fn := func(string) pact.Middleware {
|
||||
return func(next http.Handler) http.Handler { return next }
|
||||
}
|
||||
if err := r.RegisterMiddlewareFactory("golem15.fonoteka", "inv.scope", fn); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := r.RegisterMiddlewareFactory("golem15.other", "inv.scope", fn)
|
||||
if err == nil || !strings.Contains(err.Error(), "golem15.fonoteka") || !strings.Contains(err.Error(), "inv.scope") {
|
||||
t.Fatalf("want plugin and factory name in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user