Skip to content

Commit

Permalink
Merge pull request #33 from patrickrand/handle-with-context
Browse files Browse the repository at this point in the history
Added http.HandlerFunc support when using Go 1.7
  • Loading branch information
dimfeld authored Sep 13, 2016
2 parents 4417943 + c6b0ff0 commit 0fd4c76
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 8 deletions.
39 changes: 31 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@ There are a lot of good routers out there. But looking at the ones that were rea
## Handler
The handler is a simple function with the prototype `func(w http.ResponseWriter, r *http.Request, params map[string]string)`. The params argument contains the parameters parsed from wildcards and catch-alls in the URL, as described below. This type is aliased as httptreemux.HandlerFunc.

### Using http.HandlerFunc
Due to the inclusion of the [context](https://godoc.org/context) package as of Go 1.7, `httptreemux` now supports handlers of type [http.HandlerFunc](https://godoc.org/net/http#HandlerFunc):

```go
router := httptreemux.New()

group := tree.NewGroup("/api")
group.GET("/v1/:id", func(w http.ResponseWriter, r *http.Request, params map[string]string) {
id := params["id"]
fmt.Fprintf(w, "GET /api/v1/%s", id)
})

ctxGroup := group.UsingContext() // sibling to 'group' node in tree
ctxGroup.GET("/v2", func(w http.ResponseWriter, r *http.Request) {
params := httptreemux.ContextParams(r.Context())
id := params["id"]
fmt.Fprintf(w, "GET /api/v2/%s", id)
})

http.ListenAndServe(":8080", router)
```

## Routing Rules
The syntax here is also modeled after httprouter. Each variable in a path may match on one segment only, except for an optional catch-all variable at the end of the URL.

Expand Down Expand Up @@ -72,15 +94,17 @@ router.GET("/:year/:month/:post", postHandler)
router.GET("/:year/:month", archiveHandler)
router.GET("/images/*path", staticHandler)
router.GET("/favicon.ico", staticHandler)

/abc will match /:page
/2014/05 will match /:year/:month
/2014/05/really-great-blog-post will match /:year/:month/:post
/images/CoolImage.gif will match /images/*path
/images/2014/05/MayImage.jpg will also match /images/*path, with all the text after /images stored in the variable path.
/favicon.ico will match /favicon.ico
```

#### Example scenarios

- `/abc` will match `/:page`
- `/2014/05` will match `/:year/:month`
- `/2014/05/really-great-blog-post` will match `/:year/:month/:post`
- `/images/CoolImage.gif` will match `/images/*path`
- `/images/2014/05/MayImage.jpg` will also match `/images/*path`, with all the text after `/images` stored in the variable path.
- `/favicon.ico` will match `/favicon.ico`

### Special Method Behavior
If TreeMux.HeadCanUseGet is set to true, the router will call the GET handler for a pattern when a HEAD request is processed, if no HEAD handler has been added for that pattern. This behavior is enabled by default.

Expand Down Expand Up @@ -172,7 +196,6 @@ When matching on parameters in a route, the `gorilla/pat` router will modify
query string. `httptreemux` does not do this. See [Issue #26](https://github.com/dimfeld/httptreemux/issues/26) for more details and a
code snippet that can perform this transformation for you, should you want it.

## Middleware
This package provides no middleware. But there are a lot of great options out there and it's pretty easy to write your own.

Expand Down
99 changes: 99 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// +build go1.7

package httptreemux

import (
"context"
"net/http"
)

// ContextGroup is a wrapper around Group, with the purpose of mimicking its API, but with the use of http.HandlerFunc-based handlers.
// Instead of passing a parameter map via the handler (i.e. httptreemux.HandlerFunc), the path parameters are accessed via the request
// object's context.
type ContextGroup struct {
group *Group
}

// UsingContext wraps the receiver to return a new instance of a ContextGroup.
// The returned ContextGroup is a sibling to its wrapped Group, within the parent TreeMux.
// The choice of using a *Group as the reciever, as opposed to a function parameter, allows chaining
// while method calls between a TreeMux, Group, and ContextGroup. For example:
//
// tree := httptreemux.New()
// group := tree.NewGroup("/api")
//
// group.GET("/v1", func(w http.ResponseWriter, r *http.Request, params map[string]string) {
// w.Write([]byte(`GET /api/v1`))
// })
//
// group.UsingContext().GET("/v2", func(w http.ResponseWriter, r *http.Request) {
// w.Write([]byte(`GET /api/v2`))
// })
//
// http.ListenAndServe(":8080", tree)
//
func (g *Group) UsingContext() *ContextGroup {
return &ContextGroup{g}
}

// NewContextGroup adds a child context group to its path.
func (cg *ContextGroup) NewContextGroup(path string) *ContextGroup {
return &ContextGroup{cg.group.NewGroup(path)}
}

// Handle allows handling HTTP requests via an http.HandlerFunc, as opposed to an httptreemux.HandlerFunc.
// Any parameters from the request URL are stored in via a map[string]string in the request's context.
func (cg *ContextGroup) Handle(method, path string, handler http.HandlerFunc) {
cg.group.Handle(method, path, func(w http.ResponseWriter, r *http.Request, params map[string]string) {
if params != nil {
r = r.WithContext(context.WithValue(r.Context(), ParamsContextKey, params))
}
handler(w, r)
})
}

// GET is convenience method for handling GET requests on a context group.
func (cg *ContextGroup) GET(path string, handler http.HandlerFunc) {
cg.Handle("GET", path, handler)
}

// POST is convenience method for handling POST requests on a context group.
func (cg *ContextGroup) POST(path string, handler http.HandlerFunc) {
cg.Handle("POST", path, handler)
}

// PUT is convenience method for handling PUT requests on a context group.
func (cg *ContextGroup) PUT(path string, handler http.HandlerFunc) {
cg.Handle("PUT", path, handler)
}

// DELETE is convenience method for handling DELETE requests on a context group.
func (cg *ContextGroup) DELETE(path string, handler http.HandlerFunc) {
cg.Handle("DELETE", path, handler)
}

// PATCH is convenience method for handling PATCH requests on a context group.
func (cg *ContextGroup) PATCH(path string, handler http.HandlerFunc) {
cg.Handle("PATCH", path, handler)
}

// HEAD is convenience method for handling HEAD requests on a context group.
func (cg *ContextGroup) HEAD(path string, handler http.HandlerFunc) {
cg.Handle("HEAD", path, handler)
}

// OPTIONS is convenience method for handling OPTIONS requests on a context group.
func (cg *ContextGroup) OPTIONS(path string, handler http.HandlerFunc) {
cg.Handle("OPTIONS", path, handler)
}

// ContextParams returns the params map associated with the given context if one exists. Otherwise, an empty map is returned.
func ContextParams(ctx context.Context) map[string]string {
if p, ok := ctx.Value(ParamsContextKey).(map[string]string); ok {
return p
}
return map[string]string{}
}

// ParamsContextKey is used to retrieve a path's params map from a request's context.
const ParamsContextKey = "params.context.key"
133 changes: 133 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// +build go1.7

package httptreemux

import (
"context"
"net/http"
"net/http/httptest"
"testing"
)

func TestContextParams(t *testing.T) {
m := map[string]string{"id": "123"}
ctx := context.WithValue(context.Background(), ParamsContextKey, m)

params := ContextParams(ctx)
if params == nil {
t.Errorf("expected '%#v', but got '%#v'", m, params)
}

if v := params["id"]; v != "123" {
t.Errorf("expected '%s', but got '%#v'", m["id"], params["id"])
}
}

func TestContextGroupMethods(t *testing.T) {
for _, scenario := range scenarios {
t.Log(scenario.description)
testContextGroupMethods(t, scenario.RequestCreator, true)
testContextGroupMethods(t, scenario.RequestCreator, false)
}
}

func testContextGroupMethods(t *testing.T, reqGen RequestCreator, headCanUseGet bool) {
var result string
makeHandler := func(method string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
result = method

v, ok := ContextParams(r.Context())["param"]
if !ok {
t.Error("missing key 'param' in context")
}

if headCanUseGet && method == "GET" && v == "HEAD" {
return
}

if v != method {
t.Errorf("invalid key 'param' in context; expected '%s' but got '%s'", method, v)
}
}
}

router := New()
router.HeadCanUseGet = headCanUseGet

cg := router.UsingContext().NewContextGroup("/base").NewContextGroup("/user")
cg.GET("/:param", makeHandler("GET"))
cg.POST("/:param", makeHandler("POST"))
cg.PATCH("/:param", makeHandler("PATCH"))
cg.PUT("/:param", makeHandler("PUT"))
cg.DELETE("/:param", makeHandler("DELETE"))

testMethod := func(method, expect string) {
result = ""
w := httptest.NewRecorder()
r, _ := reqGen(method, "/base/user/"+method, nil)
router.ServeHTTP(w, r)
if expect == "" && w.Code != http.StatusMethodNotAllowed {
t.Errorf("Method %s not expected to match but saw code %d", method, w.Code)
}

if result != expect {
t.Errorf("Method %s got result %s", method, result)
}
}

testMethod("GET", "GET")
testMethod("POST", "POST")
testMethod("PATCH", "PATCH")
testMethod("PUT", "PUT")
testMethod("DELETE", "DELETE")

if headCanUseGet {
t.Log("Test implicit HEAD with HeadCanUseGet = true")
testMethod("HEAD", "GET")
} else {
t.Log("Test implicit HEAD with HeadCanUseGet = false")
testMethod("HEAD", "")
}

cg.HEAD("/:param", makeHandler("HEAD"))
testMethod("HEAD", "HEAD")
}

func TestNewContextGroup(t *testing.T) {
router := New()
group := router.NewGroup("/api")

group.GET("/v1", func(w http.ResponseWriter, r *http.Request, params map[string]string) {
w.Write([]byte(`200 OK GET /api/v1`))
})

group.UsingContext().GET("/v2", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`200 OK GET /api/v2`))
})

tests := []struct {
uri, expected string
}{
{"/api/v1", "200 OK GET /api/v1"},
{"/api/v2", "200 OK GET /api/v2"},
}

for _, tc := range tests {
r, err := http.NewRequest("GET", tc.uri, nil)
if err != nil {
t.Fatal(err)
}

w := httptest.NewRecorder()
router.ServeHTTP(w, r)

if w.Code != http.StatusOK {
t.Errorf("GET %s: expected %d, but got %d", tc.uri, http.StatusOK, w.Code)
}
if got := w.Body.String(); got != tc.expected {
t.Errorf("GET %s : expected %q, but got %q", tc.uri, tc.expected, got)
}

}
}

0 comments on commit 0fd4c76

Please sign in to comment.