From e68107dd75d6ce4563833878080fada919452ef1 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 17 Oct 2018 04:28:43 +0300 Subject: [PATCH] add 'smart' middleware helpers, see _examples/6_middleware for more --- _examples/6_middleware/main.go | 93 ++++++++++++++++++++++++++++ mux.go | 107 +++++++++++++++++++++++++++++++-- params_writer.go | 3 +- trie.go | 23 +++---- trie_benchmark_test.go | 2 +- trie_test.go | 4 +- 6 files changed, 210 insertions(+), 22 deletions(-) create mode 100644 _examples/6_middleware/main.go diff --git a/_examples/6_middleware/main.go b/_examples/6_middleware/main.go new file mode 100644 index 0000000..e1dfa6f --- /dev/null +++ b/_examples/6_middleware/main.go @@ -0,0 +1,93 @@ +// Package main will explore the helpers for middleware(s) that Muxie has to offer, +// but they are totally optional, you can still use your favourite pattern +// to wrap route handlers. +// +// Example of usage of an external net/http common middleware: +// +// import "github.com/rs/cors" +// +// mux := muxie.New() +// mux.Use(cors.Default().Handler) +// +// +// To wrap a specific route or even if for some reason you want to wrap the entire router +// use the `Pre(middlewares...).For(mainHandler)` as : +// +// wrapped := muxie.Pre(cors.Default().Handler, ...).For(mux) +// http.ListenAndServe(..., wrapped) +package main + +import ( + "log" + "net/http" + + "github.com/kataras/muxie" +) + +func main() { + mux := muxie.NewMux() + // Globally, will be inherited by all sub muxes as well unless `Of(...).Unlink()` called. + mux.Use(myGlobalMiddleware) + + // Per Route. + mux.Handle("/", muxie.Pre(myFirstRouteMiddleware, mySecondRouteMiddleware).ForFunc(myMainRouteHandler)) + + // Per Group. + inheritor := mux.Of("/inheritor") + inheritor.Use(myMiddlewareForSubmux) + inheritor.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + log.Println("execute: my inheritor's main index route's handler") + w.Write([]byte("Hello from /inheritor\n")) + }) + + // Per Group, without its parents' middlewares. + // Unlink will clear all middlewares for this sub mux. + orphan := mux.Of("/orphan").Unlink() + orphan.Use(myMiddlewareForSubmux) + orphan.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + log.Println("execute: orphan's main index route's handler") + w.Write([]byte("Hello from /orphan\n")) + }) + + // Open your web browser or any other HTTP Client + // and navigate through the below endpoinds, one by one, + // and check the console output of your webserver. + // + // http://localhost:8080 + // http://localhost:8080/inheritor + // http://localhost:8080/orphan + http.ListenAndServe(":8080", mux) +} + +func myGlobalMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Println("execute: my global and first of all middleware for all following mux' routes and sub muxes") + next.ServeHTTP(w, r) + }) +} + +func myMiddlewareForSubmux(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Println("execute: my submux' routes middleware") + next.ServeHTTP(w, r) + }) +} + +func myFirstRouteMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Println("execute: my first specific route's middleware") + next.ServeHTTP(w, r) + }) +} + +func mySecondRouteMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Println("execute: my second specific route's middleware") + next.ServeHTTP(w, r) + }) +} + +func myMainRouteHandler(w http.ResponseWriter, r *http.Request) { + log.Println("execute: my main route's handler") + w.Write([]byte("Hello World!\n")) +} diff --git a/mux.go b/mux.go index ae12dce..6537c3f 100644 --- a/mux.go +++ b/mux.go @@ -27,7 +27,10 @@ type Mux struct { Routes *Trie paramsPool *sync.Pool - root string + + // per mux + root string + beginHandlers []Wrapper } // NewMux returns a new HTTP multiplexer which uses a fast, if not the fastest @@ -44,9 +47,69 @@ func NewMux() *Mux { } } +// Use adds middleware that should be called before each mux route's main handler. +// Should be called before `Handle/HandleFunc`. Order matters. +// +// A Wrapper is just a type of `func(http.Handler) http.Handler` +// which is a common type definition for net/http middlewares. +// +// To add a middleware for a specific route and not in the whole mux +// use the `Handle/HandleFunc` with the package-level `muxie.Pre` function instead. +// Functionality of `Use` is pretty self-explained but new gophers should +// take a look of the examples for further details. +func (m *Mux) Use(middlewares ...Wrapper) { + m.beginHandlers = append(m.beginHandlers, middlewares...) +} + +type ( + // Wrapper is just a type of `func(http.Handler) http.Handler` + // which is a common type definition for net/http middlewares. + Wrapper func(http.Handler) http.Handler + + // Wrappers contains `Wrapper`s that can be registered and used by a "main route handler". + // Look the `Pre` and `For/ForFunc` functions too. + Wrappers struct { + pre []Wrapper + } +) + +// For registers the wrappers for a specific handler and returns a handler +// that can be passed via the `Handle` function. +func (w Wrappers) For(main http.Handler) http.Handler { + if len(w.pre) > 0 { + for i, lidx := 0, len(w.pre)-1; i <= lidx; i++ { + main = w.pre[lidx-i](main) + } + } + + // keep note that if no middlewares then + // it will return the main one untouched, + // the check of length of w.pre is because we may add "done handlers" as well in the future, if community asks for that. + return main +} + +// ForFunc registers the wrappers for a specific raw handler function +// and returns a handler that can be passed via the `Handle` function. +func (w Wrappers) ForFunc(mainFunc func(http.ResponseWriter, *http.Request)) http.Handler { + return w.For(http.HandlerFunc(mainFunc)) +} + +// Pre starts a chain of handlers for wrapping a "main route handler" +// the registered "middleware" will run before the main handler(see `Wrappers#For/ForFunc`). +// +// Usage: +// mux := muxie.NewMux() +// myMiddlewares := muxie.Pre(myFirstMiddleware, mySecondMiddleware) +// mux.Handle("/", myMiddlewares.ForFunc(myMainRouteHandler)) +func Pre(middleware ...Wrapper) Wrappers { + return Wrappers{pre: middleware} +} + // Handle registers a route handler for a path pattern. func (m *Mux) Handle(pattern string, handler http.Handler) { - m.Routes.Insert(m.root+pattern, WithHandler(handler)) + m.Routes.Insert(m.root+pattern, + WithHandler( + Pre(m.beginHandlers...).For(handler))) } // HandleFunc registers a route handler function for a path pattern. @@ -113,9 +176,11 @@ func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { // SubMux is the child of a main Mux. type SubMux interface { + Of(prefix string) SubMux + Unlink() SubMux + Use(middlewares ...Wrapper) Handle(pattern string, handler http.Handler) HandleFunc(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) - Of(prefix string) SubMux } // Of returns a new Mux which its Handle and HandleFunc will register the path based on given "prefix", i.e: @@ -147,6 +212,40 @@ func (m *Mux) Of(prefix string) SubMux { return &Mux{ Routes: m.Routes, - root: prefix, + + root: prefix, + beginHandlers: append([]Wrapper{}, m.beginHandlers...), } } + +/* Notes: + +Four options to solve optionally "inherition" of parent's middlewares but dismissed: + +- I could add options for "inherition" of middlewares inside the `Mux#Use` itself. + But this is a problem because the end-dev will have to use a specific muxie's constant even if he doesn't care about the other option. +- Create a new function like `UseOnly` or `UseExplicit` + which will remove any previous middlewares and use only the new one. + But this has a problem of making the `Use` func to act differently and debugging will be a bit difficult if big app if called after the `UseOnly`. +- Add a new func for creating new groups to remove any inherited middlewares from the parent. + But with this, we will have two functions for the same thing and users may be confused about this API design. +- Put the options to the existing `Of` function, and make them optionally by functional design of options. + But this will make things ugly and may confuse users as well, there is a better way. + +Solution: just add a function like `Unlink` +to remove any inherited fields (now and future feature requests), so we don't have +breaking changes and etc. This `Unlink`, which will return the same SubMux, it can be used like `v1 := mux.Of(..).Unlink()` +*/ + +// Unlink will remove any inheritance fields from the parent mux (and its parent) +// that are inherited with the `Of` function. +// Returns the current SubMux. Usage: +// +// mux := NewMux() +// mux.Use(myLoggerMiddleware) +// v1 := mux.Of("/v1").Unlink() // v1 will no longer have the "myLoggerMiddleware". +// v1.HandleFunc("/users", myHandler) +func (m *Mux) Unlink() SubMux { + m.beginHandlers = m.beginHandlers[0:0] + return m +} diff --git a/params_writer.go b/params_writer.go index 827610d..fe0d7d3 100644 --- a/params_writer.go +++ b/params_writer.go @@ -32,7 +32,8 @@ func GetParams(w http.ResponseWriter) []ParamEntry { } // SetParam sets manually a parameter to the "w" http.ResponseWriter which should be a *paramsWriter. -// This is not commonly used by the end-developers. +// This is not commonly used by the end-developers, +// unless sharing values(string messages only) between handlers is absolutely necessary. func SetParam(w http.ResponseWriter, key, value string) bool { if store, ok := w.(*paramsWriter); ok { store.Set(key, value) diff --git a/trie.go b/trie.go index 70e42f7..2124897 100644 --- a/trie.go +++ b/trie.go @@ -46,6 +46,10 @@ type InsertOption func(*Node) // WithHandler sets the node's `Handler` field (useful for HTTP). func WithHandler(handler http.Handler) InsertOption { + if handler == nil { + panic("muxie/WithHandler: empty handler") + } + return func(n *Node) { if n.Handler == nil { n.Handler = handler @@ -71,24 +75,15 @@ func WithData(data interface{}) InsertOption { } // Insert adds a node to the trie. -func (t *Trie) Insert(key string, options ...InsertOption) { - n := t.insert(key, "", nil, nil) - for _, opt := range options { - opt(n) - } -} - -// InsertRoute adds a route node to the trie, handler and pattern are required. -func (t *Trie) InsertRoute(pattern, routeName string, handler http.Handler) { +func (t *Trie) Insert(pattern string, options ...InsertOption) { if pattern == "" { - panic("muxie/trie#InsertRoute: empty pattern") + panic("muxie/trie#Insert: empty pattern") } - if handler == nil { - panic("muxie/trie#InsertRoute: empty handler") + n := t.insert(pattern, "", nil, nil) + for _, opt := range options { + opt(n) } - - t.insert(pattern, routeName, nil, handler) } const ( diff --git a/trie_benchmark_test.go b/trie_benchmark_test.go index f00ae5a..941d2b0 100644 --- a/trie_benchmark_test.go +++ b/trie_benchmark_test.go @@ -6,7 +6,7 @@ import ( func initTree(tree *Trie) { for _, tt := range tests { - tree.Insert(tt.key, WithTag(tt.routeName)) + tree.insert(tt.key, tt.routeName, nil, nil) } } diff --git a/trie_test.go b/trie_test.go index e267535..69ce570 100644 --- a/trie_test.go +++ b/trie_test.go @@ -118,7 +118,7 @@ func testTrie(t *testing.T, oneByOne bool) { // insert. for idx, tt := range tests { if !oneByOne { - tree.Insert(tt.key, WithTag(tt.routeName)) + tree.insert(tt.key, tt.routeName, nil, nil) } for reqIdx, req := range tt.requests { @@ -131,7 +131,7 @@ func testTrie(t *testing.T, oneByOne bool) { // run. for idx, tt := range tests { if oneByOne { - tree.Insert(tt.key, WithTag(tt.routeName)) + tree.insert(tt.key, tt.routeName, nil, nil) } params := new(paramsWriter) for reqIdx, req := range tt.requests {