├── .gitignore
├── template_test.go
├── .travis.yml
├── examples
├── template
│ ├── pages
│ │ ├── index.html
│ │ ├── layout.html
│ │ └── user
│ │ │ └── index.html
│ └── main.go
└── context
│ └── main.go
├── bench_test.go
├── LICENSE
├── template.go
├── server.go
├── README.md
├── weavebox_test.go
└── weavebox.go
/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template_test.go:
--------------------------------------------------------------------------------
1 | package weavebox
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go:
3 | - tip
4 |
--------------------------------------------------------------------------------
/examples/template/pages/index.html:
--------------------------------------------------------------------------------
1 |
hello
2 |
--------------------------------------------------------------------------------
/examples/template/pages/layout.html:
--------------------------------------------------------------------------------
1 | hello
2 |
3 | {{ template "content" . }}
4 |
--------------------------------------------------------------------------------
/examples/template/pages/user/index.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 | welcome {{ . }}
4 |
5 | {{ end }}
6 |
--------------------------------------------------------------------------------
/bench_test.go:
--------------------------------------------------------------------------------
1 | package weavebox
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 | )
7 |
8 | func BenchmarkGetWithValues(b *testing.B) {
9 | app := New()
10 | app.Get("/hello/:name", func(ctx *Context) error { return nil })
11 |
12 | for i := 0; i < b.N; i++ {
13 | r, err := http.NewRequest("GET", "/hello/anthony", nil)
14 | if err != nil {
15 | panic(err)
16 | }
17 | app.ServeHTTP(nil, r)
18 | }
19 | }
20 |
21 | func BenchmarkBoxGetWithValues(b *testing.B) {
22 | app := New()
23 | admin := app.Box("/admin")
24 | admin.Get("/:name", func(ctx *Context) error { return nil })
25 |
26 | for i := 0; i < b.N; i++ {
27 | r, err := http.NewRequest("GET", "/admin/anthony", nil)
28 | if err != nil {
29 | panic(err)
30 | }
31 | app.ServeHTTP(nil, r)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/examples/template/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/twanies/weavebox"
4 |
5 | func main() {
6 | app := weavebox.New()
7 | t := initTemplates()
8 | app.SetTemplateEngine(t)
9 |
10 | app.Get("/", renderIndex)
11 | app.Get("/user", renderUserDetail)
12 | app.Serve(3000)
13 | }
14 |
15 | func renderIndex(ctx *weavebox.Context) error {
16 | return ctx.Render("index.html", nil)
17 | }
18 |
19 | func renderUserDetail(ctx *weavebox.Context) error {
20 | username := "anthony"
21 | return ctx.Render("user/index.html", username)
22 | }
23 |
24 | func initTemplates() *weavebox.TemplateEngine {
25 | t := weavebox.NewTemplateEngine("pages")
26 |
27 | // Set single templates
28 | t.SetTemplates("index.html")
29 |
30 | // Set templates that have a layout
31 | t.SetTemplatesWithLayout("layout.html", "user/index.html")
32 | t.Init()
33 | return t
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Anthony De Meulemeester
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, 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,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/template.go:
--------------------------------------------------------------------------------
1 | package weavebox
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "io/ioutil"
7 | "path"
8 | "text/template"
9 | )
10 |
11 | // TemplateEngine provides simple, fast and powerfull rendering of HTML pages.
12 | type TemplateEngine struct {
13 | root string
14 | cache map[string]*template.Template
15 | templates []string
16 | templWithLayout map[string][]string
17 | }
18 |
19 | // NewTemplateEngine returns a new TemplateEngine object that will look for
20 | // templates at the given root.
21 | func NewTemplateEngine(root string) *TemplateEngine {
22 | return &TemplateEngine{
23 | cache: map[string]*template.Template{},
24 | templWithLayout: map[string][]string{},
25 | root: root,
26 | }
27 | }
28 |
29 | // Render renders the template and satisfies the weavebox.Renderer interface.
30 | func (t *TemplateEngine) Render(w io.Writer, name string, data interface{}) error {
31 | if templ, exist := t.cache[name]; exist {
32 | return templ.ExecuteTemplate(w, "_", data)
33 | }
34 | return fmt.Errorf("template %s could not be found", name)
35 | }
36 |
37 | // SetTemplates sets single templates that not need to be parsed with a layout
38 | func (t *TemplateEngine) SetTemplates(templates ...string) {
39 | for _, template := range templates {
40 | t.templates = append(t.templates, template)
41 | }
42 | }
43 |
44 | // SetTemplatesWithLayout sets a layout and parses all given templates with that
45 | // layout.
46 | func (t *TemplateEngine) SetTemplatesWithLayout(layout string, templates ...string) {
47 | t.templWithLayout[layout] = templates
48 | }
49 |
50 | // Init parses all the given singel and layout templates. And stores them in the
51 | // template cache.
52 | func (t *TemplateEngine) Init() {
53 | for layout, templates := range t.templWithLayout {
54 | layout, err := ioutil.ReadFile(path.Join(t.root, layout))
55 | handleErr(err)
56 |
57 | for _, page := range templates {
58 | parsedLayout, err := template.New("_").Parse(string(layout))
59 | handleErr(err)
60 |
61 | templ, err := ioutil.ReadFile(path.Join(t.root, page))
62 | handleErr(err)
63 |
64 | parsedTempl, err := parsedLayout.Parse(string(templ))
65 | handleErr(err)
66 |
67 | t.cache[page] = parsedTempl
68 | }
69 | }
70 |
71 | for _, file := range t.templates {
72 | templ, err := ioutil.ReadFile(path.Join(t.root, file))
73 | handleErr(err)
74 |
75 | parsedTempl, err := template.New("_").Parse(string(templ))
76 | handleErr(err)
77 |
78 | t.cache[file] = parsedTempl
79 | }
80 | }
81 |
82 | func handleErr(err error) {
83 | if err != nil {
84 | panic(err)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/examples/context/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "flag"
6 | "fmt"
7 | "log"
8 | "net/http"
9 |
10 | "golang.org/x/net/context"
11 |
12 | "github.com/twanies/weavebox"
13 | )
14 |
15 | // Simpel example how to use weavebox with a "datastore" by making use
16 | // of weavebox.Context to pass information between middleware and handlers
17 |
18 | func main() {
19 | listen := flag.Int("listen", 3000, "listen address of the application")
20 | flag.Parse()
21 |
22 | app := weavebox.New()
23 |
24 | // centralizing our errors returned from middleware and request handlers
25 | app.SetErrorHandler(errorHandler)
26 |
27 | app.Get("/hello/:name", greetingHandler)
28 | app.Use(dbContextHandler)
29 |
30 | // make a subrouter and register some middleware for it
31 | admin := app.Box("/admin")
32 | admin.Get("/:name", adminGreetingHandler)
33 | admin.Use(authenticate)
34 |
35 | log.Fatal(app.Serve(*listen))
36 | }
37 |
38 | type datastore struct {
39 | name string
40 | }
41 |
42 | type dbContext struct {
43 | context.Context
44 | ds *datastore
45 | }
46 |
47 | func (c *dbContext) Value(key interface{}) interface{} {
48 | if key == "datastore" {
49 | return c.ds
50 | }
51 | return c.Context.Value(key)
52 | }
53 |
54 | func newDatastoreContext(parent context.Context, ds *datastore) context.Context {
55 | return &dbContext{parent, ds}
56 | }
57 |
58 | func dbContextHandler(ctx *weavebox.Context) error {
59 | db := datastore{"mydatabase"}
60 | ctx.Context = newDatastoreContext(ctx.Context, &db)
61 | return nil
62 | }
63 |
64 | // Only the powerfull have access to the admin routes
65 | func authenticate(ctx *weavebox.Context) error {
66 | admins := []string{"toby", "master iy", "c.froome"}
67 | name := ctx.Param("name")
68 |
69 | for _, admin := range admins {
70 | if admin == name {
71 | return nil
72 | }
73 | }
74 | return errors.New("access forbidden")
75 | }
76 |
77 | // context helper function to stay lean and mean in your handlers
78 | func datastoreFromContext(ctx context.Context) *datastore {
79 | return ctx.Value("datastore").(*datastore)
80 | }
81 |
82 | func greetingHandler(ctx *weavebox.Context) error {
83 | name := ctx.Param("name")
84 | db := datastoreFromContext(ctx.Context)
85 | greeting := fmt.Sprintf("Greetings, %s\nYour database %s is ready", name, db.name)
86 | return ctx.Text(http.StatusOK, greeting)
87 | }
88 |
89 | func adminGreetingHandler(ctx *weavebox.Context) error {
90 | name := ctx.Param("name")
91 | db := datastoreFromContext(ctx.Context)
92 | greeting := fmt.Sprintf("Greetings powerfull admin, %s\nYour database %s is ready", name, db.name)
93 | return ctx.Text(http.StatusOK, greeting)
94 | }
95 |
96 | // custom centralized error handling
97 | func errorHandler(ctx *weavebox.Context, err error) {
98 | http.Error(ctx.Response(), "Hey some error occured: "+err.Error(), http.StatusInternalServerError)
99 | }
100 |
--------------------------------------------------------------------------------
/server.go:
--------------------------------------------------------------------------------
1 | package weavebox
2 |
3 | import (
4 | "crypto/tls"
5 | "errors"
6 | "net"
7 | "net/http"
8 | "os"
9 | "os/signal"
10 | "strings"
11 | "sync"
12 | "syscall"
13 | "time"
14 |
15 | "github.com/bradfitz/http2"
16 | )
17 |
18 | const useClosedConn = "use of closed network connection"
19 |
20 | // Server provides a gracefull shutdown of http server.
21 | type server struct {
22 | *http.Server
23 | quit chan struct{}
24 | fquit chan struct{}
25 | wg sync.WaitGroup
26 | }
27 |
28 | func newServer(addr string, h http.Handler, HTTP2 bool) *http.Server {
29 | srv := &http.Server{
30 | Addr: addr,
31 | Handler: h,
32 | ReadTimeout: 5 * time.Second,
33 | WriteTimeout: 10 * time.Second,
34 | }
35 | if HTTP2 {
36 | http2.ConfigureServer(srv, &http2.Server{})
37 | }
38 | return srv
39 | }
40 |
41 | func (s *server) ListenAndServe() error {
42 | l, err := net.Listen("tcp", s.Addr)
43 | if err != nil {
44 | return err
45 | }
46 | return s.serve(l)
47 | }
48 |
49 | func (s *server) ListenAndServeTLS(cert, key string) error {
50 | var err error
51 | config := &tls.Config{}
52 | if s.TLSConfig != nil {
53 | *config = *s.TLSConfig
54 | }
55 | if config.NextProtos == nil {
56 | config.NextProtos = []string{"http/1.1"}
57 | }
58 | config.Certificates = make([]tls.Certificate, 1)
59 | config.Certificates[0], err = tls.LoadX509KeyPair(cert, key)
60 | if err != nil {
61 | return err
62 | }
63 |
64 | l, err := net.Listen("tcp", s.Addr)
65 | if err != nil {
66 | return err
67 | }
68 | tlsList := tls.NewListener(l.(*net.TCPListener), config)
69 | return s.serve(tlsList)
70 | }
71 |
72 | // serve hooks in the Server.ConnState to incr and decr the waitgroup based on
73 | // the connection state.
74 | func (s *server) serve(l net.Listener) error {
75 | s.Server.ConnState = func(conn net.Conn, state http.ConnState) {
76 | switch state {
77 | case http.StateNew:
78 | s.wg.Add(1)
79 | case http.StateClosed, http.StateHijacked:
80 | s.wg.Done()
81 | }
82 | }
83 | go s.closeNotify(l)
84 |
85 | errChan := make(chan error, 1)
86 | go func() {
87 | errChan <- s.Server.Serve(l)
88 | }()
89 |
90 | for {
91 | select {
92 | case err := <-errChan:
93 | if strings.Contains(err.Error(), useClosedConn) {
94 | continue
95 | }
96 | return err
97 | case <-s.quit:
98 | s.SetKeepAlivesEnabled(false)
99 | s.wg.Wait()
100 | return errors.New("server stopped gracefully")
101 | case <-s.fquit:
102 | return errors.New("server stopped: process killed")
103 | }
104 | }
105 | }
106 |
107 | func (s *server) closeNotify(l net.Listener) {
108 | sig := make(chan os.Signal, 1)
109 |
110 | signal.Notify(
111 | sig,
112 | syscall.SIGTERM,
113 | syscall.SIGKILL,
114 | syscall.SIGQUIT,
115 | syscall.SIGUSR2,
116 | syscall.SIGINT,
117 | )
118 | sign := <-sig
119 | switch sign {
120 | case syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT:
121 | l.Close()
122 | s.quit <- struct{}{}
123 | case syscall.SIGKILL:
124 | l.Close()
125 | s.fquit <- struct{}{}
126 | case syscall.SIGUSR2:
127 | panic("USR2 => not implemented")
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # weavebox [](https://godoc.org/github.com/twanies/weavebox) [](https://travis-ci.org/twanies/weavebox)
2 | Minimalistic web framework for the Go programming language.
3 |
4 | ## Installation
5 | `go get github.com/twanies/weavebox`
6 |
7 | ## Features
8 | - fast route dispatching backed by httprouter
9 | - easy to add middleware handlers
10 | - subrouting with seperated middleware handlers
11 | - central based error handling
12 | - build in template engine
13 | - fast, lightweight and extendable
14 |
15 | ## Basic usage
16 | package main
17 | import "github.com/twanies/weavebox"
18 |
19 | func main() {
20 | app := weavebox.New()
21 |
22 | app.Get("/foo", fooHandler)
23 | app.Post("/bar", barHandler)
24 | app.Use(middleware1, middleware2)
25 |
26 | friends := app.Box("/friends")
27 | friends.Get("/profile", profileHandler)
28 | friends.Use(middleware3, middleware4)
29 |
30 | app.Serve(8080)
31 | }
32 | More complete examples can be found in the examples folder
33 |
34 | ## Routes
35 | app := weavebox.New()
36 |
37 | app.Get("/", func(ctx *weavebox.Context) error {
38 | .. do something ..
39 | })
40 | app.Post("/", func(ctx *weavebox.Context) error {
41 | .. do something ..
42 | })
43 | app.Put("/", func(ctx *weavebox.Context) error {
44 | .. do something ..
45 | })
46 | app.Delete("/", func(ctx *weavebox.Context) error {
47 | .. do something ..
48 | })
49 |
50 | get named url parameters
51 |
52 | app.Get("/hello/:name", func(ctx *weavebox.Context) error {
53 | name := ctx.Param("name")
54 | })
55 |
56 | ## Box (subrouting)
57 | Box lets you manage routes, contexts and middleware separate from each other.
58 |
59 | Create a new weavebox object and attach some middleware and context to it.
60 |
61 | app := weavebox.New()
62 | app.BindContext(context.WithValue(context.Background(), "foo", "bar")
63 | app.Get("/", somHandler)
64 | app.Use(middleware1, middleware2)
65 |
66 | Create a box and attach its own middleware and context to it
67 |
68 | friends := app.Box("/friends")
69 | app.BindContext(context.WithValue(context.Background(), "friend1", "john")
70 | friends.Post("/create", someHandler)
71 | friends.Use(middleware3, middleware4)
72 |
73 | In this case box friends will inherit middleware1 and middleware2 from its parent app. We can reset the middleware from its parent by calling `Reset()`
74 |
75 | friends := app.Box("/friends").Reset()
76 | friends.Use(middleware3, middleware4)
77 |
78 | Now box friends will have only middleware3 and middleware4 attached.
79 |
80 | ## Static files
81 | Make our assets are accessable trough /assets/styles.css
82 |
83 | app := weavebox.New()
84 | app.Static("/assets", "public/assets")
85 |
86 | ## Handlers
87 | ### A definition of a weavebox.Handler
88 |
89 | func(ctx *weavebox.Context) error
90 |
91 | Weavebox only accepts handlers of type `weavebox.Handler` to be passed as functions in routes. You can convert any type of handler to a `weavebox.Handler`.
92 |
93 | func myHandler(name string) weavebox.Handler{
94 | .. do something ..
95 | return func(ctx *weavebox.Context) error {
96 | return ctx.Text(w, http.StatusOK, name)
97 | }
98 | }
99 |
100 | ### Returning errors
101 | Each handler requires an error to be returned. This is personal idiom but it brings some benifits for handling your errors inside request handlers.
102 |
103 | func someHandler(ctx *weavebox.Context) error {
104 | // simple error handling by returning all errors
105 | err := someFunc(); err != nil {
106 | return err
107 | }
108 | ...
109 | req, err := http.NewRequest(...)
110 | if err != nil {
111 | return err
112 | }
113 | }
114 |
115 | A weavebox ErrorHandlerFunc
116 |
117 | func(ctx *weavebox.Context, err error)
118 |
119 | Handle all errors returned by adding a custom errorHandler for our application.
120 |
121 | app := weavebox.New()
122 | errHandler := func(ctx *weavebox.Context, err error) {
123 | .. handle the error ..
124 | }
125 | app.SetErrorHandler(errHandler)
126 |
127 | ## Context
128 | Context is a request based object helping you with a series of functions performed against the current request scope.
129 |
130 | ### Passing values arround middleware functions
131 | Context provides a context.Context for passing request scoped values arround middleware functions.
132 |
133 | Create a new context and pass some values
134 |
135 | func someMiddleware(ctx *weavebox.Context) error {
136 | ctx.Context = context.WithValue(ctx.Context, "foo", "bar")
137 | return someMiddleware2(ctx)
138 | }
139 |
140 | Get the value back from the context in another middleware function
141 |
142 | func someMiddleware2(ctx *weavebox.Context) error {
143 | value := ctx.Context.Value("foo").(string)
144 | ..
145 | }
146 |
147 | ### Binding a context
148 | In some cases you want to intitialize a context from the the main function, like a datastore for example. You can set a context out of a request scope by calling `BindContext()`.
149 |
150 | app.BindContext(context.WithValue(context.Background(), "foo", "bar"))
151 |
152 | As mentioned in the Box section, you can add different contexts to different boxes.
153 |
154 | mybox := app.Box("/foo", ..)
155 | mybox.BindContext(..)
156 |
157 | ### Helper functions
158 | Context also provides a series of helper functions like responding JSON en text, JSON decoding etc..
159 |
160 | func createUser(ctx *weavebox.Context) error {
161 | user := model.User{}
162 | if err := ctx.DecodeJSON(&user); err != nil {
163 | return errors.New("failed to decode the response body")
164 | }
165 | ..
166 | return ctx.JSON(http.StatusCreated, user)
167 | }
168 |
169 | func login(ctx *weavebox.Context) error {
170 | token := ctx.Header("x-hmac-token")
171 | if token == "" {
172 | ctx.Redirect("/login", http.StatusMovedPermanently)
173 | return nil
174 | }
175 | ..
176 | }
177 |
178 |
179 | ## View / Templates
180 |
181 | ## Logging
182 | ### Access Log
183 | Weavebox provides an access-log in an Apache log format for each incomming request. The access-log is disabled by default, to enable the access-log set `app.EnableAccessLog = true`.
184 |
185 | `127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326`
186 |
187 | ### Logging errors and information
188 |
189 | ## Server
190 | Weavebox HTTP server is a wrapper arround the default std HTTP server, the only difference is that it provides a gracefull shutdown. Weavebox provides both HTTP and HTTPS (TLS).
191 |
192 | app := weavebox.New()
193 | app.ServeTLS(8080, cert, key)
194 | // or
195 | app.Serve(8080)
196 |
197 | ### Gracefull stopping a weavebox app
198 | Gracefull stopping a weavebox app is done by sending one of these signals to the process.
199 | - SIGINT
200 | - SIGQUIT
201 | - SIGTERM
202 |
203 | You can also force-quit your app by sending it `SIGKILL` signal
204 |
205 | SIGUSR2 signal is not yet implemented. Reloading a new binary by forking the main process is something that wil be implemented when the need for it is there. Feel free to give some feedback on this feature if you think it can provide a bonus to the package.
206 |
--------------------------------------------------------------------------------
/weavebox_test.go:
--------------------------------------------------------------------------------
1 | package weavebox
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "io"
7 | "net/http"
8 | "net/http/httptest"
9 | "net/url"
10 | "strings"
11 | "testing"
12 |
13 | "golang.org/x/net/context"
14 | )
15 |
16 | var noopHandler = func(ctx *Context) error { return nil }
17 |
18 | func TestHandle(t *testing.T) {
19 | w := New()
20 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
21 | for _, method := range []string{"GET", "PUT", "POST", "DELETE"} {
22 | w.Handle(method, "/", handler)
23 | code, _ := doRequest(t, method, "/", nil, w)
24 | isHTTPStatusOK(t, code)
25 | }
26 | }
27 |
28 | func TestMethodGet(t *testing.T) {
29 | w := New()
30 | w.Get("/", noopHandler)
31 | code, _ := doRequest(t, "GET", "/", nil, w)
32 | isHTTPStatusOK(t, code)
33 | }
34 |
35 | func TestMethodPost(t *testing.T) {
36 | w := New()
37 | w.Post("/", noopHandler)
38 | code, _ := doRequest(t, "POST", "/", nil, w)
39 | isHTTPStatusOK(t, code)
40 | }
41 |
42 | func TestMethodPut(t *testing.T) {
43 | w := New()
44 | w.Put("/", noopHandler)
45 | code, _ := doRequest(t, "PUT", "/", nil, w)
46 | isHTTPStatusOK(t, code)
47 | }
48 |
49 | func TestMethodDelete(t *testing.T) {
50 | w := New()
51 | w.Delete("/", noopHandler)
52 | code, _ := doRequest(t, "DELETE", "/", nil, w)
53 | isHTTPStatusOK(t, code)
54 | }
55 |
56 | func TestMethodHead(t *testing.T) {
57 | w := New()
58 | w.Head("/", noopHandler)
59 | code, _ := doRequest(t, "HEAD", "/", nil, w)
60 | isHTTPStatusOK(t, code)
61 | }
62 |
63 | func TestMethodOptions(t *testing.T) {
64 | w := New()
65 | w.Options("/", noopHandler)
66 | code, _ := doRequest(t, "OPTIONS", "/", nil, w)
67 | isHTTPStatusOK(t, code)
68 | }
69 |
70 | func TestBox(t *testing.T) {
71 | w := New()
72 | sr := w.Box("/foo")
73 | sr.Get("/bar", noopHandler)
74 | code, _ := doRequest(t, "GET", "/foo/bar", nil, w)
75 | isHTTPStatusOK(t, code)
76 | }
77 |
78 | func TestStatic(t *testing.T) {
79 | w := New()
80 | w.Static("/public", "./")
81 | code, body := doRequest(t, "GET", "/public/README.md", nil, w)
82 | isHTTPStatusOK(t, code)
83 | if len(body) == 0 {
84 | t.Error("body cannot be empty")
85 | }
86 | if !strings.Contains(body, "weavebox") {
87 | t.Error("expecting body containing string (weavebox)")
88 | }
89 |
90 | code, body = doRequest(t, "GET", "/public/nofile", nil, w)
91 | if code != http.StatusNotFound {
92 | t.Error("expecting status 404 got %d", code)
93 | }
94 | }
95 |
96 | func TestContext(t *testing.T) {
97 | w := New()
98 | w.Get("/", checkContext(t, "m1", "m1"))
99 | w.Use(func(ctx *Context) error {
100 | ctx.Context = context.WithValue(ctx.Context, "m1", "m1")
101 | return nil
102 | })
103 | code, _ := doRequest(t, "GET", "/", nil, w)
104 | isHTTPStatusOK(t, code)
105 |
106 | w.Get("/some", checkContext(t, "m1", "m2"))
107 | w.Use(func(ctx *Context) error {
108 | ctx.Context = context.WithValue(ctx.Context, "m1", "m2")
109 | ctx.Response().WriteHeader(http.StatusBadRequest)
110 | return nil
111 | })
112 | code, _ = doRequest(t, "GET", "/some", nil, w)
113 | if code != http.StatusBadRequest {
114 | t.Error("expecting %d, got %d", http.StatusBadRequest, code)
115 | }
116 | }
117 |
118 | func TestContextWithSubrouter(t *testing.T) {
119 | w := New()
120 | sub := w.Box("/test")
121 | sub.Get("/", checkContext(t, "a", "b"))
122 | sub.Use(func(ctx *Context) error {
123 | ctx.Context = context.WithValue(ctx.Context, "a", "b")
124 | return nil
125 | })
126 | code, _ := doRequest(t, "GET", "/test", nil, w)
127 | if code != http.StatusOK {
128 | t.Errorf("expected status code 200 got %d", code)
129 | }
130 | }
131 |
132 | func TestBindContext(t *testing.T) {
133 | w := New()
134 | w.BindContext(context.WithValue(context.Background(), "a", "b"))
135 |
136 | w.Get("/", checkContext(t, "a", "b"))
137 |
138 | sub := w.Box("/foo")
139 | sub.Get("/", checkContext(t, "a", "b"))
140 |
141 | code, _ := doRequest(t, "GET", "/", nil, w)
142 | isHTTPStatusOK(t, code)
143 | code, _ = doRequest(t, "GET", "/foo", nil, w)
144 | isHTTPStatusOK(t, code)
145 | }
146 |
147 | func TestBindContextSubrouter(t *testing.T) {
148 | w := New()
149 | sub := w.Box("/foo")
150 | sub.Get("/", checkContext(t, "foo", "bar"))
151 | sub.BindContext(context.WithValue(context.Background(), "foo", "bar"))
152 |
153 | code, _ := doRequest(t, "GET", "/foo", nil, w)
154 | isHTTPStatusOK(t, code)
155 | }
156 |
157 | func checkContext(t *testing.T, key, expect string) Handler {
158 | return func(ctx *Context) error {
159 | value := ctx.Context.Value(key).(string)
160 | if value != expect {
161 | t.Errorf("expected %s got %s", expect, value)
162 | }
163 | return nil
164 | }
165 | }
166 |
167 | func TestMiddleware(t *testing.T) {
168 | buf := &bytes.Buffer{}
169 | w := New()
170 | w.Use(func(ctx *Context) error {
171 | buf.WriteString("a")
172 | return nil
173 | })
174 | w.Use(func(ctx *Context) error {
175 | buf.WriteString("b")
176 | return nil
177 | })
178 | w.Use(func(ctx *Context) error {
179 | buf.WriteString("c")
180 | return nil
181 | })
182 | w.Use(func(ctx *Context) error {
183 | buf.WriteString("d")
184 | return nil
185 | })
186 | w.Get("/", noopHandler)
187 | code, _ := doRequest(t, "GET", "/", nil, w)
188 | isHTTPStatusOK(t, code)
189 | if buf.String() != "abcd" {
190 | t.Error("expecting abcd got %s", buf.String())
191 | }
192 | }
193 |
194 | func TestBoxMiddlewareReset(t *testing.T) {
195 | buf := &bytes.Buffer{}
196 | w := New()
197 | w.Use(func(ctx *Context) error {
198 | buf.WriteString("a")
199 | return nil
200 | })
201 | w.Use(func(ctx *Context) error {
202 | buf.WriteString("b")
203 | return nil
204 | })
205 | sub := w.Box("/sub").Reset()
206 | sub.Get("/", noopHandler)
207 | code, _ := doRequest(t, "GET", "/sub", nil, w)
208 | isHTTPStatusOK(t, code)
209 | if buf.String() != "" {
210 | t.Error("expecting empty buffer got %s", buf.String())
211 | }
212 | }
213 |
214 | func TestBoxMiddlewareInheritsParent(t *testing.T) {
215 | buf := &bytes.Buffer{}
216 | w := New()
217 | w.Use(func(ctx *Context) error {
218 | buf.WriteString("a")
219 | return nil
220 | })
221 | w.Use(func(ctx *Context) error {
222 | buf.WriteString("b")
223 | return nil
224 | })
225 | sub := w.Box("/sub")
226 | sub.Get("/", noopHandler)
227 | code, _ := doRequest(t, "GET", "/sub", nil, w)
228 | isHTTPStatusOK(t, code)
229 | if buf.String() != "ab" {
230 | t.Error("expecting ab got %s", buf.String())
231 | }
232 | }
233 |
234 | func TestErrorHandler(t *testing.T) {
235 | w := New()
236 | errorMsg := "oops! something went wrong"
237 | w.SetErrorHandler(func(ctx *Context, err error) {
238 | ctx.Response().WriteHeader(http.StatusInternalServerError)
239 | if err.Error() != errorMsg {
240 | t.Error("expecting %s, got %s", errorMsg, err.Error())
241 | }
242 | })
243 | w.Use(func(ctx *Context) error {
244 | return errors.New(errorMsg)
245 | })
246 | w.Get("/", noopHandler)
247 | code, _ := doRequest(t, "GET", "/", nil, w)
248 | if code != http.StatusInternalServerError {
249 | t.Error("expecting code 500 got %s", code)
250 | }
251 | }
252 |
253 | func TestWeaveboxHandler(t *testing.T) {
254 | w := New()
255 | handle := func(respStr string) Handler {
256 | return func(ctx *Context) error {
257 | return ctx.Text(http.StatusOK, respStr)
258 | }
259 | }
260 | w.Get("/a", handle("a"))
261 | w.Get("/b", handle("b"))
262 | w.Get("/c", handle("c"))
263 |
264 | for _, r := range []string{"a", "b", "c"} {
265 | code, body := doRequest(t, "GET", "/"+r, nil, w)
266 | isHTTPStatusOK(t, code)
267 | if body != r {
268 | t.Errorf("expecting %s got %s", r, body)
269 | }
270 | }
271 | }
272 |
273 | func TestNotFoundHandler(t *testing.T) {
274 | w := New()
275 | code, body := doRequest(t, "GET", "/", nil, w)
276 | if code != http.StatusNotFound {
277 | t.Errorf("expecting code 404 got %d", code)
278 | }
279 | if !strings.Contains(body, "404 page not found") {
280 | t.Errorf("expecting body: 404 page not found got %s", body)
281 | }
282 | }
283 |
284 | func TestSetNotFound(t *testing.T) {
285 | w := New()
286 | notFoundMsg := "hey! not found"
287 | h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
288 | w.WriteHeader(http.StatusNotFound)
289 | w.Write([]byte(notFoundMsg))
290 | })
291 | w.SetNotFound(h)
292 |
293 | code, body := doRequest(t, "GET", "/", nil, w)
294 | if code != http.StatusNotFound {
295 | t.Errorf("expecting code 404 got %d", code)
296 | }
297 | if !strings.Contains(body, notFoundMsg) {
298 | t.Errorf("expecting body: %s got %s", notFoundMsg, body)
299 | }
300 | }
301 |
302 | func TestMethodNotAllowed(t *testing.T) {
303 | w := New()
304 | w.Get("/", noopHandler)
305 | code, body := doRequest(t, "POST", "/", nil, w)
306 | if code != http.StatusMethodNotAllowed {
307 | t.Errorf("expecting code 405 got %d", code)
308 | }
309 | if !strings.Contains(body, "Method Not Allowed") {
310 | t.Errorf("expecting body: Method Not Allowed got %s", body)
311 | }
312 | }
313 |
314 | func TestSetMethodNotAllowed(t *testing.T) {
315 | w := New()
316 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
317 | w.WriteHeader(http.StatusMethodNotAllowed)
318 | w.Write([]byte("foo"))
319 | })
320 | w.SetMethodNotAllowed(handler)
321 | w.Get("/", noopHandler)
322 |
323 | code, body := doRequest(t, "POST", "/", nil, w)
324 | if code != http.StatusMethodNotAllowed {
325 | t.Errorf("expecting code 405 got %d", code)
326 | }
327 | if !strings.Contains(body, "foo") {
328 | t.Errorf("expecting body: foo got %s", body)
329 | }
330 | }
331 |
332 | func TestContextURLQuery(t *testing.T) {
333 | req, _ := http.NewRequest("GET", "/?name=anthony", nil)
334 | ctx := &Context{request: req}
335 | if ctx.Query("name") != "anthony" {
336 | t.Errorf("expected anthony got %s", ctx.Query("name"))
337 | }
338 | }
339 |
340 | func TestContextForm(t *testing.T) {
341 | values := url.Values{}
342 | values.Set("email", "john@gmail.com")
343 | req, _ := http.NewRequest("POST", "/", strings.NewReader(values.Encode()))
344 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
345 | ctx := &Context{request: req}
346 | if ctx.Form("email") != "john@gmail.com" {
347 | t.Errorf("expected john@gmail.com got %s", ctx.Form("email"))
348 | }
349 | }
350 |
351 | func TestContextHeader(t *testing.T) {
352 | req, _ := http.NewRequest("GET", "/", nil)
353 | req.Header.Add("x-test", "test")
354 | ctx := &Context{request: req}
355 | if ctx.Header("x-test") != "test" {
356 | t.Error("expected header to be (test) got %s", ctx.Header("x-test"))
357 | }
358 | }
359 |
360 | func isHTTPStatusOK(t *testing.T, code int) {
361 | if code != http.StatusOK {
362 | t.Errorf("Expecting status 200 got %d", code)
363 | }
364 | }
365 |
366 | func doRequest(t *testing.T, method, route string, body io.Reader, w *Weavebox) (int, string) {
367 | r, err := http.NewRequest(method, route, body)
368 | if err != nil {
369 | t.Fatal(err)
370 | }
371 | rw := httptest.NewRecorder()
372 | w.ServeHTTP(rw, r)
373 | return rw.Code, rw.Body.String()
374 | }
375 |
--------------------------------------------------------------------------------
/weavebox.go:
--------------------------------------------------------------------------------
1 | package weavebox
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "net"
9 | "net/http"
10 | "os"
11 | "path"
12 | "time"
13 |
14 | "github.com/julienschmidt/httprouter"
15 | "golang.org/x/net/context"
16 | )
17 |
18 | // Package weavebox is opinion based minimalistic web framework for making fast and
19 | // powerfull web application in the Go programming language. It is backed by
20 | // the fastest and most optimized request router available. Weavebox also
21 | // provides a gracefull webserver that can serve TLS encripted requests aswell.
22 |
23 | var defaultErrorHandler = func(ctx *Context, err error) {
24 | http.Error(ctx.Response(), err.Error(), http.StatusInternalServerError)
25 | }
26 |
27 | // Weavebox first class object that is created by calling New()
28 | type Weavebox struct {
29 | // ErrorHandler is invoked whenever a Handler returns an error
30 | ErrorHandler ErrorHandlerFunc
31 |
32 | // Output writes the access-log and debug parameters
33 | Output io.Writer
34 |
35 | // EnableAccessLog lets you turn of the default access-log
36 | EnableAccessLog bool
37 |
38 | // HTTP2 enables the HTTP2 protocol on the server. HTTP2 wil be default proto
39 | // in the future. Currently browsers only supports HTTP/2 over encrypted TLS.
40 | HTTP2 bool
41 |
42 | templateEngine Renderer
43 | router *httprouter.Router
44 | middleware []Handler
45 | prefix string
46 | context context.Context
47 | }
48 |
49 | // New returns a new Weavebox object
50 | func New() *Weavebox {
51 | return &Weavebox{
52 | router: httprouter.New(),
53 | Output: os.Stderr,
54 | ErrorHandler: defaultErrorHandler,
55 | EnableAccessLog: false,
56 | }
57 | }
58 |
59 | // Serve serves the application on the given port
60 | func (w *Weavebox) Serve(port int) error {
61 | srv := newServer(fmt.Sprintf(":%d", port), w, w.HTTP2)
62 | return w.serve(srv)
63 | }
64 |
65 | // ServeTLS serves the application one the given port with TLS encription.
66 | func (w *Weavebox) ServeTLS(port int, certFile, keyFile string) error {
67 | srv := newServer(fmt.Sprintf(":%d", port), w, w.HTTP2)
68 | return w.serve(srv, certFile, keyFile)
69 | }
70 |
71 | // ServeCustom serves the application with custom server configuration.
72 | func (w *Weavebox) ServeCustom(s *http.Server) error {
73 | return w.serve(s)
74 | }
75 |
76 | // ServeCustomTLS serves the application with TLS encription and custom server configuration.
77 | func (w *Weavebox) ServeCustomTLS(s *http.Server, certFile, keyFile string) error {
78 | return w.serve(s, certFile, keyFile)
79 | }
80 |
81 | func (w *Weavebox) serve(s *http.Server, files ...string) error {
82 | srv := &server{
83 | Server: s,
84 | quit: make(chan struct{}, 1),
85 | fquit: make(chan struct{}, 1),
86 | }
87 | if len(files) == 0 {
88 | fmt.Fprintf(w.Output, "app listening on 0.0.0.0:%s\n", s.Addr)
89 | return srv.ListenAndServe()
90 | }
91 | if len(files) == 2 {
92 | fmt.Fprintf(w.Output, "app listening TLS on 0.0.0.0:%s\n", s.Addr)
93 | return srv.ListenAndServeTLS(files[0], files[1])
94 | }
95 | return errors.New("invalid server configuration")
96 | }
97 |
98 | // Handle adapts the usage of an http.Handler and will be invoked when
99 | // the router matches the prefix and request method
100 | func (w *Weavebox) Handle(method, path string, h http.Handler) {
101 | w.router.Handler(method, path, h)
102 | }
103 |
104 | // Get registers a route prefix and will invoke the Handler when the route
105 | // matches the prefix and the request METHOD is GET
106 | func (w *Weavebox) Get(route string, h Handler) {
107 | w.add("GET", route, h)
108 | }
109 |
110 | // Post registers a route prefix and will invoke the Handler when the route
111 | // matches the prefix and the request METHOD is POST
112 | func (w *Weavebox) Post(route string, h Handler) {
113 | w.add("POST", route, h)
114 | }
115 |
116 | // Put registers a route prefix and will invoke the Handler when the route
117 | // matches the prefix and the request METHOD is PUT
118 | func (w *Weavebox) Put(route string, h Handler) {
119 | w.add("PUT", route, h)
120 | }
121 |
122 | // Delete registers a route prefix and will invoke the Handler when the route
123 | // matches the prefix and the request METHOD is DELETE
124 | func (w *Weavebox) Delete(route string, h Handler) {
125 | w.add("DELETE", route, h)
126 | }
127 |
128 | // Head registers a route prefix and will invoke the Handler when the route
129 | // matches the prefix and the request METHOD is HEAD
130 | func (w *Weavebox) Head(route string, h Handler) {
131 | w.add("HEAD", route, h)
132 | }
133 |
134 | // Options registers a route prefix and will invoke the Handler when the route
135 | // matches the prefix and the request METHOD is OPTIONS
136 | func (w *Weavebox) Options(route string, h Handler) {
137 | w.add("OPTIONS", route, h)
138 | }
139 |
140 | // Static registers the prefix to the router and start to act as a fileserver
141 | // app.Static("/public", "./assets")
142 | func (w *Weavebox) Static(prefix, dir string) {
143 | w.router.ServeFiles(path.Join(prefix, "*filepath"), http.Dir(dir))
144 | }
145 |
146 | // BindContext lets you provide a context that will live a full http roundtrip
147 | // BindContext is mostly used in a func main() to provide init variables that
148 | // may be created only once, like a database connection. If BindContext is not
149 | // called, weavebox will use a context.Background()
150 | func (w *Weavebox) BindContext(ctx context.Context) {
151 | w.context = ctx
152 | }
153 |
154 | // Use appends a Handler to the box middleware. Different middleware can be set
155 | // for each subrouter (Box).
156 | func (w *Weavebox) Use(handlers ...Handler) {
157 | for _, h := range handlers {
158 | w.middleware = append(w.middleware, h)
159 | }
160 | }
161 |
162 | // Box returns a new Box that will inherit all of its parents middleware.
163 | // you can reset the middleware registered to the box by calling Reset()
164 | func (w *Weavebox) Box(prefix string) *Box {
165 | b := &Box{*w}
166 | b.Weavebox.prefix += prefix
167 | return b
168 | }
169 |
170 | // Box act as a subrouter and wil inherit all of its parents middleware
171 | type Box struct {
172 | Weavebox
173 | }
174 |
175 | // Reset clears all middleware
176 | func (b *Box) Reset() *Box {
177 | b.Weavebox.middleware = nil
178 | return b
179 | }
180 |
181 | // SetTemplateEngine allows the use of any template engine out there, if it
182 | // satisfies the Renderer interface
183 | func (w *Weavebox) SetTemplateEngine(t Renderer) {
184 | w.templateEngine = t
185 | }
186 |
187 | // SetNotFound sets a custom handler that is invoked whenever the
188 | // router could not match a route against the request url.
189 | func (w *Weavebox) SetNotFound(h http.Handler) {
190 | w.router.NotFound = h
191 | }
192 |
193 | // SetMethodNotAllowed sets a custom handler that is invoked whenever the router
194 | // could not match the method against the predefined routes.
195 | func (w *Weavebox) SetMethodNotAllowed(h http.Handler) {
196 | w.router.MethodNotAllowed = h
197 | }
198 |
199 | // SetErrorHandler sets a centralized errorHandler that is invoked whenever
200 | // a Handler returns an error.
201 | func (w *Weavebox) SetErrorHandler(h ErrorHandlerFunc) {
202 | w.ErrorHandler = h
203 | }
204 |
205 | // ServeHTTP satisfies the http.Handler interface
206 | func (w *Weavebox) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
207 | if rw != nil {
208 | rw.Header().Set("Server", "weavebox/1.0")
209 | }
210 | if w.EnableAccessLog {
211 | start := time.Now()
212 | logger := &responseLogger{w: rw}
213 | w.router.ServeHTTP(logger, r)
214 | w.writeLog(r, start, logger.Status(), logger.Size())
215 | // saves an allocation by seperating the whole logger if log is disabled
216 | } else {
217 | w.router.ServeHTTP(rw, r)
218 | }
219 | }
220 |
221 | func (w *Weavebox) add(method, route string, h Handler) {
222 | path := path.Join(w.prefix, route)
223 | w.router.Handle(method, path, w.makeHTTPRouterHandle(h))
224 | }
225 |
226 | func (w *Weavebox) makeHTTPRouterHandle(h Handler) httprouter.Handle {
227 | return func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) {
228 | if w.context == nil {
229 | w.context = context.Background()
230 | }
231 | ctx := &Context{
232 | Context: w.context,
233 | vars: params,
234 | response: rw,
235 | request: r,
236 | weavebox: w,
237 | }
238 | for _, handler := range w.middleware {
239 | if err := handler(ctx); err != nil {
240 | w.ErrorHandler(ctx, err)
241 | return
242 | }
243 | }
244 | if err := h(ctx); err != nil {
245 | w.ErrorHandler(ctx, err)
246 | return
247 | }
248 | }
249 | }
250 |
251 | func (w *Weavebox) writeLog(r *http.Request, start time.Time, status, size int) {
252 | host, _, _ := net.SplitHostPort(r.Host)
253 | username := "-"
254 | if r.URL.User != nil {
255 | if name := r.URL.User.Username(); name != "" {
256 | username = name
257 | }
258 | }
259 | fmt.Fprintf(w.Output, "%s - %s [%s] \"%s %s %s\" %d %d\n",
260 | host,
261 | username,
262 | start.Format("02/Jan/2006:15:04:05 -0700"),
263 | r.Method,
264 | r.RequestURI,
265 | r.Proto,
266 | status,
267 | size,
268 | )
269 | }
270 |
271 | // Handler is a weavebox idiom for handling http.Requests
272 | type Handler func(ctx *Context) error
273 |
274 | // ErrorHandlerFunc is invoked when a Handler returns an error, and can be used
275 | // to centralize error handling.
276 | type ErrorHandlerFunc func(ctx *Context, err error)
277 |
278 | // Context is required in each weavebox Handler and can be used to pass information
279 | // between requests.
280 | type Context struct {
281 | // Context is a idiomatic way to pass information between requests.
282 | // More information about context.Context can be found here:
283 | // https://godoc.org/golang.org/x/net/context
284 | Context context.Context
285 | response http.ResponseWriter
286 | request *http.Request
287 | vars httprouter.Params
288 | weavebox *Weavebox
289 | }
290 |
291 | // Response returns a default http.ResponseWriter
292 | func (c *Context) Response() http.ResponseWriter {
293 | return c.response
294 | }
295 |
296 | // Request returns a default http.Request ptr
297 | func (c *Context) Request() *http.Request {
298 | return c.request
299 | }
300 |
301 | // JSON is a helper function for writing a JSON encoded representation of v to
302 | // the ResponseWriter.
303 | func (c *Context) JSON(code int, v interface{}) error {
304 | c.Response().Header().Set("Content-Type", "application/json")
305 | c.Response().WriteHeader(code)
306 | return json.NewEncoder(c.Response()).Encode(v)
307 | }
308 |
309 | // Text is a helper function for writing a text/plain string to the ResponseWriter
310 | func (c *Context) Text(code int, text string) error {
311 | c.Response().Header().Set("Content-Type", "text/plain")
312 | c.Response().WriteHeader(code)
313 | c.Response().Write([]byte(text))
314 | return nil
315 | }
316 |
317 | // DecodeJSON is a helper that decodes the request Body to v.
318 | // For a more in depth use of decoding and encoding JSON, use the std JSON package.
319 | func (c *Context) DecodeJSON(v interface{}) error {
320 | return json.NewDecoder(c.Request().Body).Decode(v)
321 | }
322 |
323 | // Render calls the templateEngines Render function
324 | func (c *Context) Render(name string, data interface{}) error {
325 | return c.weavebox.templateEngine.Render(c.Response(), name, data)
326 | }
327 |
328 | // Param returns the url named parameter given in the route prefix by its name
329 | // app.Get("/:name", ..) => ctx.Param("name")
330 | func (c *Context) Param(name string) string {
331 | return c.vars.ByName(name)
332 | }
333 |
334 | // Query returns the url query parameter by its name.
335 | // app.Get("/api?limit=25", ..) => ctx.Query("limit")
336 | func (c *Context) Query(name string) string {
337 | return c.request.URL.Query().Get(name)
338 | }
339 |
340 | // Form returns the form parameter by its name
341 | func (c *Context) Form(name string) string {
342 | return c.request.FormValue(name)
343 | }
344 |
345 | // Header returns the request header by name
346 | func (c *Context) Header(name string) string {
347 | return c.request.Header.Get(name)
348 | }
349 |
350 | // Redirect redirects the request to the provided URL with the given status code.
351 | func (c *Context) Redirect(url string, code int) error {
352 | if code < http.StatusMultipleChoices || code > http.StatusTemporaryRedirect {
353 | return errors.New("invalid redirect code")
354 | }
355 | http.Redirect(c.response, c.request, url, code)
356 | return nil
357 | }
358 |
359 | type responseLogger struct {
360 | w http.ResponseWriter
361 | status int
362 | size int
363 | }
364 |
365 | func (l *responseLogger) Write(p []byte) (int, error) {
366 | if l.status == 0 {
367 | l.status = http.StatusOK
368 | }
369 | size, err := l.w.Write(p)
370 | l.size += size
371 | return size, err
372 | }
373 |
374 | func (l *responseLogger) Header() http.Header {
375 | return l.w.Header()
376 | }
377 |
378 | func (l *responseLogger) WriteHeader(code int) {
379 | l.w.WriteHeader(code)
380 | l.status = code
381 | }
382 |
383 | func (l *responseLogger) Status() int {
384 | return l.status
385 | }
386 |
387 | func (l *responseLogger) Size() int {
388 | return l.size
389 | }
390 |
391 | // Renderer renders any kind of template. Weavebox allows the use of different
392 | // template engines, if they implement the Render method.
393 | type Renderer interface {
394 | Render(w io.Writer, name string, data interface{}) error
395 | }
396 |
--------------------------------------------------------------------------------