From 0f938a0e68d1b5c61ecfa74bb97b9d4deba7a23e Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 24 Aug 2016 05:12:35 +0900 Subject: [PATCH 1/5] support new context package and stay backwards compatible with the old one! (unfortunately, LogHandler and Middleware/Afterware have changed) --- README.md | 10 +- globalmux.go | 79 +++++++++ globalmux_17.go | 85 ++++++++++ globalmux_old.go | 84 ++++++++++ handler_17.go | 67 ++++++++ handler.go => handler_old.go | 2 + kami.go | 2 + kami_17.go | 76 +++++++++ kami_17_test.go | 266 +++++++++++++++++++++++++++++++ kami_test.go => kami_old_test.go | 2 + middleware.go | 179 +-------------------- middleware_17.go | 242 ++++++++++++++++++++++++++++ middleware_old.go | 184 +++++++++++++++++++++ middleware_test.go | 4 +- mux.go | 2 + mux_17.go | 144 +++++++++++++++++ 16 files changed, 1244 insertions(+), 184 deletions(-) create mode 100644 globalmux.go create mode 100644 globalmux_17.go create mode 100644 globalmux_old.go create mode 100644 handler_17.go rename handler.go => handler_old.go (98%) create mode 100644 kami_17.go create mode 100644 kami_17_test.go rename kami_test.go => kami_old_test.go (99%) create mode 100644 middleware_17.go create mode 100644 middleware_old.go create mode 100644 mux_17.go diff --git a/README.md b/README.md index 6d78eb5..3ffb7c0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## kami [![GoDoc](https://godoc.org/github.com/guregu/kami?status.svg)](https://godoc.org/github.com/guregu/kami) [![CircleCI](https://circleci.com/gh/guregu/kami.svg?style=svg)](https://circleci.com/gh/guregu/kami) `import "github.com/guregu/kami"` [or](http://gopkg.in) `import "gopkg.in/guregu/kami.v1"` -kami (ē„ž) is a tiny web framework using [x/net/context](https://blog.golang.org/context) for request context and [httptreemux](https://github.com/dimfeld/httptreemux) for routing. It includes a simple system for running hierarchical middleware before and after requests, in addition to log and panic hooks. Graceful restart via einhorn is also supported. +kami (ē„ž) is a tiny web framework using [context](https://blog.golang.org/context) for request context and [httptreemux](https://github.com/dimfeld/httptreemux) for routing. It includes a simple system for running hierarchical middleware before and after requests, in addition to log and panic hooks. Graceful restart via einhorn is also supported. kami is designed to be used as central registration point for your routes, middleware, and context "god object". You are encouraged to use the global functions, but kami supports multiple muxes with `kami.New()`. @@ -9,8 +9,10 @@ You are free to mount `kami.Handler()` wherever you wish, but a helpful `kami.Se Here is a [presentation about the birth of kami](http://go-talks.appspot.com/github.com/guregu/slides/kami/kami.slide), explaining some of the design choices. +Both `context` and `x/net/context` are supported. + ### Example -A contrived example using kami and x/net/context to localize greetings. +A contrived example using kami and context to localize greetings. [Skip :fast_forward:](#usage) @@ -22,9 +24,9 @@ package main import ( "fmt" "net/http" + "context" "github.com/guregu/kami" - "golang.org/x/net/context" "github.com/my-github/greeting" // see package greeting below ) @@ -52,8 +54,8 @@ package greeting import ( "net/http" + "context" - "golang.org/x/net/context" "golang.org/x/text/language" ) diff --git a/globalmux.go b/globalmux.go new file mode 100644 index 0000000..3aebedf --- /dev/null +++ b/globalmux.go @@ -0,0 +1,79 @@ +package kami + +import ( + "net/http" + + "github.com/dimfeld/httptreemux" +) + +var ( + routes = newRouter() + enable405 = true +) + +func init() { + // set up the default 404/405 handlers + NotFound(nil) + MethodNotAllowed(nil) +} + +func newRouter() *httptreemux.TreeMux { + r := httptreemux.New() + r.PathSource = httptreemux.URLPath + r.RedirectBehavior = httptreemux.Redirect307 + r.RedirectMethodBehavior = map[string]httptreemux.RedirectBehavior{ + "GET": httptreemux.Redirect301, + } + return r +} + +// Handler returns an http.Handler serving registered routes. +func Handler() http.Handler { + return routes +} + +// Handle registers an arbitrary method handler under the given path. +func Handle(method, path string, handler HandlerType) { + routes.Handle(method, path, bless(wrap(handler))) +} + +// Get registers a GET handler under the given path. +func Get(path string, handler HandlerType) { + Handle("GET", path, handler) +} + +// Post registers a POST handler under the given path. +func Post(path string, handler HandlerType) { + Handle("POST", path, handler) +} + +// Put registers a PUT handler under the given path. +func Put(path string, handler HandlerType) { + Handle("PUT", path, handler) +} + +// Patch registers a PATCH handler under the given path. +func Patch(path string, handler HandlerType) { + Handle("PATCH", path, handler) +} + +// Head registers a HEAD handler under the given path. +func Head(path string, handler HandlerType) { + Handle("HEAD", path, handler) +} + +// Head registers a OPTIONS handler under the given path. +func Options(path string, handler HandlerType) { + Handle("OPTIONS", path, handler) +} + +// Delete registers a DELETE handler under the given path. +func Delete(path string, handler HandlerType) { + Handle("DELETE", path, handler) +} + +// EnableMethodNotAllowed enables or disables automatic Method Not Allowed handling. +// Note that this is enabled by default. +func EnableMethodNotAllowed(enabled bool) { + enable405 = enabled +} diff --git a/globalmux_17.go b/globalmux_17.go new file mode 100644 index 0000000..7671b79 --- /dev/null +++ b/globalmux_17.go @@ -0,0 +1,85 @@ +// +build go1.7 + +package kami + +import ( + "context" + "net/http" + + "github.com/dimfeld/httptreemux" + "github.com/zenazn/goji/web/mutil" +) + +var ( + // Context is the root "god object" from which every request's context will derive. + Context = context.Background() + + // PanicHandler will, if set, be called on panics. + // You can use kami.Exception(ctx) within the panic handler to get panic details. + PanicHandler HandlerType + // LogHandler will, if set, wrap every request and be called at the very end. + LogHandler func(context.Context, mutil.WriterProxy, *http.Request) +) + +// NotFound registers a special handler for unregistered (404) paths. +// If handle is nil, use the default http.NotFound behavior. +func NotFound(handler HandlerType) { + // set up the default handler if needed + // we need to bless this so middleware will still run for a 404 request + if handler == nil { + handler = HandlerFunc(func(_ context.Context, w http.ResponseWriter, r *http.Request) { + http.NotFound(w, r) + }) + } + + h := bless(wrap(handler)) + routes.NotFoundHandler = func(w http.ResponseWriter, r *http.Request) { + h(w, r, nil) + } +} + +// MethodNotAllowed registers a special handler for automatically responding +// to invalid method requests (405). +func MethodNotAllowed(handler HandlerType) { + if handler == nil { + handler = HandlerFunc(func(_ context.Context, w http.ResponseWriter, r *http.Request) { + http.Error(w, + http.StatusText(http.StatusMethodNotAllowed), + http.StatusMethodNotAllowed, + ) + }) + } + + h := bless(wrap(handler)) + routes.MethodNotAllowedHandler = func(w http.ResponseWriter, r *http.Request, methods map[string]httptreemux.HandlerFunc) { + if !enable405 { + routes.NotFoundHandler(w, r) + return + } + h(w, r, nil) + } +} + +// bless creates a new kamified handler using the global mux and middleware. +func bless(h ContextHandler) httptreemux.HandlerFunc { + k := kami{ + handler: h, + base: &Context, + middleware: defaultMW, + panicHandler: &PanicHandler, + logHandler: &LogHandler, + } + return k.handle +} + +// Reset changes the root Context to context.Background(). +// It removes every handler and all middleware. +func Reset() { + Context = context.Background() + PanicHandler = nil + LogHandler = nil + defaultMW = newWares() + routes = newRouter() + NotFound(nil) + MethodNotAllowed(nil) +} diff --git a/globalmux_old.go b/globalmux_old.go new file mode 100644 index 0000000..f4b20fc --- /dev/null +++ b/globalmux_old.go @@ -0,0 +1,84 @@ +// +build !go1.7 + +package kami + +import ( + "net/http" + + "github.com/zenazn/goji/web/mutil" + "golang.org/x/net/context" +) + +var ( + // Context is the root "god object" from which every request's context will derive. + Context = context.Background() + + // PanicHandler will, if set, be called on panics. + // You can use kami.Exception(ctx) within the panic handler to get panic details. + PanicHandler HandlerType + // LogHandler will, if set, wrap every request and be called at the very end. + LogHandler func(context.Context, mutil.WriterProxy, *http.Request) +) + +// NotFound registers a special handler for unregistered (404) paths. +// If handle is nil, use the default http.NotFound behavior. +func NotFound(handler HandlerType) { + // set up the default handler if needed + // we need to bless this so middleware will still run for a 404 request + if handler == nil { + handler = HandlerFunc(func(_ context.Context, w http.ResponseWriter, r *http.Request) { + http.NotFound(w, r) + }) + } + + h := bless(wrap(handler)) + routes.NotFoundHandler = func(w http.ResponseWriter, r *http.Request) { + h(w, r, nil) + } +} + +// MethodNotAllowed registers a special handler for automatically responding +// to invalid method requests (405). +func MethodNotAllowed(handler HandlerType) { + if handler == nil { + handler = HandlerFunc(func(_ context.Context, w http.ResponseWriter, r *http.Request) { + http.Error(w, + http.StatusText(http.StatusMethodNotAllowed), + http.StatusMethodNotAllowed, + ) + }) + } + + h := bless(wrap(handler)) + routes.MethodNotAllowedHandler = func(w http.ResponseWriter, r *http.Request, methods map[string]httptreemux.HandlerFunc) { + if !enable405 { + routes.NotFoundHandler(w, r) + return + } + h(w, r, nil) + } +} + +// bless creates a new kamified handler using the global mux and middleware. +func bless(h ContextHandler) httptreemux.HandlerFunc { + k := kami{ + handler: h, + base: &Context, + middleware: defaultMW, + panicHandler: &PanicHandler, + logHandler: &LogHandler, + } + return k.handle +} + +// Reset changes the root Context to context.Background(). +// It removes every handler and all middleware. +func Reset() { + Context = context.Background() + PanicHandler = nil + LogHandler = nil + defaultMW = newWares() + routes = newRouter() + NotFound(nil) + MethodNotAllowed(nil) +} diff --git a/handler_17.go b/handler_17.go new file mode 100644 index 0000000..679276c --- /dev/null +++ b/handler_17.go @@ -0,0 +1,67 @@ +// +build go1.7 + +package kami + +import ( + "context" + "fmt" + "net/http" + + netcontext "golang.org/x/net/context" +) + +// HandlerType is the type of Handlers and types that kami internally converts to +// ContextHandler. In order to provide an expressive API, this type is an alias for +// interface{} that is named for the purposes of documentation, however only the +// following concrete types are accepted: +// - types that implement http.Handler +// - types that implement ContextHandler +// - func(http.ResponseWriter, *http.Request) +// - func(context.Context, http.ResponseWriter, *http.Request) +type HandlerType interface{} + +// ContextHandler is like http.Handler but supports context. +type ContextHandler interface { + ServeHTTPContext(context.Context, http.ResponseWriter, *http.Request) +} + +// OldContextHandler is like ContextHandler but uses the old x/net/context. +type OldContextHandler interface { + ServeHTTPContext(netcontext.Context, http.ResponseWriter, *http.Request) +} + +func old2new(old OldContextHandler) ContextHandler { + return HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + old.ServeHTTPContext(ctx, w, r) + }) +} + +// HandlerFunc is like http.HandlerFunc with context. +type HandlerFunc func(context.Context, http.ResponseWriter, *http.Request) + +func (h HandlerFunc) ServeHTTPContext(ctx context.Context, w http.ResponseWriter, r *http.Request) { + h(ctx, w, r) +} + +// wrap tries to turn a HandlerType into a ContextHandler +func wrap(h HandlerType) ContextHandler { + switch x := h.(type) { + case ContextHandler: + return x + case func(context.Context, http.ResponseWriter, *http.Request): + return HandlerFunc(x) + case func(netcontext.Context, http.ResponseWriter, *http.Request): + return HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + x(ctx, w, r) + }) + case http.Handler: + return HandlerFunc(func(_ context.Context, w http.ResponseWriter, r *http.Request) { + x.ServeHTTP(w, r) + }) + case func(http.ResponseWriter, *http.Request): + return HandlerFunc(func(_ context.Context, w http.ResponseWriter, r *http.Request) { + x(w, r) + }) + } + panic(fmt.Errorf("unsupported HandlerType: %T", h)) +} diff --git a/handler.go b/handler_old.go similarity index 98% rename from handler.go rename to handler_old.go index 0f703a4..5200215 100644 --- a/handler.go +++ b/handler_old.go @@ -1,3 +1,5 @@ +// +build !go1.7 + package kami import ( diff --git a/kami.go b/kami.go index 52a1ab4..831393c 100644 --- a/kami.go +++ b/kami.go @@ -1,3 +1,5 @@ +// +build !go1.7 + package kami import ( diff --git a/kami_17.go b/kami_17.go new file mode 100644 index 0000000..4b5f295 --- /dev/null +++ b/kami_17.go @@ -0,0 +1,76 @@ +// +build go1.7 + +package kami + +import ( + "context" + "net/http" + + "github.com/zenazn/goji/web/mutil" +) + +// kami is the heart of the package. +// It wraps a ContextHandler into an httprouter compatible request, +// in order to run all the middleware and other special handlers. +type kami struct { + handler ContextHandler + base *context.Context + middleware *wares + panicHandler *HandlerType + logHandler *func(context.Context, mutil.WriterProxy, *http.Request) +} + +func (k kami) handle(w http.ResponseWriter, r *http.Request, params map[string]string) { + var ( + ctx = defaultContext(*k.base, r) + handler = k.handler + mw = *k.middleware + panicHandler = *k.panicHandler + logHandler = *k.logHandler + ranLogHandler = false // track this in case the log handler blows up + ) + if len(params) > 0 { + ctx = newContextWithParams(ctx, params) + } + + if ctx != context.Background() { + r = r.WithContext(ctx) + } + + var proxy mutil.WriterProxy + if logHandler != nil || mw.needsWrapper() { + proxy = mutil.WrapWriter(w) + w = proxy + } + + if panicHandler != nil { + defer func() { + if err := recover(); err != nil { + ctx = newContextWithException(ctx, err) + r = r.WithContext(ctx) + wrap(panicHandler).ServeHTTPContext(ctx, w, r) + + if logHandler != nil && !ranLogHandler { + logHandler(ctx, proxy, r) + // should only happen if header hasn't been written + proxy.WriteHeader(http.StatusInternalServerError) + } + } + }() + } + + r, ctx, ok := mw.run(ctx, w, r) + if ok { + handler.ServeHTTPContext(ctx, w, r) + } + if proxy != nil { + r, ctx = mw.after(ctx, proxy, r) + } + + if logHandler != nil { + ranLogHandler = true + logHandler(ctx, proxy, r) + // should only happen if header hasn't been written + proxy.WriteHeader(http.StatusInternalServerError) + } +} diff --git a/kami_17_test.go b/kami_17_test.go new file mode 100644 index 0000000..e961987 --- /dev/null +++ b/kami_17_test.go @@ -0,0 +1,266 @@ +// +build go1.7 + +package kami_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/zenazn/goji/web/mutil" + + "github.com/guregu/kami" +) + +func TestKami(t *testing.T) { + kami.Reset() + + expect := func(ctx context.Context, i int) context.Context { + if prev := ctx.Value(i - 1).(int); prev != i-1 { + t.Error("missing", i) + } + if curr := ctx.Value(i); curr != nil { + t.Error("pre-existing", i) + } + return context.WithValue(ctx, i, i) + } + expectEqual := func(one, two context.Context, i int) { + if one != two { + t.Error(i, "mismatched contexes", one, "\nā‰ \n", two) + } + } + + kami.Use("/", func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + expectEqual(ctx, r.Context(), 1) + ctx = context.WithValue(ctx, 1, 1) + ctx = context.WithValue(ctx, "handler", new(bool)) + ctx = context.WithValue(ctx, "done", new(bool)) + ctx = context.WithValue(ctx, "recovered", new(bool)) + return ctx + }) + kami.Use("/a/", func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + expectEqual(ctx, r.Context(), 2) + ctx = expect(ctx, 2) + return ctx + }) + kami.Use("/a/", func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + expectEqual(ctx, r.Context(), 3) + ctx = expect(ctx, 3) + return ctx + }) + kami.Use("/a/b", func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + expectEqual(ctx, r.Context(), 4) + ctx = expect(ctx, 4) + return ctx + }) + kami.Use("/a/*files", func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + expectEqual(ctx, r.Context(), 5) + ctx = expect(ctx, 5) + return ctx + }) + kami.Get("/a/b", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + expectEqual(ctx, r.Context(), 5) + if prev := ctx.Value(5).(int); prev != 5 { + t.Error("handler: missing", 5) + } + *(ctx.Value("handler").(*bool)) = true + + w.WriteHeader(http.StatusTeapot) + }) + kami.After("/a/*files", func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + expectEqual(ctx, r.Context(), 6) + ctx = expect(ctx, 6) + if !*(ctx.Value("handler").(*bool)) { + t.Error("ran before handler") + } + return ctx + }) + kami.After("/a/b", kami.Afterware(func(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { + expectEqual(ctx, r.Context(), 7) + ctx = expect(ctx, 7) + return ctx + })) + kami.After("/a/", func(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { + expectEqual(ctx, r.Context(), 9) + ctx = expect(ctx, 9) + return ctx + }) + kami.After("/a/", func(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { + expectEqual(ctx, r.Context(), 8) + ctx = expect(ctx, 8) + return ctx + }) + kami.After("/", func(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { + expectEqual(ctx, r.Context(), 10) + if status := w.Status(); status != http.StatusTeapot { + t.Error("wrong status", status) + } + + ctx = expect(ctx, 10) + *(ctx.Value("done").(*bool)) = true + panic("šŸ£") + return nil + }) + kami.PanicHandler = func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + expectEqual(ctx, r.Context(), 11) + if got := kami.Exception(ctx); got.(string) != "šŸ£" { + t.Error("panic handler: expected sushi, got", got) + } + if !*(ctx.Value("done").(*bool)) { + t.Error("didn't finish") + } + *(ctx.Value("recovered").(*bool)) = true + } + kami.LogHandler = func(ctx context.Context, w mutil.WriterProxy, r *http.Request) { + expectEqual(ctx, r.Context(), 12) + if !*(ctx.Value("recovered").(*bool)) { + t.Error("didn't recover") + } + } + + expectResponseCode(t, "GET", "/a/b", http.StatusTeapot) +} + +func TestLoggerAndPanic(t *testing.T) { + kami.Reset() + // test logger with panic + status := 0 + kami.LogHandler = func(ctx context.Context, w mutil.WriterProxy, r *http.Request) { + status = w.Status() + } + kami.PanicHandler = kami.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + err := kami.Exception(ctx) + if err != "test panic" { + t.Error("unexpected exception:", err) + } + w.WriteHeader(http.StatusServiceUnavailable) + }) + kami.Post("/test", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + panic("test panic") + }) + kami.Put("/ok", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + expectResponseCode(t, "POST", "/test", http.StatusServiceUnavailable) + if status != http.StatusServiceUnavailable { + t.Error("log handler received wrong status code", status, "ā‰ ", http.StatusServiceUnavailable) + } + + // test loggers without panics + expectResponseCode(t, "PUT", "/ok", http.StatusOK) + if status != http.StatusOK { + t.Error("log handler received wrong status code", status, "ā‰ ", http.StatusOK) + } +} + +func TestPanickingLogger(t *testing.T) { + kami.Reset() + kami.LogHandler = func(ctx context.Context, w mutil.WriterProxy, r *http.Request) { + t.Log("log handler") + panic("test panic") + } + kami.PanicHandler = kami.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + t.Log("panic handler") + err := kami.Exception(ctx) + if err != "test panic" { + t.Error("unexpected exception:", err) + } + w.WriteHeader(http.StatusServiceUnavailable) + }) + kami.Options("/test", noop) + + expectResponseCode(t, "OPTIONS", "/test", http.StatusServiceUnavailable) +} + +func TestNotFound(t *testing.T) { + kami.Reset() + kami.Use("/missing/", func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + return context.WithValue(ctx, "ok", true) + }) + kami.NotFound(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ok, _ := ctx.Value("ok").(bool) + if !ok { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusTeapot) + }) + + expectResponseCode(t, "GET", "/missing/hello", http.StatusTeapot) +} + +func TestNotFoundDefault(t *testing.T) { + kami.Reset() + + expectResponseCode(t, "GET", "/missing/hello", http.StatusNotFound) +} + +func TestMethodNotAllowed(t *testing.T) { + kami.Reset() + kami.Use("/test", func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + return context.WithValue(ctx, "ok", true) + }) + kami.Post("/test", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + panic("test panic") + }) + + kami.MethodNotAllowed(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ok, _ := ctx.Value("ok").(bool) + if !ok { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusTeapot) + }) + + expectResponseCode(t, "GET", "/test", http.StatusTeapot) +} + +func TestEnableMethodNotAllowed(t *testing.T) { + kami.Reset() + kami.Post("/test", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + panic("test panic") + }) + + // Handling enabled by default + expectResponseCode(t, "GET", "/test", http.StatusMethodNotAllowed) + + // Not found deals with it when handling disabled + kami.EnableMethodNotAllowed(false) + expectResponseCode(t, "GET", "/test", http.StatusNotFound) + + // And MethodNotAllowed status when handling enabled + kami.EnableMethodNotAllowed(true) + expectResponseCode(t, "GET", "/test", http.StatusMethodNotAllowed) +} + +func TestMethodNotAllowedDefault(t *testing.T) { + kami.Reset() + kami.Post("/test", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + panic("test panic") + }) + + expectResponseCode(t, "GET", "/test", http.StatusMethodNotAllowed) +} + +func noop(ctx context.Context, w http.ResponseWriter, r *http.Request) {} + +func noopMW(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + return ctx +} + +func expectResponseCode(t *testing.T, method, path string, expected int) { + resp := httptest.NewRecorder() + req, err := http.NewRequest(method, path, nil) + if err != nil { + t.Fatal(err) + } + + kami.Handler().ServeHTTP(resp, req) + + if resp.Code != expected { + t.Error("should return HTTP", http.StatusText(expected)+":", resp.Code, "ā‰ ", expected) + } +} diff --git a/kami_test.go b/kami_old_test.go similarity index 99% rename from kami_test.go rename to kami_old_test.go index ad7fccf..bcc01f1 100644 --- a/kami_test.go +++ b/kami_old_test.go @@ -1,3 +1,5 @@ +// +build !go1.7 + package kami_test import ( diff --git a/middleware.go b/middleware.go index b0c669f..0e49c71 100644 --- a/middleware.go +++ b/middleware.go @@ -1,47 +1,11 @@ package kami import ( - "fmt" - "net/http" "strings" - "unicode/utf8" - - "github.com/zenazn/goji/web/mutil" - "golang.org/x/net/context" "github.com/guregu/kami/treemux" ) -// Middleware is a function that takes the current request context and returns a new request context. -// You can use middleware to build your context before your handler handles a request. -// As a special case, middleware that returns nil will halt middleware and handler execution (LogHandler will still run). -type Middleware func(context.Context, http.ResponseWriter, *http.Request) context.Context - -// MiddlewareType represents types that kami can convert to Middleware. -// kami will try its best to convert standard, non-context middleware. -// See the Use function for important information about how kami middleware is run. -// The following concrete types are accepted: -// - Middleware -// - func(context.Context, http.ResponseWriter, *http.Request) context.Context -// - func(http.Handler) http.Handler [* see Use docs] -// - func(http.ContextHandler) http.ContextHandler [* see Use docs] -type MiddlewareType interface{} - -// Afterware is a function that will run after middleware and the request. -// Afterware takes the request context and returns a new context, but unlike middleware, -// returning nil won't halt execution of other afterware. -type Afterware func(context.Context, mutil.WriterProxy, *http.Request) context.Context - -// Afterware represents types that kami can convert to Afterware. -// The following concrete types are accepted: -// - Afterware -// - func(context.Context, mutil.WriterProxy, *http.Request) context.Context -// - func(context.Context, http.ResponseWriter, *http.Request) context.Context -// - func(context.Context, *http.Request) context.Context -// - func(context.Context) context.Context -// - Middleware -type AfterwareType interface{} - type wares struct { middleware map[string][]Middleware afterware map[string][]Afterware @@ -117,153 +81,12 @@ func After(path string, aw AfterwareType) { defaultMW.After(path, aw) } -// run runs the middleware chain for a particular request. -// run returns false if it should stop early. -func (m *wares) run(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool) { - if m.middleware != nil { - // hierarchical middleware - for i, c := range r.URL.Path { - if c == '/' || i == len(r.URL.Path)-1 { - mws, ok := m.middleware[r.URL.Path[:i+1]] - if !ok { - continue - } - for _, mw := range mws { - // return nil context to stop - result := mw(ctx, w, r) - if result == nil { - return ctx, false - } - ctx = result - } - } - } - } - - if m.wildcards != nil { - // wildcard middleware - if wild, params := m.wildcards.Get(r.URL.Path); wild != nil { - if mw, ok := wild.(Middleware); ok { - ctx = mergeParams(ctx, params) - result := mw(ctx, w, r) - if result == nil { - return ctx, false - } - ctx = result - } - } - } - - return ctx, true -} - -// after runs the afterware chain for a particular request. -// after can't stop early -func (m *wares) after(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { - if m.afterWildcards != nil { - // wildcard afterware - if wild, params := m.afterWildcards.Get(r.URL.Path); wild != nil { - if aw, ok := wild.(Afterware); ok { - ctx = mergeParams(ctx, params) - result := aw(ctx, w, r) - if result != nil { - ctx = result - } - } - } - } - - if m.afterware != nil { - // hierarchical afterware, like middleware in reverse - path := r.URL.Path - for len(path) > 0 { - chr, size := utf8.DecodeLastRuneInString(path) - if chr == '/' || len(path) == len(r.URL.Path) { - for _, aw := range m.afterware[path] { - result := aw(ctx, w, r) - if result != nil { - ctx = result - } - } - } - path = path[:len(path)-size] - } - } - - return ctx -} +// Middleware run functions are in versioned files. func (m *wares) needsWrapper() bool { return m.afterware != nil || m.afterWildcards != nil } -// convert turns standard http middleware into kami Middleware if needed. -func convert(mw MiddlewareType) Middleware { - switch x := mw.(type) { - case Middleware: - return x - case func(context.Context, http.ResponseWriter, *http.Request) context.Context: - return Middleware(x) - case func(ContextHandler) ContextHandler: - return func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { - var dh dummyHandler - x(&dh).ServeHTTPContext(ctx, w, r) - if !dh { - return nil - } - return ctx - } - case func(http.Handler) http.Handler: - return func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { - var dh dummyHandler - x(&dh).ServeHTTP(w, r) - if !dh { - return nil - } - return ctx - } - } - panic(fmt.Errorf("unsupported MiddlewareType: %T", mw)) -} - -// convertAW -func convertAW(aw AfterwareType) Afterware { - switch x := aw.(type) { - case Afterware: - return x - case func(context.Context, mutil.WriterProxy, *http.Request) context.Context: - return Afterware(x) - case func(context.Context, *http.Request) context.Context: - return func(ctx context.Context, _ mutil.WriterProxy, r *http.Request) context.Context { - return x(ctx, r) - } - case func(context.Context) context.Context: - return func(ctx context.Context, _ mutil.WriterProxy, _ *http.Request) context.Context { - return x(ctx) - } - case Middleware: - return func(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { - return x(ctx, w, r) - } - case func(context.Context, http.ResponseWriter, *http.Request) context.Context: - return func(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { - return x(ctx, w, r) - } - } - panic(fmt.Errorf("unsupported AfterwareType: %T", aw)) -} - -// dummyHandler is used to keep track of whether the next middleware was called or not. -type dummyHandler bool - -func (dh *dummyHandler) ServeHTTP(http.ResponseWriter, *http.Request) { - *dh = true -} - -func (dh *dummyHandler) ServeHTTPContext(_ context.Context, _ http.ResponseWriter, _ *http.Request) { - *dh = true -} - func containsWildcard(path string) bool { return strings.Contains(path, "/:") || strings.Contains(path, "/*") } diff --git a/middleware_17.go b/middleware_17.go new file mode 100644 index 0000000..d066014 --- /dev/null +++ b/middleware_17.go @@ -0,0 +1,242 @@ +// +build go1.7 + +package kami + +import ( + "context" + "fmt" + "net/http" + "unicode/utf8" + + "github.com/zenazn/goji/web/mutil" + netcontext "golang.org/x/net/context" +) + +// Middleware is a function that takes the current request context and returns a new request context. +// You can use middleware to build your context before your handler handles a request. +// As a special case, middleware that returns nil will halt middleware and handler execution (LogHandler will still run). +type Middleware func(context.Context, http.ResponseWriter, *http.Request) context.Context + +// MiddlewareType represents types that kami can convert to Middleware. +// kami will try its best to convert standard, non-context middleware. +// See the Use function for important information about how kami middleware is run. +// The following concrete types are accepted: +// - Middleware +// - func(context.Context, http.ResponseWriter, *http.Request) context.Context +// - func(http.Handler) http.Handler [* see Use docs] +// - func(http.ContextHandler) http.ContextHandler [* see Use docs] +// The old x/net/context is also supported. +type MiddlewareType interface{} + +// Afterware is a function that will run after middleware and the request. +// Afterware takes the request context and returns a new context, but unlike middleware, +// returning nil won't halt execution of other afterware. +type Afterware func(context.Context, mutil.WriterProxy, *http.Request) context.Context + +// Afterware represents types that kami can convert to Afterware. +// The following concrete types are accepted: +// - Afterware +// - func(context.Context, mutil.WriterProxy, *http.Request) context.Context +// - func(context.Context, http.ResponseWriter, *http.Request) context.Context +// - func(context.Context, *http.Request) context.Context +// - func(context.Context) context.Context +// - Middleware +// The old x/net/context is also supported. +type AfterwareType interface{} + +// run runs the middleware chain for a particular request. +// run returns false if it should stop early. +func (m *wares) run(ctx context.Context, w http.ResponseWriter, r *http.Request) (*http.Request, context.Context, bool) { + if m.middleware != nil { + // hierarchical middleware + for i, c := range r.URL.Path { + if c == '/' || i == len(r.URL.Path)-1 { + mws, ok := m.middleware[r.URL.Path[:i+1]] + if !ok { + continue + } + for _, mw := range mws { + // return nil context to stop + result := mw(ctx, w, r) + if result == nil { + return r, ctx, false + } + if result != ctx { + r = r.WithContext(result) + } + ctx = result + } + } + } + } + + if m.wildcards != nil { + // wildcard middleware + if wild, params := m.wildcards.Get(r.URL.Path); wild != nil { + if mw, ok := wild.(Middleware); ok { + ctx = mergeParams(ctx, params) + r = r.WithContext(ctx) + result := mw(ctx, w, r) + if result == nil { + return r, ctx, false + } + if result != ctx { + r = r.WithContext(result) + } + ctx = result + } + } + } + + return r, ctx, true +} + +// after runs the afterware chain for a particular request. +// after can't stop early +func (m *wares) after(ctx context.Context, w mutil.WriterProxy, r *http.Request) (*http.Request, context.Context) { + if m.afterWildcards != nil { + // wildcard afterware + if wild, params := m.afterWildcards.Get(r.URL.Path); wild != nil { + if aw, ok := wild.(Afterware); ok { + ctx = mergeParams(ctx, params) + r = r.WithContext(ctx) + result := aw(ctx, w, r) + if result != nil { + if result != ctx { + r = r.WithContext(result) + } + ctx = result + } + } + } + } + + if m.afterware != nil { + // hierarchical afterware, like middleware in reverse + path := r.URL.Path + for len(path) > 0 { + chr, size := utf8.DecodeLastRuneInString(path) + if chr == '/' || len(path) == len(r.URL.Path) { + for _, aw := range m.afterware[path] { + result := aw(ctx, w, r) + if result != nil { + if result != ctx { + r = r.WithContext(result) + } + ctx = result + } + } + } + path = path[:len(path)-size] + } + } + + return r, ctx +} + +// convert turns standard http middleware into kami Middleware if needed. +func convert(mw MiddlewareType) Middleware { + switch x := mw.(type) { + case Middleware: + return x + case func(context.Context, http.ResponseWriter, *http.Request) context.Context: + return Middleware(x) + case func(netcontext.Context, http.ResponseWriter, *http.Request) netcontext.Context: + return Middleware(func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + return x(ctx, w, r) + }) + case func(ContextHandler) ContextHandler: + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + var dh dummyHandler + x(&dh).ServeHTTPContext(ctx, w, r) + if !dh { + return nil + } + return ctx + } + case func(OldContextHandler) OldContextHandler: + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + var dh oldDummyHandler + x(&dh).ServeHTTPContext(ctx, w, r) + if !dh { + return nil + } + return ctx + } + case func(http.Handler) http.Handler: + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + var dh dummyHandler + x(&dh).ServeHTTP(w, r) + if !dh { + return nil + } + return ctx + } + } + panic(fmt.Errorf("unsupported MiddlewareType: %T", mw)) +} + +// convertAW +func convertAW(aw AfterwareType) Afterware { + switch x := aw.(type) { + case Afterware: + return x + case func(context.Context, mutil.WriterProxy, *http.Request) context.Context: + return Afterware(x) + case func(netcontext.Context, mutil.WriterProxy, *http.Request) netcontext.Context: + return func(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { + return x(ctx, w, r) + } + case func(context.Context, *http.Request) context.Context: + return func(ctx context.Context, _ mutil.WriterProxy, r *http.Request) context.Context { + return x(ctx, r) + } + case func(netcontext.Context, *http.Request) netcontext.Context: + return func(ctx context.Context, _ mutil.WriterProxy, r *http.Request) context.Context { + return x(ctx, r) + } + case func(context.Context) context.Context: + return func(ctx context.Context, _ mutil.WriterProxy, _ *http.Request) context.Context { + return x(ctx) + } + case func(netcontext.Context) netcontext.Context: + return func(ctx context.Context, _ mutil.WriterProxy, _ *http.Request) context.Context { + return x(ctx) + } + case Middleware: + return func(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { + return x(ctx, w, r) + } + case func(context.Context, http.ResponseWriter, *http.Request) context.Context: + return func(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { + return x(ctx, w, r) + } + case func(netcontext.Context, http.ResponseWriter, *http.Request) netcontext.Context: + return func(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { + return x(ctx, w, r) + } + } + panic(fmt.Errorf("unsupported AfterwareType: %T", aw)) +} + +// dummyHandler is used to keep track of whether the next middleware was called or not. +type dummyHandler bool + +func (dh *dummyHandler) ServeHTTP(http.ResponseWriter, *http.Request) { + *dh = true +} + +func (dh *dummyHandler) ServeHTTPContext(_ context.Context, _ http.ResponseWriter, _ *http.Request) { + *dh = true +} + +// oldDummyHandler is dummyHandler compatible with the old context type. +type oldDummyHandler bool + +func (dh *oldDummyHandler) ServeHTTP(http.ResponseWriter, *http.Request) { + *dh = true +} + +func (dh *oldDummyHandler) ServeHTTPContext(_ netcontext.Context, _ http.ResponseWriter, _ *http.Request) { + *dh = true +} diff --git a/middleware_old.go b/middleware_old.go new file mode 100644 index 0000000..ac4a0fe --- /dev/null +++ b/middleware_old.go @@ -0,0 +1,184 @@ +// +build !go1.7 + +package kami + +import ( + "fmt" + "net/http" + + "github.com/zenazn/goji/web/mutil" + "golang.org/x/net/context" +) + +// Middleware is a function that takes the current request context and returns a new request context. +// You can use middleware to build your context before your handler handles a request. +// As a special case, middleware that returns nil will halt middleware and handler execution (LogHandler will still run). +type Middleware func(context.Context, http.ResponseWriter, *http.Request) context.Context + +// MiddlewareType represents types that kami can convert to Middleware. +// kami will try its best to convert standard, non-context middleware. +// See the Use function for important information about how kami middleware is run. +// The following concrete types are accepted: +// - Middleware +// - func(context.Context, http.ResponseWriter, *http.Request) context.Context +// - func(http.Handler) http.Handler [* see Use docs] +// - func(http.ContextHandler) http.ContextHandler [* see Use docs] +type MiddlewareType interface{} + +// Afterware is a function that will run after middleware and the request. +// Afterware takes the request context and returns a new context, but unlike middleware, +// returning nil won't halt execution of other afterware. +type Afterware func(context.Context, mutil.WriterProxy, *http.Request) context.Context + +// Afterware represents types that kami can convert to Afterware. +// The following concrete types are accepted: +// - Afterware +// - func(context.Context, mutil.WriterProxy, *http.Request) context.Context +// - func(context.Context, http.ResponseWriter, *http.Request) context.Context +// - func(context.Context, *http.Request) context.Context +// - func(context.Context) context.Context +// - Middleware +type AfterwareType interface{} + +// run runs the middleware chain for a particular request. +// run returns false if it should stop early. +func (m *wares) run(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool) { + if m.middleware != nil { + // hierarchical middleware + for i, c := range r.URL.Path { + if c == '/' || i == len(r.URL.Path)-1 { + mws, ok := m.middleware[r.URL.Path[:i+1]] + if !ok { + continue + } + for _, mw := range mws { + // return nil context to stop + result := mw(ctx, w, r) + if result == nil { + return ctx, false + } + ctx = result + } + } + } + } + + if m.wildcards != nil { + // wildcard middleware + if wild, params := m.wildcards.Get(r.URL.Path); wild != nil { + if mw, ok := wild.(Middleware); ok { + ctx = mergeParams(ctx, params) + result := mw(ctx, w, r) + if result == nil { + return ctx, false + } + ctx = result + } + } + } + + return ctx, true +} + +// after runs the afterware chain for a particular request. +// after can't stop early +func (m *wares) after(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { + if m.afterWildcards != nil { + // wildcard afterware + if wild, params := m.afterWildcards.Get(r.URL.Path); wild != nil { + if aw, ok := wild.(Afterware); ok { + ctx = mergeParams(ctx, params) + result := aw(ctx, w, r) + if result != nil { + ctx = result + } + } + } + } + + if m.afterware != nil { + // hierarchical afterware, like middleware in reverse + path := r.URL.Path + for len(path) > 0 { + chr, size := utf8.DecodeLastRuneInString(path) + if chr == '/' || len(path) == len(r.URL.Path) { + for _, aw := range m.afterware[path] { + result := aw(ctx, w, r) + if result != nil { + ctx = result + } + } + } + path = path[:len(path)-size] + } + } + + return ctx +} + +// convert turns standard http middleware into kami Middleware if needed. +func convert(mw MiddlewareType) Middleware { + switch x := mw.(type) { + case Middleware: + return x + case func(context.Context, http.ResponseWriter, *http.Request) context.Context: + return Middleware(x) + case func(ContextHandler) ContextHandler: + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + var dh dummyHandler + x(&dh).ServeHTTPContext(ctx, w, r) + if !dh { + return nil + } + return ctx + } + case func(http.Handler) http.Handler: + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + var dh dummyHandler + x(&dh).ServeHTTP(w, r) + if !dh { + return nil + } + return ctx + } + } + panic(fmt.Errorf("unsupported MiddlewareType: %T", mw)) +} + +// convertAW +func convertAW(aw AfterwareType) Afterware { + switch x := aw.(type) { + case Afterware: + return x + case func(context.Context, mutil.WriterProxy, *http.Request) context.Context: + return Afterware(x) + case func(context.Context, *http.Request) context.Context: + return func(ctx context.Context, _ mutil.WriterProxy, r *http.Request) context.Context { + return x(ctx, r) + } + case func(context.Context) context.Context: + return func(ctx context.Context, _ mutil.WriterProxy, _ *http.Request) context.Context { + return x(ctx) + } + case Middleware: + return func(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { + return x(ctx, w, r) + } + case func(context.Context, http.ResponseWriter, *http.Request) context.Context: + return func(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { + return x(ctx, w, r) + } + } + panic(fmt.Errorf("unsupported AfterwareType: %T", aw)) +} + +// dummyHandler is used to keep track of whether the next middleware was called or not. +type dummyHandler bool + +func (dh *dummyHandler) ServeHTTP(http.ResponseWriter, *http.Request) { + *dh = true +} + +func (dh *dummyHandler) ServeHTTPContext(_ context.Context, _ http.ResponseWriter, _ *http.Request) { + *dh = true +} diff --git a/middleware_test.go b/middleware_test.go index 086590f..5344e22 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -48,10 +48,10 @@ func TestWildcardMiddleware(t *testing.T) { func TestHierarchicalStop(t *testing.T) { kami.Reset() - kami.Use("/nope/", kami.Middleware(func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { + kami.Use("/nope/", func(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { w.WriteHeader(http.StatusForbidden) return nil - })) + }) kami.Delete("/nope/test", func(ctx context.Context, w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) diff --git a/mux.go b/mux.go index b749c19..8f46f56 100644 --- a/mux.go +++ b/mux.go @@ -1,3 +1,5 @@ +// +build !go1.7 + package kami import ( diff --git a/mux_17.go b/mux_17.go new file mode 100644 index 0000000..dfcabea --- /dev/null +++ b/mux_17.go @@ -0,0 +1,144 @@ +// +build go1.7 + +package kami + +import ( + "context" + "net/http" + + "github.com/dimfeld/httptreemux" + "github.com/zenazn/goji/web/mutil" +) + +// Mux is an independent kami router and middleware stack. Manipulating it is not threadsafe. +type Mux struct { + // Context is the root "god object" for this mux, + // from which every request's context will derive. + Context context.Context + // PanicHandler will, if set, be called on panics. + // You can use kami.Exception(ctx) within the panic handler to get panic details. + PanicHandler HandlerType + // LogHandler will, if set, wrap every request and be called at the very end. + LogHandler func(context.Context, mutil.WriterProxy, *http.Request) + + routes *httptreemux.TreeMux + enable405 bool + *wares +} + +// New creates a new independent kami router and middleware stack. +// It is totally separate from the global kami.Context and middleware stack. +func New() *Mux { + m := &Mux{ + Context: context.Background(), + routes: newRouter(), + wares: newWares(), + enable405: true, + } + m.NotFound(nil) + m.MethodNotAllowed(nil) + return m +} + +// ServeHTTP handles an HTTP request, running middleware and forwarding the request to the appropriate handler. +// Implements the http.Handler interface for easy composition with other frameworks. +func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + m.routes.ServeHTTP(w, r) +} + +// Handle registers an arbitrary method handler under the given path. +func (m *Mux) Handle(method, path string, handler HandlerType) { + m.routes.Handle(method, path, m.bless(wrap(handler))) +} + +// Get registers a GET handler under the given path. +func (m *Mux) Get(path string, handler HandlerType) { + m.Handle("GET", path, handler) +} + +// Post registers a POST handler under the given path. +func (m *Mux) Post(path string, handler HandlerType) { + m.Handle("POST", path, handler) +} + +// Put registers a PUT handler under the given path. +func (m *Mux) Put(path string, handler HandlerType) { + m.Handle("PUT", path, handler) +} + +// Patch registers a PATCH handler under the given path. +func (m *Mux) Patch(path string, handler HandlerType) { + m.Handle("PATCH", path, handler) +} + +// Head registers a HEAD handler under the given path. +func (m *Mux) Head(path string, handler HandlerType) { + m.Handle("HEAD", path, handler) +} + +// Options registers a OPTIONS handler under the given path. +func (m *Mux) Options(path string, handler HandlerType) { + m.Handle("OPTIONS", path, handler) +} + +// Delete registers a DELETE handler under the given path. +func (m *Mux) Delete(path string, handler HandlerType) { + m.Handle("DELETE", path, handler) +} + +// NotFound registers a special handler for unregistered (404) paths. +// If handle is nil, use the default http.NotFound behavior. +func (m *Mux) NotFound(handler HandlerType) { + // set up the default handler if needed + // we need to bless this so middleware will still run for a 404 request + if handler == nil { + handler = HandlerFunc(func(_ context.Context, w http.ResponseWriter, r *http.Request) { + http.NotFound(w, r) + }) + } + + h := m.bless(wrap(handler)) + m.routes.NotFoundHandler = func(w http.ResponseWriter, r *http.Request) { + h(w, r, nil) + } +} + +// MethodNotAllowed registers a special handler for automatically responding +// to invalid method requests (405). +func (m *Mux) MethodNotAllowed(handler HandlerType) { + if handler == nil { + handler = HandlerFunc(func(_ context.Context, w http.ResponseWriter, r *http.Request) { + http.Error(w, + http.StatusText(http.StatusMethodNotAllowed), + http.StatusMethodNotAllowed, + ) + }) + } + + h := m.bless(wrap(handler)) + m.routes.MethodNotAllowedHandler = func(w http.ResponseWriter, r *http.Request, methods map[string]httptreemux.HandlerFunc) { + if !m.enable405 { + m.routes.NotFoundHandler(w, r) + return + } + h(w, r, nil) + } +} + +// EnableMethodNotAllowed enables or disables automatic Method Not Allowed handling. +// Note that this is enabled by default. +func (m *Mux) EnableMethodNotAllowed(enabled bool) { + m.enable405 = enabled +} + +// bless creates a new kamified handler. +func (m *Mux) bless(h ContextHandler) httptreemux.HandlerFunc { + k := kami{ + handler: h, + base: &m.Context, + middleware: m.wares, + panicHandler: &m.PanicHandler, + logHandler: &m.LogHandler, + } + return k.handle +} From 966fddf84b1b3066e7bd4b52cf103330a47d2e2e Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 24 Aug 2016 05:19:50 +0900 Subject: [PATCH 2/5] move global mux stuff out of here --- kami.go | 146 -------------------------------------------------------- 1 file changed, 146 deletions(-) diff --git a/kami.go b/kami.go index 831393c..4ef0814 100644 --- a/kami.go +++ b/kami.go @@ -10,140 +10,6 @@ import ( "golang.org/x/net/context" ) -var ( - // Context is the root "god object" from which every request's context will derive. - Context = context.Background() - - // PanicHandler will, if set, be called on panics. - // You can use kami.Exception(ctx) within the panic handler to get panic details. - PanicHandler HandlerType - // LogHandler will, if set, wrap every request and be called at the very end. - LogHandler func(context.Context, mutil.WriterProxy, *http.Request) -) - -var ( - routes = newRouter() - enable405 = true -) - -func init() { - // set up the default 404/405 handlers - NotFound(nil) - MethodNotAllowed(nil) -} - -func newRouter() *httptreemux.TreeMux { - r := httptreemux.New() - r.PathSource = httptreemux.URLPath - r.RedirectBehavior = httptreemux.Redirect307 - r.RedirectMethodBehavior = map[string]httptreemux.RedirectBehavior{ - "GET": httptreemux.Redirect301, - } - return r -} - -// Handler returns an http.Handler serving registered routes. -func Handler() http.Handler { - return routes -} - -// Handle registers an arbitrary method handler under the given path. -func Handle(method, path string, handler HandlerType) { - routes.Handle(method, path, bless(wrap(handler))) -} - -// Get registers a GET handler under the given path. -func Get(path string, handler HandlerType) { - Handle("GET", path, handler) -} - -// Post registers a POST handler under the given path. -func Post(path string, handler HandlerType) { - Handle("POST", path, handler) -} - -// Put registers a PUT handler under the given path. -func Put(path string, handler HandlerType) { - Handle("PUT", path, handler) -} - -// Patch registers a PATCH handler under the given path. -func Patch(path string, handler HandlerType) { - Handle("PATCH", path, handler) -} - -// Head registers a HEAD handler under the given path. -func Head(path string, handler HandlerType) { - Handle("HEAD", path, handler) -} - -// Head registers a OPTIONS handler under the given path. -func Options(path string, handler HandlerType) { - Handle("OPTIONS", path, handler) -} - -// Delete registers a DELETE handler under the given path. -func Delete(path string, handler HandlerType) { - Handle("DELETE", path, handler) -} - -// NotFound registers a special handler for unregistered (404) paths. -// If handle is nil, use the default http.NotFound behavior. -func NotFound(handler HandlerType) { - // set up the default handler if needed - // we need to bless this so middleware will still run for a 404 request - if handler == nil { - handler = HandlerFunc(func(_ context.Context, w http.ResponseWriter, r *http.Request) { - http.NotFound(w, r) - }) - } - - h := bless(wrap(handler)) - routes.NotFoundHandler = func(w http.ResponseWriter, r *http.Request) { - h(w, r, nil) - } -} - -// MethodNotAllowed registers a special handler for automatically responding -// to invalid method requests (405). -func MethodNotAllowed(handler HandlerType) { - if handler == nil { - handler = HandlerFunc(func(_ context.Context, w http.ResponseWriter, r *http.Request) { - http.Error(w, - http.StatusText(http.StatusMethodNotAllowed), - http.StatusMethodNotAllowed, - ) - }) - } - - h := bless(wrap(handler)) - routes.MethodNotAllowedHandler = func(w http.ResponseWriter, r *http.Request, methods map[string]httptreemux.HandlerFunc) { - if !enable405 { - routes.NotFoundHandler(w, r) - return - } - h(w, r, nil) - } -} - -// EnableMethodNotAllowed enables or disables automatic Method Not Allowed handling. -// Note that this is enabled by default. -func EnableMethodNotAllowed(enabled bool) { - enable405 = enabled -} - -// bless creates a new kamified handler using the global mux and middleware. -func bless(h ContextHandler) httptreemux.HandlerFunc { - k := kami{ - handler: h, - base: &Context, - middleware: defaultMW, - panicHandler: &PanicHandler, - logHandler: &LogHandler, - } - return k.handle -} - // kami is the heart of the package. // It wraps a ContextHandler into an httprouter compatible request, // in order to run all the middleware and other special handlers. @@ -204,15 +70,3 @@ func (k kami) handle(w http.ResponseWriter, r *http.Request, params map[string]s proxy.WriteHeader(http.StatusInternalServerError) } } - -// Reset changes the root Context to context.Background(). -// It removes every handler and all middleware. -func Reset() { - Context = context.Background() - PanicHandler = nil - LogHandler = nil - defaultMW = newWares() - routes = newRouter() - NotFound(nil) - MethodNotAllowed(nil) -} From 5289c79e782bfc5f8dd026bd329f4ba558ca4aee Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 24 Aug 2016 05:21:58 +0900 Subject: [PATCH 3/5] 1.6 fixes --- globalmux_old.go | 1 + kami.go | 1 - middleware_old.go | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/globalmux_old.go b/globalmux_old.go index f4b20fc..41e575b 100644 --- a/globalmux_old.go +++ b/globalmux_old.go @@ -5,6 +5,7 @@ package kami import ( "net/http" + "github.com/dimfeld/httptreemux" "github.com/zenazn/goji/web/mutil" "golang.org/x/net/context" ) diff --git a/kami.go b/kami.go index 4ef0814..91cb943 100644 --- a/kami.go +++ b/kami.go @@ -5,7 +5,6 @@ package kami import ( "net/http" - "github.com/dimfeld/httptreemux" "github.com/zenazn/goji/web/mutil" "golang.org/x/net/context" ) diff --git a/middleware_old.go b/middleware_old.go index ac4a0fe..82a2a46 100644 --- a/middleware_old.go +++ b/middleware_old.go @@ -5,6 +5,7 @@ package kami import ( "fmt" "net/http" + "unicode/utf8" "github.com/zenazn/goji/web/mutil" "golang.org/x/net/context" From 0afa5bf68a78fc069e645f678f92c355467543d0 Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 24 Aug 2016 05:34:02 +0900 Subject: [PATCH 4/5] read-only vanilla MW --- middleware_17.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/middleware_17.go b/middleware_17.go index d066014..b35cc3d 100644 --- a/middleware_17.go +++ b/middleware_17.go @@ -25,6 +25,8 @@ type Middleware func(context.Context, http.ResponseWriter, *http.Request) contex // - func(context.Context, http.ResponseWriter, *http.Request) context.Context // - func(http.Handler) http.Handler [* see Use docs] // - func(http.ContextHandler) http.ContextHandler [* see Use docs] +// - http.Handler [read only] +// - func(w http.ResponseWriter, r *http.Request) [read only] // The old x/net/context is also supported. type MiddlewareType interface{} @@ -172,6 +174,16 @@ func convert(mw MiddlewareType) Middleware { } return ctx } + case http.Handler: + return Middleware(func(_ context.Context, w http.ResponseWriter, r *http.Request) context.Context { + x.ServeHTTP(w, r) + return r.Context() + }) + case func(w http.ResponseWriter, r *http.Request): + return Middleware(func(_ context.Context, w http.ResponseWriter, r *http.Request) context.Context { + x(w, r) + return r.Context() + }) } panic(fmt.Errorf("unsupported MiddlewareType: %T", mw)) } @@ -215,6 +227,16 @@ func convertAW(aw AfterwareType) Afterware { return func(ctx context.Context, w mutil.WriterProxy, r *http.Request) context.Context { return x(ctx, w, r) } + case http.Handler: + return Afterware(func(_ context.Context, w mutil.WriterProxy, r *http.Request) context.Context { + x.ServeHTTP(w, r) + return r.Context() + }) + case func(w http.ResponseWriter, r *http.Request): + return Afterware(func(_ context.Context, w mutil.WriterProxy, r *http.Request) context.Context { + x(w, r) + return r.Context() + }) } panic(fmt.Errorf("unsupported AfterwareType: %T", aw)) } From 8d1d846628fba54e918064a0b82bdfbf5898c7a6 Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 24 Aug 2016 05:38:37 +0900 Subject: [PATCH 5/5] change import path to v2 --- README.md | 2 +- handler_17.go | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index 3ffb7c0..2bd3fea 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ## kami [![GoDoc](https://godoc.org/github.com/guregu/kami?status.svg)](https://godoc.org/github.com/guregu/kami) [![CircleCI](https://circleci.com/gh/guregu/kami.svg?style=svg)](https://circleci.com/gh/guregu/kami) -`import "github.com/guregu/kami"` [or](http://gopkg.in) `import "gopkg.in/guregu/kami.v1"` +`import "github.com/guregu/kami"` [or](http://gopkg.in) `import "gopkg.in/guregu/kami.v2"` kami (ē„ž) is a tiny web framework using [context](https://blog.golang.org/context) for request context and [httptreemux](https://github.com/dimfeld/httptreemux) for routing. It includes a simple system for running hierarchical middleware before and after requests, in addition to log and panic hooks. Graceful restart via einhorn is also supported. diff --git a/handler_17.go b/handler_17.go index 679276c..4cde993 100644 --- a/handler_17.go +++ b/handler_17.go @@ -30,12 +30,6 @@ type OldContextHandler interface { ServeHTTPContext(netcontext.Context, http.ResponseWriter, *http.Request) } -func old2new(old OldContextHandler) ContextHandler { - return HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - old.ServeHTTPContext(ctx, w, r) - }) -} - // HandlerFunc is like http.HandlerFunc with context. type HandlerFunc func(context.Context, http.ResponseWriter, *http.Request)