├── examples ├── appengine │ ├── views │ │ ├── includes │ │ │ ├── footer.tpl │ │ │ └── header.tpl │ │ └── index.tpl │ ├── app.yaml │ └── hello │ │ └── hello.go ├── templates │ ├── views │ │ ├── includes │ │ │ ├── footer.tpl │ │ │ └── header.tpl │ │ ├── index.tpl │ │ └── about.tpl │ └── main.go ├── splat │ ├── public │ │ ├── images │ │ │ └── go.png │ │ └── css │ │ │ └── base.css │ ├── views │ │ ├── includes │ │ │ ├── footer.tpl │ │ │ └── header.tpl │ │ └── index.tpl │ └── main.go ├── static-files │ ├── public │ │ ├── images │ │ │ └── go.png │ │ └── css │ │ │ └── base.css │ ├── views │ │ ├── includes │ │ │ ├── footer.tpl │ │ │ └── header.tpl │ │ └── index.tpl │ └── main.go ├── configuration │ ├── traffic.conf │ └── main.go ├── show-errors │ └── main.go ├── error-handler │ └── main.go ├── not-found │ └── main.go ├── simple │ └── main.go ├── middleware │ └── main.go └── before-filter │ └── main.go ├── traffic ├── new_project_templates │ ├── views │ │ ├── includes │ │ │ ├── footer.tpl │ │ │ └── header.tpl │ │ └── index.tpl │ ├── main.go │ ├── root_handler.go │ ├── root_handler_test.go │ └── helper_test.go ├── main.go └── command_new.go ├── .travis.yml ├── doc.go ├── .gitignore ├── request.go ├── logger_middleware.go ├── request_test.go ├── static_middleware.go ├── router_middleware.go ├── LICENSE ├── route.go ├── router_middleware_test.go ├── response_writer_test.go ├── response_writer.go ├── route_test.go ├── template.go ├── utils.go ├── utils_test.go ├── router_test.go ├── show_errors_middleware.go ├── README.md └── router.go /examples/appengine/views/includes/footer.tpl: -------------------------------------------------------------------------------- 1 |

Footer (views/includes/footer.tpl)

2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/templates/views/includes/footer.tpl: -------------------------------------------------------------------------------- 1 |

Footer (views/includes/footer.tpl)

2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/splat/public/images/go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravityblast/traffic/HEAD/examples/splat/public/images/go.png -------------------------------------------------------------------------------- /examples/splat/views/includes/footer.tpl: -------------------------------------------------------------------------------- 1 |

Footer (views/includes/footer.tpl)

2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /traffic/new_project_templates/views/includes/footer.tpl: -------------------------------------------------------------------------------- 1 |

Footer (views/includes/footer.tpl)

2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/static-files/public/images/go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravityblast/traffic/HEAD/examples/static-files/public/images/go.png -------------------------------------------------------------------------------- /examples/static-files/views/includes/footer.tpl: -------------------------------------------------------------------------------- 1 |

Footer (views/includes/footer.tpl)

2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/appengine/views/index.tpl: -------------------------------------------------------------------------------- 1 | {{ template "includes/header" }} 2 | {{ .Message }} (views/index.tpl) 3 | {{ template "includes/footer" }} 4 | 5 | -------------------------------------------------------------------------------- /examples/appengine/app.yaml: -------------------------------------------------------------------------------- 1 | application: test 2 | version: 1 3 | runtime: go 4 | api_version: go1 5 | 6 | handlers: 7 | - url: /.* 8 | script: _go_app 9 | -------------------------------------------------------------------------------- /examples/templates/views/index.tpl: -------------------------------------------------------------------------------- 1 | {{ template "includes/header" }} 2 | {{ upcase .Message }} (views/index.tpl) 3 | {{ template "includes/footer" }} 4 | 5 | -------------------------------------------------------------------------------- /traffic/new_project_templates/views/index.tpl: -------------------------------------------------------------------------------- 1 | {{ template "includes/header" }} 2 | {{ .Message }} (views/index.tpl) 3 | {{ template "includes/footer" }} 4 | 5 | -------------------------------------------------------------------------------- /examples/configuration/traffic.conf: -------------------------------------------------------------------------------- 1 | foo: Hello world 2 | 3 | [development] 4 | bar: Development variable 5 | 6 | [production] 7 | bar: Production variable 8 | 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 1.2 3 | 4 | install: 5 | - go get github.com/pilu/config 6 | - go get github.com/pilu/fresh 7 | - go get github.com/pilu/miniassert 8 | 9 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package traffic - a Sinatra inspired regexp/pattern mux for Go. 3 | 4 | Docs and examples are on github: https://github.com/pilu/traffic/ 5 | */ 6 | package traffic 7 | -------------------------------------------------------------------------------- /examples/appengine/views/includes/header.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Traffic example 5 | 6 | 7 |

Header (views/includes/header.tpl)

8 | 9 | -------------------------------------------------------------------------------- /examples/templates/views/includes/header.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Traffic example 5 | 6 | 7 |

Header (views/includes/header.tpl)

8 | 9 | -------------------------------------------------------------------------------- /examples/static-files/views/index.tpl: -------------------------------------------------------------------------------- 1 | {{ template "includes/header" }} 2 |

3 |

{{ .Message }} (views/index.tpl)

4 | {{ template "includes/footer" }} 5 | 6 | -------------------------------------------------------------------------------- /traffic/new_project_templates/views/includes/header.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Traffic example 5 | 6 | 7 |

Header (views/includes/header.tpl)

8 | 9 | -------------------------------------------------------------------------------- /examples/splat/public/css/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | text-align: center; 3 | background: #ccc; 4 | } 5 | 6 | .container { 7 | background: #fff; 8 | width: 800px; 9 | margin: 0 auto; 10 | text-align: left; 11 | padding: 5px 10px; 12 | } 13 | -------------------------------------------------------------------------------- /examples/static-files/public/css/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | text-align: center; 3 | background: #ccc; 4 | } 5 | 6 | .container { 7 | background: #fff; 8 | width: 800px; 9 | margin: 0 auto; 10 | text-align: left; 11 | padding: 5px 10px; 12 | } 13 | -------------------------------------------------------------------------------- /traffic/new_project_templates/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/pilu/traffic" 5 | ) 6 | 7 | var router *traffic.Router 8 | 9 | func init() { 10 | router = traffic.New() 11 | router.Get("/", RootHandler) 12 | } 13 | 14 | func main() { 15 | router.Run() 16 | } 17 | -------------------------------------------------------------------------------- /examples/splat/views/includes/header.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Traffic example 5 | 6 | 7 | 8 |
9 |

Header (views/includes/header.tpl)

10 | 11 | -------------------------------------------------------------------------------- /examples/static-files/views/includes/header.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Traffic example 5 | 6 | 7 | 8 |
9 |

Header (views/includes/header.tpl)

10 | 11 | -------------------------------------------------------------------------------- /traffic/new_project_templates/root_handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/pilu/traffic" 5 | ) 6 | 7 | type ResponseData struct { 8 | Message string 9 | } 10 | 11 | func RootHandler(w traffic.ResponseWriter, r *traffic.Request) { 12 | responseData := &ResponseData{ "Hello World" } 13 | w.Render("index", responseData) 14 | } 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | -------------------------------------------------------------------------------- /examples/show-errors/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/pilu/traffic" 5 | ) 6 | 7 | func rootHandler(w traffic.ResponseWriter, r *traffic.Request) { 8 | x := 0 9 | // this will raise a 'runtime error: integer divide by zero' 10 | x = 1 / x 11 | } 12 | 13 | func main() { 14 | router := traffic.New() 15 | router.Get("/", rootHandler) 16 | router.Run() 17 | } 18 | -------------------------------------------------------------------------------- /traffic/new_project_templates/root_handler_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRootHandler(t *testing.T) { 8 | recorder := newTestRequest("GET", "/") 9 | expectedStatusCode := 200 10 | if recorder.Code != expectedStatusCode { 11 | t.Errorf("Expected response status code `%d`, got `%d`", expectedStatusCode, recorder.Code) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | ) 7 | 8 | type Request struct { 9 | *http.Request 10 | } 11 | 12 | func (r Request) Params() url.Values { 13 | return r.Request.URL.Query() 14 | } 15 | 16 | func (r Request) Param(key string) string { 17 | return r.Params().Get(key) 18 | } 19 | 20 | func newRequest(r *http.Request) *Request { 21 | return &Request{ 22 | Request: r, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/static-files/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/pilu/traffic" 5 | ) 6 | 7 | type ResponseData struct { 8 | Message string 9 | } 10 | 11 | func rootHandler(w traffic.ResponseWriter, r *traffic.Request) { 12 | responseData := &ResponseData{ "Hello World" } 13 | w.Render("index", responseData) 14 | } 15 | 16 | func main() { 17 | router := traffic.New() 18 | router.Get("/", rootHandler) 19 | router.Run() 20 | } 21 | -------------------------------------------------------------------------------- /traffic/new_project_templates/helper_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/http/httptest" 7 | ) 8 | 9 | func newTestRequest(method, path string) *httptest.ResponseRecorder { 10 | request, err := http.NewRequest(method, path, nil) 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | 15 | recorder := httptest.NewRecorder() 16 | router.ServeHTTP(recorder, request) 17 | 18 | return recorder 19 | } 20 | -------------------------------------------------------------------------------- /examples/configuration/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/pilu/traffic" 5 | ) 6 | 7 | func rootHandler(w traffic.ResponseWriter, r *traffic.Request) { 8 | w.WriteText("%s
", w.GetVar("foo")) 9 | 10 | // run with TRAFFIC_ENV=production to get the "bar" value 11 | // from the production section of the config file 12 | w.WriteText("%s
", w.GetVar("bar")) 13 | } 14 | 15 | func main() { 16 | router := traffic.New() 17 | // Routes 18 | router.Get("/", rootHandler) 19 | router.Run() 20 | } 21 | -------------------------------------------------------------------------------- /examples/splat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/pilu/traffic" 5 | ) 6 | 7 | type ResponseData struct { 8 | PagePath string 9 | } 10 | 11 | func pageHandler(w traffic.ResponseWriter, r *traffic.Request) { 12 | pagePath := r.Param("page_path") 13 | 14 | responseData := &ResponseData{ 15 | PagePath: pagePath, 16 | } 17 | 18 | w.Render("index", responseData) 19 | } 20 | 21 | func main() { 22 | router := traffic.New() 23 | router.Get("/:page_path*?", pageHandler) 24 | router.Run() 25 | } 26 | -------------------------------------------------------------------------------- /examples/templates/views/about.tpl: -------------------------------------------------------------------------------- 1 | {{ template "includes/header" }} 2 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 3 | {{ template "includes/footer" }} 4 | 5 | -------------------------------------------------------------------------------- /logger_middleware.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type LoggerMiddleware struct { 8 | router *Router 9 | } 10 | 11 | func (loggerMiddleware *LoggerMiddleware) ServeHTTP(w ResponseWriter, r *Request, next NextMiddlewareFunc) { 12 | startTime := time.Now() 13 | 14 | if nextMiddleware := next(); nextMiddleware != nil { 15 | nextMiddleware.ServeHTTP(w, r, next) 16 | } 17 | 18 | duration := time.Since(startTime).Seconds() 19 | 20 | Logger().Printf(`[%.6f] %d "%s"`, duration, w.StatusCode(), r.URL.Path) 21 | } 22 | -------------------------------------------------------------------------------- /examples/splat/views/index.tpl: -------------------------------------------------------------------------------- 1 | {{ template "includes/header" }} 2 | 16 |
17 |

Current page path: {{ .PagePath }}

18 |
19 | {{ template "includes/footer" }} 20 | 21 | -------------------------------------------------------------------------------- /examples/appengine/hello/hello.go: -------------------------------------------------------------------------------- 1 | package hello 2 | 3 | // UNCOMMENT LINE 6 AND 12-14 TO RUN THIS EXAMPLE. THEY ARE COMMENTED TO RUN ON travis-ci.org WITHOUT ERRORS 4 | 5 | import ( 6 | /* "appengine" */ 7 | "net/http" 8 | "github.com/pilu/traffic" 9 | ) 10 | 11 | func init() { 12 | /* if !appengine.IsDevAppServer() { */ 13 | /* traffic.SetVar("env", "production") */ 14 | /* } */ 15 | 16 | t := traffic.New() 17 | t.Get("/", rootHandler) 18 | 19 | http.Handle("/", t) 20 | } 21 | 22 | func rootHandler(w traffic.ResponseWriter, r *traffic.Request) { 23 | w.Render("index", struct{ 24 | Message string 25 | }{ 26 | "Hello Google App Engine", 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /request_test.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | assert "github.com/pilu/miniassert" 5 | "net/http" 6 | "net/url" 7 | "testing" 8 | ) 9 | 10 | func TestRequest_Params(t *testing.T) { 11 | r, _ := http.NewRequest("GET", "http:///example.com?f=foo&b=bar", nil) 12 | request := newRequest(r) 13 | params := make(url.Values) 14 | params.Set("f", "foo") 15 | params.Set("b", "bar") 16 | 17 | assert.Equal(t, params, request.Params()) 18 | } 19 | 20 | func TestRequest_Param(t *testing.T) { 21 | r, _ := http.NewRequest("GET", "http:///example.com?f=foo&b=bar", nil) 22 | request := newRequest(r) 23 | 24 | assert.Equal(t, "foo", request.Param("f")) 25 | assert.Equal(t, "bar", request.Param("b")) 26 | } 27 | -------------------------------------------------------------------------------- /examples/templates/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "github.com/pilu/traffic" 6 | ) 7 | 8 | type ResponseData struct { 9 | Message string 10 | } 11 | 12 | func indexHandler(w traffic.ResponseWriter, r *traffic.Request) { 13 | responseData := &ResponseData{ "hello world" } 14 | w.Render("index", responseData) 15 | } 16 | 17 | func aboutHandler(w traffic.ResponseWriter, r *traffic.Request) { 18 | w.Render("about") 19 | } 20 | 21 | func main() { 22 | traffic.TemplateFunc("upcase", strings.ToUpper) 23 | traffic.TemplateFunc("downcase", strings.ToLower) 24 | 25 | router := traffic.New() 26 | router.Get("/", indexHandler) 27 | router.Get("/about/?", aboutHandler) 28 | router.Run() 29 | } 30 | -------------------------------------------------------------------------------- /examples/error-handler/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/pilu/traffic" 5 | ) 6 | 7 | func errorHandler(w traffic.ResponseWriter, r *traffic.Request, err interface{}) { 8 | w.WriteText("This is a custom error page
\n") 9 | w.WriteText("Recovered from `%v`", err) 10 | } 11 | 12 | func rootHandler(w traffic.ResponseWriter, r *traffic.Request) { 13 | x := 0 14 | // this will raise a 'runtime error: integer divide by zero' 15 | x = 1 / x 16 | } 17 | 18 | func main() { 19 | // Setting env to `production`. 20 | // Otherwise the ShoeErrors Middleware used in development 21 | // will recover from the panics. 22 | 23 | traffic.SetVar("env", "production") 24 | 25 | router := traffic.New() 26 | router.ErrorHandler = errorHandler 27 | router.Get("/", rootHandler) 28 | router.Run() 29 | } 30 | -------------------------------------------------------------------------------- /traffic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "log" 7 | ) 8 | 9 | type Command interface { 10 | Execute(args []string) 11 | } 12 | 13 | var commands map[string]Command 14 | var logger *log.Logger 15 | 16 | func init() { 17 | logger = log.New(os.Stderr, "", 0) 18 | commands = map[string]Command{ 19 | "new": &CommandNew{}, 20 | } 21 | } 22 | 23 | func usage() { 24 | fmt.Println("Available commands:") 25 | for name, _ := range commands { 26 | fmt.Printf(" - %s\n", name) 27 | } 28 | } 29 | 30 | func main() { 31 | var command string 32 | args := os.Args 33 | 34 | if len(args) > 1 { 35 | command = args[1] 36 | } 37 | 38 | if commands[command] != nil { 39 | c := commands[command] 40 | c.Execute(args[2:len(args)]) 41 | } else { 42 | fmt.Printf("Unknown command `%s`\n", command) 43 | usage() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/not-found/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "github.com/pilu/traffic" 6 | ) 7 | 8 | func rootHandler(w traffic.ResponseWriter, r *traffic.Request) { 9 | w.WriteText("Hello World\n") 10 | } 11 | 12 | func pageHandler(w traffic.ResponseWriter, r *traffic.Request) { 13 | w.WriteText("Category ID: %s\n", r.Param("category_id")) 14 | w.WriteText("Page ID: %s\n", r.Param("id")) 15 | } 16 | 17 | func customNotFoundHandler(w traffic.ResponseWriter, r *traffic.Request) { 18 | w.WriteHeader(http.StatusNotFound) 19 | w.WriteText("Page not found: %s\n", r.URL.Path) 20 | } 21 | 22 | func main() { 23 | router := traffic.New() 24 | 25 | // Routes 26 | router.Get("/", rootHandler) 27 | router.Get("/categories/:category_id/pages/:id", pageHandler) 28 | 29 | // Custom not found handler 30 | router.NotFoundHandler = customNotFoundHandler 31 | 32 | router.Run() 33 | } 34 | -------------------------------------------------------------------------------- /static_middleware.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type StaticMiddleware struct { 8 | publicPath string 9 | } 10 | 11 | func (middleware *StaticMiddleware) ServeHTTP(w ResponseWriter, r *Request, next NextMiddlewareFunc) { 12 | callNext := func() { 13 | if nextMiddleware := next(); nextMiddleware != nil { 14 | nextMiddleware.ServeHTTP(w, r, next) 15 | } 16 | } 17 | 18 | dir := http.Dir(middleware.publicPath) 19 | path := r.URL.Path 20 | 21 | file, err := dir.Open(path) 22 | if err != nil { 23 | callNext() 24 | return 25 | } 26 | defer file.Close() 27 | 28 | if info, err := file.Stat(); err == nil && !info.IsDir() { 29 | w.Header().Del("Content-Type") 30 | http.ServeContent(w, r.Request, path, info.ModTime(), file) 31 | return 32 | } 33 | 34 | callNext() 35 | } 36 | 37 | func NewStaticMiddleware(publicPath string) *StaticMiddleware { 38 | middleware := &StaticMiddleware{ 39 | publicPath: publicPath, 40 | } 41 | 42 | return middleware 43 | } 44 | -------------------------------------------------------------------------------- /router_middleware.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type RouterMiddleware struct { 8 | router *Router 9 | } 10 | 11 | func (routerMiddleware *RouterMiddleware) ServeHTTP(w ResponseWriter, r *Request, next NextMiddlewareFunc) { 12 | for _, route := range routerMiddleware.router.routes[HttpMethod(r.Method)] { 13 | values, ok := route.Match(r.URL.Path) 14 | if ok { 15 | newValues := r.URL.Query() 16 | for k, v := range values { 17 | newValues[k] = v 18 | } 19 | 20 | r.URL.RawQuery = newValues.Encode() 21 | 22 | handlers := append(routerMiddleware.router.beforeFilters, route.beforeFilters...) 23 | handlers = append(handlers, route.Handlers...) 24 | 25 | for _, handler := range handlers { 26 | handler(w, r) 27 | if w.Written() { 28 | break 29 | } 30 | } 31 | 32 | if w.StatusCode() == http.StatusNotFound && !w.BodyWritten() { 33 | routerMiddleware.router.handleNotFound(w, r) 34 | } 35 | 36 | return 37 | } 38 | } 39 | 40 | routerMiddleware.router.handleNotFound(w, r) 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Andrea Franz (http://gravityblast.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/pilu/traffic" 5 | ) 6 | 7 | func rootHandler(w traffic.ResponseWriter, r *traffic.Request) { 8 | traffic.Logger().Print("Hello") 9 | w.WriteText("Hello World\n") 10 | } 11 | 12 | func jsonTestHandler(w traffic.ResponseWriter, r *traffic.Request) { 13 | data := map[string]string{ 14 | "foo": "bar", 15 | } 16 | w.WriteJSON(data) 17 | } 18 | 19 | func xmlTestHandler(w traffic.ResponseWriter, r *traffic.Request) { 20 | type Person struct { 21 | FirstName string `xml:"name>first"` 22 | LastName string `xml:"name>last"` 23 | } 24 | 25 | w.WriteXML(&Person{ 26 | FirstName: "foo", 27 | LastName: "bar", 28 | }) 29 | } 30 | 31 | func pageHandler(w traffic.ResponseWriter, r *traffic.Request) { 32 | w.WriteText("Category ID: %s\n", r.Param("category_id")) 33 | w.WriteText("Page ID: %s\n", r.Param("id")) 34 | } 35 | 36 | func main() { 37 | router := traffic.New() 38 | // Routes 39 | router.Get("/", rootHandler) 40 | router.Get("/json", jsonTestHandler) 41 | router.Get("/xml", xmlTestHandler) 42 | router.Get("/categories/:category_id/pages/:id", pageHandler) 43 | 44 | router.Run() 45 | } 46 | -------------------------------------------------------------------------------- /examples/middleware/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/pilu/traffic" 5 | ) 6 | 7 | type PingMiddleware struct {} 8 | 9 | // If the request path is "/ping", it writes PONG in the response and returns without calling the next middleware 10 | // Otherwise it sets the variable "PING" with PONG as value and calls the next middleware. 11 | // The next middleware and the final handler can get that variable with: 12 | // w.GetVar("ping") 13 | func (c *PingMiddleware) ServeHTTP(w traffic.ResponseWriter, r *traffic.Request, next traffic.NextMiddlewareFunc) { 14 | if r.URL.Path == "/ping" { 15 | w.WriteText("pong\n") 16 | 17 | return 18 | } 19 | 20 | if nextMiddleware := next(); nextMiddleware != nil { 21 | w.SetVar("ping", "pong") 22 | nextMiddleware.ServeHTTP(w, r, next) 23 | } 24 | 25 | return 26 | } 27 | 28 | func root(w traffic.ResponseWriter, r *traffic.Request) { 29 | w.WriteText("Router var foo: %v.\n", w.GetVar("foo")) 30 | w.WriteText("Middleware var ping: %v\n", w.GetVar("ping")) 31 | } 32 | 33 | func main() { 34 | t := traffic.New() 35 | // Add PingMiddleware 36 | t.Use(&PingMiddleware{}) 37 | // Set router var "foo" 38 | t.SetVar("foo", "bar") 39 | // Add root handler 40 | t.Get("/", root) 41 | 42 | t.Run() 43 | } 44 | -------------------------------------------------------------------------------- /route.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "net/url" 5 | "regexp" 6 | ) 7 | 8 | type Route struct { 9 | Path string 10 | PathRegexp *regexp.Regexp 11 | IsStatic bool 12 | Handlers []HttpHandleFunc 13 | beforeFilters []HttpHandleFunc 14 | } 15 | 16 | func (route *Route) AddBeforeFilter(beforeFilters ...HttpHandleFunc) *Route { 17 | route.beforeFilters = append(route.beforeFilters, beforeFilters...) 18 | 19 | return route 20 | } 21 | 22 | func NewRoute(path string, handlers ...HttpHandleFunc) *Route { 23 | regexp, isStatic := pathToRegexp(path) 24 | route := &Route{ 25 | Path: path, 26 | PathRegexp: regexp, 27 | IsStatic: isStatic, 28 | Handlers: handlers, 29 | } 30 | 31 | return route 32 | } 33 | 34 | func (route Route) Match(path string) (url.Values, bool) { 35 | values := make(url.Values) 36 | 37 | if route.IsStatic { 38 | return values, route.Path == path 39 | } 40 | 41 | matches := route.PathRegexp.FindAllStringSubmatch(path, -1) 42 | if matches != nil { 43 | names := route.PathRegexp.SubexpNames() 44 | for i := 1; i < len(names); i++ { 45 | name := names[i] 46 | value := matches[0][i] 47 | if len(name) > 0 && len(value) > 0 { 48 | values.Set(name, value) 49 | } 50 | } 51 | 52 | return values, true 53 | } 54 | 55 | return values, false 56 | } 57 | -------------------------------------------------------------------------------- /examples/before-filter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | "net/http" 7 | "github.com/pilu/traffic" 8 | ) 9 | 10 | func rootHandler(w traffic.ResponseWriter, r *traffic.Request) { 11 | w.WriteText("Hello World\n") 12 | } 13 | 14 | func privatePageHandler(w traffic.ResponseWriter, r *traffic.Request) { 15 | w.WriteText("Hello Private Page\n") 16 | } 17 | 18 | func pageHandler(w traffic.ResponseWriter, r *traffic.Request) { 19 | w.WriteText("Category ID: %s\n", r.Param("category_id")) 20 | w.WriteText("Page ID: %s\n", r.Param("id")) 21 | } 22 | 23 | func checkApiKey(w traffic.ResponseWriter, r *traffic.Request) { 24 | if r.Param("api_key") != "foo" { 25 | w.WriteHeader(http.StatusUnauthorized) 26 | w.WriteText("Not authorized\n") 27 | } 28 | } 29 | 30 | func checkPrivatePageApiKey(w traffic.ResponseWriter, r *traffic.Request) { 31 | if r.Param("private_api_key") != "bar" { 32 | w.WriteHeader(http.StatusUnauthorized) 33 | w.WriteText("Not authorized\n") 34 | } 35 | } 36 | 37 | func addAppNameHeader(w traffic.ResponseWriter, r *traffic.Request) { 38 | w.Header().Add("X-APP-NAME", "My App") 39 | } 40 | 41 | func addTimeHeader(w traffic.ResponseWriter, r *traffic.Request) { 42 | t := fmt.Sprintf("%s", time.Now()) 43 | w.Header().Add("X-APP-TIME", t) 44 | } 45 | 46 | func main() { 47 | router := traffic.New() 48 | 49 | // Routes 50 | router.Get("/", rootHandler) 51 | router.Get("/categories/:category_id/pages/:id", pageHandler) 52 | // "/private" has one more before filter that checks for a second api key (private_api_key) 53 | router.Get("/private", privatePageHandler). 54 | AddBeforeFilter(checkPrivatePageApiKey) 55 | 56 | // Executed before all handlers 57 | router.AddBeforeFilter(checkApiKey, addAppNameHeader, addTimeHeader) 58 | 59 | router.Run() 60 | } 61 | -------------------------------------------------------------------------------- /router_middleware_test.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "fmt" 5 | assert "github.com/pilu/miniassert" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | type fakeNotFoundHandlerContainer struct { 12 | callsCount int 13 | } 14 | 15 | func (f *fakeNotFoundHandlerContainer) Handler(w ResponseWriter, r *Request) { 16 | f.callsCount++ 17 | } 18 | 19 | func newTestRequest(method, path string) (ResponseWriter, *httptest.ResponseRecorder, *Request) { 20 | r, _ := http.NewRequest(method, path, nil) 21 | request := newRequest(r) 22 | recorder := httptest.NewRecorder() 23 | 24 | env := make(map[string]interface{}) 25 | responseWriter := newResponseWriter(recorder, &env) 26 | 27 | return responseWriter, recorder, request 28 | } 29 | 30 | func newTestRouterMiddleware() *RouterMiddleware { 31 | router := &Router{} 32 | router.routes = make(map[HttpMethod][]*Route) 33 | router.beforeFilters = make([]HttpHandleFunc, 0) 34 | router.middlewares = make([]Middleware, 0) 35 | routerMiddleware := &RouterMiddleware{router} 36 | 37 | return routerMiddleware 38 | } 39 | 40 | func TestRouterMiddleware_NotFound(t *testing.T) { 41 | routerMiddleware := newTestRouterMiddleware() 42 | fakeNotFound := new(fakeNotFoundHandlerContainer) 43 | routerMiddleware.router.NotFoundHandler = fakeNotFound.Handler 44 | 45 | responseWriter, recorder, request := newTestRequest("GET", "/") 46 | routerMiddleware.ServeHTTP(responseWriter, request, func() Middleware { return nil }) 47 | 48 | // checks that the router middleware calls router.handleNotFound 49 | assert.Equal(t, 1, fakeNotFound.callsCount) 50 | assert.Equal(t, "", string(recorder.Body.Bytes())) 51 | } 52 | 53 | func TestRouterMiddleware_Found(t *testing.T) { 54 | routerMiddleware := newTestRouterMiddleware() 55 | responseWriter, recorder, request := newTestRequest("GET", "/") 56 | 57 | testRootHandler := func(w ResponseWriter, r *Request) { 58 | fmt.Fprint(w, "Hello World") 59 | } 60 | 61 | routerMiddleware.router.Get("/", testRootHandler) 62 | 63 | routerMiddleware.ServeHTTP(responseWriter, request, func() Middleware { return nil }) 64 | assert.Equal(t, 200, recorder.Code) 65 | assert.Equal(t, "Hello World", string(recorder.Body.Bytes())) 66 | } 67 | -------------------------------------------------------------------------------- /response_writer_test.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | assert "github.com/pilu/miniassert" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func buildTestResponseWriter(globalEnv *map[string]interface{}) (*responseWriter, *httptest.ResponseRecorder) { 11 | recorder := httptest.NewRecorder() 12 | rw := newResponseWriter( 13 | recorder, 14 | globalEnv, 15 | ) 16 | return rw, recorder 17 | } 18 | 19 | func newTestResponseWriter(globalEnv *map[string]interface{}) *responseWriter { 20 | rw, _ := buildTestResponseWriter(globalEnv) 21 | 22 | return rw 23 | } 24 | 25 | func TestResponseWriter(t *testing.T) { 26 | routerEnv := make(map[string]interface{}) 27 | rw := newTestResponseWriter(&routerEnv) 28 | assert.Equal(t, http.StatusOK, rw.statusCode) 29 | assert.Equal(t, 0, len(rw.env)) 30 | assert.Equal(t, &routerEnv, rw.routerEnv) 31 | assert.Equal(t, 0, len(rw.beforeWriteHandlers)) 32 | } 33 | 34 | func TestResponseWriter_SetVar(t *testing.T) { 35 | globalEnv := make(map[string]interface{}) 36 | rw := newTestResponseWriter(&globalEnv) 37 | rw.SetVar("foo", "bar") 38 | assert.Equal(t, "bar", rw.env["foo"]) 39 | } 40 | 41 | func TestResponseWriter_GetVar(t *testing.T) { 42 | resetGlobalEnv() 43 | 44 | routerEnv := map[string]interface{}{} 45 | rw := newTestResponseWriter(&routerEnv) 46 | 47 | env["global-foo"] = "global-bar" 48 | assert.Equal(t, "global-bar", rw.GetVar("global-foo")) 49 | 50 | routerEnv["global-foo"] = "router-bar" 51 | assert.Equal(t, "router-bar", rw.GetVar("global-foo")) 52 | 53 | rw.env["global-foo"] = "local-bar" 54 | assert.Equal(t, "local-bar", rw.GetVar("global-foo")) 55 | 56 | resetGlobalEnv() 57 | } 58 | 59 | func TestResponseWriter_Write(t *testing.T) { 60 | routerEnv := make(map[string]interface{}) 61 | rw, recorder := buildTestResponseWriter(&routerEnv) 62 | 63 | assert.False(t, rw.Written()) 64 | 65 | rw.Write([]byte("foo")) 66 | 67 | assert.True(t, rw.Written()) 68 | assert.Equal(t, []byte("foo"), recorder.Body.Bytes()) 69 | } 70 | 71 | func TestResponseWriter_WriteHeader(t *testing.T) { 72 | routerEnv := make(map[string]interface{}) 73 | rw, recorder := buildTestResponseWriter(&routerEnv) 74 | 75 | assert.False(t, rw.Written()) 76 | 77 | rw.WriteHeader(http.StatusUnauthorized) 78 | 79 | assert.True(t, rw.Written()) 80 | assert.Equal(t, http.StatusUnauthorized, rw.StatusCode()) 81 | assert.Equal(t, http.StatusUnauthorized, recorder.Code) 82 | } 83 | -------------------------------------------------------------------------------- /response_writer.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | type ResponseWriter interface { 11 | http.ResponseWriter 12 | SetVar(string, interface{}) 13 | GetVar(string) interface{} 14 | StatusCode() int 15 | Written() bool 16 | BodyWritten() bool 17 | Render(string, ...interface{}) 18 | WriteJSON(data interface{}) 19 | WriteXML(data interface{}) 20 | WriteText(string, ...interface{}) 21 | } 22 | 23 | type responseWriter struct { 24 | http.ResponseWriter 25 | written bool 26 | bodyWritten bool 27 | statusCode int 28 | env map[string]interface{} 29 | routerEnv *map[string]interface{} 30 | beforeWriteHandlers []func() 31 | } 32 | 33 | func (w *responseWriter) Write(data []byte) (n int, err error) { 34 | w.written = true 35 | w.bodyWritten = true 36 | 37 | return w.ResponseWriter.Write(data) 38 | } 39 | 40 | func (w *responseWriter) WriteHeader(statusCode int) { 41 | w.statusCode = statusCode 42 | w.ResponseWriter.WriteHeader(statusCode) 43 | w.written = true 44 | } 45 | 46 | func (w *responseWriter) StatusCode() int { 47 | return w.statusCode 48 | } 49 | 50 | func (w *responseWriter) SetVar(key string, value interface{}) { 51 | w.env[key] = value 52 | } 53 | 54 | func (w *responseWriter) Written() bool { 55 | return w.written 56 | } 57 | 58 | func (w *responseWriter) BodyWritten() bool { 59 | return w.bodyWritten 60 | } 61 | 62 | func (w *responseWriter) GetVar(key string) interface{} { 63 | // local env 64 | value := w.env[key] 65 | if value != nil { 66 | return value 67 | } 68 | 69 | // router env 70 | value = (*w.routerEnv)[key] 71 | if value != nil { 72 | return value 73 | } 74 | 75 | // global env 76 | return GetVar(key) 77 | } 78 | 79 | func (w *responseWriter) Render(templateName string, data ...interface{}) { 80 | RenderTemplate(w, templateName, data...) 81 | } 82 | 83 | func (w *responseWriter) WriteJSON(data interface{}) { 84 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 85 | json.NewEncoder(w).Encode(data) 86 | } 87 | 88 | func (w *responseWriter) WriteXML(data interface{}) { 89 | w.Header().Set("Content-Type", "application/xml; charset=utf-8") 90 | xml.NewEncoder(w).Encode(data) 91 | } 92 | 93 | func (w *responseWriter) WriteText(textFormat string, data ...interface{}) { 94 | fmt.Fprintf(w, textFormat, data...) 95 | } 96 | 97 | func newResponseWriter(w http.ResponseWriter, routerEnv *map[string]interface{}) *responseWriter { 98 | rw := &responseWriter{ 99 | ResponseWriter: w, 100 | statusCode: http.StatusOK, 101 | env: make(map[string]interface{}), 102 | routerEnv: routerEnv, 103 | beforeWriteHandlers: make([]func(), 0), 104 | } 105 | 106 | return rw 107 | } 108 | -------------------------------------------------------------------------------- /traffic/command_new.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "go/build" 7 | "io/ioutil" 8 | "path/filepath" 9 | ) 10 | 11 | const NewProjectTemplateFolder = "new_project_templates" 12 | 13 | type CommandNew struct {} 14 | 15 | func (c CommandNew) Execute(args []string) { 16 | if len(args) > 0 { 17 | c.generateProject(args[0]) 18 | } else { 19 | c.Usage() 20 | } 21 | } 22 | 23 | func (c CommandNew) Usage() { 24 | fmt.Println("Usage:") 25 | fmt.Println(" traffic new APP_PATH") 26 | } 27 | 28 | func (c CommandNew) generateProject(projectName string) { 29 | currentPath, err := os.Getwd() 30 | if err != nil { 31 | logger.Fatal(err) 32 | } 33 | 34 | projectPath := filepath.Join(currentPath, projectName) 35 | 36 | if _, err := os.Stat(projectPath); err == nil { 37 | logger.Fatalf("Project `%s` already exists (%s).", projectName, projectPath) 38 | } 39 | 40 | logger.Printf("Creating project folder `%s`", projectPath) 41 | c.createProjectFolder(projectPath) 42 | c.createFiles(projectPath) 43 | } 44 | 45 | func (c CommandNew) createProjectFolder(path string) { 46 | os.Mkdir(path, 0755) 47 | } 48 | 49 | func (c CommandNew) createFiles(targetPath string) { 50 | pkg, err := build.Import("github.com/pilu/traffic/traffic", "", build.FindOnly) 51 | if err != nil { 52 | logger.Fatal(err) 53 | } 54 | 55 | sourcePath := filepath.Join(pkg.Dir, NewProjectTemplateFolder) 56 | filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error { 57 | if err != nil { 58 | logger.Fatal(err) 59 | } 60 | 61 | relPath, err := filepath.Rel(sourcePath, path) 62 | if relPath == "." { 63 | return nil 64 | } 65 | 66 | if err != nil { 67 | logger.Fatal(err) 68 | } 69 | 70 | newPath := filepath.Join(targetPath, relPath) 71 | c.createFile(path, newPath) 72 | 73 | return nil 74 | }) 75 | } 76 | 77 | func (c CommandNew) createFile(sourcePath, targetPath string) { 78 | sourceStat, err := os.Stat(sourcePath) 79 | if err != nil { 80 | logger.Fatal(err) 81 | } 82 | 83 | targetStat, err := os.Stat(targetPath) 84 | if err == nil { 85 | fileType := "File" 86 | if targetStat.IsDir() { 87 | fileType = "Directory" 88 | } 89 | logger.Printf("%s already exists `%s`", fileType, targetPath) 90 | return 91 | } 92 | 93 | if sourceStat.IsDir() { 94 | logger.Printf("Creating folder `%s`", targetPath) 95 | os.Mkdir(targetPath, 0755) 96 | return 97 | } 98 | 99 | content, err := ioutil.ReadFile(sourcePath) 100 | if err != nil { 101 | logger.Fatal(err) 102 | } 103 | 104 | err = ioutil.WriteFile(targetPath, content, 0644) 105 | if err != nil { 106 | logger.Fatal(err) 107 | } 108 | 109 | logger.Printf("File created `%s`", targetPath) 110 | } 111 | -------------------------------------------------------------------------------- /route_test.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | assert "github.com/pilu/miniassert" 5 | "net/url" 6 | "reflect" 7 | "regexp" 8 | "testing" 9 | ) 10 | 11 | func httpHandlerExample(r ResponseWriter, req *Request) {} 12 | func httpHandlerExample2(r ResponseWriter, req *Request) {} 13 | 14 | func TestNewRoute(t *testing.T) { 15 | path := "/categories/:category_id/posts/:id" 16 | route := NewRoute(path, httpHandlerExample, httpHandlerExample2) 17 | assert.Type(t, "*traffic.Route", route) 18 | assert.Equal(t, path, route.Path) 19 | assert.Equal(t, 2, len(route.Handlers)) 20 | 21 | expectedPathRegexp := regexp.MustCompile(`\A/categories/(?P[^/#?]+)/posts/(?P[^/#?]+)\z`) 22 | assert.Equal(t, expectedPathRegexp, route.PathRegexp) 23 | } 24 | 25 | func TestRoute_Match(t *testing.T) { 26 | tests := [][]string{ 27 | { 28 | "/", 29 | "/", 30 | "", 31 | }, 32 | { 33 | "/:foo/?", 34 | "/bar", 35 | "foo=bar", 36 | }, 37 | { 38 | "/:foo/?", 39 | "/bar/", // with trailing slash 40 | "foo=bar", 41 | }, 42 | { 43 | "/categories/:category_id/posts/:id", 44 | "/categories/foo/posts/bar", 45 | "category_id=foo&id=bar", 46 | }, 47 | { 48 | "/pages/:page_path*", 49 | "/pages/foo/bar/baz", 50 | "page_path=foo%2Fbar%2Fbaz", 51 | }, 52 | { 53 | "/pages/:page.html", 54 | "/pages/foo.html", 55 | "page=foo", 56 | }, 57 | } 58 | 59 | for _, opts := range tests { 60 | routePath := opts[0] 61 | requestPath := opts[1] 62 | expectedQuery := opts[2] 63 | 64 | route := NewRoute(routePath, httpHandlerExample) 65 | values, ok := route.Match(requestPath) 66 | assert.True(t, ok) 67 | assert.Equal(t, expectedQuery, values.Encode()) 68 | } 69 | 70 | route := NewRoute("/foo", httpHandlerExample) 71 | values, ok := route.Match("/bar") 72 | assert.False(t, ok) 73 | assert.Equal(t, values, make(url.Values)) 74 | } 75 | 76 | func TestRoute_Match_WithOptionalSegments(t *testing.T) { 77 | routePath := "((/sites/:site_id)?/categories/:category_id)?/posts/:id" 78 | tests := [][]string{ 79 | { 80 | "/sites/foo/categories/bar/posts/baz", 81 | "category_id=bar&id=baz&site_id=foo", 82 | }, 83 | { 84 | "/categories/bar/posts/baz", 85 | "category_id=bar&id=baz", 86 | }, 87 | { 88 | "/posts/baz", 89 | "id=baz", 90 | }, 91 | } 92 | route := NewRoute(routePath, httpHandlerExample) 93 | for _, opts := range tests { 94 | requestPath := opts[0] 95 | expectedQuery := opts[1] 96 | values, ok := route.Match(requestPath) 97 | assert.True(t, ok) 98 | assert.Equal(t, expectedQuery, values.Encode()) 99 | } 100 | } 101 | 102 | func TestRoute_AddBeforeFilterToRoute(t *testing.T) { 103 | route := NewRoute("/", httpHandlerExample) 104 | assert.Equal(t, 0, len(route.beforeFilters)) 105 | filterA := HttpHandleFunc(func(w ResponseWriter, r *Request) {}) 106 | filterB := HttpHandleFunc(func(w ResponseWriter, r *Request) {}) 107 | 108 | route.AddBeforeFilter(filterA) 109 | assert.Equal(t, 1, len(route.beforeFilters)) 110 | route.AddBeforeFilter(filterB) 111 | assert.Equal(t, 2, len(route.beforeFilters)) 112 | 113 | assert.Equal(t, reflect.ValueOf(filterA), reflect.ValueOf(route.beforeFilters[0])) 114 | assert.Equal(t, reflect.ValueOf(filterB), reflect.ValueOf(route.beforeFilters[1])) 115 | } 116 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | const TemplateExtension = ".tpl" 12 | 13 | type RenderFunc func(w ResponseWriter, template string, data interface{}) 14 | 15 | var templateManager *TemplateManager 16 | 17 | type TemplateManager struct { 18 | viewsBasePath string 19 | templates *template.Template 20 | renderFunc RenderFunc 21 | templatesReadError error 22 | templatesParseError error 23 | } 24 | 25 | var templateFuncs = make(map[string]interface{}) 26 | 27 | func TemplateFuncs(funcs map[string]interface{}) { 28 | for name, fn := range funcs { 29 | TemplateFunc(name, fn) 30 | } 31 | } 32 | 33 | func TemplateFunc(name string, fn interface{}) { 34 | templateFuncs[name] = fn 35 | } 36 | 37 | func (t *TemplateManager) loadTemplates() { 38 | t.templates = template.New("templates") 39 | t.templates.Funcs(templateFuncs) 40 | 41 | if t.viewsBasePath == "" { 42 | panic("views base path is blank") 43 | } 44 | filepath.Walk(t.viewsBasePath, t.WalkFunc) 45 | } 46 | 47 | func (t *TemplateManager) WalkFunc(path string, info os.FileInfo, err error) error { 48 | if err != nil { 49 | t.templatesReadError = err 50 | t.renderFunc = t.RenderTemplateErrors 51 | 52 | return err 53 | } 54 | 55 | if extension := filepath.Ext(path); !info.IsDir() && extension == TemplateExtension { 56 | relativePath, err := filepath.Rel(t.viewsBasePath, path) 57 | if err != nil { 58 | t.templatesReadError = err 59 | t.renderFunc = t.RenderTemplateErrors 60 | 61 | return err 62 | } 63 | 64 | templateName := relativePath[0:(len(relativePath) - len(extension))] 65 | t.addTemplate(templateName, path) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func (t *TemplateManager) addTemplate(name, path string) { 72 | fileContent, err := ioutil.ReadFile(path) 73 | if err != nil { 74 | t.templatesReadError = err 75 | t.renderFunc = t.RenderTemplateErrors 76 | 77 | return 78 | } 79 | 80 | templateContent := fmt.Sprintf(`{{ define "%s" }}%s{{ end }}`, name, fileContent) 81 | tmpl, err := t.templates.Parse(templateContent) 82 | if tmpl == nil && err != nil { 83 | t.templatesParseError = err 84 | t.renderFunc = t.RenderTemplateErrors 85 | } 86 | } 87 | 88 | func (t *TemplateManager) templatesPath() string { 89 | if path := GetVar("views"); path != nil { 90 | return path.(string) 91 | } 92 | 93 | return DefaultViewsPath 94 | } 95 | 96 | func (t *TemplateManager) Render(w ResponseWriter, template string, data interface{}) { 97 | err := t.templates.ExecuteTemplate(w, template, data) 98 | if err != nil { 99 | panic(err) 100 | } 101 | } 102 | 103 | func (t *TemplateManager) RenderTemplateErrors(w ResponseWriter, template string, data interface{}) { 104 | if t.templatesReadError != nil { 105 | panic(t.templatesReadError) 106 | } 107 | 108 | if t.templatesParseError != nil { 109 | panic(t.templatesParseError) 110 | } 111 | } 112 | 113 | func newTemplateManager() *TemplateManager { 114 | t := &TemplateManager{} 115 | t.viewsBasePath = ViewsPath() 116 | t.renderFunc = t.Render 117 | t.loadTemplates() 118 | 119 | return t 120 | } 121 | 122 | func initTemplateManager() { 123 | templateManager = newTemplateManager() 124 | } 125 | 126 | func RenderTemplate(w ResponseWriter, templateName string, data ...interface{}) { 127 | if len(data) == 0 { 128 | templateManager.renderFunc(w, templateName, nil) 129 | } else { 130 | templateManager.renderFunc(w, templateName, data[0]) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | type ILogger interface { 13 | Print(...interface{}) 14 | Printf(string, ...interface{}) 15 | } 16 | 17 | const ( 18 | EnvDevelopment = "development" 19 | DefaultViewsPath = "views" 20 | DefaultPublicPath = "public" 21 | DefaultConfigFile = "traffic.conf" 22 | DefaultPort = 3000 23 | DefaultHost = "127.0.0.1" 24 | ) 25 | 26 | var ( 27 | env map[string]interface{} 28 | logger ILogger 29 | ) 30 | 31 | func Logger() ILogger { 32 | return logger 33 | } 34 | 35 | func SetLogger(customLogger ILogger) { 36 | logger = customLogger 37 | } 38 | 39 | func SetVar(key string, value interface{}) { 40 | env[key] = value 41 | } 42 | 43 | func GetVar(key string) interface{} { 44 | if value := env[key]; value != nil { 45 | return value 46 | } 47 | 48 | envKey := fmt.Sprintf("TRAFFIC_%s", strings.ToUpper(key)) 49 | if value := os.Getenv(envKey); value != "" { 50 | return value 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func getStringVar(key string) string { 57 | value := GetVar(key) 58 | if s, ok := value.(string); ok { 59 | return s 60 | } 61 | 62 | return "" 63 | } 64 | 65 | func pathToRegexp(routePath string) (*regexp.Regexp, bool) { 66 | var ( 67 | re *regexp.Regexp 68 | isStatic bool 69 | ) 70 | 71 | regexpString := routePath 72 | 73 | isStaticRegexp := regexp.MustCompile(`[\(\)\?\<\>:]`) 74 | if !isStaticRegexp.MatchString(routePath) { 75 | isStatic = true 76 | } 77 | 78 | // Dots 79 | re = regexp.MustCompile(`([^\\])\.`) 80 | regexpString = re.ReplaceAllStringFunc(regexpString, func(m string) string { 81 | return fmt.Sprintf(`%s\.`, string(m[0])) 82 | }) 83 | 84 | // Wildcard names 85 | re = regexp.MustCompile(`:[^/#?()\.\\]+\*`) 86 | regexpString = re.ReplaceAllStringFunc(regexpString, func(m string) string { 87 | return fmt.Sprintf("(?P<%s>.+)", m[1:len(m)-1]) 88 | }) 89 | 90 | re = regexp.MustCompile(`:[^/#?()\.\\]+`) 91 | regexpString = re.ReplaceAllStringFunc(regexpString, func(m string) string { 92 | return fmt.Sprintf(`(?P<%s>[^/#?]+)`, m[1:len(m)]) 93 | }) 94 | 95 | s := fmt.Sprintf(`\A%s\z`, regexpString) 96 | 97 | return regexp.MustCompile(s), isStatic 98 | } 99 | 100 | func getStringVarWithDefault(key, defaultValue string) string { 101 | value := getStringVar(key) 102 | if value == "" { 103 | return defaultValue 104 | } 105 | 106 | return value 107 | } 108 | 109 | func Env() string { 110 | return getStringVarWithDefault("env", EnvDevelopment) 111 | } 112 | 113 | func RootPath() string { 114 | return getStringVarWithDefault("root", ".") 115 | } 116 | 117 | func ViewsPath() string { 118 | viewsPath := getStringVarWithDefault("views", DefaultViewsPath) 119 | if path.IsAbs(viewsPath) { 120 | return viewsPath 121 | } 122 | 123 | return path.Join(RootPath(), viewsPath) 124 | } 125 | 126 | func ConfigFilePath() string { 127 | filePath := getStringVarWithDefault("config_file", DefaultConfigFile) 128 | if path.IsAbs(filePath) { 129 | return filePath 130 | } 131 | 132 | return path.Join(RootPath(), filePath) 133 | } 134 | 135 | func PublicPath() string { 136 | publicPath := getStringVarWithDefault("public", DefaultPublicPath) 137 | if path.IsAbs(publicPath) { 138 | return publicPath 139 | } 140 | 141 | return path.Join(RootPath(), publicPath) 142 | } 143 | 144 | func SetPort(port int) { 145 | SetVar("port", port) 146 | } 147 | 148 | func Port() int { 149 | port := GetVar("port") 150 | 151 | if port == nil { 152 | return DefaultPort 153 | } 154 | 155 | if i, ok := port.(int); ok { 156 | return i 157 | } 158 | 159 | if s, ok := port.(string); ok { 160 | i, err := strconv.Atoi(s) 161 | if err != nil { 162 | return DefaultPort 163 | } 164 | 165 | return i 166 | } 167 | 168 | return DefaultPort 169 | } 170 | 171 | func Host() string { 172 | host := getStringVarWithDefault("host", DefaultHost) 173 | 174 | return host 175 | } 176 | 177 | func SetHost(host string) { 178 | SetVar("host", host) 179 | } 180 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | assert "github.com/pilu/miniassert" 8 | ) 9 | 10 | func resetGlobalEnv() { 11 | env = make(map[string]interface{}) 12 | } 13 | 14 | func TestSetVar(t *testing.T) { 15 | resetGlobalEnv() 16 | SetVar("foo", "bar") 17 | assert.Equal(t, "bar", env["foo"]) 18 | resetGlobalEnv() 19 | } 20 | 21 | func TestGetVar(t *testing.T) { 22 | resetGlobalEnv() 23 | env["foo-2"] = "bar-2" 24 | assert.Equal(t, "bar-2", GetVar("foo-2")) 25 | 26 | assert.Nil(t, GetVar("os_foo")) 27 | os.Setenv("TRAFFIC_OS_FOO", "bar") 28 | assert.Equal(t, "bar", GetVar("os_foo")) 29 | 30 | resetGlobalEnv() 31 | } 32 | 33 | func TestGetStringVar(t *testing.T) { 34 | resetGlobalEnv() 35 | 36 | assert.Equal(t, "", getStringVar("foo")) 37 | SetVar("foo", "bar") 38 | assert.Equal(t, "bar", getStringVar("foo")) 39 | 40 | resetGlobalEnv() 41 | } 42 | 43 | func TestPathToRegexp(t *testing.T) { 44 | tests := [][]interface{}{ 45 | { 46 | `/`, 47 | `\A/\z`, 48 | true, 49 | }, 50 | { 51 | `/foo/bar`, 52 | `\A/foo/bar\z`, 53 | true, 54 | }, 55 | { 56 | `/foo/bar`, 57 | `\A/foo/bar\z`, 58 | true, 59 | }, 60 | { 61 | `/:foo/bar/:baz`, 62 | `\A/(?P[^/#?]+)/bar/(?P[^/#?]+)\z`, 63 | false, 64 | }, 65 | { 66 | `(/categories/:category_id)?/posts/:id`, 67 | `\A(/categories/(?P[^/#?]+))?/posts/(?P[^/#?]+)\z`, 68 | false, 69 | }, 70 | } 71 | 72 | for _, pair := range tests { 73 | path := pair[0].(string) 74 | expectedRegexp := pair[1].(string) 75 | expectedIsStatic := pair[2].(bool) 76 | 77 | r, isStatic := pathToRegexp(path) 78 | assert.Equal(t, expectedRegexp, r.String()) 79 | assert.Equal(t, expectedIsStatic, isStatic) 80 | } 81 | } 82 | 83 | func TestEnv(t *testing.T) { 84 | resetGlobalEnv() 85 | assert.Equal(t, EnvDevelopment, Env()) 86 | 87 | SetVar("env", "production") 88 | assert.Equal(t, "production", Env()) 89 | resetGlobalEnv() 90 | } 91 | 92 | func TestRootPath(t *testing.T) { 93 | resetGlobalEnv() 94 | assert.Equal(t, ".", RootPath()) 95 | 96 | SetVar("root", "foo") 97 | assert.Equal(t, "foo", RootPath()) 98 | resetGlobalEnv() 99 | } 100 | 101 | func TestViewsPath(t *testing.T) { 102 | resetGlobalEnv() 103 | assert.Equal(t, DefaultViewsPath, ViewsPath()) 104 | 105 | SetVar("views", "foo/views") 106 | assert.Equal(t, "foo/views", ViewsPath()) 107 | 108 | SetVar("root", "/root") 109 | assert.Equal(t, "/root/foo/views", ViewsPath()) 110 | 111 | SetVar("views", "/absolute/path") 112 | assert.Equal(t, "/absolute/path", ViewsPath()) 113 | 114 | resetGlobalEnv() 115 | } 116 | 117 | func TestConfigFilePath(t *testing.T) { 118 | resetGlobalEnv() 119 | 120 | assert.Equal(t, DefaultConfigFile, ConfigFilePath()) 121 | 122 | SetVar("config_file", "foo.conf") 123 | assert.Equal(t, "foo.conf", ConfigFilePath()) 124 | 125 | SetVar("root", "/root") 126 | assert.Equal(t, "/root/foo.conf", ConfigFilePath()) 127 | 128 | SetVar("config_file", "/absolute/foo.conf") 129 | assert.Equal(t, "/absolute/foo.conf", ConfigFilePath()) 130 | 131 | resetGlobalEnv() 132 | } 133 | 134 | func TestPublicPath(t *testing.T) { 135 | resetGlobalEnv() 136 | 137 | assert.Equal(t, DefaultPublicPath, PublicPath()) 138 | 139 | SetVar("public", "custom-public") 140 | assert.Equal(t, "custom-public", PublicPath()) 141 | 142 | SetVar("root", "/root") 143 | assert.Equal(t, "/root/custom-public", PublicPath()) 144 | 145 | SetVar("public", "/absolute/public") 146 | assert.Equal(t, "/absolute/public", PublicPath()) 147 | 148 | resetGlobalEnv() 149 | } 150 | 151 | func TestPort(t *testing.T) { 152 | resetGlobalEnv() 153 | 154 | assert.Equal(t, DefaultPort, Port()) 155 | 156 | SetVar("port", 80) 157 | assert.Equal(t, 80, Port()) 158 | 159 | SetVar("port", "80") 160 | assert.Equal(t, 80, Port()) 161 | 162 | SetVar("port", "") 163 | assert.Equal(t, DefaultPort, Port()) 164 | 165 | resetGlobalEnv() 166 | } 167 | 168 | func TestSetPort(t *testing.T) { 169 | resetGlobalEnv() 170 | 171 | assert.Nil(t, GetVar("port")) 172 | 173 | SetPort(80) 174 | assert.Equal(t, 80, GetVar("port")) 175 | 176 | resetGlobalEnv() 177 | } 178 | 179 | func TestHost(t *testing.T) { 180 | resetGlobalEnv() 181 | 182 | assert.Equal(t, DefaultHost, Host()) 183 | 184 | SetVar("host", "1.2.3.4") 185 | assert.Equal(t, "1.2.3.4", Host()) 186 | 187 | resetGlobalEnv() 188 | } 189 | 190 | func TestSetHost(t *testing.T) { 191 | resetGlobalEnv() 192 | 193 | assert.Nil(t, GetVar("host")) 194 | 195 | SetHost("1.2.3.4") 196 | assert.Equal(t, "1.2.3.4", GetVar("host")) 197 | 198 | resetGlobalEnv() 199 | } 200 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "fmt" 5 | assert "github.com/pilu/miniassert" 6 | "net/http" 7 | "net/http/httptest" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestNew(t *testing.T) { 13 | router := New() 14 | assert.Type(t, "*traffic.Router", router) 15 | assert.Type(t, "map[traffic.HttpMethod][]*traffic.Route", router.routes) 16 | 17 | emptyMap := make(map[HttpMethod][]*Route) 18 | assert.Equal(t, emptyMap, router.routes) 19 | 20 | assert.Equal(t, 4, len(router.middlewares)) 21 | assert.Type(t, "*traffic.ShowErrorsMiddleware", router.middlewares[0]) 22 | assert.Type(t, "*traffic.LoggerMiddleware", router.middlewares[1]) 23 | assert.Type(t, "*traffic.StaticMiddleware", router.middlewares[2]) 24 | assert.Type(t, "*traffic.RouterMiddleware", router.middlewares[3]) 25 | 26 | assert.Equal(t, 0, len(router.env)) 27 | } 28 | 29 | func TestRouter_Add(t *testing.T) { 30 | router := New() 31 | assert.Equal(t, 0, len(router.routes["GET"])) 32 | route := router.Add(HttpMethod("GET"), "/", httpHandlerExample) 33 | assert.Type(t, "*traffic.Route", route) 34 | assert.Equal(t, 1, len(router.routes["GET"])) 35 | } 36 | 37 | func TestRouter_Get(t *testing.T) { 38 | router := New() 39 | assert.Equal(t, 0, len(router.routes["GET"])) 40 | assert.Equal(t, 0, len(router.routes["HEAD"])) 41 | route := router.Get("/", httpHandlerExample) 42 | assert.Type(t, "*traffic.Route", route) 43 | assert.Equal(t, 1, len(router.routes["GET"])) 44 | assert.Equal(t, 1, len(router.routes["HEAD"])) 45 | assert.Equal(t, router.routes["GET"][0], router.routes["HEAD"][0]) 46 | } 47 | 48 | func TestRoute_Post(t *testing.T) { 49 | router := New() 50 | assert.Equal(t, 0, len(router.routes["POST"])) 51 | route := router.Post("/", httpHandlerExample) 52 | assert.Type(t, "*traffic.Route", route) 53 | assert.Equal(t, 1, len(router.routes["POST"])) 54 | } 55 | 56 | func TestRouter_Delete(t *testing.T) { 57 | router := New() 58 | assert.Equal(t, 0, len(router.routes["DELETE"])) 59 | route := router.Delete("/", httpHandlerExample) 60 | assert.Type(t, "*traffic.Route", route) 61 | assert.Equal(t, 1, len(router.routes["DELETE"])) 62 | } 63 | 64 | func TestRouter_Put(t *testing.T) { 65 | router := New() 66 | assert.Equal(t, 0, len(router.routes["PUT"])) 67 | route := router.Put("/", httpHandlerExample) 68 | assert.Type(t, "*traffic.Route", route) 69 | assert.Equal(t, 1, len(router.routes["PUT"])) 70 | } 71 | 72 | func TestRouter_Patch(t *testing.T) { 73 | router := New() 74 | assert.Equal(t, 0, len(router.routes["PATCH"])) 75 | route := router.Patch("/", httpHandlerExample) 76 | assert.Type(t, "*traffic.Route", route) 77 | assert.Equal(t, 1, len(router.routes["PATCH"])) 78 | } 79 | 80 | func TestRouter_AddBeforeFilter(t *testing.T) { 81 | router := New() 82 | assert.Equal(t, 0, len(router.beforeFilters)) 83 | 84 | filterA := HttpHandleFunc(func(w ResponseWriter, r *Request) {}) 85 | filterB := HttpHandleFunc(func(w ResponseWriter, r *Request) {}) 86 | 87 | router.AddBeforeFilter(filterA) 88 | assert.Equal(t, 1, len(router.beforeFilters)) 89 | router.AddBeforeFilter(filterB) 90 | assert.Equal(t, 2, len(router.beforeFilters)) 91 | 92 | assert.Equal(t, reflect.ValueOf(filterA), reflect.ValueOf(router.beforeFilters[0])) 93 | assert.Equal(t, reflect.ValueOf(filterB), reflect.ValueOf(router.beforeFilters[1])) 94 | } 95 | 96 | func TestRouter_SetVar(t *testing.T) { 97 | defer resetGlobalEnv() 98 | router := New() 99 | router.SetVar("foo", "bar") 100 | assert.Equal(t, "bar", router.env["foo"]) 101 | } 102 | 103 | func TestRouter_GetVar(t *testing.T) { 104 | defer resetGlobalEnv() 105 | router := New() 106 | env["global-foo"] = "global-foo" 107 | assert.Equal(t, "global-foo", router.GetVar("global-foo")) 108 | router.env["global-foo"] = "router-foo" 109 | assert.Equal(t, "router-foo", router.GetVar("global-foo")) 110 | } 111 | 112 | func TestRouter_ServeHTTP_NotFound(t *testing.T) { 113 | defer resetGlobalEnv() 114 | SetVar("env", "test") 115 | 116 | router := New() 117 | request, _ := http.NewRequest("GET", "/", nil) 118 | recorder := httptest.NewRecorder() 119 | router.ServeHTTP(recorder, request) 120 | 121 | assert.Equal(t, "404 page not found", string(recorder.Body.Bytes())) 122 | 123 | router.NotFoundHandler = func(w ResponseWriter, r *Request) { 124 | fmt.Fprint(w, "custom 404 messages") 125 | } 126 | 127 | request, _ = http.NewRequest("GET", "/", nil) 128 | recorder = httptest.NewRecorder() 129 | router.ServeHTTP(recorder, request) 130 | 131 | assert.Equal(t, "custom 404 messages", string(recorder.Body.Bytes())) 132 | 133 | // test-1 handler writes header but does't write in the body. 134 | router.Get("/test-1", func(w ResponseWriter, r *Request) { 135 | w.WriteHeader(http.StatusNotFound) 136 | }) 137 | 138 | request, _ = http.NewRequest("GET", "/test-1", nil) 139 | recorder = httptest.NewRecorder() 140 | router.ServeHTTP(recorder, request) 141 | assert.Equal(t, "custom 404 messages", string(recorder.Body.Bytes())) 142 | 143 | // test-2 handler sends a 404 but write in the body too, 144 | // so the custom not found handler should not be called. 145 | router.Get("/test-2", func(w ResponseWriter, r *Request) { 146 | w.WriteHeader(http.StatusNotFound) 147 | fmt.Fprint(w, "test 2 body") 148 | }) 149 | 150 | request, _ = http.NewRequest("GET", "/test-2", nil) 151 | recorder = httptest.NewRecorder() 152 | router.ServeHTTP(recorder, request) 153 | assert.Equal(t, "test 2 body", string(recorder.Body.Bytes())) 154 | } 155 | -------------------------------------------------------------------------------- /show_errors_middleware.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "bufio" 5 | "html/template" 6 | "os" 7 | "runtime" 8 | "strings" 9 | ) 10 | 11 | type ShowErrorsMiddleware struct{} 12 | 13 | func (middleware ShowErrorsMiddleware) readErrorFileLines(filePath string, errorLine int) map[int]string { 14 | lines := make(map[int]string) 15 | 16 | file, err := os.Open(filePath) 17 | if err != nil { 18 | return lines 19 | } 20 | 21 | defer file.Close() 22 | 23 | reader := bufio.NewReader(file) 24 | currentLine := 0 25 | for { 26 | line, err := reader.ReadString('\n') 27 | if err != nil || currentLine > errorLine+5 { 28 | break 29 | } 30 | 31 | currentLine++ 32 | 33 | if currentLine >= errorLine-5 { 34 | lines[currentLine] = strings.Replace(line, "\n", "", -1) 35 | } 36 | } 37 | 38 | return lines 39 | } 40 | 41 | func (middleware ShowErrorsMiddleware) RenderError(w ResponseWriter, r *Request, err interface{}, stack []byte) { 42 | _, filePath, line, _ := runtime.Caller(5) 43 | 44 | data := map[string]interface{}{ 45 | "Error": err, 46 | "Stack": string(stack), 47 | "Params": r.URL.Query(), 48 | "Method": r.Method, 49 | "FilePath": filePath, 50 | "Line": line, 51 | "Lines": middleware.readErrorFileLines(filePath, line), 52 | } 53 | 54 | w.Header().Set("Content-Type", "text/html") 55 | tpl := template.Must(template.New("ErrorPage").Parse(panicPageTpl)) 56 | tpl.Execute(w, data) 57 | } 58 | 59 | func (middleware ShowErrorsMiddleware) ServeHTTP(w ResponseWriter, r *Request, next NextMiddlewareFunc) { 60 | defer func() { 61 | if err := recover(); err != nil { 62 | const size = 4096 63 | stack := make([]byte, size) 64 | stack = stack[:runtime.Stack(stack, false)] 65 | 66 | middleware.RenderError(w, r, err, stack) 67 | } 68 | }() 69 | 70 | if nextMiddleware := next(); nextMiddleware != nil { 71 | nextMiddleware.ServeHTTP(w, r, next) 72 | } 73 | } 74 | 75 | const panicPageTpl string = ` 76 | 77 | 78 | Traffic Panic 79 | 80 | 147 | 148 | 149 |
150 |
151 |

Error

152 |
153 |
154 | 155 |
156 |

{{ .Error }}

157 |
158 | 159 |
160 |

161 | In {{ .FilePath }}:{{ .Line }}

162 |

163 | 164 | 165 | 166 | 169 | 172 | 173 |
167 |
{{ range $lineNumber, $line :=  .Lines }}{{ $lineNumber }}{{ end }}
168 |
170 |
{{ range $lineNumber, $line :=  .Lines }}{{ $line }}
{{ end }}
171 |
174 |

Stack

175 |
{{ .Stack }}
176 |

Request

177 |

Method: {{ .Method }}

178 |

Paramenters:

179 |
    180 | {{ range $key, $value := .Params }} 181 |
  • {{ $key }}: {{ $value }}
  • 182 | {{ end }} 183 |
184 |
185 | 186 | 187 | ` 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Traffic 2 | 3 | [![Build Status](https://travis-ci.org/pilu/traffic.png?branch=master)](https://travis-ci.org/pilu/traffic) 4 | 5 | Package traffic - a Sinatra inspired regexp/pattern mux for [Go](http://golang.org/ "The Go programming language"). 6 | 7 | ## Installation 8 | 9 | go get github.com/pilu/traffic 10 | 11 | ## Features 12 | 13 | * [Regexp routing](https://github.com/pilu/traffic/blob/master/examples/simple/main.go) 14 | * [Before Filters](https://github.com/pilu/traffic/blob/master/examples/before-filter/main.go) 15 | * [Custom not found handler](https://github.com/pilu/traffic/blob/master/examples/not-found/main.go) 16 | * [Middlewares](https://github.com/pilu/traffic/blob/master/examples/middleware/main.go) 17 | * Examples: [Airbrake Middleware](https://github.com/pilu/traffic-airbrake), [Chrome Logger Middleware](https://github.com/pilu/traffic-chromelogger) 18 | * [Templates/Views](https://github.com/pilu/traffic/tree/master/examples/templates) 19 | * [Easy Configuration](https://github.com/pilu/traffic/tree/master/examples/configuration) 20 | 21 | ## Development Features 22 | 23 | * [Shows errors and stacktrace in browser](https://github.com/pilu/traffic/tree/master/examples/show-errors) 24 | * [Serves static files](https://github.com/pilu/traffic/tree/master/examples/static-files) 25 | * Project Generator 26 | 27 | `development` is the default environment. The above middlewares are loaded only in `development`. 28 | 29 | If you want to run your application in `production`, export `TRAFFIC_ENV` with `production` as value. 30 | 31 | ```bash 32 | TRAFFIC_ENV=production your-executable-name 33 | ``` 34 | 35 | ## Installation 36 | 37 | Dowload the `Traffic` code: 38 | 39 | ```bash 40 | go get github.com/pilu/traffic 41 | ``` 42 | 43 | Build the command line tool: 44 | 45 | ```bash 46 | go get github.com/pilu/traffic/traffic 47 | ``` 48 | 49 | Create a new project: 50 | ```bash 51 | traffic new hello 52 | ``` 53 | 54 | Run your project: 55 | ```bash 56 | cd hello 57 | go build && ./hello 58 | ``` 59 | 60 | You can use [Fresh](https://github.com/pilu/fresh) if you want to build and restart your application every time you create/modify/delete a file. 61 | 62 | ## Example: 63 | The following code is a simple example, the documentation in still in development. 64 | For more examples check the `examples` folder. 65 | 66 | ```go 67 | package main 68 | 69 | import ( 70 | "net/http" 71 | "github.com/pilu/traffic" 72 | "fmt" 73 | ) 74 | 75 | func rootHandler(w traffic.ResponseWriter, r *traffic.Request) { 76 | fmt.Fprint(w, "Hello World\n") 77 | } 78 | 79 | func pageHandler(w traffic.ResponseWriter, r *traffic.Request) { 80 | params := r.URL.Query() 81 | fmt.Fprintf(w, "Category ID: %s\n", params.Get("category_id")) 82 | fmt.Fprintf(w, "Page ID: %s\n", params.Get("id")) 83 | } 84 | 85 | func main() { 86 | router := traffic.New() 87 | 88 | // Routes 89 | router.Get("/", rootHandler) 90 | router.Get("/categories/:category_id/pages/:id", pageHandler) 91 | 92 | router.Run() 93 | } 94 | ``` 95 | 96 | ## Before Filters 97 | 98 | You can also add "before filters" to all your routes or just to some of them: 99 | 100 | ```go 101 | router := traffic.New() 102 | 103 | // Executed before all handlers 104 | router.AddBeforeFilter(checkApiKey). 105 | AddBeforeFilter(addAppNameHeader). 106 | AddBeforeFilter(addTimeHeader) 107 | 108 | // Routes 109 | router.Get("/", rootHandler) 110 | router.Get("/categories/:category_id/pages/:id", pageHandler) 111 | 112 | // "/private" has one more before filter that checks for a second api key (private_api_key) 113 | router.Get("/private", privatePageHandler). 114 | AddBeforeFilter(checkPrivatePageApiKey) 115 | ``` 116 | 117 | Complete example: 118 | 119 | ```go 120 | func rootHandler(w traffic.ResponseWriter, r *traffic.Request) { 121 | fmt.Fprint(w, "Hello World\n") 122 | } 123 | 124 | func privatePageHandler(w traffic.ResponseWriter, r *traffic.Request) { 125 | fmt.Fprint(w, "Hello Private Page\n") 126 | } 127 | 128 | func pageHandler(w traffic.ResponseWriter, r *traffic.Request) { 129 | params := r.URL.Query() 130 | fmt.Fprintf(w, "Category ID: %s\n", params.Get("category_id")) 131 | fmt.Fprintf(w, "Page ID: %s\n", params.Get("id")) 132 | } 133 | 134 | func checkApiKey(w traffic.ResponseWriter, r *traffic.Request) { 135 | params := r.URL.Query() 136 | if params.Get("api_key") != "foo" { 137 | w.WriteHeader(http.StatusUnauthorized) 138 | } 139 | } 140 | 141 | func checkPrivatePageApiKey(w traffic.ResponseWriter, r *traffic.Request) { 142 | params := r.URL.Query() 143 | if params.Get("private_api_key") != "bar" { 144 | w.WriteHeader(http.StatusUnauthorized) 145 | } 146 | } 147 | 148 | func addAppNameHeader(w traffic.ResponseWriter, r *traffic.Request) { 149 | w.Header().Add("X-APP-NAME", "My App") 150 | } 151 | 152 | func addTimeHeader(w traffic.ResponseWriter, r *traffic.Request) { 153 | t := fmt.Sprintf("%s", time.Now()) 154 | w.Header().Add("X-APP-TIME", t) 155 | } 156 | 157 | func main() { 158 | router := traffic.New() 159 | 160 | // Routes 161 | router.Get("/", rootHandler) 162 | router.Get("/categories/:category_id/pages/:id", pageHandler) 163 | // "/private" has one more before filter that checks for a second api key (private_api_key) 164 | router.Get("/private", privatePageHandler). 165 | AddBeforeFilter(checkPrivatePageApiKey) 166 | 167 | // Executed before all handlers 168 | router.AddBeforeFilter(checkApiKey). 169 | AddBeforeFilter(addAppNameHeader). 170 | AddBeforeFilter(addTimeHeader) 171 | 172 | router.Run() 173 | } 174 | ``` 175 | 176 | ## Author 177 | 178 | * [Andrea Franz](http://gravityblast.com) 179 | 180 | ## More 181 | 182 | * Code: 183 | * Mailing List: 184 | * Chat: 185 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package traffic 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "runtime" 9 | 10 | "github.com/pilu/config" 11 | ) 12 | 13 | type HttpMethod string 14 | 15 | type ErrorHandlerFunc func(ResponseWriter, *Request, interface{}) 16 | 17 | type NextMiddlewareFunc func() Middleware 18 | 19 | type HttpHandleFunc func(ResponseWriter, *Request) 20 | 21 | type Middleware interface { 22 | ServeHTTP(ResponseWriter, *Request, NextMiddlewareFunc) 23 | } 24 | 25 | type Router struct { 26 | NotFoundHandler HttpHandleFunc 27 | ErrorHandler ErrorHandlerFunc 28 | routes map[HttpMethod][]*Route 29 | beforeFilters []HttpHandleFunc 30 | middlewares []Middleware 31 | env map[string]interface{} 32 | } 33 | 34 | func (router Router) MiddlewareEnumerator() func() Middleware { 35 | index := 0 36 | next := func() Middleware { 37 | if len(router.middlewares) > index { 38 | nextMiddleware := router.middlewares[index] 39 | index++ 40 | return nextMiddleware 41 | } 42 | 43 | return nil 44 | } 45 | 46 | return next 47 | } 48 | 49 | func (router *Router) Add(method HttpMethod, path string, handlers ...HttpHandleFunc) *Route { 50 | route := NewRoute(path, handlers...) 51 | router.addRoute(method, route) 52 | 53 | return route 54 | } 55 | 56 | func (router *Router) addRoute(method HttpMethod, route *Route) { 57 | router.routes[method] = append(router.routes[method], route) 58 | } 59 | 60 | func (router *Router) Get(path string, handlers ...HttpHandleFunc) *Route { 61 | route := router.Add(HttpMethod("GET"), path, handlers...) 62 | router.addRoute(HttpMethod("HEAD"), route) 63 | 64 | return route 65 | } 66 | 67 | func (router *Router) Post(path string, handlers ...HttpHandleFunc) *Route { 68 | return router.Add(HttpMethod("POST"), path, handlers...) 69 | } 70 | 71 | func (router *Router) Delete(path string, handlers ...HttpHandleFunc) *Route { 72 | return router.Add(HttpMethod("DELETE"), path, handlers...) 73 | } 74 | 75 | func (router *Router) Put(path string, handlers ...HttpHandleFunc) *Route { 76 | return router.Add(HttpMethod("PUT"), path, handlers...) 77 | } 78 | 79 | func (router *Router) Patch(path string, handlers ...HttpHandleFunc) *Route { 80 | return router.Add(HttpMethod("PATCH"), path, handlers...) 81 | } 82 | 83 | func (router *Router) AddBeforeFilter(beforeFilters ...HttpHandleFunc) *Router { 84 | router.beforeFilters = append(router.beforeFilters, beforeFilters...) 85 | 86 | return router 87 | } 88 | 89 | func (router *Router) handleNotFound(w ResponseWriter, r *Request) { 90 | if router.NotFoundHandler != nil { 91 | router.NotFoundHandler(w, r) 92 | } else { 93 | w.WriteHeader(http.StatusNotFound) 94 | fmt.Fprint(w, "404 page not found") 95 | } 96 | } 97 | 98 | func (router *Router) handlePanic(w ResponseWriter, r *Request, err interface{}) { 99 | if router.ErrorHandler != nil { 100 | w.WriteHeader(http.StatusInternalServerError) 101 | router.ErrorHandler(w, r, err) 102 | } else { 103 | http.Error(w, "Something went wrong", http.StatusInternalServerError) 104 | } 105 | 106 | const size = 4096 107 | stack := make([]byte, size) 108 | stack = stack[:runtime.Stack(stack, false)] 109 | 110 | logger.Printf("%v\n", err) 111 | logger.Printf("%s\n", string(stack)) 112 | } 113 | 114 | func (router *Router) ServeHTTP(httpResponseWriter http.ResponseWriter, httpRequest *http.Request) { 115 | w := newResponseWriter(httpResponseWriter, &router.env) 116 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 117 | 118 | r := newRequest(httpRequest) 119 | 120 | defer func() { 121 | if recovered := recover(); recovered != nil { 122 | router.handlePanic(w, r, recovered) 123 | } 124 | }() 125 | 126 | nextMiddlewareFunc := router.MiddlewareEnumerator() 127 | if nextMiddleware := nextMiddlewareFunc(); nextMiddleware != nil { 128 | nextMiddleware.ServeHTTP(w, r, nextMiddlewareFunc) 129 | } 130 | } 131 | 132 | func (router *Router) Use(middleware Middleware) { 133 | router.middlewares = append([]Middleware{middleware}, router.middlewares...) 134 | } 135 | 136 | func (router *Router) SetVar(key string, value interface{}) { 137 | router.env[key] = value 138 | } 139 | 140 | func (router *Router) GetVar(key string) interface{} { 141 | value := router.env[key] 142 | if value != nil { 143 | return value 144 | } 145 | 146 | return GetVar(key) 147 | } 148 | 149 | func addDevelopmentMiddlewares(router *Router) { 150 | // Static middleware 151 | router.Use(NewStaticMiddleware(PublicPath())) 152 | 153 | // Logger middleware 154 | loggerMiddleware := &LoggerMiddleware{ 155 | router: router, 156 | } 157 | router.Use(loggerMiddleware) 158 | 159 | // ShowErrors middleware 160 | router.Use(&ShowErrorsMiddleware{}) 161 | } 162 | 163 | func (router *Router) Run() { 164 | address := fmt.Sprintf("%s:%d", Host(), Port()) 165 | Logger().Printf("Starting in %s on %s", Env(), address) 166 | err := http.ListenAndServe(address, router) 167 | if err != nil { 168 | log.Fatal(err) 169 | } 170 | } 171 | 172 | func (router *Router) RunSSL(certFile, keyFile string) { 173 | address := fmt.Sprintf("%s:%d", Host(), Port()) 174 | Logger().Printf("Starting in %s on %s", Env(), address) 175 | err := http.ListenAndServeTLS(address, certFile, keyFile, router) 176 | if err != nil { 177 | log.Fatal(err) 178 | } 179 | } 180 | 181 | func loadConfigurationsFromFile(path, env string) { 182 | mainSectionName := "main" 183 | sections, err := config.ParseFile(path, mainSectionName) 184 | if err != nil { 185 | panic(err) 186 | } 187 | 188 | for section, options := range sections { 189 | if section == mainSectionName || section == env { 190 | for key, value := range options { 191 | SetVar(key, value) 192 | } 193 | } 194 | } 195 | } 196 | 197 | func init() { 198 | env = make(map[string]interface{}) 199 | SetLogger(log.New(os.Stdout, "", log.LstdFlags)) 200 | 201 | // configuration 202 | configFile := ConfigFilePath() 203 | if _, err := os.Stat(configFile); err == nil { 204 | loadConfigurationsFromFile(configFile, Env()) 205 | } 206 | } 207 | 208 | func New() *Router { 209 | router := &Router{ 210 | routes: make(map[HttpMethod][]*Route), 211 | beforeFilters: make([]HttpHandleFunc, 0), 212 | middlewares: make([]Middleware, 0), 213 | env: make(map[string]interface{}), 214 | } 215 | 216 | routerMiddleware := &RouterMiddleware{router} 217 | router.Use(routerMiddleware) 218 | 219 | // Environment 220 | env := Env() 221 | 222 | // Add useful middlewares for development 223 | if env == EnvDevelopment { 224 | addDevelopmentMiddlewares(router) 225 | } 226 | 227 | initTemplateManager() 228 | 229 | return router 230 | } 231 | --------------------------------------------------------------------------------