3 Commits
mux ... v0.3.0

6 changed files with 335 additions and 79 deletions

View File

@@ -2,14 +2,13 @@ package log
// Logger is a logging interface with only the essentials that a function that needs to log should care about. Compatible with standard Go logger. // Logger is a logging interface with only the essentials that a function that needs to log should care about. Compatible with standard Go logger.
type Logger interface { type Logger interface {
Print(v ...interface{}) Fatal(v ...any)
Printf(format string, v ...interface{}) Fatalf(format string, v ...any)
Println(v ...interface{}) Fatalln(v ...any)
Panic(v ...any)
Panicf(format string, v ...any)
Panicln(v ...any)
Print(v ...any)
Printf(format string, v ...any)
Println(v ...any)
} }
// None provides a logger that doesnt log anything
type None struct{}
func (n None) Print(v ...interface{}) {}
func (n None) Printf(format string, v ...interface{}) {}
func (n None) Println(v ...interface{}) {}

14
log/none.go Normal file
View File

@@ -0,0 +1,14 @@
package log
// None provides a logger that doesnt log anything
type None struct{}
func (n None) Fatal(v ...any) {}
func (n None) Fatalf(format string, v ...any) {}
func (n None) Fatalln(v ...any) {}
func (n None) Panic(v ...any) {}
func (n None) Panicf(format string, v ...any) {}
func (n None) Panicln(v ...any) {}
func (n None) Print(v ...any) {}
func (n None) Printf(format string, v ...any) {}
func (n None) Println(v ...any) {}

View File

@@ -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
} }

View File

@@ -1,95 +1,106 @@
package http package http
import ( import (
"context"
"net/http" "net/http"
"strconv"
"strings" "strings"
) )
// wayContextKey is the context key type for storing
// parameters in context.Context.
type wayContextKey string
// Router routes HTTP requests.
type ServeMux struct { type ServeMux struct {
routes []route routes []*route
// NotFound is the http.Handler to call when no routes
// match. By default uses http.NotFoundHandler().
NotFound http.Handler
} }
func (mux *ServeMux) Handle(pattern string, handler http.Handler, pathParams ...any) { // NewRouter makes a new Router.
mux.routes = append(mux.routes, newRoute(pattern, handler, pathParams...)) func NewServeMux() *ServeMux {
return &ServeMux{
NotFound: http.NotFoundHandler(),
}
} }
func (mux *ServeMux) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request), pathParams ...any) { func (r *ServeMux) pathSegments(p string) []string {
mux.routes = append(mux.routes, newRoute(pattern, http.HandlerFunc(handler), pathParams...)) return strings.Split(strings.Trim(p, "/"), "/")
} }
func (mux *ServeMux) Handler(r *http.Request) (h http.Handler, pattern string) { // Handle adds a handler with the specified pattern.
for _, rte := range mux.routes { // Pattern can contain path segments such as: /item/:id which is
switch { // accessible via the Param function.
case rte.matcher(r): // If pattern ends with trailing /, it acts as a prefix.
return rte.handler, rte.pattern 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
} }
} }
return http.HandlerFunc(http.NotFound), "" r.NotFound.ServeHTTP(w, req)
} }
func (mux *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Param gets the path parameter from the specified Context.
if r.RequestURI == "*" { // Returns an empty string if the parameter was not found.
if r.ProtoAtLeast(1, 1) { func Param(ctx context.Context, param string) string {
w.Header().Set("Connection", "close") vStr, ok := ctx.Value(wayContextKey(param)).(string)
} if !ok {
w.WriteHeader(http.StatusBadRequest) return ""
return
} }
h, _ := mux.Handler(r) return vStr
h.ServeHTTP(w, r)
} }
type route struct { type route struct {
pattern string segs []string
matcher func(r *http.Request) bool handler http.Handler
handler http.HandlerFunc prefix bool
} }
func newRoute(pattern string, handler http.Handler, vars ...interface{}) route { func (r *route) match(ctx context.Context, router *ServeMux, segs []string) (context.Context, bool) {
return route{ if len(segs) > len(r.segs) && !r.prefix {
pattern, return nil, false
func(r *http.Request) bool {
return match(r.URL.Path, pattern, vars...)
},
handler.ServeHTTP,
} }
} for i, seg := range r.segs {
if i > len(segs)-1 {
// match reports whether path matches the given pattern, which is a return nil, false
// path with '+' wildcards wherever you want to use a parameter. Path }
// parameters are assigned to the pointers in vars (len(vars) must be isParam := false
// the number of wildcards), which must be of type *string or *int. if strings.HasPrefix(seg, ":") {
func match(path, pattern string, vars ...interface{}) bool { isParam = true
for ; pattern != "" && path != ""; pattern = pattern[1:] { seg = strings.TrimPrefix(seg, ":")
switch pattern[0] { }
case '+': if !isParam { // verbatim check
// '+' matches till next slash in path if strings.HasSuffix(seg, "...") {
slash := strings.IndexByte(path, '/') if strings.HasPrefix(segs[i], seg[:len(seg)-3]) {
if slash < 0 { return ctx, true
slash = len(path)
}
segment := path[:slash]
path = path[slash:]
switch p := vars[0].(type) {
case *string:
*p = segment
case *int:
n, err := strconv.Atoi(segment)
if err != nil || n < 0 {
return false
} }
*p = n
default:
panic("vars must be *string or *int")
} }
vars = vars[1:] if seg != segs[i] {
case path[0]: return nil, false
// non-'+' pattern byte must match path byte }
path = path[1:] }
default: if isParam {
return false ctx = context.WithValue(ctx, wayContextKey(seg), segs[i])
} }
} }
return path == "" && pattern == "" return ctx, true
} }

231
net/http/mux_test.go Normal file
View 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)
}
}

View File

@@ -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)
) )