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 {
|
||||
hdlr.ServeHTTP(w, r)
|
||||
} else {
|
||||
NotImplementedHandler.ServeHTTP(w, r)
|
||||
NotAllowedHandler.ServeHTTP(w, r)
|
||||
}
|
||||
}, 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 (
|
||||
NotFoundHandler = StatusHandler(404)
|
||||
NotImplementedHandler = StatusHandler(501)
|
||||
NotLegalHandler = StatusHandler(451)
|
||||
NotFoundHandler = StatusHandler(http.StatusNotFound)
|
||||
NotImplementedHandler = StatusHandler(http.StatusNotImplemented)
|
||||
NotLegalHandler = StatusHandler(http.StatusUnavailableForLegalReasons)
|
||||
NotAllowedHandler = StatusHandler(http.StatusMethodNotAllowed)
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user