new ServeMux based on Mat Ryer's Way. removed method support in the mux to allow it to be handled by the handlers themselves. Keeps the standard library net/http handler contract and provides better support for the handlers provided by x's net/http.
This commit is contained in:
parent
62911f042d
commit
dc5f8b6e5a
@ -30,7 +30,7 @@ func MutliHandler(h map[string]http.Handler) (http.HandlerFunc, error) {
|
|||||||
if hdlr, ok := h[r.Method]; ok {
|
if hdlr, ok := h[r.Method]; ok {
|
||||||
hdlr.ServeHTTP(w, r)
|
hdlr.ServeHTTP(w, r)
|
||||||
} else {
|
} else {
|
||||||
NotImplementedHandler.ServeHTTP(w, r)
|
NotAllowedHandler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
106
net/http/mux.go
Normal file
106
net/http/mux.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wayContextKey is the context key type for storing
|
||||||
|
// parameters in context.Context.
|
||||||
|
type wayContextKey string
|
||||||
|
|
||||||
|
// Router routes HTTP requests.
|
||||||
|
type ServeMux struct {
|
||||||
|
routes []*route
|
||||||
|
// NotFound is the http.Handler to call when no routes
|
||||||
|
// match. By default uses http.NotFoundHandler().
|
||||||
|
NotFound http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRouter makes a new Router.
|
||||||
|
func NewServeMux() *ServeMux {
|
||||||
|
return &ServeMux{
|
||||||
|
NotFound: http.NotFoundHandler(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ServeMux) pathSegments(p string) []string {
|
||||||
|
return strings.Split(strings.Trim(p, "/"), "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle adds a handler with the specified pattern.
|
||||||
|
// Pattern can contain path segments such as: /item/:id which is
|
||||||
|
// accessible via the Param function.
|
||||||
|
// If pattern ends with trailing /, it acts as a prefix.
|
||||||
|
func (r *ServeMux) Handle(pattern string, handler http.Handler) {
|
||||||
|
route := &route{
|
||||||
|
segs: r.pathSegments(pattern),
|
||||||
|
handler: handler,
|
||||||
|
prefix: strings.HasSuffix(pattern, "/") || strings.HasSuffix(pattern, "..."),
|
||||||
|
}
|
||||||
|
r.routes = append(r.routes, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFunc is the http.HandlerFunc alternative to http.Handle.
|
||||||
|
func (r *ServeMux) HandleFunc(pattern string, fn http.HandlerFunc) {
|
||||||
|
r.Handle(pattern, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP routes the incoming http.Request based on path
|
||||||
|
func (r *ServeMux) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
segs := r.pathSegments(req.URL.Path)
|
||||||
|
for _, route := range r.routes {
|
||||||
|
if ctx, ok := route.match(req.Context(), r, segs); ok {
|
||||||
|
route.handler.ServeHTTP(w, req.WithContext(ctx))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.NotFound.ServeHTTP(w, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param gets the path parameter from the specified Context.
|
||||||
|
// Returns an empty string if the parameter was not found.
|
||||||
|
func Param(ctx context.Context, param string) string {
|
||||||
|
vStr, ok := ctx.Value(wayContextKey(param)).(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return vStr
|
||||||
|
}
|
||||||
|
|
||||||
|
type route struct {
|
||||||
|
segs []string
|
||||||
|
handler http.Handler
|
||||||
|
prefix bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *route) match(ctx context.Context, router *ServeMux, segs []string) (context.Context, bool) {
|
||||||
|
if len(segs) > len(r.segs) && !r.prefix {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
for i, seg := range r.segs {
|
||||||
|
if i > len(segs)-1 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
isParam := false
|
||||||
|
if strings.HasPrefix(seg, ":") {
|
||||||
|
isParam = true
|
||||||
|
seg = strings.TrimPrefix(seg, ":")
|
||||||
|
}
|
||||||
|
if !isParam { // verbatim check
|
||||||
|
if strings.HasSuffix(seg, "...") {
|
||||||
|
if strings.HasPrefix(segs[i], seg[:len(seg)-3]) {
|
||||||
|
return ctx, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seg != segs[i] {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isParam {
|
||||||
|
ctx = context.WithValue(ctx, wayContextKey(seg), segs[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx, true
|
||||||
|
}
|
231
net/http/mux_test.go
Normal file
231
net/http/mux_test.go
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tests = []struct {
|
||||||
|
// RouteMethod string
|
||||||
|
RoutePattern string
|
||||||
|
|
||||||
|
Method string
|
||||||
|
Path string
|
||||||
|
Match bool
|
||||||
|
Params map[string]string
|
||||||
|
}{
|
||||||
|
// simple path matching
|
||||||
|
{
|
||||||
|
"/one",
|
||||||
|
"GET", "/one", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/two",
|
||||||
|
"GET", "/two", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/three",
|
||||||
|
"GET", "/three", true, nil,
|
||||||
|
},
|
||||||
|
// methods
|
||||||
|
{
|
||||||
|
"/methodcase",
|
||||||
|
"GET", "/methodcase", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/methodcase",
|
||||||
|
"get", "/methodcase", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/methodcase",
|
||||||
|
"get", "/methodcase", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/method1",
|
||||||
|
"POST", "/method1", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/method2",
|
||||||
|
"GET", "/method2", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/method3",
|
||||||
|
"PUT", "/method3", true, nil,
|
||||||
|
},
|
||||||
|
// all methods
|
||||||
|
{
|
||||||
|
"/all-methods",
|
||||||
|
"GET", "/all-methods", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/all-methods",
|
||||||
|
"POST", "/all-methods", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/all-methods",
|
||||||
|
"PUT", "/all-methods", true, nil,
|
||||||
|
},
|
||||||
|
// nested
|
||||||
|
{
|
||||||
|
"/parent/child/one",
|
||||||
|
"GET", "/parent/child/one", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/parent/child/two",
|
||||||
|
"GET", "/parent/child/two", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/parent/child/three",
|
||||||
|
"GET", "/parent/child/three", true, nil,
|
||||||
|
},
|
||||||
|
// slashes
|
||||||
|
{
|
||||||
|
"slashes/one",
|
||||||
|
"GET", "/slashes/one", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/slashes/two",
|
||||||
|
"GET", "slashes/two", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slashes/three/",
|
||||||
|
"GET", "/slashes/three", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/slashes/four",
|
||||||
|
"GET", "slashes/four/", true, nil,
|
||||||
|
},
|
||||||
|
// prefix
|
||||||
|
{
|
||||||
|
"/prefix/",
|
||||||
|
"GET", "/prefix/anything/else", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/not-prefix",
|
||||||
|
"GET", "/not-prefix/anything/else", false, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/prefixdots...",
|
||||||
|
"GET", "/prefixdots/anything/else", true, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/prefixdots...",
|
||||||
|
"GET", "/prefixdots", true, nil,
|
||||||
|
},
|
||||||
|
// path params
|
||||||
|
{
|
||||||
|
"/path-param/:id",
|
||||||
|
"GET", "/path-param/123", true, map[string]string{"id": "123"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/path-params/:era/:group/:member",
|
||||||
|
"GET", "/path-params/60s/beatles/lennon", true, map[string]string{
|
||||||
|
"era": "60s",
|
||||||
|
"group": "beatles",
|
||||||
|
"member": "lennon",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/path-params-prefix/:era/:group/:member/",
|
||||||
|
"GET", "/path-params-prefix/60s/beatles/lennon/yoko", true, map[string]string{
|
||||||
|
"era": "60s",
|
||||||
|
"group": "beatles",
|
||||||
|
"member": "lennon",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// misc no matches
|
||||||
|
{
|
||||||
|
"/not/enough",
|
||||||
|
"GET", "/not/enough/items", false, nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/not/enough/items",
|
||||||
|
"GET", "/not/enough", false, nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWay(t *testing.T) {
|
||||||
|
for _, test := range tests {
|
||||||
|
r := NewServeMux()
|
||||||
|
match := false
|
||||||
|
var ctx context.Context
|
||||||
|
r.Handle(test.RoutePattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
match = true
|
||||||
|
ctx = r.Context()
|
||||||
|
}))
|
||||||
|
req, err := http.NewRequest(test.Method, test.Path, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("NewRequest: %s", err)
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if match != test.Match {
|
||||||
|
t.Errorf("expected match %v but was %v: %s %s", test.Match, match, test.Method, test.Path)
|
||||||
|
}
|
||||||
|
if len(test.Params) > 0 {
|
||||||
|
for expK, expV := range test.Params {
|
||||||
|
// check using helper
|
||||||
|
actualValStr := Param(ctx, expK)
|
||||||
|
if actualValStr != expV {
|
||||||
|
t.Errorf("Param: context value %s expected \"%s\" but was \"%s\"", expK, expV, actualValStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultipleRoutesDifferentMethods(t *testing.T) {
|
||||||
|
r := NewServeMux()
|
||||||
|
var match string
|
||||||
|
|
||||||
|
r.Handle("/route", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
match = "GET /route"
|
||||||
|
case http.MethodDelete:
|
||||||
|
match = "DELETE /route"
|
||||||
|
case http.MethodPost:
|
||||||
|
match = "POST /route"
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
r.Handle("/route", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
match = "GET /route"
|
||||||
|
}))
|
||||||
|
r.Handle("/route", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
match = "DELETE /route"
|
||||||
|
}))
|
||||||
|
r.Handle("/route", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
match = "POST /route"
|
||||||
|
}))
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "/route", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("NewRequest: %s", err)
|
||||||
|
}
|
||||||
|
r.ServeHTTP(httptest.NewRecorder(), req)
|
||||||
|
if match != "GET /route" {
|
||||||
|
t.Errorf("unexpected: %s", match)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err = http.NewRequest(http.MethodDelete, "/route", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("NewRequest: %s", err)
|
||||||
|
}
|
||||||
|
r.ServeHTTP(httptest.NewRecorder(), req)
|
||||||
|
if match != "DELETE /route" {
|
||||||
|
t.Errorf("unexpected: %s", match)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err = http.NewRequest(http.MethodPost, "/route", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("NewRequest: %s", err)
|
||||||
|
}
|
||||||
|
r.ServeHTTP(httptest.NewRecorder(), req)
|
||||||
|
if match != "POST /route" {
|
||||||
|
t.Errorf("unexpected: %s", match)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,7 +15,8 @@ func (s StatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
NotFoundHandler = StatusHandler(404)
|
NotFoundHandler = StatusHandler(http.StatusNotFound)
|
||||||
NotImplementedHandler = StatusHandler(501)
|
NotImplementedHandler = StatusHandler(http.StatusNotImplemented)
|
||||||
NotLegalHandler = StatusHandler(451)
|
NotLegalHandler = StatusHandler(http.StatusUnavailableForLegalReasons)
|
||||||
|
NotAllowedHandler = StatusHandler(http.StatusMethodNotAllowed)
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user