├── .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 [![GoDoc](https://godoc.org/github.com/twanies/weavebox?status.svg)](https://godoc.org/github.com/twanies/weavebox) [![Travis CI](https://travis-ci.org/twanies/weavebox.svg?branch=master)](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 | --------------------------------------------------------------------------------