Skip to content
Browse files

Push to github

0 parents commit 5233dc769e6a2c31d7a9d4b176d048e3c818cad1 @celrenheit committed
Showing with 3,179 additions and 0 deletions.
  1. +56 −0 .gitignore
  2. +12 −0 .travis.yml
  3. +23 −0 LICENSE
  4. +461 −0 README.md
  5. +364 −0 bench_test.go
  6. +75 −0 context.go
  7. +62 −0 examples/basic-auth/basic-auth.go
  8. +39 −0 examples/custom-logger/custom-logger.go
  9. +24 −0 examples/hello/hello.go
  10. +56 −0 examples/modular-hello/modular-hello.go
  11. +52 −0 examples/module/module.go
  12. +31 −0 examples/resource/resource.go
  13. +48 −0 lion.go
  14. +56 −0 matcher.go
  15. +331 −0 middlewares.go
  16. +33 −0 module.go
  17. +55 −0 module_test.go
  18. +97 −0 resource.go
  19. +85 −0 resource_test.go
  20. +105 −0 response_writer.go
  21. +343 −0 router.go
  22. +347 −0 router_test.go
  23. +324 −0 tree.go
  24. +100 −0 utils.go
56 .gitignore
@@ -0,0 +1,56 @@
+### https://raw.github.com/github/gitignore/fdb7293c82a5554ceb748db99e01672c17808292/Global/OSX.gitignore
+
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+
+### https://raw.github.com/github/gitignore/fdb7293c82a5554ceb748db99e01672c17808292/Go.gitignore
+
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof
+
+
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
461 README.md
@@ -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

Please sign in to comment.
Something went wrong with that request. Please try again.