-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #33 from patrickrand/handle-with-context
Added http.HandlerFunc support when using Go 1.7
- Loading branch information
Showing
3 changed files
with
263 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
|
||
} | ||
} |