routing: allows routes to be added to existing groups

this enables services to add routing to ex /api
without causing conflicts.
This commit is contained in:
bergquist
2018-06-25 16:50:27 +02:00
parent 503c8cd8ef
commit dbfafa1cb5
5 changed files with 117 additions and 38 deletions
+161
View File
@@ -0,0 +1,161 @@
package routing
import (
"net/http"
"strings"
"gopkg.in/macaron.v1"
)
type Router interface {
Handle(method, pattern string, handlers []macaron.Handler) *macaron.Route
Get(pattern string, handlers ...macaron.Handler) *macaron.Route
}
// RouteRegister allows you to add routes and macaron.Handlers
// that the web server should serve.
type RouteRegister interface {
// Get adds a list of handlers to a given route with a GET HTTP verb
Get(string, ...macaron.Handler)
// Post adds a list of handlers to a given route with a POST HTTP verb
Post(string, ...macaron.Handler)
// Delete adds a list of handlers to a given route with a DELETE HTTP verb
Delete(string, ...macaron.Handler)
// Put adds a list of handlers to a given route with a PUT HTTP verb
Put(string, ...macaron.Handler)
// Patch adds a list of handlers to a given route with a PATCH HTTP verb
Patch(string, ...macaron.Handler)
// Any adds a list of handlers to a given route with any HTTP verb
Any(string, ...macaron.Handler)
// Group allows you to pass a function that can add multiple routes
// with a shared prefix route.
Group(string, func(RouteRegister), ...macaron.Handler)
// Insert adds more routes to an existing Group.
Insert(string, func(RouteRegister), ...macaron.Handler)
// Register iterates over all routes added to the RouteRegister
// and add them to the `Router` pass as an parameter.
Register(Router) *macaron.Router
}
type RegisterNamedMiddleware func(name string) macaron.Handler
// NewRouteRegister creates a new RouteRegister with all middlewares sent as params
func NewRouteRegister(namedMiddleware ...RegisterNamedMiddleware) RouteRegister {
return &routeRegister{
prefix: "",
routes: []route{},
subfixHandlers: []macaron.Handler{},
namedMiddleware: namedMiddleware,
}
}
type route struct {
method string
pattern string
handlers []macaron.Handler
}
type routeRegister struct {
prefix string
subfixHandlers []macaron.Handler
namedMiddleware []RegisterNamedMiddleware
routes []route
groups []*routeRegister
}
func (rr *routeRegister) Insert(pattern string, fn func(RouteRegister), handlers ...macaron.Handler) {
//loop over all groups at current level
for _, g := range rr.groups {
// apply routes if the prefix matches the pattern
if g.prefix == pattern {
g.Group("", fn)
break
}
// go down one level if the prefix can be find in the pattern
if strings.HasPrefix(pattern, g.prefix) {
g.Insert(pattern, fn)
}
}
}
func (rr *routeRegister) Group(pattern string, fn func(rr RouteRegister), handlers ...macaron.Handler) {
group := &routeRegister{
prefix: rr.prefix + pattern,
subfixHandlers: append(rr.subfixHandlers, handlers...),
routes: []route{},
namedMiddleware: rr.namedMiddleware,
}
fn(group)
rr.groups = append(rr.groups, group)
}
func (rr *routeRegister) Register(router Router) *macaron.Router {
for _, r := range rr.routes {
// GET requests have to be added to macaron routing using Get()
// Otherwise HEAD requests will not be allowed.
// https://github.com/go-macaron/macaron/blob/a325110f8b392bce3e5cdeb8c44bf98078ada3be/router.go#L198
if r.method == http.MethodGet {
router.Get(r.pattern, r.handlers...)
} else {
router.Handle(r.method, r.pattern, r.handlers)
}
}
for _, g := range rr.groups {
g.Register(router)
}
return &macaron.Router{}
}
func (rr *routeRegister) route(pattern, method string, handlers ...macaron.Handler) {
h := make([]macaron.Handler, 0)
for _, fn := range rr.namedMiddleware {
h = append(h, fn(pattern))
}
h = append(h, rr.subfixHandlers...)
h = append(h, handlers...)
rr.routes = append(rr.routes, route{
method: method,
pattern: rr.prefix + pattern,
handlers: h,
})
}
func (rr *routeRegister) Get(pattern string, handlers ...macaron.Handler) {
rr.route(pattern, http.MethodGet, handlers...)
}
func (rr *routeRegister) Post(pattern string, handlers ...macaron.Handler) {
rr.route(pattern, http.MethodPost, handlers...)
}
func (rr *routeRegister) Delete(pattern string, handlers ...macaron.Handler) {
rr.route(pattern, http.MethodDelete, handlers...)
}
func (rr *routeRegister) Put(pattern string, handlers ...macaron.Handler) {
rr.route(pattern, http.MethodPut, handlers...)
}
func (rr *routeRegister) Patch(pattern string, handlers ...macaron.Handler) {
rr.route(pattern, http.MethodPatch, handlers...)
}
func (rr *routeRegister) Any(pattern string, handlers ...macaron.Handler) {
rr.route(pattern, "*", handlers...)
}
+250
View File
@@ -0,0 +1,250 @@
package routing
import (
"net/http"
"strconv"
"testing"
"gopkg.in/macaron.v1"
)
type fakeRouter struct {
route []route
}
func (fr *fakeRouter) Handle(method, pattern string, handlers []macaron.Handler) *macaron.Route {
fr.route = append(fr.route, route{
pattern: pattern,
method: method,
handlers: handlers,
})
return &macaron.Route{}
}
func (fr *fakeRouter) Get(pattern string, handlers ...macaron.Handler) *macaron.Route {
fr.route = append(fr.route, route{
pattern: pattern,
method: http.MethodGet,
handlers: handlers,
})
return &macaron.Route{}
}
func emptyHandlers(n int) []macaron.Handler {
var res []macaron.Handler
for i := 1; n >= i; i++ {
res = append(res, emptyHandler(strconv.Itoa(i)))
}
return res
}
func emptyHandler(name string) macaron.Handler {
return struct{ name string }{name: name}
}
func TestRouteSimpleRegister(t *testing.T) {
testTable := []route{
{method: "DELETE", pattern: "/admin", handlers: emptyHandlers(2)},
{method: "GET", pattern: "/down", handlers: emptyHandlers(3)},
}
// Setup
rr := NewRouteRegister(func(name string) macaron.Handler {
return emptyHandler(name)
})
rr.Delete("/admin", emptyHandler("1"))
rr.Get("/down", emptyHandler("1"), emptyHandler("2"))
fr := &fakeRouter{}
rr.Register(fr)
// Validation
if len(fr.route) != len(testTable) {
t.Errorf("want %v routes, got %v", len(testTable), len(fr.route))
}
for i := range testTable {
if testTable[i].method != fr.route[i].method {
t.Errorf("want %s got %v", testTable[i].method, fr.route[i].method)
}
if testTable[i].pattern != fr.route[i].pattern {
t.Errorf("want %s got %v", testTable[i].pattern, fr.route[i].pattern)
}
if len(testTable[i].handlers) != len(fr.route[i].handlers) {
t.Errorf("want %d handlers got %d handlers \ntestcase: %v\nroute: %v\n",
len(testTable[i].handlers),
len(fr.route[i].handlers),
testTable[i],
fr.route[i])
}
}
}
func TestRouteGroupedRegister(t *testing.T) {
testTable := []route{
{method: "DELETE", pattern: "/admin", handlers: emptyHandlers(1)},
{method: "GET", pattern: "/down", handlers: emptyHandlers(2)},
{method: "POST", pattern: "/user", handlers: emptyHandlers(1)},
{method: "PUT", pattern: "/user/friends", handlers: emptyHandlers(1)},
{method: "DELETE", pattern: "/user/admin", handlers: emptyHandlers(2)},
{method: "GET", pattern: "/user/admin/all", handlers: emptyHandlers(4)},
}
// Setup
rr := NewRouteRegister()
rr.Delete("/admin", emptyHandler("1"))
rr.Get("/down", emptyHandler("1"), emptyHandler("2"))
rr.Group("/user", func(user RouteRegister) {
user.Post("", emptyHandler("1"))
user.Put("/friends", emptyHandler("2"))
user.Group("/admin", func(admin RouteRegister) {
admin.Delete("", emptyHandler("3"))
admin.Get("/all", emptyHandler("3"), emptyHandler("4"), emptyHandler("5"))
}, emptyHandler("3"))
})
fr := &fakeRouter{}
rr.Register(fr)
// Validation
if len(fr.route) != len(testTable) {
t.Errorf("want %v routes, got %v", len(testTable), len(fr.route))
}
for i := range testTable {
if testTable[i].method != fr.route[i].method {
t.Errorf("want %s got %v", testTable[i].method, fr.route[i].method)
}
if testTable[i].pattern != fr.route[i].pattern {
t.Errorf("want %s got %v", testTable[i].pattern, fr.route[i].pattern)
}
if len(testTable[i].handlers) != len(fr.route[i].handlers) {
t.Errorf("want %d handlers got %d handlers \ntestcase: %v\nroute: %v\n",
len(testTable[i].handlers),
len(fr.route[i].handlers),
testTable[i],
fr.route[i])
}
}
}
func TestRouteGroupInserting(t *testing.T) {
testTable := []route{
{method: http.MethodGet, pattern: "/api/", handlers: emptyHandlers(1)},
{method: http.MethodPost, pattern: "/api/group/endpoint", handlers: emptyHandlers(1)},
{method: http.MethodGet, pattern: "/api/group/inserted", handlers: emptyHandlers(1)},
{method: http.MethodDelete, pattern: "/api/inserted-endpoint", handlers: emptyHandlers(1)},
}
// Setup
rr := NewRouteRegister()
rr.Group("/api", func(api RouteRegister) {
api.Get("/", emptyHandler("1"))
api.Group("/group", func(group RouteRegister) {
group.Post("/endpoint", emptyHandler("1"))
})
})
rr.Insert("/api", func(api RouteRegister) {
api.Delete("/inserted-endpoint", emptyHandler("1"))
})
rr.Insert("/api/group", func(group RouteRegister) {
group.Get("/inserted", emptyHandler("1"))
})
fr := &fakeRouter{}
rr.Register(fr)
// Validation
if len(fr.route) != len(testTable) {
t.Fatalf("want %v routes, got %v", len(testTable), len(fr.route))
}
for i := range testTable {
if testTable[i].method != fr.route[i].method {
t.Errorf("want %s got %v", testTable[i].method, fr.route[i].method)
}
if testTable[i].pattern != fr.route[i].pattern {
t.Errorf("want %s got %v", testTable[i].pattern, fr.route[i].pattern)
}
if len(testTable[i].handlers) != len(fr.route[i].handlers) {
t.Errorf("want %d handlers got %d handlers \ntestcase: %v\nroute: %v\n",
len(testTable[i].handlers),
len(fr.route[i].handlers),
testTable[i],
fr.route[i])
}
}
}
func TestNamedMiddlewareRouteRegister(t *testing.T) {
testTable := []route{
{method: "DELETE", pattern: "/admin", handlers: emptyHandlers(2)},
{method: "GET", pattern: "/down", handlers: emptyHandlers(3)},
{method: "POST", pattern: "/user", handlers: emptyHandlers(2)},
{method: "PUT", pattern: "/user/friends", handlers: emptyHandlers(2)},
{method: "DELETE", pattern: "/user/admin", handlers: emptyHandlers(3)},
{method: "GET", pattern: "/user/admin/all", handlers: emptyHandlers(5)},
}
// Setup
rr := NewRouteRegister(func(name string) macaron.Handler {
return emptyHandler(name)
})
rr.Delete("/admin", emptyHandler("1"))
rr.Get("/down", emptyHandler("1"), emptyHandler("2"))
rr.Group("/user", func(user RouteRegister) {
user.Post("", emptyHandler("1"))
user.Put("/friends", emptyHandler("2"))
user.Group("/admin", func(admin RouteRegister) {
admin.Delete("", emptyHandler("3"))
admin.Get("/all", emptyHandler("3"), emptyHandler("4"), emptyHandler("5"))
}, emptyHandler("3"))
})
fr := &fakeRouter{}
rr.Register(fr)
// Validation
if len(fr.route) != len(testTable) {
t.Errorf("want %v routes, got %v", len(testTable), len(fr.route))
}
for i := range testTable {
if testTable[i].method != fr.route[i].method {
t.Errorf("want %s got %v", testTable[i].method, fr.route[i].method)
}
if testTable[i].pattern != fr.route[i].pattern {
t.Errorf("want %s got %v", testTable[i].pattern, fr.route[i].pattern)
}
if len(testTable[i].handlers) != len(fr.route[i].handlers) {
t.Errorf("want %d handlers got %d handlers \ntestcase: %v\nroute: %v\n",
len(testTable[i].handlers),
len(fr.route[i].handlers),
testTable[i],
fr.route[i])
}
}
}