Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issues/134: Add a router/muxer with a bit more functionality #140

Merged
merged 43 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1f7aec7
take code from github.com/matryer/way
komuw Sep 27, 2022
cf65e5f
add t.parallel and name tests
komuw Sep 27, 2022
fb6dcc2
f
komuw Sep 27, 2022
6407688
remove pointer
komuw Sep 27, 2022
dbf0621
remove the feature where uri paths/routes can have ... suffix. Like `…
komuw Sep 27, 2022
e42aec2
remove the need to handle prefixes
komuw Sep 27, 2022
39f8df5
add detection for route conflicts;
komuw Sep 27, 2022
8231c4e
add test for conflicts
komuw Sep 27, 2022
0e84e91
f
komuw Sep 27, 2022
9673830
add test for conflicts
komuw Sep 27, 2022
1e06c10
d
komuw Sep 27, 2022
02cf4d1
f
komuw Sep 27, 2022
7c8fcca
f
komuw Sep 27, 2022
b0da261
f
komuw Sep 27, 2022
2a0e832
f
komuw Sep 27, 2022
eb15a8a
d
komuw Sep 27, 2022
4b72b4e
stop using * for methods
komuw Sep 27, 2022
21d19ff
s
komuw Sep 27, 2022
b257433
ServeHTTP() method has no business to decide whether to admit a reque…
komuw Sep 27, 2022
2071abf
f
komuw Sep 27, 2022
9af8ff1
f
komuw Sep 27, 2022
2e3283d
f
komuw Sep 27, 2022
d780d7f
f
komuw Sep 27, 2022
cb791ec
f
komuw Sep 27, 2022
ea3f7ee
f
komuw Sep 27, 2022
86bb90d
f
komuw Sep 27, 2022
b00b84f
f
komuw Sep 27, 2022
aae6371
f
komuw Sep 27, 2022
587139a
f
komuw Sep 27, 2022
930755a
f
komuw Sep 27, 2022
c2b54b5
f
komuw Sep 27, 2022
c4bd221
f
komuw Sep 27, 2022
a2feb05
f
komuw Sep 27, 2022
5d49f79
f
komuw Sep 27, 2022
21f997d
f
komuw Sep 27, 2022
8fd25cf
f
komuw Sep 27, 2022
bf050eb
f
komuw Sep 27, 2022
9071414
f
komuw Sep 27, 2022
6d28c85
f
komuw Sep 27, 2022
66ce59d
f
komuw Sep 27, 2022
1a2f361
f
komuw Sep 27, 2022
050c349
f
komuw Sep 27, 2022
0de5610
f
komuw Sep 27, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Most recent version is listed first.
- Add password hashing capabilities: https://github.com/komuw/ong/pull/137
- Simplify loadshedding implementation: https://github.com/komuw/ong/pull/138
- Make automax to be a stand-alone package: https://github.com/komuw/ong/pull/139
- Add a router/muxer with a bit more functionality: https://github.com/komuw/ong/pull/140

## v0.0.8
- Improve documentation.
Expand Down
200 changes: 200 additions & 0 deletions server/newmux/mux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// TODO: docs.
komuw marked this conversation as resolved.
Show resolved Hide resolved
package mux

import (
"context"
"fmt"
"net/http"
"reflect"
"runtime"
"strings"
)

// Most of the code here is insipired by(or taken from):
// (a) https://github.com/matryer/way whose license(MIT) can be found here: https://github.com/matryer/way/blob/9632d0c407b008073d19d0c4da1e0fc3e9477508/LICENSE

// muxContextKey is the context key type for storing path parameters in context.Context.
type muxContextKey string

type route struct {
method string
segs []string
handler http.Handler
}

func (r route) String() string {
return fmt.Sprintf("route{method: %s, segs: %s}", r.method, r.segs)
}

func (r route) match(ctx context.Context, router *Router, segs []string) (context.Context, bool) {
if len(segs) > len(r.segs) {
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 seg != segs[i] {
return nil, false
}
}
if isParam {
ctx = context.WithValue(ctx, muxContextKey(seg), segs[i])
}
}
return ctx, true
}

// Router routes HTTP requests.
type Router struct {
routes []route
// notFoundHandler is the http.Handler to call when no routes
// match. By default uses http.NotFoundHandler().
notFoundHandler http.Handler
}

// NewRouter makes a new Router.
func NewRouter() *Router {
return &Router{
// TODO: add ability for someone to pass in a notFound handler.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// If they pass in `nil` we default to `http.NotFoundHandler()`
notFoundHandler: http.NotFoundHandler(),
}
}

func (r *Router) pathSegments(p string) []string {
return strings.Split(strings.Trim(p, "/"), "/")
}

// Handle adds a handler with the specified method and pattern.
// Method can be any HTTP method string or "*" to match all methods.
// Pattern can contain path segments such as: /item/:id which is
// accessible via the Param function.
func (r *Router) Handle(method, pattern string, handler http.Handler) {
if !strings.HasSuffix(pattern, "/") {
// this will make the mux send requests for;
// - localhost:80/check
// - localhost:80/check/
// to the same handler.
pattern = pattern + "/"
}
if !strings.HasPrefix(pattern, "/") {
pattern = "/" + pattern
}

// Try and detect conflict before adding a new route.
r.detectConflict(method, pattern, handler)

route := route{
method: strings.ToLower(method),
segs: r.pathSegments(pattern),
handler: handler,
}
r.routes = append(r.routes, route)
}

// HandleFunc is the http.HandlerFunc alternative to http.Handle.
func (r *Router) HandleFunc(method, pattern string, fn http.HandlerFunc) {
r.Handle(method, pattern, fn)
}

// ServeHTTP routes the incoming http.Request based on method and path
// extracting path parameters as it goes.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
method := strings.ToLower(req.Method)
segs := r.pathSegments(req.URL.Path)
for _, route := range r.routes {
if route.method != method && route.method != "*" {
// TODO: fix how we handle "*" methods.
komuw marked this conversation as resolved.
Show resolved Hide resolved
continue
}
if ctx, ok := route.match(req.Context(), r, segs); ok {
route.handler.ServeHTTP(w, req.WithContext(ctx))
return
}
}
r.notFoundHandler.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(muxContextKey(param)).(string)
if !ok {
return ""
}
return vStr
}

// detectConflict panics with a diagnostic message when you try to add a route that would conflict with an already existing one.
//
// The panic message looks like:
//
// You are trying to add
// pattern: /post/:id/
// method: GET
// handler: github.com/komuw/ong/server/newmux.secondRoute.func1 - /home/komuw/mystuff/ong/server/newmux/mux_test.go:351
// However
// pattern: post/create
// method: GET
// handler: github.com/komuw/ong/server/newmux.firstRoute.func1 - /home/komuw/mystuff/ong/server/newmux/mux_test.go:345
// already exists and would conflict.
//
// /
func (r *Router) detectConflict(method, pattern string, handler http.Handler) {
// Conflicting routes are a bad thing.
// They can be a source of bugs and confusion.
// see: https://www.alexedwards.net/blog/which-go-router-should-i-use

incomingSegments := r.pathSegments(pattern)
for _, route := range r.routes {
existingSegments := route.segs
sameLen := len(incomingSegments) == len(existingSegments)
if !sameLen {
// no conflict
break
}

panicMsg := fmt.Sprintf(`

You are trying to add
pattern: %s
method: %s
handler: %v
However
pattern: %s
method: %s
handler: %v
already exists and would conflict.`,
pattern,
method,
getfunc(handler),
strings.Join(route.segs, "/"),
strings.ToUpper(route.method),
getfunc(route.handler),
)

for _, v := range incomingSegments {
if strings.Contains(v, ":") {
panic(panicMsg)
}
}
for _, v := range existingSegments {
if strings.Contains(v, ":") {
panic(panicMsg)
}
}
}
}

func getfunc(handler interface{}) string {
fn := runtime.FuncForPC(reflect.ValueOf(handler).Pointer())
file, line := fn.FileLine(fn.Entry())
return fmt.Sprintf("%s - %s:%d", fn.Name(), file, line)
}
Loading