├── .travis.yml
├── LICENSE
├── README.md
├── context.go
├── context_test.go
├── stack.go
└── stack_test.go
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: go
3 | go:
4 | - 1.1
5 | - 1.2
6 | - 1.3
7 | - 1.4
8 | - tip
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Alex Edwards
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Stack
[](https://travis-ci.org/alexedwards/stack) [](http://godoc.org/github.com/alexedwards/stack)
2 |
3 | Stack provides an easy way to chain your HTTP middleware and handlers together and to pass request-scoped context between them. It's essentially a context-aware version of [Alice](https://github.com/justinas/alice).
4 |
5 | [Skip to the example ›](#example)
6 |
7 | ### Usage
8 |
9 | #### Making a chain
10 |
11 | Middleware chains are constructed with [`stack.New()`](http://godoc.org/github.com/alexedwards/stack#New):
12 |
13 | ```go
14 | stack.New(middlewareOne, middlewareTwo, middlewareThree)
15 | ```
16 |
17 | You can also store middleware chains as variables, and then [`Append()`](http://godoc.org/github.com/alexedwards/stack#Chain.Append) to them:
18 |
19 | ```go
20 | stdStack := stack.New(middlewareOne, middlewareTwo)
21 | extStack := stdStack.Append(middlewareThree, middlewareFour)
22 | ```
23 |
24 | Your middleware should have the signature `func(*stack.Context, http.Handler) http.Handler`. For example:
25 |
26 | ```go
27 | func middlewareOne(ctx *stack.Context, next http.Handler) http.Handler {
28 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
29 | // do something middleware-ish, accessing ctx
30 | next.ServeHTTP(w, r)
31 | })
32 | }
33 | ```
34 |
35 | You can also use middleware with the signature `func(http.Handler) http.Handler` by adapting it with [`stack.Adapt()`](http://godoc.org/github.com/alexedwards/stack#Adapt). For example, if you had the middleware:
36 |
37 | ```go
38 | func middlewareTwo(next http.Handler) http.Handler {
39 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
40 | // do something else middleware-ish
41 | next.ServeHTTP(w, r)
42 | })
43 | }
44 | ```
45 |
46 | You can add it to a chain like this:
47 |
48 | ```go
49 | stack.New(middlewareOne, stack.Adapt(middlewareTwo), middlewareThree)
50 | ```
51 |
52 | See the [codes samples](#code-samples) for real-life use of third-party middleware with Stack.
53 |
54 | #### Adding an application handler
55 |
56 | Application handlers should have the signature `func(*stack.Context, http.ResponseWriter, *http.Request)`. You add them to the end of a middleware chain with the [`Then()`](http://godoc.org/github.com/alexedwards/stack#Chain.Then) method.
57 |
58 | So an application handler like this:
59 |
60 | ```go
61 | func appHandler(ctx *stack.Context, w http.ResponseWriter, r *http.Request) {
62 | // do something handler-ish, accessing ctx
63 | }
64 | ```
65 |
66 | Is added to the end of a middleware chain like this:
67 |
68 | ```go
69 | stack.New(middlewareOne, middlewareTwo).Then(appHandler)
70 | ```
71 |
72 | For convenience [`ThenHandler()`](http://godoc.org/github.com/alexedwards/stack#Chain.ThenHandler) and [`ThenHandlerFunc()`](http://godoc.org/github.com/alexedwards/stack#Chain.ThenHandlerFunc) methods are also provided. These allow you to finish a chain with a standard `http.Handler` or `http.HandlerFunc` respectively.
73 |
74 | For example, you could use a standard `http.FileServer` as the application handler:
75 |
76 | ```go
77 | fs := http.FileServer(http.Dir("./static/"))
78 | http.Handle("/", stack.New(middlewareOne, middlewareTwo).ThenHandler(fs))
79 | ```
80 |
81 | Once a chain is 'closed' with any of these methods it is converted into a [`HandlerChain`](http://godoc.org/github.com/alexedwards/stack#HandlerChain) object which satisfies the `http.Handler` interface, and can be used with the `http.DefaultServeMux` and many other routers.
82 |
83 | #### Using context
84 |
85 | Request-scoped data (or *context*) can be passed through the chain by storing it in `stack.Context`. This is implemented as a pointer to a `map[string]interface{}` and scoped to the goroutine executing the current HTTP request. Operations on `stack.Context` are protected by a mutex, so if you need to pass the context pointer to another goroutine (say for logging or completing a background process) it is safe for concurrent use.
86 |
87 | Data is added with [`Context.Put()`](http://godoc.org/github.com/alexedwards/stack#Context.Put). The first parameter is a string (which acts as a key) and the second is the value you need to store. For example:
88 |
89 | ```go
90 | func middlewareOne(ctx *stack.Context, next http.Handler) http.Handler {
91 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
92 | ctx.Put("token", "c9e452805dee5044ba520198628abcaa")
93 | next.ServeHTTP(w, r)
94 | })
95 | }
96 | ```
97 |
98 | You retrieve data with [`Context.Get()`](http://godoc.org/github.com/alexedwards/stack#Context.Get). Remember to type assert the returned value into the type you're expecting.
99 |
100 | ```go
101 | func appHandler(ctx *stack.Context, w http.ResponseWriter, r *http.Request) {
102 | token, ok := ctx.Get("token").(string)
103 | if !ok {
104 | http.Error(w, http.StatusText(500), 500)
105 | return
106 | }
107 | fmt.Fprintf(w, "Token is: %s", token)
108 | }
109 | ```
110 |
111 | Note that `Context.Get()` will return `nil` if a key does not exist. If you need to tell the difference between a key having a `nil` value and it explicitly not existing, please check with [`Context.Exists()`](http://godoc.org/github.com/alexedwards/stack#Context.Exists).
112 |
113 | Keys (and their values) can be deleted with [`Context.Delete()`](http://godoc.org/github.com/alexedwards/stack#Context.Delete).
114 |
115 | #### Injecting context
116 |
117 | It's possible to inject values into `stack.Context` during a request cycle but *before* the chain starts to be executed. This is useful if you need to inject parameters from a router into the context.
118 |
119 | The [`Inject()`](http://godoc.org/github.com/alexedwards/stack#Inject) function returns a new copy of the chain containing the injected context. You should make sure that you use this new copy – not the original – for subsequent processing.
120 |
121 | Here's an example of a wrapper for injecting [httprouter](https://github.com/julienschmidt/httprouter) params into the context:
122 |
123 | ```go
124 | func InjectParams(hc stack.HandlerChain) httprouter.Handle {
125 | return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
126 | newHandlerChain := stack.Inject(hc, "params", ps)
127 | newHandlerChain.ServeHTTP(w, r)
128 | }
129 | }
130 | ```
131 |
132 | A full example is available in the [code samples](#code-samples).
133 |
134 | ### Example
135 |
136 | ```go
137 | package main
138 |
139 | import (
140 | "net/http"
141 | "github.com/alexedwards/stack"
142 | "fmt"
143 | )
144 |
145 | func main() {
146 | stk := stack.New(token, stack.Adapt(language))
147 |
148 | http.Handle("/", stk.Then(final))
149 |
150 | http.ListenAndServe(":3000", nil)
151 | }
152 |
153 | func token(ctx *stack.Context, next http.Handler) http.Handler {
154 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
155 | ctx.Put("token", "c9e452805dee5044ba520198628abcaa")
156 | next.ServeHTTP(w, r)
157 | })
158 | }
159 |
160 | func language(next http.Handler) http.Handler {
161 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
162 | w.Header().Set("Content-Language", "en-gb")
163 | next.ServeHTTP(w, r)
164 | })
165 | }
166 |
167 | func final(ctx *stack.Context, w http.ResponseWriter, r *http.Request) {
168 | token, ok := ctx.Get("token").(string)
169 | if !ok {
170 | http.Error(w, http.StatusText(500), 500)
171 | return
172 | }
173 | fmt.Fprintf(w, "Token is: %s", token)
174 | }
175 | ```
176 |
177 | ### Code samples
178 |
179 | * [Integrating with httprouter](https://gist.github.com/alexedwards/4d20c505f389597c3360)
180 | * *More to follow*
181 |
182 | ### TODO
183 |
184 | - Add more code samples (using 3rd party middleware)
185 | - Make a `chain.Merge()` method
186 | - Mirror master in v1 branch (and mention gopkg.in in README)
187 | - Add benchmarks
188 |
--------------------------------------------------------------------------------
/context.go:
--------------------------------------------------------------------------------
1 | package stack
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | type Context struct {
8 | mu sync.RWMutex
9 | m map[string]interface{}
10 | }
11 |
12 | func NewContext() *Context {
13 | m := make(map[string]interface{})
14 | return &Context{m: m}
15 | }
16 |
17 | func (c *Context) Get(key string) interface{} {
18 | if !c.Exists(key) {
19 | return nil
20 | }
21 | c.mu.RLock()
22 | defer c.mu.RUnlock()
23 | return c.m[key]
24 | }
25 |
26 | func (c *Context) Put(key string, val interface{}) *Context {
27 | c.mu.Lock()
28 | defer c.mu.Unlock()
29 | c.m[key] = val
30 | return c
31 | }
32 |
33 | func (c *Context) Delete(key string) *Context {
34 | c.mu.Lock()
35 | defer c.mu.Unlock()
36 | delete(c.m, key)
37 | return c
38 | }
39 |
40 | func (c *Context) Exists(key string) bool {
41 | c.mu.RLock()
42 | defer c.mu.RUnlock()
43 | _, ok := c.m[key]
44 | return ok
45 | }
46 |
47 | func (c *Context) copy() *Context {
48 | nc := NewContext()
49 | c.mu.RLock()
50 | defer c.mu.RUnlock()
51 | for k, v := range c.m {
52 | nc.m[k] = v
53 | }
54 | return nc
55 | }
56 |
--------------------------------------------------------------------------------
/context_test.go:
--------------------------------------------------------------------------------
1 | package stack
2 |
3 | import "testing"
4 |
5 | func TestGet(t *testing.T) {
6 | ctx := NewContext()
7 | ctx.m["flip"] = "flop"
8 | ctx.m["bish"] = nil
9 |
10 | val := ctx.Get("flip")
11 | assertEquals(t, "flop", val)
12 |
13 | val = ctx.Get("bish")
14 | assertEquals(t, nil, val)
15 | }
16 |
17 | func TestPut(t *testing.T) {
18 | ctx := NewContext()
19 |
20 | ctx.Put("bish", "bash")
21 | assertEquals(t, "bash", ctx.m["bish"])
22 | }
23 |
24 | func TestDelete(t *testing.T) {
25 | ctx := NewContext()
26 | ctx.m["flip"] = "flop"
27 |
28 | ctx.Delete("flip")
29 | assertEquals(t, nil, ctx.m["flip"])
30 | }
31 |
32 | func TestCopy(t *testing.T) {
33 | ctx := NewContext()
34 | ctx.m["flip"] = "flop"
35 |
36 | ctx2 := ctx.copy()
37 | ctx2.m["bish"] = "bash"
38 | assertEquals(t, nil, ctx.m["bish"])
39 | assertEquals(t, "bash", ctx2.m["bish"])
40 | }
41 |
42 | func TestExists(t *testing.T) {
43 | ctx := NewContext()
44 | ctx.m["flip"] = "flop"
45 |
46 | assertEquals(t, true, ctx.Exists("flip"))
47 | assertEquals(t, false, ctx.Exists("bash"))
48 | }
49 |
--------------------------------------------------------------------------------
/stack.go:
--------------------------------------------------------------------------------
1 | package stack
2 |
3 | import "net/http"
4 |
5 | type chainHandler func(*Context) http.Handler
6 | type chainMiddleware func(*Context, http.Handler) http.Handler
7 |
8 | type Chain struct {
9 | mws []chainMiddleware
10 | h chainHandler
11 | }
12 |
13 | func New(mws ...chainMiddleware) Chain {
14 | return Chain{mws: mws}
15 | }
16 |
17 | func (c Chain) Append(mws ...chainMiddleware) Chain {
18 | newMws := make([]chainMiddleware, len(c.mws)+len(mws))
19 | copy(newMws[:len(c.mws)], c.mws)
20 | copy(newMws[len(c.mws):], mws)
21 | c.mws = newMws
22 | return c
23 | }
24 |
25 | func (c Chain) Then(chf func(ctx *Context, w http.ResponseWriter, r *http.Request)) HandlerChain {
26 | c.h = adaptContextHandlerFunc(chf)
27 | return newHandlerChain(c)
28 | }
29 |
30 | func (c Chain) ThenHandler(h http.Handler) HandlerChain {
31 | c.h = adaptHandler(h)
32 | return newHandlerChain(c)
33 | }
34 |
35 | func (c Chain) ThenHandlerFunc(fn func(http.ResponseWriter, *http.Request)) HandlerChain {
36 | c.h = adaptHandlerFunc(fn)
37 | return newHandlerChain(c)
38 | }
39 |
40 | type HandlerChain struct {
41 | context *Context
42 | Chain
43 | }
44 |
45 | func newHandlerChain(c Chain) HandlerChain {
46 | return HandlerChain{context: NewContext(), Chain: c}
47 | }
48 |
49 | func (hc HandlerChain) ServeHTTP(w http.ResponseWriter, r *http.Request) {
50 | // Always take a copy of context (i.e. pointing to a brand new memory location)
51 | ctx := hc.context.copy()
52 |
53 | final := hc.h(ctx)
54 | for i := len(hc.mws) - 1; i >= 0; i-- {
55 | final = hc.mws[i](ctx, final)
56 | }
57 | final.ServeHTTP(w, r)
58 | }
59 |
60 | func Inject(hc HandlerChain, key string, val interface{}) HandlerChain {
61 | hc.context = hc.context.copy().Put(key, val)
62 | return hc
63 | }
64 |
65 | // Adapt third party middleware with the signature
66 | // func(http.Handler) http.Handler into chainMiddleware
67 | func Adapt(fn func(http.Handler) http.Handler) chainMiddleware {
68 | return func(ctx *Context, h http.Handler) http.Handler {
69 | return fn(h)
70 | }
71 | }
72 |
73 | // Adapt http.Handler into a chainHandler
74 | func adaptHandler(h http.Handler) chainHandler {
75 | return func(ctx *Context) http.Handler {
76 | return h
77 | }
78 | }
79 |
80 | // Adapt a function with the signature
81 | // func(http.ResponseWriter, *http.Request) into a chainHandler
82 | func adaptHandlerFunc(fn func(w http.ResponseWriter, r *http.Request)) chainHandler {
83 | return adaptHandler(http.HandlerFunc(fn))
84 | }
85 |
86 | // Adapt a function with the signature
87 | // func(Context, http.ResponseWriter, *http.Request) into a chainHandler
88 | func adaptContextHandlerFunc(fn func(ctx *Context, w http.ResponseWriter, r *http.Request)) chainHandler {
89 | return func(ctx *Context) http.Handler {
90 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
91 | fn(ctx, w, r)
92 | })
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/stack_test.go:
--------------------------------------------------------------------------------
1 | package stack
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "log"
7 | "net/http"
8 | "net/http/httptest"
9 | "testing"
10 | )
11 |
12 | func assertEquals(t *testing.T, e interface{}, o interface{}) {
13 | if e != o {
14 | t.Errorf("\n...expected = %v\n...obtained = %v", e, o)
15 | }
16 | }
17 |
18 | func serveAndRequest(h http.Handler) string {
19 | ts := httptest.NewServer(h)
20 | defer ts.Close()
21 | res, err := http.Get(ts.URL)
22 | if err != nil {
23 | log.Fatal(err)
24 | }
25 | resBody, err := ioutil.ReadAll(res.Body)
26 | res.Body.Close()
27 | if err != nil {
28 | log.Fatal(err)
29 | }
30 | return string(resBody)
31 | }
32 |
33 | func bishMiddleware(ctx *Context, next http.Handler) http.Handler {
34 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
35 | ctx.Put("bish", "bash")
36 | fmt.Fprintf(w, "bishMiddleware>")
37 | next.ServeHTTP(w, r)
38 | })
39 | }
40 |
41 | func flipMiddleware(ctx *Context, next http.Handler) http.Handler {
42 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
43 | fmt.Fprintf(w, "flipMiddleware>")
44 | next.ServeHTTP(w, r)
45 | })
46 | }
47 |
48 | func wobbleMiddleware(next http.Handler) http.Handler {
49 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
50 | fmt.Fprintf(w, "wobbleMiddleware>")
51 | next.ServeHTTP(w, r)
52 | })
53 | }
54 |
55 | func bishHandler(ctx *Context, w http.ResponseWriter, r *http.Request) {
56 | val := ctx.Get("bish")
57 | fmt.Fprintf(w, "bishHandler [bish=%v]", val)
58 | }
59 |
60 | func flipHandler(ctx *Context, w http.ResponseWriter, r *http.Request) {
61 | valb := ctx.Get("bish")
62 | valf := ctx.Get("flip")
63 | fmt.Fprintf(w, "flipHandler [bish=%v,flip=%v]", valb, valf)
64 | }
65 |
66 | func TestNew(t *testing.T) {
67 | st := New(bishMiddleware, flipMiddleware).Then(bishHandler)
68 | res := serveAndRequest(st)
69 | assertEquals(t, "bishMiddleware>flipMiddleware>bishHandler [bish=bash]", res)
70 | }
71 |
72 | func TestAppend(t *testing.T) {
73 | st := New(bishMiddleware).Append(flipMiddleware, flipMiddleware).Then(bishHandler)
74 | res := serveAndRequest(st)
75 | assertEquals(t, "bishMiddleware>flipMiddleware>flipMiddleware>bishHandler [bish=bash]", res)
76 | }
77 |
78 | func TestAppendDoesNotMutate(t *testing.T) {
79 | st1 := New(bishMiddleware, flipMiddleware)
80 | st2 := st1.Append(flipMiddleware, flipMiddleware)
81 | res := serveAndRequest(st1.Then(bishHandler))
82 | assertEquals(t, "bishMiddleware>flipMiddleware>bishHandler [bish=bash]", res)
83 | res = serveAndRequest(st2.Then(bishHandler))
84 | assertEquals(t, "bishMiddleware>flipMiddleware>flipMiddleware>flipMiddleware>bishHandler [bish=bash]", res)
85 | }
86 |
87 | func TestThen(t *testing.T) {
88 | chf := func(ctx *Context, w http.ResponseWriter, r *http.Request) {
89 | fmt.Fprint(w, "An anonymous ContextHandlerFunc")
90 | }
91 | st := New().Then(chf)
92 | res := serveAndRequest(st)
93 | assertEquals(t, "An anonymous ContextHandlerFunc", res)
94 | }
95 |
96 | func TestThenHandler(t *testing.T) {
97 | st := New().ThenHandler(http.NotFoundHandler())
98 | res := serveAndRequest(st)
99 | assertEquals(t, "404 page not found\n", res)
100 | }
101 |
102 | func TestThenHandlerFunc(t *testing.T) {
103 | hf := func(w http.ResponseWriter, r *http.Request) {
104 | fmt.Fprint(w, "An anonymous HandlerFunc")
105 | }
106 | st := New().ThenHandlerFunc(hf)
107 | res := serveAndRequest(st)
108 | assertEquals(t, "An anonymous HandlerFunc", res)
109 | }
110 |
111 | func TestMixedMiddleware(t *testing.T) {
112 | st := New(bishMiddleware, Adapt(wobbleMiddleware), flipMiddleware).Then(bishHandler)
113 | res := serveAndRequest(st)
114 | assertEquals(t, "bishMiddleware>wobbleMiddleware>flipMiddleware>bishHandler [bish=bash]", res)
115 | }
116 |
117 | func TestInject(t *testing.T) {
118 | st := New(flipMiddleware).Then(flipHandler)
119 | st2 := Inject(st, "bish", "boop")
120 |
121 | res := serveAndRequest(st2)
122 | assertEquals(t, "flipMiddleware>flipHandler [bish=boop,flip=]", res)
123 |
124 | res = serveAndRequest(st)
125 | assertEquals(t, "flipMiddleware>flipHandler [bish=,flip=]", res)
126 | }
127 |
--------------------------------------------------------------------------------