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 [![Build Status](https://img.shields.io/travis/celrenheit/lion.svg?style=flat-square)](https://travis-ci.org/celrenheit/lion) [![GoDoc](https://img.shields.io/badge/godoc-reference-5272B4.svg?style=flat-square)](https://godoc.org/github.com/celrenheit/lion) [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](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