Permalink
Please sign in to comment.
Showing
with
3,179 additions
and 0 deletions.
- +56 −0 .gitignore
- +12 −0 .travis.yml
- +23 −0 LICENSE
- +461 −0 README.md
- +364 −0 bench_test.go
- +75 −0 context.go
- +62 −0 examples/basic-auth/basic-auth.go
- +39 −0 examples/custom-logger/custom-logger.go
- +24 −0 examples/hello/hello.go
- +56 −0 examples/modular-hello/modular-hello.go
- +52 −0 examples/module/module.go
- +31 −0 examples/resource/resource.go
- +48 −0 lion.go
- +56 −0 matcher.go
- +331 −0 middlewares.go
- +33 −0 module.go
- +55 −0 module_test.go
- +97 −0 resource.go
- +85 −0 resource_test.go
- +105 −0 response_writer.go
- +343 −0 router.go
- +347 −0 router_test.go
- +324 −0 tree.go
- +100 −0 utils.go
56
.gitignore
12
.travis.yml
| @@ -0,0 +1,12 @@ | ||
| +language: go | ||
| +go: | ||
| + - 1.2 | ||
| + - 1.3 | ||
| + - 1.4 | ||
| + - 1.5 | ||
| + - 1.6 | ||
| + - tip | ||
| +install: | ||
| + - go get -t -v ./... | ||
| +script: | ||
| + - go test -v ./... |
23
LICENSE
| @@ -0,0 +1,23 @@ | ||
| +The MIT License (MIT) | ||
| + | ||
| +Copyright (c) 2015 Salim Alami | ||
| + | ||
| +Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| +of this software and associated documentation files (the "Software"), to deal | ||
| +in the Software without restriction, including without limitation the rights | ||
| +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| +copies of the Software, and to permit persons to whom the Software is | ||
| +furnished to do so, subject to the following conditions: | ||
| + | ||
| +The above copyright notice and this permission notice shall be included in all | ||
| +copies or substantial portions of the Software. | ||
| + | ||
| +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| +SOFTWARE. | ||
| + | ||
| +Status API Training Shop Blog About |
| @@ -0,0 +1,461 @@ | ||
| +# Lion [](https://travis-ci.org/celrenheit/lion) [](https://godoc.org/github.com/celrenheit/lion) [](LICENSE) | ||
| + | ||
| +Lion is a fast http(2) router for Go with support for middlewares for building modern scalable modular REST APIs. | ||
| + | ||
| +## Features | ||
| + | ||
| +* **Zero allocations**: Lion generates zero garbage. | ||
| +* **Context-Aware**: Lion uses [net/Context](https://golang.org/x/net/context) for storing route params and sharing variables between middlewares and HTTP handlers. Which [_could_](https://github.com/golang/go/issues/14660) be integrated in the [standard library](https://github.com/golang/go/issues/13021) for Go 1.7 in 2016. | ||
| +* **Modular**: You can define your own modules to easily build a scalable architecture | ||
| +* **REST friendly**: You can define | ||
| + | ||
| +<!-- START doctoc generated TOC please keep comment here to allow auto update --> | ||
| +<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> | ||
| +## Table of contents | ||
| + | ||
| + - [Install/Update](#installupdate) | ||
| + - [Hello World](#hello-world) | ||
| + - [Getting started with modules and resources](#getting-started-with-modules-and-resources) | ||
| + - [Handlers](#handlers) | ||
| + - [Using HandlerFuncs](#using-handlerfuncs) | ||
| + - [Using native http.Handler using *lion.Wrap()*](#using-native-httphandler-using-lionwrap) | ||
| + - [Using native http.Handler using *lion.WrapFunc()*](#using-native-httphandler-using-lionwrapfunc) | ||
| + - [Middlewares](#middlewares) | ||
| + - [Resources](#resources) | ||
| + - [Examples](#examples) | ||
| + - [Using GET, POST, PUT, DELETE http methods](#using-get-post-put-delete-http-methods) | ||
| + - [Using middlewares](#using-middlewares) | ||
| + - [Group routes by a base path](#group-routes-by-a-base-path) | ||
| + - [Mouting a router into a base path](#mouting-a-router-into-a-base-path) | ||
| + - [Default middlewares](#default-middlewares) | ||
| +- [Custom Middlewares](#custom-middlewares) | ||
| + - [Custom Logger example](#custom-logger-example) | ||
| +- [Todo](#todo) | ||
| +- [Credits](#credits) | ||
| + | ||
| +<!-- END doctoc generated TOC please keep comment here to allow auto update --> | ||
| + | ||
| + | ||
| +## Install/Update | ||
| + | ||
| +```shell | ||
| +$ go get -u github.com/celrenheit/lion | ||
| +``` | ||
| + | ||
| + | ||
| +## Hello World | ||
| + | ||
| +```go | ||
| +package main | ||
| + | ||
| +import ( | ||
| + "fmt" | ||
| + "net/http" | ||
| + | ||
| + "github.com/celrenheit/lion" | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +func Home(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "Home\n") | ||
| +} | ||
| + | ||
| +func Hello(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "Hello "+c.Value("name").(string)) | ||
| +} | ||
| + | ||
| +func main() { | ||
| + l := lion.Classic() | ||
| + l.GetFunc("/", Home) | ||
| + l.GetFunc("/hello/:name", Hello) | ||
| + l.Run() | ||
| +} | ||
| +``` | ||
| + | ||
| +Try it your self by running the following command from the current directory: | ||
| + | ||
| +```shell | ||
| +$ go run examples/hello/hello.go | ||
| +``` | ||
| + | ||
| +## Getting started with modules and resources | ||
| + | ||
| +We are going to build a sample products listing REST api (without database handling to keep it simple): | ||
| + | ||
| +```go | ||
| + | ||
| +func main() { | ||
| + l := lion.Classic() | ||
| + api := l.Group("/api") | ||
| + api.Module(Products{}) | ||
| + l.Run() | ||
| +} | ||
| + | ||
| +// Products module is accessible at url: /api/products | ||
| +// It handles getting a list of products or creating a new product | ||
| +type Products struct{} | ||
| + | ||
| +func (p Products) Base() string { | ||
| + return "/products" | ||
| +} | ||
| + | ||
| +func (p Products) Get(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "Fetching all products") | ||
| +} | ||
| + | ||
| +func (p Products) Post(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "Creating a new product") | ||
| +} | ||
| + | ||
| +func (p Products) Routes(r *lion.Router) { | ||
| + // Defining a resource for getting, editing and deleting a single product | ||
| + r.Resource("/:id", OneProduct{}) | ||
| +} | ||
| + | ||
| +// OneProduct resource is accessible at url: /api/products/:id | ||
| +// It handles getting, editing and deleting a single product | ||
| +type OneProduct struct{} | ||
| + | ||
| +func (p OneProduct) Get(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + id := lion.Param(c, "id") | ||
| + fmt.Fprintf(w, "Getting product: %s", id) | ||
| +} | ||
| + | ||
| +func (p OneProduct) Put(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + id := lion.Param(c, "id") | ||
| + fmt.Fprintf(w, "Updating article: %s", id) | ||
| +} | ||
| + | ||
| +func (p OneProduct) Delete(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + id := lion.Param(c, "id") | ||
| + fmt.Fprintf(w, "Deleting article: %s", id) | ||
| +} | ||
| +``` | ||
| + | ||
| +Try it. Run: | ||
| +```shell | ||
| +$ go run examples/modular-hello/modular-hello.go | ||
| +``` | ||
| + | ||
| +Open your web browser to [http://localhost:3000/api/products](http://localhost:3000/api/products) or [http://localhost:3000/api/products/123](http://localhost:3000/api/products/123). You should see "_Fetching all products_" or "_Getting product: 123_". | ||
| + | ||
| +## Handlers | ||
| + | ||
| +Handlers should implement the Handler interface: | ||
| +### Handler interface | ||
| +```go | ||
| +type Handler interface { | ||
| + ServeHTTPC(context.Context, http.ResponseWriter, *http.Request) | ||
| +} | ||
| +``` | ||
| + | ||
| +### Using HandlerFuncs | ||
| + | ||
| +HandlerFuncs shoud have this function signature: | ||
| + | ||
| +```go | ||
| +func Get(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + | ||
| +} | ||
| + | ||
| +l.GetFunc("/somepath", Get) | ||
| +``` | ||
| + | ||
| +### Using native http.Handler using *lion.Wrap()* | ||
| + | ||
| +*Note*: using native http handler you cannot access url params. | ||
| + | ||
| +```go | ||
| +type nativehandler struct {} | ||
| + | ||
| +func (_ nativehandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
| + | ||
| +} | ||
| + | ||
| +func main() { | ||
| + l := lion.New() | ||
| + l.Get("/somepath", lion.Wrap(nativehandler{})) | ||
| +} | ||
| +``` | ||
| + | ||
| +### Using native http.Handler using *lion.WrapFunc()* | ||
| + | ||
| + | ||
| +```go | ||
| +func getHandlerFunc(w http.ResponseWriter, r *http.Request) { | ||
| + | ||
| +} | ||
| + | ||
| +func main() { | ||
| + l := lion.New() | ||
| + l.Get("/somepath", lion.WrapFunc(getHandlerFunc)) | ||
| +} | ||
| +``` | ||
| + | ||
| +## Middlewares | ||
| + | ||
| +Middlewares should implement the Middleware interface: | ||
| + | ||
| +```go | ||
| +type Middleware interface { | ||
| + ServeNext(Handler) Handler | ||
| +} | ||
| +``` | ||
| + | ||
| +The ServeNext function accepts a Handler and returns a Handler. | ||
| + | ||
| +You can also use MiddlewareFuncs. For example: | ||
| + | ||
| +```go | ||
| +func middlewareFunc(next Handler) Handler { | ||
| + return next | ||
| +} | ||
| +``` | ||
| + | ||
| +You can also use Negroni middlewares by registering them using: | ||
| + | ||
| +```go | ||
| +l := lion.New() | ||
| +l.UseNegroni(negroni.NewRecovery()) | ||
| +l.Run() | ||
| +``` | ||
| + | ||
| +## Resources | ||
| + | ||
| +You can define a resource to represent a REST, CRUD api resource. | ||
| +You define global middlewares using Uses() method. For defining custom middlewares for each http method, you have to create a function which name is composed of the http method suffixed by "Middlewares". For example, if you want to define middlewares for the Get method you will have to create a method called: **GetMiddlewares()**. | ||
| + | ||
| +A resource is defined by the following methods. **Everything is optional**: | ||
| +```go | ||
| + | ||
| +// Global middlewares for the resource (Optional) | ||
| +Uses() Middlewares | ||
| + | ||
| +// Middlewares for the http methods (Optional) | ||
| +GetMiddlewares() Middlewares | ||
| +PostMiddlewares() Middlewares | ||
| +PutMiddlewares() Middlewares | ||
| +DeleteMiddlewares() Middlewares | ||
| + | ||
| + | ||
| +// HandlerFuncs for each HTTP Methods (Optional) | ||
| +Get(c context.Context, w http.ResponseWriter, r *http.Request) | ||
| +Post(c context.Context, w http.ResponseWriter, r *http.Request) | ||
| +Put(c context.Context, w http.ResponseWriter, r *http.Request) | ||
| +Delete(c context.Context, w http.ResponseWriter, r *http.Request) | ||
| +``` | ||
| + | ||
| +**_Example_**: | ||
| + | ||
| +```go | ||
| +package main | ||
| + | ||
| +type todolist struct{} | ||
| + | ||
| +func (t todolist) Uses() lion.Middlewares { | ||
| + return lion.Middlewares{lion.NewLogger()} | ||
| +} | ||
| + | ||
| +func (t todolist) Get(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "getting todos") | ||
| +} | ||
| + | ||
| +func main() { | ||
| + l := lion.New() | ||
| + l.Resource("/todos", todolist{}) | ||
| + l.Run() | ||
| +} | ||
| +``` | ||
| + | ||
| + | ||
| +## Modules | ||
| + | ||
| +Modules are a way to modularize an api which can then define submodules, subresources and custom routes. | ||
| +A module is defined by the following methods: | ||
| + | ||
| +```go | ||
| +// Required: Base url pattern of the module | ||
| +Base() string | ||
| + | ||
| +// Routes accepts a Router instance. This method is used to define the routes of this module. | ||
| +// Each routes defined are relative to the Base() url pattern | ||
| +Routes(*Router) | ||
| + | ||
| +// Optional: Requires named middlewares. Refer to Named Middlewares section | ||
| +Requires() []string | ||
| +``` | ||
| + | ||
| +```go | ||
| +package main | ||
| + | ||
| +type api struct{} | ||
| + | ||
| +// Required: Base url | ||
| +func (t api) Base() string { return "/api" } | ||
| + | ||
| +// Required: Here you can declare sub-resources, submodules and custom routes. | ||
| +func (t api) Routes(r *lion.Router) { | ||
| + r.Module(v1{}) | ||
| + r.Get("/custom", t.CustomRoute) | ||
| +} | ||
| + | ||
| +// Optional: Attach Get method to this Module. | ||
| +// ====> A Module is also a Resource. | ||
| +func (t api) Get(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "This also a resource accessible at http://localhost:3000/api") | ||
| +} | ||
| + | ||
| +// Optional: Defining custom routes | ||
| +func (t api) CustomRoute(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "This a custom route for this module http://localhost:3000/api/") | ||
| +} | ||
| + | ||
| +func main() { | ||
| + l := lion.New() | ||
| + // Registering the module | ||
| + l.Module(api{}) | ||
| + l.Run() | ||
| +} | ||
| +``` | ||
| + | ||
| +## Examples | ||
| + | ||
| +### Using GET, POST, PUT, DELETE http methods | ||
| + | ||
| +```go | ||
| +l := lion.Classic() | ||
| + | ||
| +// Using Handlers | ||
| +l.Get("/get", get) | ||
| +l.Post("/post", post) | ||
| +l.Put("/put", put) | ||
| +l.Delete("/delete", delete) | ||
| + | ||
| +// Using functions | ||
| +l.GetFunc("/get", getFunc) | ||
| +l.PostFunc("/post", postFunc) | ||
| +l.PutFunc("/put", putFunc) | ||
| +l.DeleteFunc("/delete", deleteFunc) | ||
| + | ||
| +l.Run() | ||
| +``` | ||
| + | ||
| +### Using middlewares | ||
| + | ||
| +```go | ||
| +func main() { | ||
| + l := lion.Classic() | ||
| + | ||
| + // Using middleware | ||
| + l.Use(lion.NewLogger()) | ||
| + | ||
| + // Using middleware functions | ||
| + l.UseFunc(someMiddlewareFunc) | ||
| + | ||
| + l.GetFunc("/hello/:name", Hello) | ||
| + | ||
| + l.Run() | ||
| +} | ||
| +``` | ||
| + | ||
| + | ||
| +### Group routes by a base path | ||
| + | ||
| +```go | ||
| +l := lion.Classic() | ||
| +api := l.Group("/api") | ||
| + | ||
| +v1 := l.Group("/v1") | ||
| +v1.GetFunc("/somepath", gettingFromV1) | ||
| + | ||
| +v2 := l.Group("/v2") | ||
| +v2.GetFunc("/somepath", gettingFromV2) | ||
| + | ||
| +l.Run() | ||
| +``` | ||
| + | ||
| +### Mouting a router into a base path | ||
| + | ||
| + | ||
| +```go | ||
| +l := lion.Classic() | ||
| + | ||
| +sub := lion.New() | ||
| +sub.GetFunc("/somepath", getting) | ||
| + | ||
| + | ||
| +l.Mount("/api", sub) | ||
| +``` | ||
| + | ||
| +### Default middlewares | ||
| + | ||
| +`lion.Classic()` creates a router with default middlewares (Recovery, RealIP, Logger, Static). | ||
| +If you wish to create a blank router without any middlewares you can use `lion.New()`. | ||
| + | ||
| +```go | ||
| +func main() { | ||
| + // This a no middlewares registered | ||
| + l := lion.New() | ||
| + l.Use(lion.NewLogger()) | ||
| + | ||
| + l.GetFunc("/hello/:name", Hello) | ||
| + | ||
| + l.Run() | ||
| +} | ||
| +``` | ||
| + | ||
| +# Custom Middlewares | ||
| + | ||
| +Custom middlewares should implement the Middleware interface: | ||
| + | ||
| +```go | ||
| +type Middleware interface { | ||
| + ServeNext(Handler) Handler | ||
| +} | ||
| +``` | ||
| + | ||
| +You can also make MiddlewareFuncs to use using `.UseFunc()` method. | ||
| +It has to accept a Handler and return a Handler: | ||
| +```go | ||
| +func(next Handler) Handler | ||
| +``` | ||
| + | ||
| + | ||
| +### Custom Logger example | ||
| + | ||
| +```go | ||
| +type logger struct{} | ||
| + | ||
| +func (*logger) ServeNext(next lion.Handler) lion.Handler { | ||
| + return lion.HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + start := time.Now() | ||
| + | ||
| + next.ServeHTTPC(c, w, r) | ||
| + | ||
| + fmt.Printf("Served %s in %s\n", r.URL.Path, time.Since(start)) | ||
| + }) | ||
| +} | ||
| +``` | ||
| + | ||
| +Then in the main function you can use the middleware using: | ||
| + | ||
| +```go | ||
| +l := lion.New() | ||
| + | ||
| +l.Use(&logger{}) | ||
| +l.GetFunc("/hello/:name", Hello) | ||
| +l.Run() | ||
| +``` | ||
| + | ||
| +# Todo | ||
| + | ||
| +[ ] Better static file handling | ||
| +[ ] Better docs | ||
| + | ||
| +# Credits | ||
| + | ||
| +* @codegangsta for https://github.com/codegangsta/negroni | ||
| + * Static and Recovery middlewares are taken from Negroni | ||
| +* @zenazn for https://github.com/zenazn/goji/ | ||
| + * RealIP middleware is taken from goji | ||
| +* @armon for https://github.com/armon/go-radix |
364
bench_test.go
| @@ -0,0 +1,364 @@ | ||
| +package lion | ||
| + | ||
| +import ( | ||
| + "net/http" | ||
| + "testing" | ||
| +) | ||
| + | ||
| +var githubLion http.Handler | ||
| + | ||
| +func init() { | ||
| + githubLion = loadLion(githubAPI) | ||
| +} | ||
| + | ||
| +func BenchmarkGithubAPI(b *testing.B) { | ||
| + benchRoutes(b, githubLion, githubAPI) | ||
| +} | ||
| + | ||
| +func BenchmarkLion_GithubParam(b *testing.B) { | ||
| + req, _ := http.NewRequest("GET", "/repos/julienschmidt/httprouter/stargazers", nil) | ||
| + benchRequest(b, githubLion, req) | ||
| +} | ||
| + | ||
| +func benchRequest(b *testing.B, router http.Handler, r *http.Request) { | ||
| + w := new(mockResponseWriter) | ||
| + u := r.URL | ||
| + rq := u.RawQuery | ||
| + r.RequestURI = u.RequestURI() | ||
| + | ||
| + b.ReportAllocs() | ||
| + b.ResetTimer() | ||
| + | ||
| + for i := 0; i < b.N; i++ { | ||
| + u.RawQuery = rq | ||
| + router.ServeHTTP(w, r) | ||
| + } | ||
| +} | ||
| + | ||
| +func benchRoutes(b *testing.B, router http.Handler, routes []route) { | ||
| + w := new(mockResponseWriter) | ||
| + r, _ := http.NewRequest("GET", "/", nil) | ||
| + u := r.URL | ||
| + rq := u.RawQuery | ||
| + | ||
| + b.ReportAllocs() | ||
| + b.ResetTimer() | ||
| + | ||
| + for i := 0; i < b.N; i++ { | ||
| + for _, route := range routes { | ||
| + r.Method = route.method | ||
| + r.RequestURI = route.path | ||
| + u.Path = route.path | ||
| + u.RawQuery = rq | ||
| + router.ServeHTTP(w, r) | ||
| + } | ||
| + } | ||
| +} | ||
| + | ||
| +type route struct { | ||
| + method string | ||
| + path string | ||
| +} | ||
| +type mockResponseWriter struct{} | ||
| + | ||
| +func (m *mockResponseWriter) Header() (h http.Header) { | ||
| + return http.Header{} | ||
| +} | ||
| + | ||
| +func (m *mockResponseWriter) Write(p []byte) (n int, err error) { | ||
| + return len(p), nil | ||
| +} | ||
| + | ||
| +func (m *mockResponseWriter) WriteString(s string) (n int, err error) { | ||
| + return len(s), nil | ||
| +} | ||
| + | ||
| +func (m *mockResponseWriter) WriteHeader(int) {} | ||
| + | ||
| +func loadLion(routes []route) http.Handler { | ||
| + hn := httpHandlerFunc | ||
| + h := Wrap(http.HandlerFunc(hn)) | ||
| + mux := New() | ||
| + for _, route := range routes { | ||
| + switch route.method { | ||
| + case "GET": | ||
| + mux.Get(route.path, h) | ||
| + case "POST": | ||
| + mux.Post(route.path, h) | ||
| + case "PUT": | ||
| + mux.Put(route.path, h) | ||
| + case "PATCH": | ||
| + mux.Patch(route.path, h) | ||
| + case "DELETE": | ||
| + mux.Delete(route.path, h) | ||
| + default: | ||
| + panic("Unknown HTTP method: " + route.method) | ||
| + } | ||
| + } | ||
| + // mux.Matcher.DisplayTree(0) | ||
| + return mux | ||
| +} | ||
| +func httpHandlerFunc(w http.ResponseWriter, r *http.Request) {} | ||
| + | ||
| +var githubAPI = []route{ | ||
| + // OAuth Authorizations | ||
| + {"GET", "/authorizations"}, | ||
| + {"GET", "/authorizations/:id"}, | ||
| + {"POST", "/authorizations"}, | ||
| + //{"PUT", "/authorizations/clients/:client_id"}, | ||
| + //{"PATCH", "/authorizations/:id"}, | ||
| + {"DELETE", "/authorizations/:id"}, | ||
| + {"GET", "/applications/:client_id/tokens/:access_token"}, | ||
| + {"DELETE", "/applications/:client_id/tokens"}, | ||
| + {"DELETE", "/applications/:client_id/tokens/:access_token"}, | ||
| + | ||
| + // Activity | ||
| + {"GET", "/events"}, | ||
| + {"GET", "/repos/:owner/:repo/events"}, | ||
| + {"GET", "/networks/:owner/:repo/events"}, | ||
| + {"GET", "/orgs/:org/events"}, | ||
| + {"GET", "/users/:user/received_events"}, | ||
| + {"GET", "/users/:user/received_events/public"}, | ||
| + {"GET", "/users/:user/events"}, | ||
| + {"GET", "/users/:user/events/public"}, | ||
| + {"GET", "/users/:user/events/orgs/:org"}, | ||
| + {"GET", "/feeds"}, | ||
| + {"GET", "/notifications"}, | ||
| + {"GET", "/repos/:owner/:repo/notifications"}, | ||
| + {"PUT", "/notifications"}, | ||
| + {"PUT", "/repos/:owner/:repo/notifications"}, | ||
| + {"GET", "/notifications/threads/:id"}, | ||
| + //{"PATCH", "/notifications/threads/:id"}, | ||
| + {"GET", "/notifications/threads/:id/subscription"}, | ||
| + {"PUT", "/notifications/threads/:id/subscription"}, | ||
| + {"DELETE", "/notifications/threads/:id/subscription"}, | ||
| + {"GET", "/repos/:owner/:repo/stargazers"}, | ||
| + {"GET", "/users/:user/starred"}, | ||
| + {"GET", "/user/starred"}, | ||
| + {"GET", "/user/starred/:owner/:repo"}, | ||
| + {"PUT", "/user/starred/:owner/:repo"}, | ||
| + {"DELETE", "/user/starred/:owner/:repo"}, | ||
| + {"GET", "/repos/:owner/:repo/subscribers"}, | ||
| + {"GET", "/users/:user/subscriptions"}, | ||
| + {"GET", "/user/subscriptions"}, | ||
| + {"GET", "/repos/:owner/:repo/subscription"}, | ||
| + {"PUT", "/repos/:owner/:repo/subscription"}, | ||
| + {"DELETE", "/repos/:owner/:repo/subscription"}, | ||
| + {"GET", "/user/subscriptions/:owner/:repo"}, | ||
| + {"PUT", "/user/subscriptions/:owner/:repo"}, | ||
| + {"DELETE", "/user/subscriptions/:owner/:repo"}, | ||
| + | ||
| + // Gists | ||
| + {"GET", "/users/:user/gists"}, | ||
| + {"GET", "/gists"}, | ||
| + //{"GET", "/gists/public"}, | ||
| + //{"GET", "/gists/starred"}, | ||
| + {"GET", "/gists/:id"}, | ||
| + {"POST", "/gists"}, | ||
| + //{"PATCH", "/gists/:id"}, | ||
| + {"PUT", "/gists/:id/star"}, | ||
| + {"DELETE", "/gists/:id/star"}, | ||
| + {"GET", "/gists/:id/star"}, | ||
| + {"POST", "/gists/:id/forks"}, | ||
| + {"DELETE", "/gists/:id"}, | ||
| + | ||
| + // Git Data | ||
| + {"GET", "/repos/:owner/:repo/git/blobs/:sha"}, | ||
| + {"POST", "/repos/:owner/:repo/git/blobs"}, | ||
| + {"GET", "/repos/:owner/:repo/git/commits/:sha"}, | ||
| + {"POST", "/repos/:owner/:repo/git/commits"}, | ||
| + //{"GET", "/repos/:owner/:repo/git/refs/*ref"}, | ||
| + {"GET", "/repos/:owner/:repo/git/refs"}, | ||
| + {"POST", "/repos/:owner/:repo/git/refs"}, | ||
| + //{"PATCH", "/repos/:owner/:repo/git/refs/*ref"}, | ||
| + //{"DELETE", "/repos/:owner/:repo/git/refs/*ref"}, | ||
| + {"GET", "/repos/:owner/:repo/git/tags/:sha"}, | ||
| + {"POST", "/repos/:owner/:repo/git/tags"}, | ||
| + {"GET", "/repos/:owner/:repo/git/trees/:sha"}, | ||
| + {"POST", "/repos/:owner/:repo/git/trees"}, | ||
| + | ||
| + // Issues | ||
| + {"GET", "/issues"}, | ||
| + {"GET", "/user/issues"}, | ||
| + {"GET", "/orgs/:org/issues"}, | ||
| + {"GET", "/repos/:owner/:repo/issues"}, | ||
| + {"GET", "/repos/:owner/:repo/issues/:number"}, | ||
| + {"POST", "/repos/:owner/:repo/issues"}, | ||
| + //{"PATCH", "/repos/:owner/:repo/issues/:number"}, | ||
| + {"GET", "/repos/:owner/:repo/assignees"}, | ||
| + {"GET", "/repos/:owner/:repo/assignees/:assignee"}, | ||
| + {"GET", "/repos/:owner/:repo/issues/:number/comments"}, | ||
| + //{"GET", "/repos/:owner/:repo/issues/comments"}, | ||
| + //{"GET", "/repos/:owner/:repo/issues/comments/:id"}, | ||
| + {"POST", "/repos/:owner/:repo/issues/:number/comments"}, | ||
| + //{"PATCH", "/repos/:owner/:repo/issues/comments/:id"}, | ||
| + //{"DELETE", "/repos/:owner/:repo/issues/comments/:id"}, | ||
| + {"GET", "/repos/:owner/:repo/issues/:number/events"}, | ||
| + //{"GET", "/repos/:owner/:repo/issues/events"}, | ||
| + //{"GET", "/repos/:owner/:repo/issues/events/:id"}, | ||
| + {"GET", "/repos/:owner/:repo/labels"}, | ||
| + {"GET", "/repos/:owner/:repo/labels/:name"}, | ||
| + {"POST", "/repos/:owner/:repo/labels"}, | ||
| + //{"PATCH", "/repos/:owner/:repo/labels/:name"}, | ||
| + {"DELETE", "/repos/:owner/:repo/labels/:name"}, | ||
| + {"GET", "/repos/:owner/:repo/issues/:number/labels"}, | ||
| + {"POST", "/repos/:owner/:repo/issues/:number/labels"}, | ||
| + {"DELETE", "/repos/:owner/:repo/issues/:number/labels/:name"}, | ||
| + {"PUT", "/repos/:owner/:repo/issues/:number/labels"}, | ||
| + {"DELETE", "/repos/:owner/:repo/issues/:number/labels"}, | ||
| + {"GET", "/repos/:owner/:repo/milestones/:number/labels"}, | ||
| + {"GET", "/repos/:owner/:repo/milestones"}, | ||
| + {"GET", "/repos/:owner/:repo/milestones/:number"}, | ||
| + {"POST", "/repos/:owner/:repo/milestones"}, | ||
| + //{"PATCH", "/repos/:owner/:repo/milestones/:number"}, | ||
| + {"DELETE", "/repos/:owner/:repo/milestones/:number"}, | ||
| + | ||
| + // Miscellaneous | ||
| + {"GET", "/emojis"}, | ||
| + {"GET", "/gitignore/templates"}, | ||
| + {"GET", "/gitignore/templates/:name"}, | ||
| + {"POST", "/markdown"}, | ||
| + {"POST", "/markdown/raw"}, | ||
| + {"GET", "/meta"}, | ||
| + {"GET", "/rate_limit"}, | ||
| + | ||
| + // Organizations | ||
| + {"GET", "/users/:user/orgs"}, | ||
| + {"GET", "/user/orgs"}, | ||
| + {"GET", "/orgs/:org"}, | ||
| + //{"PATCH", "/orgs/:org"}, | ||
| + {"GET", "/orgs/:org/members"}, | ||
| + {"GET", "/orgs/:org/members/:user"}, | ||
| + {"DELETE", "/orgs/:org/members/:user"}, | ||
| + {"GET", "/orgs/:org/public_members"}, | ||
| + {"GET", "/orgs/:org/public_members/:user"}, | ||
| + {"PUT", "/orgs/:org/public_members/:user"}, | ||
| + {"DELETE", "/orgs/:org/public_members/:user"}, | ||
| + {"GET", "/orgs/:org/teams"}, | ||
| + {"GET", "/teams/:id"}, | ||
| + {"POST", "/orgs/:org/teams"}, | ||
| + //{"PATCH", "/teams/:id"}, | ||
| + {"DELETE", "/teams/:id"}, | ||
| + {"GET", "/teams/:id/members"}, | ||
| + {"GET", "/teams/:id/members/:user"}, | ||
| + {"PUT", "/teams/:id/members/:user"}, | ||
| + {"DELETE", "/teams/:id/members/:user"}, | ||
| + {"GET", "/teams/:id/repos"}, | ||
| + {"GET", "/teams/:id/repos/:owner/:repo"}, | ||
| + {"PUT", "/teams/:id/repos/:owner/:repo"}, | ||
| + {"DELETE", "/teams/:id/repos/:owner/:repo"}, | ||
| + {"GET", "/user/teams"}, | ||
| + | ||
| + // Pull Requests | ||
| + {"GET", "/repos/:owner/:repo/pulls"}, | ||
| + {"GET", "/repos/:owner/:repo/pulls/:number"}, | ||
| + {"POST", "/repos/:owner/:repo/pulls"}, | ||
| + //{"PATCH", "/repos/:owner/:repo/pulls/:number"}, | ||
| + {"GET", "/repos/:owner/:repo/pulls/:number/commits"}, | ||
| + {"GET", "/repos/:owner/:repo/pulls/:number/files"}, | ||
| + {"GET", "/repos/:owner/:repo/pulls/:number/merge"}, | ||
| + {"PUT", "/repos/:owner/:repo/pulls/:number/merge"}, | ||
| + {"GET", "/repos/:owner/:repo/pulls/:number/comments"}, | ||
| + //{"GET", "/repos/:owner/:repo/pulls/comments"}, | ||
| + //{"GET", "/repos/:owner/:repo/pulls/comments/:number"}, | ||
| + {"PUT", "/repos/:owner/:repo/pulls/:number/comments"}, | ||
| + //{"PATCH", "/repos/:owner/:repo/pulls/comments/:number"}, | ||
| + //{"DELETE", "/repos/:owner/:repo/pulls/comments/:number"}, | ||
| + | ||
| + // Repositories | ||
| + {"GET", "/user/repos"}, | ||
| + {"GET", "/users/:user/repos"}, | ||
| + {"GET", "/orgs/:org/repos"}, | ||
| + {"GET", "/repositories"}, | ||
| + {"POST", "/user/repos"}, | ||
| + {"POST", "/orgs/:org/repos"}, | ||
| + {"GET", "/repos/:owner/:repo"}, | ||
| + //{"PATCH", "/repos/:owner/:repo"}, | ||
| + {"GET", "/repos/:owner/:repo/contributors"}, | ||
| + {"GET", "/repos/:owner/:repo/languages"}, | ||
| + {"GET", "/repos/:owner/:repo/teams"}, | ||
| + {"GET", "/repos/:owner/:repo/tags"}, | ||
| + {"GET", "/repos/:owner/:repo/branches"}, | ||
| + {"GET", "/repos/:owner/:repo/branches/:branch"}, | ||
| + {"DELETE", "/repos/:owner/:repo"}, | ||
| + {"GET", "/repos/:owner/:repo/collaborators"}, | ||
| + {"GET", "/repos/:owner/:repo/collaborators/:user"}, | ||
| + {"PUT", "/repos/:owner/:repo/collaborators/:user"}, | ||
| + {"DELETE", "/repos/:owner/:repo/collaborators/:user"}, | ||
| + {"GET", "/repos/:owner/:repo/comments"}, | ||
| + {"GET", "/repos/:owner/:repo/commits/:sha/comments"}, | ||
| + {"POST", "/repos/:owner/:repo/commits/:sha/comments"}, | ||
| + {"GET", "/repos/:owner/:repo/comments/:id"}, | ||
| + //{"PATCH", "/repos/:owner/:repo/comments/:id"}, | ||
| + {"DELETE", "/repos/:owner/:repo/comments/:id"}, | ||
| + {"GET", "/repos/:owner/:repo/commits"}, | ||
| + {"GET", "/repos/:owner/:repo/commits/:sha"}, | ||
| + {"GET", "/repos/:owner/:repo/readme"}, | ||
| + //{"GET", "/repos/:owner/:repo/contents/*path"}, | ||
| + //{"PUT", "/repos/:owner/:repo/contents/*path"}, | ||
| + //{"DELETE", "/repos/:owner/:repo/contents/*path"}, | ||
| + //{"GET", "/repos/:owner/:repo/:archive_format/:ref"}, | ||
| + {"GET", "/repos/:owner/:repo/keys"}, | ||
| + {"GET", "/repos/:owner/:repo/keys/:id"}, | ||
| + {"POST", "/repos/:owner/:repo/keys"}, | ||
| + //{"PATCH", "/repos/:owner/:repo/keys/:id"}, | ||
| + {"DELETE", "/repos/:owner/:repo/keys/:id"}, | ||
| + {"GET", "/repos/:owner/:repo/downloads"}, | ||
| + {"GET", "/repos/:owner/:repo/downloads/:id"}, | ||
| + {"DELETE", "/repos/:owner/:repo/downloads/:id"}, | ||
| + {"GET", "/repos/:owner/:repo/forks"}, | ||
| + {"POST", "/repos/:owner/:repo/forks"}, | ||
| + {"GET", "/repos/:owner/:repo/hooks"}, | ||
| + {"GET", "/repos/:owner/:repo/hooks/:id"}, | ||
| + {"POST", "/repos/:owner/:repo/hooks"}, | ||
| + //{"PATCH", "/repos/:owner/:repo/hooks/:id"}, | ||
| + {"POST", "/repos/:owner/:repo/hooks/:id/tests"}, | ||
| + {"DELETE", "/repos/:owner/:repo/hooks/:id"}, | ||
| + {"POST", "/repos/:owner/:repo/merges"}, | ||
| + {"GET", "/repos/:owner/:repo/releases"}, | ||
| + {"GET", "/repos/:owner/:repo/releases/:id"}, | ||
| + {"POST", "/repos/:owner/:repo/releases"}, | ||
| + //{"PATCH", "/repos/:owner/:repo/releases/:id"}, | ||
| + {"DELETE", "/repos/:owner/:repo/releases/:id"}, | ||
| + {"GET", "/repos/:owner/:repo/releases/:id/assets"}, | ||
| + {"GET", "/repos/:owner/:repo/stats/contributors"}, | ||
| + {"GET", "/repos/:owner/:repo/stats/commit_activity"}, | ||
| + {"GET", "/repos/:owner/:repo/stats/code_frequency"}, | ||
| + {"GET", "/repos/:owner/:repo/stats/participation"}, | ||
| + {"GET", "/repos/:owner/:repo/stats/punch_card"}, | ||
| + {"GET", "/repos/:owner/:repo/statuses/:ref"}, | ||
| + {"POST", "/repos/:owner/:repo/statuses/:ref"}, | ||
| + | ||
| + // Search | ||
| + {"GET", "/search/repositories"}, | ||
| + {"GET", "/search/code"}, | ||
| + {"GET", "/search/issues"}, | ||
| + {"GET", "/search/users"}, | ||
| + {"GET", "/legacy/issues/search/:owner/:repository/:state/:keyword"}, | ||
| + {"GET", "/legacy/repos/search/:keyword"}, | ||
| + {"GET", "/legacy/user/search/:keyword"}, | ||
| + {"GET", "/legacy/user/email/:email"}, | ||
| + | ||
| + // Users | ||
| + {"GET", "/users/:user"}, | ||
| + {"GET", "/user"}, | ||
| + //{"PATCH", "/user"}, | ||
| + {"GET", "/users"}, | ||
| + {"GET", "/user/emails"}, | ||
| + {"POST", "/user/emails"}, | ||
| + {"DELETE", "/user/emails"}, | ||
| + {"GET", "/users/:user/followers"}, | ||
| + {"GET", "/user/followers"}, | ||
| + {"GET", "/users/:user/following"}, | ||
| + {"GET", "/user/following"}, | ||
| + {"GET", "/user/following/:user"}, | ||
| + {"GET", "/users/:user/following/:target_user"}, | ||
| + {"PUT", "/user/following/:user"}, | ||
| + {"DELETE", "/user/following/:user"}, | ||
| + {"GET", "/users/:user/keys"}, | ||
| + {"GET", "/user/keys"}, | ||
| + {"GET", "/user/keys/:id"}, | ||
| + {"POST", "/user/keys"}, | ||
| + //{"PATCH", "/user/keys/:id"}, | ||
| + {"DELETE", "/user/keys/:id"}, | ||
| +} |
75
context.go
| @@ -0,0 +1,75 @@ | ||
| +package lion | ||
| + | ||
| +import "golang.org/x/net/context" | ||
| + | ||
| +// Check Context implements net.Context | ||
| +var _ context.Context = (*Context)(nil) | ||
| + | ||
| +// type ContextI interface { | ||
| +// context.Context | ||
| +// Param(string) string | ||
| +// } | ||
| + | ||
| +// Context implements golang.org/x/net/context.Context and stores values of url parameters | ||
| +type Context struct { | ||
| + context.Context | ||
| + parent context.Context | ||
| + | ||
| + keys []string | ||
| + values []string | ||
| +} | ||
| + | ||
| +// NewContext creates a new context instance | ||
| +func NewContext() *Context { | ||
| + return NewContextWithParent(context.Background()) | ||
| +} | ||
| + | ||
| +// NewContextWithParent creates a new context with a parent context specified | ||
| +func NewContextWithParent(c context.Context) *Context { | ||
| + return &Context{ | ||
| + parent: c, | ||
| + } | ||
| +} | ||
| + | ||
| +// Value returns the value for the passed key. If it is not found in the url params it returns parent's context Value | ||
| +func (p *Context) Value(key interface{}) interface{} { | ||
| + if k, ok := key.(string); ok { | ||
| + return p.Param(k) | ||
| + } | ||
| + | ||
| + return p.parent.Value(key) | ||
| +} | ||
| + | ||
| +func (p *Context) addParam(key, val string) { | ||
| + p.keys = append(p.keys, key) | ||
| + p.values = append(p.values, val) | ||
| +} | ||
| + | ||
| +// Param returns the value of a param | ||
| +func (p *Context) Param(key string) string { | ||
| + for i, name := range p.keys { | ||
| + if name == key { | ||
| + return p.values[i] | ||
| + } | ||
| + } | ||
| + return "" | ||
| +} | ||
| + | ||
| +func (p *Context) reset() { | ||
| + p.keys = p.keys[:0] | ||
| + p.values = p.values[:0] | ||
| + p.parent = nil | ||
| +} | ||
| + | ||
| +// C returns a Context based on a context.Context passed. If it does not convert to Context, it creates a new one with the context passed as argument. | ||
| +func C(c context.Context) *Context { | ||
| + if ctx, ok := c.(*Context); ok { | ||
| + return ctx | ||
| + } | ||
| + return NewContextWithParent(c) | ||
| +} | ||
| + | ||
| +// Param returns the value of a url param base on the passed context | ||
| +func Param(c context.Context, key string) string { | ||
| + return C(c).Param(key) | ||
| +} |
62
examples/basic-auth/basic-auth.go
| @@ -0,0 +1,62 @@ | ||
| +package main | ||
| + | ||
| +import ( | ||
| + "bytes" | ||
| + "encoding/base64" | ||
| + "fmt" | ||
| + "net/http" | ||
| + "strings" | ||
| + | ||
| + "github.com/celrenheit/lion" | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +const basicAuthPrefix = "Basic " | ||
| + | ||
| +var user = []byte("lion") | ||
| +var pass = []byte("argh") | ||
| + | ||
| +func Home(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "Home") | ||
| +} | ||
| + | ||
| +func BasicAuthMiddleware(next lion.Handler) lion.Handler { | ||
| + return lion.HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + auth := r.Header.Get("Authorization") | ||
| + | ||
| + if strings.HasPrefix(auth, basicAuthPrefix) { | ||
| + // Check credentials | ||
| + payload, err := base64.StdEncoding.DecodeString(auth[len(basicAuthPrefix):]) | ||
| + if err == nil { | ||
| + pair := bytes.SplitN(payload, []byte(":"), 2) | ||
| + if len(pair) == 2 && | ||
| + bytes.Equal(pair[0], user) && | ||
| + bytes.Equal(pair[1], pass) { | ||
| + | ||
| + // Delegate request to the given handle | ||
| + next.ServeHTTPC(c, w, r) | ||
| + return | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + // Request Basic Authentication otherwise | ||
| + w.Header().Set("WWW-Authenticate", "Basic realm=Restricted") | ||
| + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) | ||
| + }) | ||
| +} | ||
| + | ||
| +func ProtectedHome(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "Connected to the protected home") | ||
| +} | ||
| + | ||
| +func main() { | ||
| + l := lion.Classic() | ||
| + l.GetFunc("/", Home) | ||
| + | ||
| + g := l.Group("/protected") | ||
| + g.UseFunc(BasicAuthMiddleware) | ||
| + g.GetFunc("/", ProtectedHome) | ||
| + | ||
| + l.Run(":3000") | ||
| +} |
39
examples/custom-logger/custom-logger.go
| @@ -0,0 +1,39 @@ | ||
| +package main | ||
| + | ||
| +import ( | ||
| + "fmt" | ||
| + "net/http" | ||
| + "time" | ||
| + | ||
| + "github.com/celrenheit/lion" | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +func Home(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "Home") | ||
| +} | ||
| + | ||
| +func Hello(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + ctx := lion.C(c) | ||
| + fmt.Fprintf(w, "Hello "+ctx.Param("name")) | ||
| +} | ||
| + | ||
| +type logger struct{} | ||
| + | ||
| +func (*logger) ServeNext(next lion.Handler) lion.Handler { | ||
| + return lion.HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + start := time.Now() | ||
| + | ||
| + next.ServeHTTPC(c, w, r) | ||
| + | ||
| + fmt.Printf("Served %s in %s\n", r.URL.Path, time.Since(start)) | ||
| + }) | ||
| +} | ||
| + | ||
| +func main() { | ||
| + l := lion.New() | ||
| + l.Use(&logger{}) | ||
| + l.GetFunc("/", Home) | ||
| + l.GetFunc("/hello/:name", Hello) | ||
| + l.Run(":3000") | ||
| +} |
24
examples/hello/hello.go
| @@ -0,0 +1,24 @@ | ||
| +package main | ||
| + | ||
| +import ( | ||
| + "fmt" | ||
| + "net/http" | ||
| + | ||
| + "github.com/celrenheit/lion" | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +func Home(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "Home\n") | ||
| +} | ||
| + | ||
| +func Hello(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "Hello "+c.Value("name").(string)) | ||
| +} | ||
| + | ||
| +func main() { | ||
| + l := lion.Classic() | ||
| + l.GetFunc("/", Home) | ||
| + l.GetFunc("/hello/:name", Hello) | ||
| + l.Run() | ||
| +} |
56
examples/modular-hello/modular-hello.go
| @@ -0,0 +1,56 @@ | ||
| +package main | ||
| + | ||
| +import ( | ||
| + "fmt" | ||
| + "net/http" | ||
| + | ||
| + "github.com/celrenheit/lion" | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +func main() { | ||
| + l := lion.Classic() | ||
| + api := l.Group("/api") | ||
| + api.Module(Products{}) | ||
| + l.Run() | ||
| +} | ||
| + | ||
| +// Products module is accessible at url: /api/products | ||
| +// It handles getting a list of products or creating a new product | ||
| +type Products struct{} | ||
| + | ||
| +func (p Products) Base() string { | ||
| + return "/products" | ||
| +} | ||
| + | ||
| +func (p Products) Get(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "Fetching all products") | ||
| +} | ||
| + | ||
| +func (p Products) Post(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "Creating a new product") | ||
| +} | ||
| + | ||
| +func (p Products) Routes(r *lion.Router) { | ||
| + // Defining a resource for getting, editing and deleting a single product | ||
| + r.Resource("/:id", OneProduct{}) | ||
| +} | ||
| + | ||
| +// OneProduct resource is accessible at url: /api/products/:id | ||
| +// It handles getting, editing and deleting a single product | ||
| +type OneProduct struct{} | ||
| + | ||
| +func (p OneProduct) Get(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + id := lion.Param(c, "id") | ||
| + fmt.Fprintf(w, "Getting product: %s", id) | ||
| +} | ||
| + | ||
| +func (p OneProduct) Put(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + id := lion.Param(c, "id") | ||
| + fmt.Fprintf(w, "Updating article: %s", id) | ||
| +} | ||
| + | ||
| +func (p OneProduct) Delete(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + id := lion.Param(c, "id") | ||
| + fmt.Fprintf(w, "Deleting article: %s", id) | ||
| +} |
52
examples/module/module.go
| @@ -0,0 +1,52 @@ | ||
| +package main | ||
| + | ||
| +import ( | ||
| + "fmt" | ||
| + "net/http" | ||
| + | ||
| + "github.com/celrenheit/lion" | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +// Open your web browser at http://localhost:3000/api/v1/todos | ||
| + | ||
| +type api struct{} | ||
| + | ||
| +func (t api) Base() string { return "/api" } | ||
| + | ||
| +func (t api) Routes(r *lion.Router) { | ||
| + r.Module(v1{}) | ||
| +} | ||
| + | ||
| +// Attach Get methods to a Module. | ||
| +// ====> A Module is also a Resource. | ||
| +func (t api) Get(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, ` | ||
| + Description of available apis | ||
| + Go to: http://localhost:3000/api/v1/todos | ||
| + `) | ||
| +} | ||
| + | ||
| +type v1 struct{} | ||
| + | ||
| +func (t v1) Base() string { return "/v1" } | ||
| + | ||
| +func (t v1) Routes(r *lion.Router) { | ||
| + r.Resource("/todos", todoList{}) | ||
| +} | ||
| + | ||
| +type todoList struct{} | ||
| + | ||
| +func (t todoList) Uses() lion.Middlewares { | ||
| + return lion.Middlewares{lion.NewLogger()} | ||
| +} | ||
| + | ||
| +func (t todoList) Get(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "TODO") | ||
| +} | ||
| + | ||
| +func main() { | ||
| + l := lion.New() | ||
| + l.Module(api{}) | ||
| + l.Run() | ||
| +} |
31
examples/resource/resource.go
| @@ -0,0 +1,31 @@ | ||
| +package main | ||
| + | ||
| +import ( | ||
| + "fmt" | ||
| + "net/http" | ||
| + | ||
| + "github.com/celrenheit/lion" | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +type TodoList struct{} | ||
| + | ||
| +func (t TodoList) Uses() lion.Middlewares { | ||
| + return lion.Middlewares{lion.NewLogger()} | ||
| +} | ||
| + | ||
| +func (t TodoList) GetMiddlewares() lion.Middlewares { | ||
| + return lion.Middlewares{lion.NewRecovery()} | ||
| +} | ||
| + | ||
| +func (t TodoList) Get(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Fprintf(w, "TODO") | ||
| + // Should be catched by GetMiddlewares()'s Recovery middleware | ||
| + panic("test") | ||
| +} | ||
| + | ||
| +func main() { | ||
| + l := lion.New() | ||
| + l.Resource("/todos", TodoList{}) | ||
| + l.Run() | ||
| +} |
48
lion.go
| @@ -0,0 +1,48 @@ | ||
| +package lion | ||
| + | ||
| +import ( | ||
| + "net/http" | ||
| + | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +// Handler responds to an HTTP request | ||
| +type Handler interface { | ||
| + ServeHTTPC(context.Context, http.ResponseWriter, *http.Request) | ||
| +} | ||
| + | ||
| +// HandlerFunc is a wrapper for a function to implement the Handler interface | ||
| +type HandlerFunc func(context.Context, http.ResponseWriter, *http.Request) | ||
| + | ||
| +// ServeHTTP makes HandlerFunc implement net/http.Handler interface | ||
| +func (h HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
| + h(context.TODO(), w, r) | ||
| +} | ||
| + | ||
| +// ServeHTTPC makes HandlerFunc implement Handler interface | ||
| +func (h HandlerFunc) ServeHTTPC(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + h(c, w, r) | ||
| +} | ||
| + | ||
| +// Middleware interface that takes as input a Handler and returns a Handler | ||
| +type Middleware interface { | ||
| + ServeNext(Handler) Handler | ||
| +} | ||
| + | ||
| +// MiddlewareFunc wraps a function that takes as input a Handler and returns a Handler. So that it implements the Middlewares interface | ||
| +type MiddlewareFunc func(Handler) Handler | ||
| + | ||
| +// ServeNext makes MiddlewareFunc implement Middleware | ||
| +func (m MiddlewareFunc) ServeNext(next Handler) Handler { | ||
| + return m(next) | ||
| +} | ||
| + | ||
| +// Middlewares is an array of Middleware | ||
| +type Middlewares []Middleware | ||
| + | ||
| +func (middlewares Middlewares) BuildHandler(handler Handler) Handler { | ||
| + for i := len(middlewares) - 1; i >= 0; i-- { | ||
| + handler = middlewares[i].ServeNext(handler) | ||
| + } | ||
| + return handler | ||
| +} |
56
matcher.go
| @@ -0,0 +1,56 @@ | ||
| +package lion | ||
| + | ||
| +import ( | ||
| + "net/http" | ||
| +) | ||
| + | ||
| +// RegisterMatcher registers and matches routes to Handlers | ||
| +type RegisterMatcher interface { | ||
| + Register(method, pattern string, handler Handler) | ||
| + Match(*Context, *http.Request) (*Context, Handler) | ||
| +} | ||
| + | ||
| +//////////////////////////////////////////////////////////////////////////// | ||
| +/// RADIX /// | ||
| +//////////////////////////////////////////////////////////////////////////// | ||
| + | ||
| +var _ RegisterMatcher = (*radixMatcher)(nil) | ||
| + | ||
| +type radixMatcher struct { | ||
| + trees map[string]*node | ||
| +} | ||
| + | ||
| +func newRadixMatcher() *radixMatcher { | ||
| + r := &radixMatcher{ | ||
| + trees: make(map[string]*node), | ||
| + } | ||
| + return r | ||
| +} | ||
| + | ||
| +func (d *radixMatcher) Register(method, pattern string, handler Handler) { | ||
| + if len(pattern) == 0 || pattern[0] != '/' { | ||
| + panic("path must begin with '/' in path '" + pattern + "'") | ||
| + } | ||
| + | ||
| + if d.trees == nil { | ||
| + d.trees = make(map[string]*node) | ||
| + } | ||
| + | ||
| + root := d.trees[method] | ||
| + if root == nil { | ||
| + root = &node{} | ||
| + d.trees[method] = root | ||
| + } | ||
| + root.addRoute(pattern, handler) | ||
| +} | ||
| + | ||
| +func (d *radixMatcher) Match(c *Context, r *http.Request) (*Context, Handler) { | ||
| + if root, ok := d.trees[r.Method]; ok { | ||
| + n, c := root.findNode(c, cleanPath(r.URL.Path)) | ||
| + if n == nil { | ||
| + return c, nil | ||
| + } | ||
| + return c, n.handler | ||
| + } | ||
| + return c, nil | ||
| +} |
331
middlewares.go
| @@ -0,0 +1,331 @@ | ||
| +package lion | ||
| + | ||
| +import ( | ||
| + "fmt" | ||
| + "log" | ||
| + "net/http" | ||
| + "os" | ||
| + "path" | ||
| + "runtime" | ||
| + "strings" | ||
| + "time" | ||
| + | ||
| + "github.com/fatih/color" | ||
| + | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +var red = color.New(color.FgRed).SprintFunc() | ||
| +var yellow = color.New(color.FgYellow).SprintFunc() | ||
| +var magenta = color.New(color.FgHiMagenta).SprintFunc() | ||
| +var blue = color.New(color.FgBlue).SprintFunc() | ||
| +var green = color.New(color.FgGreen).SprintFunc() | ||
| +var cyan = color.New(color.FgCyan).SprintFunc() | ||
| +var hiBlue = color.New(color.FgHiBlue).SprintFunc() | ||
| +var lionColor = color.New(color.Italic, color.FgHiGreen).SprintFunc() | ||
| + | ||
| +// Classic creates a new router with some handy middlewares. | ||
| +func Classic() *Router { | ||
| + return New(NewRecovery(), RealIP(), NewLogger(), NewStatic(http.Dir("public"))) | ||
| +} | ||
| + | ||
| +var lionLogger = log.New(os.Stdout, lionColor("[lion]")+" ", log.Ldate|log.Ltime) | ||
| + | ||
| +// Logger is a middlewares that logs incomming http requests | ||
| +type Logger struct { | ||
| + *log.Logger | ||
| +} | ||
| + | ||
| +// NewLogger creates a new Logger | ||
| +func NewLogger() *Logger { | ||
| + return &Logger{ | ||
| + Logger: lionLogger, | ||
| + } | ||
| +} | ||
| + | ||
| +// ServeNext implements the Middleware interface for Logger. | ||
| +// It wraps the corresponding http.ResponseWriter and saves statistics about the status code returned, the number of bytes written and the time that requests took. | ||
| +func (l *Logger) ServeNext(next Handler) Handler { | ||
| + | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + | ||
| + res := WrapResponseWriter(w) | ||
| + start := time.Now() | ||
| + | ||
| + next.ServeHTTPC(c, res, r) | ||
| + | ||
| + l.Printf("%s %s | %s | %dB in %v from %s", magenta(r.Method), hiBlue(r.URL.Path), statusColor(res.Status()), res.BytesWritten(), timeColor(time.Since(start)), r.RemoteAddr) | ||
| + }) | ||
| +} | ||
| + | ||
| +func statusColor(status int) string { | ||
| + msg := fmt.Sprintf("%d %s", status, http.StatusText(status)) | ||
| + switch { | ||
| + case status < 200: | ||
| + return blue(msg) | ||
| + case status < 300: | ||
| + return green(msg) | ||
| + case status < 400: | ||
| + return cyan(msg) | ||
| + case status < 500: | ||
| + return yellow(msg) | ||
| + default: | ||
| + return red(msg) | ||
| + } | ||
| +} | ||
| + | ||
| +func timeColor(dur time.Duration) string { | ||
| + switch { | ||
| + case dur < 500*time.Millisecond: | ||
| + return green(dur) | ||
| + case dur < 5*time.Second: | ||
| + return yellow(dur) | ||
| + default: | ||
| + return red(dur) | ||
| + } | ||
| +} | ||
| + | ||
| +// Recovery is a middleware that recovers from panics | ||
| +// Taken from https://github.com/codegangsta/negroni/blob/master/recovery.go | ||
| +type Recovery struct { | ||
| + Logger *log.Logger | ||
| + PrintStack bool | ||
| + StackAll bool | ||
| + StackSize int | ||
| +} | ||
| + | ||
| +// NewRecovery creates a new Recovery instance | ||
| +func NewRecovery() *Recovery { | ||
| + return &Recovery{ | ||
| + Logger: lionLogger, | ||
| + PrintStack: false, | ||
| + StackAll: false, | ||
| + StackSize: 1024 * 8, | ||
| + } | ||
| +} | ||
| + | ||
| +// ServeNext is the method responsible for recovering from a panic | ||
| +func (rec *Recovery) ServeNext(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + defer func() { | ||
| + if err := recover(); err != nil { | ||
| + w.WriteHeader(http.StatusInternalServerError) | ||
| + stack := make([]byte, rec.StackSize) | ||
| + stack = stack[:runtime.Stack(stack, rec.StackAll)] | ||
| + | ||
| + f := "PANIC: %s\n%s" | ||
| + rec.Logger.Printf(f, err, stack) | ||
| + | ||
| + if rec.PrintStack { | ||
| + fmt.Fprintf(w, f, err, stack) | ||
| + } | ||
| + | ||
| + } | ||
| + }() | ||
| + | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| +} | ||
| + | ||
| +func defaultPanicHandler() Handler { | ||
| + return HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + log.Println(fmt.Sprintf("%v", ctx.Value("panic"))) | ||
| + http.Error(w, fmt.Sprintf("%v", ctx.Value("panic")), http.StatusInternalServerError) | ||
| + }) | ||
| +} | ||
| + | ||
| +// Static is a middleware handler that serves static files in the given directory/filesystem. | ||
| +// Taken from https://github.com/codegangsta/negroni/blob/master/static.go | ||
| +type Static struct { | ||
| + // Dir is the directory to serve static files from | ||
| + Dir http.FileSystem | ||
| + // Prefix is the optional prefix used to serve the static directory content | ||
| + Prefix string | ||
| + // IndexFile defines which file to serve as index if it exists. | ||
| + IndexFile string | ||
| +} | ||
| + | ||
| +// NewStatic returns a new instance of Static | ||
| +func NewStatic(directory http.FileSystem) *Static { | ||
| + return &Static{ | ||
| + Dir: directory, | ||
| + Prefix: "", | ||
| + IndexFile: "index.html", | ||
| + } | ||
| +} | ||
| + | ||
| +// ServeNext tries to find a file in the directory | ||
| +func (s *Static) ServeNext(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + | ||
| + if r.Method != "GET" && r.Method != "HEAD" { | ||
| + next.ServeHTTPC(c, w, r) | ||
| + return | ||
| + } | ||
| + | ||
| + file := r.URL.Path | ||
| + // if we have a prefix, filter requests by stripping the prefix | ||
| + if s.Prefix != "" { | ||
| + if !strings.HasPrefix(file, s.Prefix) { | ||
| + next.ServeHTTPC(c, w, r) | ||
| + return | ||
| + } | ||
| + file = file[len(s.Prefix):] | ||
| + if file != "" && file[0] != '/' { | ||
| + next.ServeHTTPC(c, w, r) | ||
| + return | ||
| + } | ||
| + } | ||
| + f, err := s.Dir.Open(file) | ||
| + if err != nil { | ||
| + // discard the error? | ||
| + next.ServeHTTPC(c, w, r) | ||
| + return | ||
| + } | ||
| + defer f.Close() | ||
| + | ||
| + fi, err := f.Stat() | ||
| + if err != nil { | ||
| + next.ServeHTTPC(c, w, r) | ||
| + return | ||
| + } | ||
| + | ||
| + // try to serve index file | ||
| + if fi.IsDir() { | ||
| + // redirect if missing trailing slash | ||
| + if !strings.HasSuffix(r.URL.Path, "/") { | ||
| + http.Redirect(w, r, r.URL.Path+"/", http.StatusFound) | ||
| + return | ||
| + } | ||
| + | ||
| + file = path.Join(file, s.IndexFile) | ||
| + f, err = s.Dir.Open(file) | ||
| + if err != nil { | ||
| + next.ServeHTTPC(c, w, r) | ||
| + return | ||
| + } | ||
| + defer f.Close() | ||
| + | ||
| + fi, err = f.Stat() | ||
| + if err != nil || fi.IsDir() { | ||
| + next.ServeHTTPC(c, w, r) | ||
| + return | ||
| + } | ||
| + } | ||
| + | ||
| + http.ServeContent(w, r, file, fi.ModTime(), f) | ||
| + }) | ||
| +} | ||
| + | ||
| +// MaxAge is a middleware that defines the max duration headers | ||
| +func MaxAge(dur time.Duration) Middleware { | ||
| + return MaxAgeWithFilter(dur, func(c context.Context, w http.ResponseWriter, r *http.Request) bool { return true }) | ||
| +} | ||
| + | ||
| +// MaxAgeWithFilter is a middleware that defines the max duration headers with a filter function. | ||
| +// If the filter returns true then the headers will be set. Otherwise, if it returns false the headers will not be set. | ||
| +func MaxAgeWithFilter(dur time.Duration, filter func(c context.Context, w http.ResponseWriter, r *http.Request) bool) Middleware { | ||
| + if filter == nil { | ||
| + filter = func(c context.Context, w http.ResponseWriter, r *http.Request) bool { return false } | ||
| + } | ||
| + return MiddlewareFunc(func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + if !filter(c, w, r) { | ||
| + return | ||
| + } | ||
| + w.Header().Add("Cache-Control", fmt.Sprintf("max-age=%d, public, must-revalidate, proxy-revalidate", int(dur.Seconds()))) | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| + }) | ||
| +} | ||
| + | ||
| +// NoCache middleware sets headers to disable browser caching. | ||
| +// Inspired by https://github.com/mytrile/nocache | ||
| +func NoCache() Middleware { | ||
| + var epoch = time.Unix(0, 0).Format(time.RFC1123) | ||
| + | ||
| + return noCache{ | ||
| + responseHeaders: map[string]string{ | ||
| + "Expires": epoch, | ||
| + "Cache-Control": "no-cache, private, must-revalidate, max-age=0", | ||
| + "Pragma": "no-cache", | ||
| + "X-Accel-Expires": "0", | ||
| + }, | ||
| + etagHeaders: []string{ | ||
| + "ETag", | ||
| + "If-Modified-Since", | ||
| + "If-Match", | ||
| + "If-Note-Match", | ||
| + "If-Range", | ||
| + "If-Unmodified-Since", | ||
| + }, | ||
| + } | ||
| +} | ||
| + | ||
| +type noCache struct { | ||
| + responseHeaders map[string]string | ||
| + etagHeaders []string | ||
| +} | ||
| + | ||
| +func (n noCache) ServeNext(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + // Delete ETag headers | ||
| + for _, v := range n.etagHeaders { | ||
| + if r.Header.Get(v) != "" { | ||
| + r.Header.Del(v) | ||
| + } | ||
| + } | ||
| + | ||
| + // Add nocache headers | ||
| + for k, v := range n.responseHeaders { | ||
| + w.Header().Set(k, v) | ||
| + } | ||
| + | ||
| + }) | ||
| +} | ||
| + | ||
| +var xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") | ||
| +var xRealIP = http.CanonicalHeaderKey("X-Real-IP") | ||
| + | ||
| +// RealIP is a middleware that sets a http.Request's RemoteAddr to the results | ||
| +// of parsing either the X-Forwarded-For header or the X-Real-IP header (in that | ||
| +// order). | ||
| +// | ||
| +// This middleware should be inserted fairly early in the middleware stack to | ||
| +// ensure that subsequent layers (e.g., request loggers) which examine the | ||
| +// RemoteAddr will see the intended value. | ||
| +// | ||
| +// You should only use this middleware if you can trust the headers passed to | ||
| +// you (in particular, the two headers this middleware uses), for example | ||
| +// because you have placed a reverse proxy like HAProxy or nginx in front of | ||
| +// Goji. If your reverse proxies are configured to pass along arbitrary header | ||
| +// values from the client, or if you use this middleware without a reverse | ||
| +// proxy, malicious clients will be able to make you very sad (or, depending on | ||
| +// how you're using RemoteAddr, vulnerable to an attack of some sort). | ||
| +// Taken from https://github.com/zenazn/goji/blob/master/web/middleware/realip.go | ||
| +func RealIP() Middleware { | ||
| + return MiddlewareFunc(func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + if rip := realIP(r); rip != "" { | ||
| + r.RemoteAddr = rip | ||
| + } | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| + }) | ||
| +} | ||
| + | ||
| +func realIP(r *http.Request) string { | ||
| + var ip string | ||
| + | ||
| + if xff := r.Header.Get(xForwardedFor); xff != "" { | ||
| + i := strings.Index(xff, ", ") | ||
| + if i == -1 { | ||
| + i = len(xff) | ||
| + } | ||
| + ip = xff[:i] | ||
| + } else if xrip := r.Header.Get(xRealIP); xrip != "" { | ||
| + ip = xrip | ||
| + } | ||
| + | ||
| + return ip | ||
| +} |
33
module.go
| @@ -0,0 +1,33 @@ | ||
| +package lion | ||
| + | ||
| +type Module interface { | ||
| + Resource | ||
| + Base() string | ||
| + Routes(*Router) | ||
| +} | ||
| + | ||
| +type ModuleRequirements interface { | ||
| + Requires() []string | ||
| +} | ||
| + | ||
| +func (r *Router) Module(modules ...Module) { | ||
| + for _, m := range modules { | ||
| + r.registerModule(m) | ||
| + } | ||
| +} | ||
| + | ||
| +func (r *Router) registerModule(m Module) { | ||
| + g := r.Group(m.Base()) | ||
| + if req, ok := m.(ModuleRequirements); ok { | ||
| + for _, dep := range req.Requires() { | ||
| + if !r.hasNamed(dep) { | ||
| + panic("Unmet middleware requirement for " + dep) | ||
| + } | ||
| + g.UseNamed(dep) | ||
| + } | ||
| + } | ||
| + | ||
| + g.Resource("/", m) | ||
| + | ||
| + m.Routes(g) | ||
| +} |
55
module_test.go
| @@ -0,0 +1,55 @@ | ||
| +package lion | ||
| + | ||
| +import ( | ||
| + "net/http" | ||
| + "testing" | ||
| + | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +type testmodule struct { | ||
| + base string | ||
| +} | ||
| + | ||
| +func (m testmodule) Routes(r *Router) { | ||
| + | ||
| +} | ||
| + | ||
| +func (m testmodule) Base() string { | ||
| + return m.base | ||
| +} | ||
| + | ||
| +func (m testmodule) Requires() []string { | ||
| + return []string{"auth", "jwt"} | ||
| +} | ||
| + | ||
| +func (m testmodule) Uses() (mws Middlewares) { | ||
| + return mws | ||
| +} | ||
| + | ||
| +func (m testmodule) Get(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Write([]byte("getmodule")) | ||
| +} | ||
| + | ||
| +func TestModule(t *testing.T) { | ||
| + l := New() | ||
| + l.DefineFunc("auth", func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Header().Set("auth", "authmw") | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| + }) | ||
| + | ||
| + l.DefineFunc("jwt", func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Header().Set("token", "jwtmw") | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| + }) | ||
| + | ||
| + l.Module(testmodule{"/admin"}) | ||
| + | ||
| + expectHeader(t, l, "GET", "/admin", "auth", "authmw") | ||
| + expectHeader(t, l, "GET", "/admin", "token", "jwtmw") | ||
| + expectBody(t, l, "GET", "/admin", "getmodule") | ||
| +} |
97
resource.go
| @@ -0,0 +1,97 @@ | ||
| +package lion | ||
| + | ||
| +import ( | ||
| + "net/http" | ||
| + | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +// Resource defines the minimum required methods | ||
| +type Resource interface{} | ||
| + | ||
| +type ResourceUses interface { | ||
| + Uses() Middlewares | ||
| +} | ||
| + | ||
| +// GetResourceMiddlewares is an interface for defining middlewares used in Resource method | ||
| +type GetResourceMiddlewares interface { | ||
| + GetMiddlewares() Middlewares | ||
| +} | ||
| + | ||
| +// PostResourceMiddlewares is an interface for defining middlewares used in Resource method | ||
| +type PostResourceMiddlewares interface { | ||
| + PostMiddlewares() Middlewares | ||
| +} | ||
| + | ||
| +// PutResourceMiddlewares is an interface for defining middlewares used in Resource method | ||
| +type PutResourceMiddlewares interface { | ||
| + PutMiddlewares() Middlewares | ||
| +} | ||
| + | ||
| +// DeleteResourceMiddlewares is an interface for defining middlewares used in Resource method | ||
| +type DeleteResourceMiddlewares interface { | ||
| + DeleteMiddlewares() Middlewares | ||
| +} | ||
| + | ||
| +// GetResource is an interface for defining a HandlerFunc used in Resource method | ||
| +type GetResource interface { | ||
| + Get(c context.Context, w http.ResponseWriter, r *http.Request) | ||
| +} | ||
| + | ||
| +// PostResource is an interface for defining a HandlerFunc used in Resource method | ||
| +type PostResource interface { | ||
| + Post(c context.Context, w http.ResponseWriter, r *http.Request) | ||
| +} | ||
| + | ||
| +// PutResource is an interface for defining a HandlerFunc used in Resource method | ||
| +type PutResource interface { | ||
| + Put(c context.Context, w http.ResponseWriter, r *http.Request) | ||
| +} | ||
| + | ||
| +// DeleteResource is an interface for defining a HandlerFunc used in Resource method | ||
| +type DeleteResource interface { | ||
| + Delete(c context.Context, w http.ResponseWriter, r *http.Request) | ||
| +} | ||
| + | ||
| +// Resource registers a Resource with the corresponding pattern | ||
| +func (r *Router) Resource(pattern string, resource Resource) { | ||
| + sub := r.Group(pattern) | ||
| + | ||
| + if usesRes, ok := resource.(ResourceUses); ok { | ||
| + if len(usesRes.Uses()) > 0 { | ||
| + sub.Use(usesRes.Uses()...) | ||
| + } | ||
| + } | ||
| + | ||
| + if res, ok := resource.(GetResource); ok { | ||
| + s := sub.Group("/") | ||
| + if mw, ok := resource.(GetResourceMiddlewares); ok { | ||
| + s.Use(mw.GetMiddlewares()...) | ||
| + } | ||
| + s.GetFunc("/", res.Get) | ||
| + } | ||
| + | ||
| + if res, ok := resource.(PostResource); ok { | ||
| + s := sub.Group("/") | ||
| + if mw, ok := resource.(PostResourceMiddlewares); ok { | ||
| + s.Use(mw.PostMiddlewares()...) | ||
| + } | ||
| + s.PostFunc("/", res.Post) | ||
| + } | ||
| + | ||
| + if res, ok := resource.(PutResource); ok { | ||
| + s := sub.Group("/") | ||
| + if mw, ok := resource.(PutResourceMiddlewares); ok { | ||
| + s.Use(mw.PutMiddlewares()...) | ||
| + } | ||
| + s.PutFunc("/", res.Put) | ||
| + } | ||
| + | ||
| + if res, ok := resource.(DeleteResource); ok { | ||
| + s := sub.Group("/") | ||
| + if mw, ok := resource.(DeleteResourceMiddlewares); ok { | ||
| + s.Use(mw.DeleteMiddlewares()...) | ||
| + } | ||
| + s.DeleteFunc("/", res.Delete) | ||
| + } | ||
| +} |
85
resource_test.go
| @@ -0,0 +1,85 @@ | ||
| +package lion | ||
| + | ||
| +import ( | ||
| + "net/http" | ||
| + "net/http/httptest" | ||
| + "testing" | ||
| + | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +type testResource struct{} | ||
| + | ||
| +func (tr testResource) Uses() Middlewares { return Middlewares{} } | ||
| + | ||
| +func (tr testResource) GetMiddlewares() Middlewares { | ||
| + return Middlewares{MiddlewareFunc(func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Header().Set("foo", "Get") | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| + })} | ||
| +} | ||
| + | ||
| +func (tr testResource) PostMiddlewares() Middlewares { | ||
| + return Middlewares{MiddlewareFunc(func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Header().Set("foo", "Post") | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| + })} | ||
| +} | ||
| +func (tr testResource) PutMiddlewares() Middlewares { | ||
| + return Middlewares{MiddlewareFunc(func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Header().Set("foo", "Put") | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| + })} | ||
| +} | ||
| +func (tr testResource) DeleteMiddlewares() Middlewares { | ||
| + return Middlewares{MiddlewareFunc(func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Header().Set("foo", "Delete") | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| + })} | ||
| +} | ||
| + | ||
| +func (tr testResource) Get(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Write([]byte("Get")) | ||
| +} | ||
| +func (tr testResource) Post(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Write([]byte("Post")) | ||
| +} | ||
| +func (tr testResource) Put(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Write([]byte("Put")) | ||
| +} | ||
| +func (tr testResource) Delete(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Write([]byte("Delete")) | ||
| +} | ||
| + | ||
| +func TestResources(t *testing.T) { | ||
| + methods := []string{"GET", "POST", "PUT", "DELETE"} | ||
| + expected := []string{"Get", "Post", "Put", "Delete"} | ||
| + tr := testResource{} | ||
| + // hfuncs := []HandlerFunc{tr.Get, tr.Post, tr.Put, tr.Delete} | ||
| + | ||
| + r := New() | ||
| + r.Resource("/testpath", tr) | ||
| + | ||
| + for i := 0; i < len(methods); i++ { | ||
| + w := httptest.NewRecorder() | ||
| + req, _ := http.NewRequest(methods[i], "/testpath", nil) | ||
| + | ||
| + r.ServeHTTP(w, req) | ||
| + | ||
| + if w.Body.String() != expected[i] { | ||
| + t.Errorf("[Resource] Expected body %s but got %s for http method %s", expected[i], w.Body.String(), methods[i]) | ||
| + } | ||
| + | ||
| + if w.Header().Get("foo") != expected[i] { | ||
| + t.Errorf("[Resource] Expected header %s but got %s for http method %s", expected[i], w.Header().Get("foo"), methods[i]) | ||
| + } | ||
| + } | ||
| +} |
105
response_writer.go
| @@ -0,0 +1,105 @@ | ||
| +package lion | ||
| + | ||
| +import ( | ||
| + "bufio" | ||
| + "fmt" | ||
| + "io" | ||
| + "net" | ||
| + "net/http" | ||
| +) | ||
| + | ||
| +// ResponseWriter is the proxy responseWriter | ||
| +type ResponseWriter interface { | ||
| + http.ResponseWriter | ||
| + http.Flusher | ||
| + http.Hijacker | ||
| + Status() int | ||
| + BytesWritten() int | ||
| + Tee(io.Writer) | ||
| + Unwrap() http.ResponseWriter | ||
| +} | ||
| + | ||
| +// WrapResponseWriter wraps an http.ResponseWriter and returns a ResponseWriter | ||
| +func WrapResponseWriter(w http.ResponseWriter) ResponseWriter { | ||
| + return &basicWriter{ResponseWriter: w} | ||
| +} | ||
| + | ||
| +var _ ResponseWriter = (*basicWriter)(nil) | ||
| +var _ http.ResponseWriter = (*basicWriter)(nil) | ||
| + | ||
| +type basicWriter struct { | ||
| + http.ResponseWriter | ||
| + code int | ||
| + bytes int | ||
| + tee io.Writer | ||
| +} | ||
| + | ||
| +func (b *basicWriter) Header() http.Header { | ||
| + return b.ResponseWriter.Header() | ||
| +} | ||
| + | ||
| +func (b *basicWriter) WriteHeader(code int) { | ||
| + if !b.Written() { | ||
| + b.ResponseWriter.WriteHeader(code) | ||
| + b.code = code | ||
| + } | ||
| +} | ||
| + | ||
| +func (b *basicWriter) Write(data []byte) (int, error) { | ||
| + if !b.Written() { | ||
| + b.WriteHeader(http.StatusOK) | ||
| + } | ||
| + size, err := b.ResponseWriter.Write(data) | ||
| + b.bytes += size | ||
| + return size, err | ||
| +} | ||
| + | ||
| +func (b *basicWriter) Written() bool { | ||
| + return b.Status() != 0 | ||
| +} | ||
| + | ||
| +func (b *basicWriter) BytesWritten() int { | ||
| + return b.bytes | ||
| +} | ||
| + | ||
| +func (b *basicWriter) Status() int { | ||
| + return b.code | ||
| +} | ||
| + | ||
| +func (b *basicWriter) Tee(w io.Writer) { | ||
| + b.tee = w | ||
| +} | ||
| + | ||
| +func (b *basicWriter) Unwrap() http.ResponseWriter { | ||
| + return b.ResponseWriter | ||
| +} | ||
| + | ||
| +func (b *basicWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { | ||
| + hijacker, ok := b.ResponseWriter.(http.Hijacker) | ||
| + if !ok { | ||
| + return nil, nil, fmt.Errorf("the ResponseWriter doesn't support the Hijacker interface") | ||
| + } | ||
| + return hijacker.Hijack() | ||
| +} | ||
| + | ||
| +func (b *basicWriter) CloseNotify() <-chan bool { | ||
| + return b.ResponseWriter.(http.CloseNotifier).CloseNotify() | ||
| +} | ||
| + | ||
| +func (b *basicWriter) Flush() { | ||
| + fl, ok := b.ResponseWriter.(http.Flusher) | ||
| + if ok { | ||
| + fl.Flush() | ||
| + } | ||
| +} | ||
| + | ||
| +func (b *basicWriter) ReadFrom(r io.Reader) (int64, error) { | ||
| + if b.tee != nil { | ||
| + return io.Copy(b, r) | ||
| + } | ||
| + rf := b.ResponseWriter.(io.ReaderFrom) | ||
| + if !b.Written() { | ||
| + b.ResponseWriter.WriteHeader(http.StatusOK) | ||
| + } | ||
| + return rf.ReadFrom(r) | ||
| +} |
343
router.go
| @@ -0,0 +1,343 @@ | ||
| +package lion | ||
| + | ||
| +import ( | ||
| + "fmt" | ||
| + "net/http" | ||
| + "os" | ||
| + "sync" | ||
| + | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +// Router is responsible for registering handlers and middlewares | ||
| +type Router struct { | ||
| + tree *tree | ||
| + rm RegisterMatcher | ||
| + | ||
| + router *Router | ||
| + | ||
| + middlewares Middlewares | ||
| + | ||
| + handler Handler // TODO: create a handler | ||
| + | ||
| + pattern string | ||
| + | ||
| + notFoundHandler Handler | ||
| + | ||
| + registeredHandlers []registeredHandler // Used for Mount() | ||
| + | ||
| + pool sync.Pool | ||
| + | ||
| + namedMiddlewares map[string]Middlewares | ||
| +} | ||
| + | ||
| +// New creates a new router instance | ||
| +func New(mws ...Middleware) *Router { | ||
| + r := &Router{ | ||
| + middlewares: Middlewares{}, | ||
| + rm: newRadixMatcher(), | ||
| + namedMiddlewares: make(map[string]Middlewares), | ||
| + } | ||
| + r.pool.New = func() interface{} { | ||
| + return NewContext() | ||
| + } | ||
| + r.router = r | ||
| + r.Use(mws...) | ||
| + return r | ||
| +} | ||
| + | ||
| +// Group creates a subrouter with parent pattern provided. | ||
| +func (r *Router) Group(pattern string, mws ...Middleware) *Router { | ||
| + p := r.pattern + pattern | ||
| + if pattern == "/" && r.pattern != "/" { | ||
| + p = r.pattern | ||
| + } | ||
| + validatePattern(p) | ||
| + | ||
| + nr := &Router{ | ||
| + router: r, | ||
| + rm: r.rm, | ||
| + pattern: p, | ||
| + middlewares: Middlewares{}, | ||
| + namedMiddlewares: make(map[string]Middlewares), | ||
| + } | ||
| + nr.Use(mws...) | ||
| + return nr | ||
| +} | ||
| + | ||
| +// Get registers an http GET method receiver with the provided Handler | ||
| +func (r *Router) Get(pattern string, handler Handler) { | ||
| + r.Handle("GET", pattern, handler) | ||
| +} | ||
| + | ||
| +// Post registers an http POST method receiver with the provided Handler | ||
| +func (r *Router) Post(pattern string, handler Handler) { | ||
| + r.Handle("POST", pattern, handler) | ||
| +} | ||
| + | ||
| +// Put registers an http PUT method receiver with the provided Handler | ||
| +func (r *Router) Put(pattern string, handler Handler) { | ||
| + r.Handle("PUT", pattern, handler) | ||
| +} | ||
| + | ||
| +// Patch registers an http PATCH method receiver with the provided Handler | ||
| +func (r *Router) Patch(pattern string, handler Handler) { | ||
| + r.Handle("PATCH", pattern, handler) | ||
| +} | ||
| + | ||
| +// Delete registers an http DELETE method receiver with the provided Handler | ||
| +func (r *Router) Delete(pattern string, handler Handler) { | ||
| + r.Handle("DELETE", pattern, handler) | ||
| +} | ||
| + | ||
| +// GetFunc wraps a HandlerFunc as a Handler and registers it to the router | ||
| +func (r *Router) GetFunc(pattern string, fn HandlerFunc) { | ||
| + r.Get(pattern, HandlerFunc(fn)) | ||
| +} | ||
| + | ||
| +// PostFunc wraps a HandlerFunc as a Handler and registers it to the router | ||
| +func (r *Router) PostFunc(pattern string, fn HandlerFunc) { | ||
| + r.Post(pattern, HandlerFunc(fn)) | ||
| +} | ||
| + | ||
| +// PutFunc wraps a HandlerFunc as a Handler and registers it to the router | ||
| +func (r *Router) PutFunc(pattern string, fn HandlerFunc) { | ||
| + r.Put(pattern, HandlerFunc(fn)) | ||
| +} | ||
| + | ||
| +// PatchFunc wraps a HandlerFunc as a Handler and registers it to the router | ||
| +func (r *Router) PatchFunc(pattern string, fn HandlerFunc) { | ||
| + r.Patch(pattern, HandlerFunc(fn)) | ||
| +} | ||
| + | ||
| +// DeleteFunc wraps a HandlerFunc as a Handler and registers it to the router | ||
| +func (r *Router) DeleteFunc(pattern string, fn HandlerFunc) { | ||
| + r.Delete(pattern, HandlerFunc(fn)) | ||
| +} | ||
| + | ||
| +// Use registers middlewares to be used | ||
| +func (r *Router) Use(middlewares ...Middleware) { | ||
| + r.middlewares = append(r.middlewares, middlewares...) | ||
| +} | ||
| + | ||
| +// UseFunc wraps a MiddlewareFunc as a Middleware and registers it middlewares to be used | ||
| +func (r *Router) UseFunc(middlewareFuncs ...MiddlewareFunc) { | ||
| + for _, fn := range middlewareFuncs { | ||
| + r.Use(MiddlewareFunc(fn)) | ||
| + } | ||
| +} | ||
| + | ||
| +type negroniHandler interface { | ||
| + ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) | ||
| +} | ||
| + | ||
| +type negroniHandlerFunc func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) | ||
| + | ||
| +func (h negroniHandlerFunc) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { | ||
| + h(rw, r, next) | ||
| +} | ||
| + | ||
| +func (r *Router) UseNegroni(n negroniHandler) { | ||
| + r.Use(MiddlewareFunc(func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + n.ServeHTTP(w, r, HandlerFunc(func(_ context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }).ServeHTTP) | ||
| + }) | ||
| + })) | ||
| +} | ||
| + | ||
| +func (r *Router) UseNegroniFunc(n func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc)) { | ||
| + r.UseNegroni(negroniHandlerFunc(n)) | ||
| +} | ||
| + | ||
| +// UseHandler uses | ||
| +func (r *Router) UseHandler(handler Handler) { | ||
| + r.UseFunc(func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + handler.ServeHTTPC(c, w, r) | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| + }) | ||
| +} | ||
| + | ||
| +// UseHandlerFunc uses | ||
| +func (r *Router) UseHandlerFunc(fn HandlerFunc) { | ||
| + r.UseHandler(HandlerFunc(fn)) | ||
| +} | ||
| + | ||
| +// Handle is the underling method responsible for registering a handler for a specific method and pattern. | ||
| +func (r *Router) Handle(method, pattern string, handler Handler) { | ||
| + validatePattern(pattern) | ||
| + | ||
| + var p string | ||
| + if !r.isRoot() && pattern == "/" { | ||
| + p = r.pattern | ||
| + } else { | ||
| + p = r.pattern + pattern | ||
| + } | ||
| + | ||
| + built := r.buildMiddlewares(handler) | ||
| + r.registeredHandlers = append(r.registeredHandlers, registeredHandler{method, pattern, built}) | ||
| + r.router.rm.Register(method, p, built) | ||
| +} | ||
| + | ||
| +type registeredHandler struct { | ||
| + method, pattern string | ||
| + handler Handler | ||
| +} | ||
| + | ||
| +// Mount mounts a subrouter at the provided pattern | ||
| +func (r *Router) Mount(pattern string, router *Router, mws ...Middleware) { | ||
| + sub := r.Group(pattern, mws...) | ||
| + for _, rh := range router.registeredHandlers { | ||
| + sub.Handle(rh.method, rh.pattern, rh.handler) | ||
| + } | ||
| +} | ||
| + | ||
| +func (r *Router) buildMiddlewares(handler Handler) Handler { | ||
| + handler = r.middlewares.BuildHandler(handler) | ||
| + if !r.isRoot() { | ||
| + handler = r.router.buildMiddlewares(handler) | ||
| + } | ||
| + return handler | ||
| +} | ||
| + | ||
| +func (r *Router) isRoot() bool { | ||
| + return r.router == r | ||
| +} | ||
| + | ||
| +// HandleFunc wraps a HandlerFunc and pass it to Handle method | ||
| +func (r *Router) HandleFunc(method, pattern string, fn HandlerFunc) { | ||
| + r.Handle(method, pattern, HandlerFunc(fn)) | ||
| +} | ||
| + | ||
| +// ServeHTTP calls ServeHTTPC with a context.Background() | ||
| +func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||
| + r.ServeHTTPC(context.TODO(), w, req) | ||
| +} | ||
| + | ||
| +// ServeHTTPC finds the handler associated with the request's path. | ||
| +// If it is not found it calls the NotFound handler | ||
| +func (r *Router) ServeHTTPC(c context.Context, w http.ResponseWriter, req *http.Request) { | ||
| + ctx := r.pool.Get().(*Context) | ||
| + ctx.parent = c | ||
| + | ||
| + if ctx, h := r.router.rm.Match(ctx, req); h != nil { | ||
| + h.ServeHTTPC(ctx, w, req) | ||
| + } else { | ||
| + r.notFound(ctx, w, req) // r.middlewares.BuildHandler(HandlerFunc(r.NotFound)).ServeHTTPC | ||
| + } | ||
| + | ||
| + ctx.reset() | ||
| + r.pool.Put(ctx) | ||
| +} | ||
| + | ||
| +// NotFound calls NotFoundHandler() if it is set. Otherwise, it calls net/http.NotFound | ||
| +func (r *Router) notFound(c context.Context, w http.ResponseWriter, req *http.Request) { | ||
| + if r.router.notFoundHandler != nil { | ||
| + r.router.notFoundHandler.ServeHTTPC(c, w, req) | ||
| + } else { | ||
| + http.NotFound(w, req) | ||
| + } | ||
| +} | ||
| + | ||
| +func (r *Router) NotFoundHandler(handler Handler) { | ||
| + r.notFoundHandler = handler | ||
| +} | ||
| + | ||
| +// ServeFiles serves files located in root http.FileSystem | ||
| +// | ||
| +// This can be used as shown below: | ||
| +// r := New() | ||
| +// r.ServeFiles("/static", http.Dir("static")) // This will serve files in the directory static with /static prefix | ||
| +func (r *Router) ServeFiles(path string, root http.FileSystem) { | ||
| + fileServer := http.FileServer(root) | ||
| + r.Get(path, HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + fmt.Println("rurl", r.URL.Path) | ||
| + fileServer.ServeHTTP(w, r) | ||
| + })) | ||
| +} | ||
| + | ||
| +// GetH wraps a http.Handler | ||
| +func (r *Router) GetH(pattern string, handler http.Handler) { | ||
| + r.Get(pattern, Wrap(handler)) | ||
| +} | ||
| + | ||
| +// PostH wraps a http.Handler | ||
| +func (r *Router) PostH(pattern string, handler http.Handler) { | ||
| + r.Post(pattern, Wrap(handler)) | ||
| +} | ||
| + | ||
| +// PutH wraps a http.Handler | ||
| +func (r *Router) PutH(pattern string, handler http.Handler) { | ||
| + r.Put(pattern, Wrap(handler)) | ||
| +} | ||
| + | ||
| +// DeleteH wraps a http.Handler | ||
| +func (r *Router) DeleteH(pattern string, handler http.Handler) { | ||
| + r.Delete(pattern, Wrap(handler)) | ||
| +} | ||
| + | ||
| +// Run calls http.ListenAndServe for the current router. | ||
| +// If no addresses are specified as arguments, it will use the PORT environnement variable if it is defined. Otherwise, it will listen on port 3000 of the localmachine | ||
| +// | ||
| +// r := New() | ||
| +// r.Run() // will call | ||
| +// r.Run(":8080") | ||
| +func (r *Router) Run(addr ...string) { | ||
| + var a string | ||
| + | ||
| + if len(addr) == 0 { | ||
| + if p := os.Getenv("PORT"); p != "" { | ||
| + a = p | ||
| + } else { | ||
| + a = ":3000" | ||
| + } | ||
| + } else { | ||
| + a = addr[0] | ||
| + } | ||
| + | ||
| + lionLogger.Printf("listening on %s", a) | ||
| + lionLogger.Fatal(http.ListenAndServe(a, r)) | ||
| +} | ||
| + | ||
| +// RunTLS calls http.ListenAndServeTLS for the current router | ||
| +// | ||
| +// r := New() | ||
| +// r.RunTLS(":3443", "cert.pem", "key.pem") | ||
| +func (r *Router) RunTLS(addr, certFile, keyFile string) { | ||
| + lionLogger.Printf("listening on %s", addr) | ||
| + lionLogger.Fatal(http.ListenAndServeTLS(addr, certFile, keyFile, r)) | ||
| +} | ||
| + | ||
| +func (r *Router) Define(name string, mws ...Middleware) { | ||
| + r.namedMiddlewares[name] = append(r.namedMiddlewares[name], mws...) | ||
| +} | ||
| + | ||
| +func (r *Router) DefineFunc(name string, mws ...MiddlewareFunc) { | ||
| + for _, mw := range mws { | ||
| + r.Define(name, mw) | ||
| + } | ||
| +} | ||
| + | ||
| +func (r *Router) UseNamed(name string) { | ||
| + if r.hasNamed(name) { // Find if it this is registered in the current router | ||
| + r.Use(r.namedMiddlewares[name]...) | ||
| + } else if !r.isRoot() { // Otherwise, look for it in parent router. | ||
| + r.router.UseNamed(name) | ||
| + } else { // not found | ||
| + panic("Unknow named middlewares: " + name) | ||
| + } | ||
| +} | ||
| + | ||
| +func (r *Router) hasNamed(name string) bool { | ||
| + _, exist := r.namedMiddlewares[name] | ||
| + return exist | ||
| +} | ||
| + | ||
| +func validatePattern(pattern string) { | ||
| + if len(pattern) > 0 && pattern[0] != '/' { | ||
| + panic("path must start with '/' in path '" + pattern + "'") | ||
| + } | ||
| +} |
347
router_test.go
| @@ -0,0 +1,347 @@ | ||
| +package lion | ||
| + | ||
| +import ( | ||
| + "fmt" | ||
| + "net/http" | ||
| + "net/http/httptest" | ||
| + "testing" | ||
| + | ||
| + "github.com/stretchr/testify/assert" | ||
| + | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +var ( | ||
| + emptyParams = map[string]string{} | ||
| +) | ||
| + | ||
| +func TestRouteMatching(t *testing.T) { | ||
| + helloHandler := HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {}) | ||
| + helloNameHandler := HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {}) | ||
| + helloNameTweetsHandler := HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {}) | ||
| + helloNameGetTweetHandler := HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {}) | ||
| + cartsHandler := HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {}) | ||
| + getCartHandler := HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {}) | ||
| + helloContactHandler := HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {}) | ||
| + helloContactByPersonHandler := HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {}) | ||
| + extensionHandler := HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {}) | ||
| + usernameHandler := HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {}) | ||
| + wildcardHandler := HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {}) | ||
| + | ||
| + routes := []struct { | ||
| + Method string | ||
| + Pattern string | ||
| + Handler Handler | ||
| + }{ | ||
| + { | ||
| + Method: "GET", | ||
| + Pattern: "/hello", | ||
| + Handler: helloHandler, | ||
| + }, | ||
| + { | ||
| + Method: "GET", | ||
| + Pattern: "/hello/contact", | ||
| + Handler: helloContactHandler, | ||
| + }, | ||
| + { | ||
| + Method: "GET", | ||
| + Pattern: "/hello/:name", | ||
| + Handler: helloNameHandler, | ||
| + }, | ||
| + { | ||
| + Method: "GET", | ||
| + Pattern: "/hello/:name/tweets", | ||
| + Handler: helloNameTweetsHandler, | ||
| + }, | ||
| + { | ||
| + Method: "GET", | ||
| + Pattern: "/hello/:name/tweets/:id", | ||
| + Handler: helloNameGetTweetHandler, | ||
| + }, | ||
| + { | ||
| + Method: "GET", | ||
| + Pattern: "/carts", | ||
| + Handler: cartsHandler, | ||
| + }, | ||
| + { | ||
| + Method: "GET", | ||
| + Pattern: "/carts/:cartid", | ||
| + Handler: getCartHandler, | ||
| + }, | ||
| + { | ||
| + Method: "GET", | ||
| + Pattern: "/hello/contact/:dest", | ||
| + Handler: helloContactByPersonHandler, | ||
| + }, | ||
| + { | ||
| + Method: "GET", | ||
| + Pattern: "/extension/:file.:ext", | ||
| + Handler: extensionHandler, | ||
| + }, | ||
| + { | ||
| + Method: "GET", | ||
| + Pattern: "/@:username", | ||
| + Handler: usernameHandler, | ||
| + }, | ||
| + { | ||
| + Method: "GET", | ||
| + Pattern: "/*", | ||
| + Handler: wildcardHandler, | ||
| + }, | ||
| + } | ||
| + | ||
| + tests := []struct { | ||
| + Input string | ||
| + ExpectedHandler Handler | ||
| + ExpectedParams map[string]string | ||
| + }{ | ||
| + { | ||
| + Input: "/hello", | ||
| + ExpectedHandler: helloHandler, | ||
| + ExpectedParams: emptyParams, | ||
| + }, | ||
| + { | ||
| + Input: "/hello/batman", | ||
| + ExpectedHandler: helloNameHandler, | ||
| + ExpectedParams: map[string]string{"name": "batman"}, | ||
| + }, | ||
| + { | ||
| + Input: "/hello/dot.inthemiddle", | ||
| + ExpectedHandler: helloNameHandler, | ||
| + ExpectedParams: map[string]string{"name": "dot.inthemiddle"}, | ||
| + }, | ||
| + { | ||
| + Input: "/hello/batman/tweets", | ||
| + ExpectedHandler: helloNameTweetsHandler, | ||
| + ExpectedParams: map[string]string{"name": "batman"}, | ||
| + }, | ||
| + { | ||
| + Input: "/hello/batman/tweets/123", | ||
| + ExpectedHandler: helloNameGetTweetHandler, | ||
| + ExpectedParams: map[string]string{"name": "batman", "id": "123"}, | ||
| + }, | ||
| + { | ||
| + Input: "/carts", | ||
| + ExpectedHandler: cartsHandler, | ||
| + ExpectedParams: emptyParams, | ||
| + }, | ||
| + { | ||
| + Input: "/carts/123456", | ||
| + ExpectedHandler: getCartHandler, | ||
| + ExpectedParams: map[string]string{"cartid": "123456"}, | ||
| + }, | ||
| + { | ||
| + Input: "/hello/contact", | ||
| + ExpectedHandler: helloContactHandler, | ||
| + ExpectedParams: emptyParams, | ||
| + }, | ||
| + { | ||
| + Input: "/hello/contact/batman", | ||
| + ExpectedHandler: helloContactByPersonHandler, | ||
| + ExpectedParams: map[string]string{"dest": "batman"}, | ||
| + }, | ||
| + { | ||
| + Input: "/extension/batman.jpg", | ||
| + ExpectedHandler: extensionHandler, | ||
| + ExpectedParams: map[string]string{"file": "batman", "ext": "jpg"}, | ||
| + }, | ||
| + { | ||
| + Input: "/@celrenheit", | ||
| + ExpectedHandler: usernameHandler, | ||
| + ExpectedParams: map[string]string{"username": "celrenheit"}, | ||
| + }, | ||
| + { | ||
| + Input: "/unkownpath/subfolder", | ||
| + ExpectedHandler: wildcardHandler, | ||
| + ExpectedParams: map[string]string{"*": "unkownpath/subfolder"}, | ||
| + }, | ||
| + } | ||
| + | ||
| + mux := New() | ||
| + for _, r := range routes { | ||
| + mux.Handle(r.Method, r.Pattern, r.Handler) | ||
| + } | ||
| + | ||
| + for _, test := range tests { | ||
| + req, _ := http.NewRequest("GET", test.Input, nil) | ||
| + | ||
| + c, h := mux.rm.Match(&Context{ | ||
| + parent: context.TODO(), | ||
| + }, req) | ||
| + | ||
| + // Compare params | ||
| + for k, v := range test.ExpectedParams { | ||
| + assert.NotNil(t, c.Value(k)) | ||
| + actual := c.Value(k).(string) | ||
| + if actual != v { | ||
| + t.Errorf("Expected %s but got %s for url: %s", green(v), red(actual), test.Input) | ||
| + } | ||
| + } | ||
| + | ||
| + // Compare handlers | ||
| + if fmt.Sprintf("%v", h) != fmt.Sprintf("%v", test.ExpectedHandler) { | ||
| + t.Errorf("Handler not match for %s", test.Input) | ||
| + } | ||
| + | ||
| + w := httptest.NewRecorder() | ||
| + | ||
| + mux.ServeHTTP(w, req) | ||
| + | ||
| + // Compare response code | ||
| + if w.Code != http.StatusOK { | ||
| + t.Errorf("Response should be 200 OK for %s", test.Input) | ||
| + } | ||
| + } | ||
| +} | ||
| + | ||
| +type testmw struct{} | ||
| + | ||
| +func (m testmw) ServeNext(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Header().Set("Test-Key", "Test-Value") | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| +} | ||
| + | ||
| +func TestMiddleware(t *testing.T) { | ||
| + mux := New() | ||
| + mux.Use(testmw{}) | ||
| + mux.Get("/hi", HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Write([]byte("Hi!")) | ||
| + })) | ||
| + | ||
| + expectHeader(t, mux, "GET", "/hi", "Test-Key", "Test-Value") | ||
| +} | ||
| + | ||
| +func TestMiddlewareFunc(t *testing.T) { | ||
| + mux := New() | ||
| + mux.UseFunc(func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Header().Set("Test-Key", "Test-Value") | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| + }) | ||
| + mux.Get("/hi", HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Write([]byte("Hi!")) | ||
| + })) | ||
| + | ||
| + expectHeader(t, mux, "GET", "/hi", "Test-Key", "Test-Value") | ||
| +} | ||
| + | ||
| +func TestMiddlewareChain(t *testing.T) { | ||
| + mux := New() | ||
| + mux.UseFunc(func(next Handler) Handler { | ||
| + return nil | ||
| + }) | ||
| +} | ||
| + | ||
| +func TestMountingSubrouter(t *testing.T) { | ||
| + mux := New() | ||
| + | ||
| + adminrouter := New() | ||
| + adminrouter.GetFunc("/:id", func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Header().Set("admin", "id") | ||
| + }) | ||
| + | ||
| + mux.Mount("/admin", adminrouter) | ||
| + | ||
| + expectHeader(t, mux, "GET", "/admin/123", "admin", "id") | ||
| +} | ||
| + | ||
| +func TestGroupSubGroup(t *testing.T) { | ||
| + s := New() | ||
| + | ||
| + admin := s.Group("/admin") | ||
| + sub := admin.Group("/") | ||
| + sub.UseFunc(func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Header().Set("Test-Key", "Get") | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| + }) | ||
| + | ||
| + sub.GetFunc("/", func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Write([]byte("Get")) | ||
| + }) | ||
| + | ||
| + sub2 := admin.Group("/") | ||
| + sub2.UseFunc(func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Header().Set("Test-Key", "Put") | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| + }) | ||
| + | ||
| + sub2.PutFunc("/", func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Write([]byte("Put")) | ||
| + }) | ||
| + | ||
| + expectHeader(t, s, "GET", "/admin", "Test-Key", "Get") | ||
| + expectHeader(t, s, "PUT", "/admin", "Test-Key", "Put") | ||
| +} | ||
| + | ||
| +func TestNamedMiddlewares(t *testing.T) { | ||
| + l := New() | ||
| + l.DefineFunc("admin", func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Header().Set("Test-Key", "admin") | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| + }) | ||
| + | ||
| + l.DefineFunc("public", func(next Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Header().Set("Test-Key", "public") | ||
| + next.ServeHTTPC(c, w, r) | ||
| + }) | ||
| + }) | ||
| + | ||
| + g := l.Group("/admin") | ||
| + g.UseNamed("admin") | ||
| + g.GetFunc("/test", func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Write([]byte("admintest")) | ||
| + }) | ||
| + | ||
| + p := l.Group("/public") | ||
| + p.UseNamed("public") | ||
| + p.GetFunc("/test", func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + w.Write([]byte("publictest")) | ||
| + }) | ||
| + | ||
| + expectHeader(t, l, "GET", "/admin/test", "Test-Key", "admin") | ||
| + expectHeader(t, l, "GET", "/public/test", "Test-Key", "public") | ||
| + expectBody(t, l, "GET", "/admin/test", "admintest") | ||
| + expectBody(t, l, "GET", "/public/test", "publictest") | ||
| +} | ||
| + | ||
| +func TestEmptyRouter(t *testing.T) { | ||
| + l := New() | ||
| + expectStatus(t, l, "GET", "/", http.StatusNotFound) | ||
| +} | ||
| + | ||
| +func expectStatus(t *testing.T, mux http.Handler, method, path string, status int) { | ||
| + req, _ := http.NewRequest(method, path, nil) | ||
| + w := httptest.NewRecorder() | ||
| + mux.ServeHTTP(w, req) | ||
| + if w.Code != status { | ||
| + t.Errorf("Expected status code to be %d but got %d for request: %s %s", status, w.Code, method, path) | ||
| + } | ||
| +} | ||
| + | ||
| +func expectHeader(t *testing.T, mux http.Handler, method, path, k, v string) { | ||
| + req, _ := http.NewRequest(method, path, nil) | ||
| + w := httptest.NewRecorder() | ||
| + mux.ServeHTTP(w, req) | ||
| + if w.Header().Get(k) != v { | ||
| + t.Errorf("Expected header to be %s but got %s for request: %s %s", v, w.Header().Get(k), method, path) | ||
| + } | ||
| +} | ||
| + | ||
| +func expectBody(t *testing.T, mux http.Handler, method, path, v string) { | ||
| + req, _ := http.NewRequest(method, path, nil) | ||
| + w := httptest.NewRecorder() | ||
| + mux.ServeHTTP(w, req) | ||
| + if w.Body.String() != v { | ||
| + t.Errorf("Expected body to be %s but got %s for request: %s %s", v, w.Body.String(), method, path) | ||
| + } | ||
| +} |
324
tree.go
| @@ -0,0 +1,324 @@ | ||
| +package lion | ||
| + | ||
| +import ( | ||
| + "sort" | ||
| + "strings" | ||
| +) | ||
| + | ||
| +type nodeType uint8 | ||
| + | ||
| +const ( | ||
| + static nodeType = iota // /hello | ||
| + regexp // TODO: /:id(regex) | ||
| + param // /:id | ||
| + wildcard // * | ||
| +) | ||
| + | ||
| +type tree struct { | ||
| + subtrees map[string]*node | ||
| +} | ||
| + | ||
| +func newTree() *tree { | ||
| + return &tree{ | ||
| + subtrees: make(map[string]*node), | ||
| + } | ||
| +} | ||
| + | ||
| +func (t *tree) addRoute(method, pattern string, handler Handler) { | ||
| + root := t.subtrees[method] | ||
| + if root == nil { | ||
| + root = &node{} | ||
| + t.subtrees[method] = root | ||
| + } | ||
| + root.addRoute(pattern, handler) | ||
| +} | ||
| + | ||
| +type node struct { | ||
| + nodeType nodeType | ||
| + pattern string | ||
| + handler Handler | ||
| + children typesToNodes | ||
| + label byte | ||
| + endinglabel byte | ||
| +} | ||
| + | ||
| +func (n *node) isLeaf() bool { | ||
| + return n.handler != nil | ||
| +} | ||
| + | ||
| +func (n *node) addRoute(pattern string, handler Handler) { | ||
| + search := pattern | ||
| + | ||
| + if len(search) == 0 { | ||
| + n.handler = handler | ||
| + return | ||
| + } | ||
| + child := n.getEdge(search[0]) | ||
| + if child == nil { | ||
| + child = &node{ | ||
| + label: search[0], | ||
| + pattern: search, | ||
| + handler: handler, | ||
| + } | ||
| + n.addChild(child) | ||
| + return | ||
| + } | ||
| + | ||
| + if child.nodeType > static { | ||
| + pos := stringsIndex(search, '/') | ||
| + if pos < 0 { | ||
| + pos = len(search) | ||
| + } | ||
| + | ||
| + search = search[pos:] | ||
| + | ||
| + child.addRoute(search, handler) | ||
| + return | ||
| + } | ||
| + | ||
| + commonPrefix := child.longestPrefix(search) | ||
| + if commonPrefix == len(child.pattern) { | ||
| + | ||
| + search = search[commonPrefix:] | ||
| + | ||
| + child.addRoute(search, handler) | ||
| + return | ||
| + } | ||
| + | ||
| + subchild := &node{ | ||
| + nodeType: static, | ||
| + pattern: search[:commonPrefix], | ||
| + } | ||
| + | ||
| + n.replaceChild(search[0], subchild) | ||
| + c2 := child | ||
| + c2.label = child.pattern[commonPrefix] | ||
| + subchild.addChild(c2) | ||
| + child.pattern = child.pattern[commonPrefix:] | ||
| + | ||
| + search = search[commonPrefix:] | ||
| + if len(search) == 0 { | ||
| + subchild.handler = handler | ||
| + return | ||
| + } | ||
| + | ||
| + subchild.addChild(&node{ | ||
| + label: search[0], | ||
| + nodeType: static, | ||
| + pattern: search, | ||
| + handler: handler, | ||
| + }) | ||
| + return | ||
| +} | ||
| + | ||
| +func (n *node) getEdge(label byte) *node { | ||
| + for _, nds := range n.children { | ||
| + for _, n := range nds { | ||
| + if n.label == label { | ||
| + return n | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + return nil | ||
| +} | ||
| + | ||
| +func (n *node) replaceChild(label byte, child *node) { | ||
| + for i := 0; i < len(n.children[child.nodeType]); i++ { | ||
| + if n.children[child.nodeType][i].label == label { | ||
| + n.children[child.nodeType][i] = child | ||
| + n.children[child.nodeType][i].label = label | ||
| + return | ||
| + } | ||
| + } | ||
| + | ||
| + panic("cannot replace child") | ||
| +} | ||
| + | ||
| +func (n *node) findNode(c *Context, path string) (*node, *Context) { | ||
| + root := n | ||
| + search := path | ||
| + | ||
| +LOOP: | ||
| + for { | ||
| + | ||
| + if len(search) == 0 { | ||
| + break | ||
| + } | ||
| + | ||
| + l := len(root.children) | ||
| + for i := 0; i < l; i++ { | ||
| + t := nodeType(i) | ||
| + | ||
| + if len(root.children[i]) == 0 { | ||
| + continue | ||
| + } | ||
| + | ||
| + var label byte | ||
| + if len(search) > 0 { | ||
| + label = search[0] | ||
| + } | ||
| + | ||
| + xn := root.findEdge(nodeType(t), label) | ||
| + if xn == nil { | ||
| + continue | ||
| + } | ||
| + | ||
| + xsearch := search | ||
| + if xn.nodeType > static { | ||
| + p := -1 | ||
| + if xn.nodeType < wildcard { | ||
| + // To match or not match . in path | ||
| + chars := "/" | ||
| + if xn.endinglabel == '.' { | ||
| + chars += "." | ||
| + } | ||
| + p = stringsIndexAny(xsearch, chars) | ||
| + } | ||
| + | ||
| + if p < 0 { | ||
| + p = len(xsearch) | ||
| + } | ||
| + | ||
| + if xn.nodeType == wildcard { | ||
| + c.addParam("*", xsearch) | ||
| + } else { | ||
| + c.addParam(xn.pattern[1:], xsearch[:p]) | ||
| + } | ||
| + | ||
| + xsearch = xsearch[p:] | ||
| + } else if strings.HasPrefix(xsearch, xn.pattern) { | ||
| + xsearch = xsearch[len(xn.pattern):] | ||
| + } else { | ||
| + continue | ||
| + } | ||
| + | ||
| + if len(xsearch) == 0 && xn.isLeaf() { | ||
| + return xn, c | ||
| + } | ||
| + | ||
| + root = xn | ||
| + search = xsearch | ||
| + continue LOOP // Search for next node (xn) | ||
| + } | ||
| + | ||
| + break | ||
| + } | ||
| + | ||
| + return nil, c | ||
| +} | ||
| + | ||
| +func (n *node) findEdge(ndtype nodeType, label byte) *node { | ||
| + nds := n.children[ndtype] | ||
| + l := len(nds) | ||
| + idx := 0 | ||
| + | ||
| +LOOP: | ||
| + for ; idx < l; idx++ { | ||
| + switch ndtype { | ||
| + case static: | ||
| + if nds[idx].label >= label { | ||
| + break LOOP | ||
| + } | ||
| + default: | ||
| + break LOOP | ||
| + } | ||
| + } | ||
| + | ||
| + if idx >= l { | ||
| + return nil | ||
| + } | ||
| + node := nds[idx] | ||
| + if node.nodeType == static && node.label == label { | ||
| + return node | ||
| + } else if node.nodeType > static { | ||
| + return node | ||
| + } | ||
| + return nil | ||
| +} | ||
| + | ||
| +func (n *node) isEdge() bool { | ||
| + return n.label != 0 | ||
| +} | ||
| + | ||
| +func (n *node) longestPrefix(pattern string) int { | ||
| + return longestPrefix(n.pattern, pattern) | ||
| +} | ||
| + | ||
| +func (n *node) addChild(child *node) { | ||
| + search := child.pattern | ||
| + | ||
| + pos := stringsIndexAny(search, ":*") | ||
| + | ||
| + ndtype := static | ||
| + if pos >= 0 { | ||
| + switch search[pos] { | ||
| + case ':': | ||
| + ndtype = param | ||
| + case '*': | ||
| + ndtype = wildcard | ||
| + } | ||
| + } | ||
| + | ||
| + switch { | ||
| + case pos == 0: // Pattern starts with wildcard | ||
| + l := len(search) | ||
| + handler := child.handler | ||
| + child.nodeType = ndtype | ||
| + if ndtype == wildcard { | ||
| + pos = -1 | ||
| + } else { | ||
| + pos = stringsIndexAny(search, "./") | ||
| + } | ||
| + if pos < 0 { | ||
| + pos = l | ||
| + } else { | ||
| + child.endinglabel = search[pos] | ||
| + } | ||
| + | ||
| + child.pattern = search[:pos] | ||
| + | ||
| + if pos != l { | ||
| + child.handler = nil | ||
| + | ||
| + search = search[pos:] | ||
| + subchild := &node{ | ||
| + label: search[0], | ||
| + pattern: search, | ||
| + nodeType: static, | ||
| + handler: handler, | ||
| + } | ||
| + child.addChild(subchild) | ||
| + } | ||
| + | ||
| + case pos > 0: // Pattern has a wildcard parameter | ||
| + handler := child.handler | ||
| + | ||
| + child.nodeType = static | ||
| + child.pattern = search[:pos] | ||
| + child.handler = nil | ||
| + | ||
| + search = search[pos:] | ||
| + subchild := &node{ | ||
| + label: search[0], | ||
| + nodeType: ndtype, | ||
| + pattern: search, | ||
| + handler: handler, | ||
| + } | ||
| + child.addChild(subchild) | ||
| + default: // all static | ||
| + child.nodeType = ndtype | ||
| + } | ||
| + | ||
| + n.children[child.nodeType] = append(n.children[child.nodeType], child) | ||
| + n.children[child.nodeType].Sort() | ||
| +} | ||
| + | ||
| +type nodes []*node | ||
| + | ||
| +func (ns nodes) Len() int { return len(ns) } | ||
| +func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label } | ||
| +func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] } | ||
| +func (ns nodes) Sort() { sort.Sort(ns) } | ||
| + | ||
| +type typesToNodes [wildcard + 1]nodes |
100
utils.go
| @@ -0,0 +1,100 @@ | ||
| +package lion | ||
| + | ||
| +import ( | ||
| + "net/http" | ||
| + "path" | ||
| + | ||
| + "golang.org/x/net/context" | ||
| +) | ||
| + | ||
| +func min(a, b int) int { | ||
| + if a <= b { | ||
| + return a | ||
| + } | ||
| + return b | ||
| +} | ||
| + | ||
| +func longestPrefix(s1, s2 string) int { | ||
| + max := min(len(s1), len(s2)) | ||
| + i := 0 | ||
| + for i < max && s1[i] == s2[i] { | ||
| + i++ | ||
| + } | ||
| + return i | ||
| +} | ||
| + | ||
| +// Wrap converts an http.Handler to returns a Handler | ||
| +func Wrap(h http.Handler) Handler { | ||
| + return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) { | ||
| + h.ServeHTTP(w, r) | ||
| + }) | ||
| +} | ||
| + | ||
| +// WrapFunc converts an http.HandlerFunc to return a Handler | ||
| +func WrapFunc(fn http.HandlerFunc) Handler { | ||
| + return Wrap(http.HandlerFunc(fn)) | ||
| +} | ||
| + | ||
| +// UnWrap converts a Handler to an http.Handler | ||
| +func UnWrap(h Handler) http.Handler { | ||
| + return HandlerFunc(h.ServeHTTPC) | ||
| +} | ||
| + | ||
| +func stringsIndexAny(str, chars string) int { | ||
| + ls := len(str) | ||
| + lc := len(chars) | ||
| + | ||
| + for i := 0; i < ls; i++ { | ||
| + s := str[i] | ||
| + for j := 0; j < lc; j++ { | ||
| + if s == chars[j] { | ||
| + return i | ||
| + } | ||
| + } | ||
| + } | ||
| + return -1 | ||
| +} | ||
| + | ||
| +func stringsIndex(str string, char byte) int { | ||
| + ls := len(str) | ||
| + | ||
| + for i := 0; i < ls; i++ { | ||
| + if str[i] == char { | ||
| + return i | ||
| + } | ||
| + } | ||
| + return -1 | ||
| +} | ||
| + | ||
| +func stringsHasPrefix(str, prefix string) bool { | ||
| + // ls := len(str) | ||
| + sl := len(str) | ||
| + pl := len(prefix) | ||
| + if sl < pl { | ||
| + return false | ||
| + } | ||
| + i := 0 | ||
| + for ; i < pl; i++ { | ||
| + if str[i] != prefix[i] { | ||
| + break | ||
| + } | ||
| + } | ||
| + if i == pl { | ||
| + return true | ||
| + } | ||
| + return false | ||
| +} | ||
| + | ||
| +func cleanPath(p string) string { | ||
| + if p == "" { | ||
| + return "/" | ||
| + } | ||
| + if p[0] != '/' { | ||
| + p = "/" + p | ||
| + } | ||
| + newpath := path.Clean(p) | ||
| + if newpath != "/" && p[len(p)-1] == '/' { | ||
| + return newpath + "/" | ||
| + } | ||
| + return newpath | ||
| +} |
0 comments on commit
5233dc7