Jelajahi Sumber

Merge branch 'develop'

Manu Mtz-Almeida 10 tahun lalu
induk
melakukan
9163ee543d
68 mengubah file dengan 6005 tambahan dan 1679 penghapusan
  1. 2 0
      .gitignore
  2. 15 1
      .travis.yml
  3. 1 2
      AUTHORS.md
  4. 48 4
      CHANGELOG.md
  5. 20 3
      Godeps/Godeps.json
  6. 3 15
      README.md
  7. 31 38
      auth.go
  8. 109 48
      auth_test.go
  9. 35 256
      binding/binding.go
  10. 96 0
      binding/binding_test.go
  11. 23 0
      binding/form.go
  12. 139 0
      binding/form_mapping.go
  13. 25 0
      binding/json.go
  14. 53 0
      binding/validate_test.go
  15. 24 0
      binding/xml.go
  16. 267 235
      context.go
  17. 371 406
      context_test.go
  18. 40 0
      debug.go
  19. 79 0
      debug_test.go
  20. 0 42
      deprecated.go
  21. 128 0
      errors.go
  22. 90 0
      errors_test.go
  23. 1 1
      examples/basic/main.go
  24. 0 58
      examples/pluggable_renderer/example_pongo2.go
  25. 0 12
      examples/pluggable_renderer/index.html
  26. 39 0
      examples/realtime-advanced/main.go
  27. 208 0
      examples/realtime-advanced/resources/room_login.templ.html
  28. 0 0
      examples/realtime-advanced/resources/static/epoch.min.css
  29. 114 0
      examples/realtime-advanced/resources/static/epoch.min.js
  30. 137 0
      examples/realtime-advanced/resources/static/prismjs.min.css
  31. 1 0
      examples/realtime-advanced/resources/static/prismjs.min.js
  32. 144 0
      examples/realtime-advanced/resources/static/realtime.js
  33. 25 0
      examples/realtime-advanced/rooms.go
  34. 96 0
      examples/realtime-advanced/routes.go
  35. 56 0
      examples/realtime-advanced/stats.go
  36. 58 0
      examples/realtime-chat/main.go
  37. 33 0
      examples/realtime-chat/rooms.go
  38. 44 0
      examples/realtime-chat/template.go
  39. 208 82
      gin.go
  40. 122 178
      gin_test.go
  41. 344 0
      githubapi_test.go
  42. 28 24
      logger.go
  43. 35 0
      logger_test.go
  44. 217 0
      middleware_test.go
  45. 15 22
      mode.go
  46. 31 0
      mode_test.go
  47. 123 0
      path.go
  48. 88 0
      path_test.go
  49. 25 17
      recovery.go
  50. 16 30
      recovery_test.go
  51. 16 0
      render/data.go
  52. 59 0
      render/html.go
  53. 33 0
      render/json.go
  54. 20 0
      render/redirect.go
  55. 13 130
      render/render.go
  56. 130 0
      render/render_test.go
  57. 26 0
      render/text.go
  58. 17 0
      render/xml.go
  59. 13 18
      response_writer.go
  60. 115 0
      response_writer_test.go
  61. 63 52
      routergroup.go
  62. 111 0
      routergroup_test.go
  63. 286 0
      routes_test.go
  64. 553 0
      tree.go
  65. 608 0
      tree_test.go
  66. 35 5
      utils.go
  67. 99 0
      utils_test.go
  68. 1 0
      wercker.yml

+ 2 - 0
.gitignore

@@ -1,2 +1,4 @@
 Godeps/*
 !Godeps/Godeps.json
+coverage.out
+count.out

+ 15 - 1
.travis.yml

@@ -1,6 +1,20 @@
 language: go
 sudo: false
 go:
-  - 1.3
   - 1.4
+  - 1.4.2
   - tip
+
+script:
+  - go get golang.org/x/tools/cmd/cover
+  - go get github.com/mattn/goveralls
+  - go test -v -covermode=count -coverprofile=coverage.out
+  - goveralls -coverprofile=coverage.out -service=travis-ci -repotoken yFj7FrCeddvBzUaaCyG33jCLfWXeb93eA
+
+notifications:
+  webhooks:
+    urls:
+      - https://webhooks.gitter.im/e/acc2c57482e94b44f557
+    on_success: change  # options: [always|never|change] default: always
+    on_failure: always  # options: [always|never|change] default: always
+    on_start: false     # default: false

+ 1 - 2
AUTHORS.md

@@ -4,8 +4,7 @@ List of all the awesome people working to make Gin the best Web Framework in Go.
 
 ##gin 0.x series authors
 
-**Original Developer:**  Manu Martinez-Almeida (@manucorporat)  
-**Long-term Maintainer:** Javier Provecho (@javierprovecho)
+**Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
 
 People and companies, who have contributed, in alphabetical order.
 

+ 48 - 4
CHANGELOG.md

@@ -1,11 +1,55 @@
 #Changelog
 
+###Gin 1.0rc1 (May 22, 2015)
+
+- [PERFORMANCE] Zero allocation router
+- [PERFORMANCE] Faster JSON, XML and text rendering
+- [PERFORMANCE] Custom hand optimized HttpRouter for Gin
+- [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations
+- [NEW] Built-in support for golang.org/x/net/context
+- [NEW] Any(path, handler). Create a route that matches any path
+- [NEW] Refactored rendering pipeline (faster and static typeded)
+- [NEW] Refactored errors API
+- [NEW] IndentedJSON() prints pretty JSON
+- [NEW] Added gin.DefaultWriter
+- [NEW] UNIX socket support
+- [NEW] RouterGroup.BasePath is exposed
+- [NEW] JSON validation using go-validate-yourself (very powerful options)
+- [NEW] Completed suite of unit tests
+- [NEW] HTTP streaming with c.Stream()
+- [NEW] StaticFile() creates a router for serving just one file.
+- [NEW] StaticFS() has an option to disable directory listing.
+- [NEW] StaticFS() for serving static files through virtual filesystems
+- [NEW] Server-Sent Events native support
+- [NEW] WrapF() and WrapH() helpers for wrapping http.HandlerFunc and http.Handler
+- [NEW] Added LoggerWithWriter() middleware
+- [NEW] Added RecoveryWithWriter() middleware
+- [NEW] Added DefaultPostFormValue()
+- [NEW] Added DefaultFormValue()
+- [NEW] Added DefaultParamValue()
+- [FIX] BasicAuth() when using custom realm
+- [FIX] Bug when serving static files in nested routing group
+- [FIX] Redirect using built-in http.Redirect()
+- [FIX] Logger when printing the requested path
+- [FIX] Documentation typos
+- [FIX] Context.Engine renamed to Context.engine
+- [FIX] Better debugging messages
+- [FIX] ErrorLogger
+- [FIX] Debug HTTP render
+- [FIX] Refactored binding and render modules 
+- [FIX] Refactored Context initialization
+- [FIX] Refactored BasicAuth()
+- [FIX] NoMethod/NoRoute handlers
+- [FIX] Hijacking http
+- [FIX] Better support for Google App Engine (using log instead of fmt)
+
+
 ###Gin 0.6 (Mar 9, 2015)
 
-- [ADD] Support multipart/form-data
-- [ADD] NoMethod handler
-- [ADD] Validate sub structures
-- [ADD] Support for HTTP Realm Auth
+- [NEW] Support multipart/form-data
+- [NEW] NoMethod handler
+- [NEW] Validate sub structures
+- [NEW] Support for HTTP Realm Auth
 - [FIX] Unsigned integers in binding
 - [FIX] Improve color logger
 

+ 20 - 3
Godeps/Godeps.json

@@ -1,10 +1,27 @@
 {
 	"ImportPath": "github.com/gin-gonic/gin",
-	"GoVersion": "go1.3",
+	"GoVersion": "go1.4.2",
 	"Deps": [
 		{
-			"ImportPath": "github.com/julienschmidt/httprouter",
-			"Rev": "b428fda53bb0a764fea9c76c9413512eda291dec"
+			"ImportPath": "github.com/manucorporat/sse",
+			"Rev": "c574f6c50c8594f93d28b03a1bbd87b4a3899093"
+		},
+		{
+			"ImportPath": "github.com/mattn/go-colorable",
+			"Rev": "043ae16291351db8465272edf465c9f388161627"
+		},
+		{
+			"ImportPath": "github.com/stretchr/testify/assert",
+			"Rev": "de7fcff264cd05cc0c90c509ea789a436a0dd206"
+		},
+		{
+			"ImportPath": "golang.org/x/net/context",
+			"Rev": "84ba27dd5b2d8135e9da1395277f2c9333a2ffda"
+		},
+		{
+			"ImportPath": "gopkg.in/joeybloggs/go-validate-yourself.v4",
+			"Comment": "v4.0",
+			"Rev": "a3cb430fa1e43b15e72d7bec5b20d0bdff4c2bb8"
 		}
 	]
 }

+ 3 - 15
README.md

@@ -1,6 +1,6 @@
-#Gin Web Framework [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin)
+#Gin Web Framework [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) [![Coverage Status](https://coveralls.io/repos/gin-gonic/gin/badge.svg?branch=develop)](https://coveralls.io/r/gin-gonic/gin?branch=develop)
 
-[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+ [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin)  [![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 
 Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. 
 
@@ -29,7 +29,7 @@ func main() {
 		c.String(http.StatusUnauthorized, "not authorized")
 	})
 	router.PUT("/error", func(c *gin.Context) {
-		c.String(http.StatusInternalServerError, "and error happened :(")
+		c.String(http.StatusInternalServerError, "an error happened :(")
 	})
 	router.Run(":8080")
 }
@@ -72,18 +72,6 @@ Then import it in your Go code:
 import "github.com/gin-gonic/gin"
 ```
 
-
-##Community
-If you'd like to help out with the project, there's a mailing list and IRC channel where Gin discussions normally happen.
-
-* IRC
- * [irc.freenode.net #getgin](irc://irc.freenode.net:6667/getgin)
- * [Webchat](http://webchat.freenode.net?randomnick=1&channels=%23getgin)
-* Mailing List
- * Subscribe: [getgin@librelist.org](mailto:getgin@librelist.org)
- * [Archives](http://librelist.com/browser/getgin/)
-
-
 ##API Examples
 
 #### Create most basic PING/PONG HTTP endpoint

+ 31 - 38
auth.go

@@ -7,9 +7,7 @@ package gin
 import (
 	"crypto/subtle"
 	"encoding/base64"
-	"errors"
-	"fmt"
-	"sort"
+	"strconv"
 )
 
 const (
@@ -25,31 +23,37 @@ type (
 	authPairs []authPair
 )
 
-func (a authPairs) Len() int           { return len(a) }
-func (a authPairs) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
-func (a authPairs) Less(i, j int) bool { return a[i].Value < a[j].Value }
+func (a authPairs) searchCredential(authValue string) (string, bool) {
+	if len(authValue) == 0 {
+		return "", false
+	}
+	for _, pair := range a {
+		if pair.Value == authValue {
+			return pair.User, true
+		}
+	}
+	return "", false
+}
 
 // Implements a basic Basic HTTP Authorization. It takes as arguments a map[string]string where
 // the key is the user name and the value is the password, as well as the name of the Realm
 // (see http://tools.ietf.org/html/rfc2617#section-1.2)
 func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
-	pairs, err := processAccounts(accounts)
-	if err != nil {
-		panic(err)
+	if realm == "" {
+		realm = "Authorization Required"
 	}
+	realm = "Basic realm=" + strconv.Quote(realm)
+	pairs := processAccounts(accounts)
 	return func(c *Context) {
 		// Search user in the slice of allowed credentials
-		user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization"))
-		if !ok {
-			// Credentials doesn't match, we return 401 Unauthorized and abort request.
-			if realm == "" {
-				realm = "Authorization Required"
-			}
-			c.Writer.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", realm))
-			c.Fail(401, errors.New("Unauthorized"))
+		user, found := pairs.searchCredential(c.Request.Header.Get("Authorization"))
+		if !found {
+			// Credentials doesn't match, we return 401 and abort handlers chain.
+			c.Header("WWW-Authenticate", realm)
+			c.AbortWithStatus(401)
 		} else {
-			// user is allowed, set UserId to key "user" in this context, the userId can be read later using
-			// c.Get(gin.AuthUserKey)
+			// The user credentials was found, set user's id to key AuthUserKey in this context, the userId can be read later using
+			// c.MustGet(gin.AuthUserKey)
 			c.Set(AuthUserKey, user)
 		}
 	}
@@ -61,38 +65,27 @@ func BasicAuth(accounts Accounts) HandlerFunc {
 	return BasicAuthForRealm(accounts, "")
 }
 
-func processAccounts(accounts Accounts) (authPairs, error) {
+func processAccounts(accounts Accounts) authPairs {
 	if len(accounts) == 0 {
-		return nil, errors.New("Empty list of authorized credentials")
+		panic("Empty list of authorized credentials")
 	}
 	pairs := make(authPairs, 0, len(accounts))
 	for user, password := range accounts {
 		if len(user) == 0 {
-			return nil, errors.New("User can not be empty")
+			panic("User can not be empty")
 		}
-		base := user + ":" + password
-		value := "Basic " + base64.StdEncoding.EncodeToString([]byte(base))
+		value := authorizationHeader(user, password)
 		pairs = append(pairs, authPair{
 			Value: value,
 			User:  user,
 		})
 	}
-	// We have to sort the credentials in order to use bsearch later.
-	sort.Sort(pairs)
-	return pairs, nil
+	return pairs
 }
 
-func searchCredential(pairs authPairs, auth string) (string, bool) {
-	if len(auth) == 0 {
-		return "", false
-	}
-	// Search user in the slice of allowed credentials
-	r := sort.Search(len(pairs), func(i int) bool { return pairs[i].Value >= auth })
-	if r < len(pairs) && secureCompare(pairs[r].Value, auth) {
-		return pairs[r].User, true
-	} else {
-		return "", false
-	}
+func authorizationHeader(user, password string) string {
+	base := user + ":" + password
+	return "Basic " + base64.StdEncoding.EncodeToString([]byte(base))
 }
 
 func secureCompare(given, actual string) bool {

+ 109 - 48
auth_test.go

@@ -9,77 +9,138 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
-func TestBasicAuthSucceed(t *testing.T) {
-	req, _ := http.NewRequest("GET", "/login", nil)
-	w := httptest.NewRecorder()
+func TestBasicAuth(t *testing.T) {
+	pairs := processAccounts(Accounts{
+		"admin": "password",
+		"foo":   "bar",
+		"bar":   "foo",
+	})
 
-	r := New()
-	accounts := Accounts{"admin": "password"}
-	r.Use(BasicAuth(accounts))
+	assert.Len(t, pairs, 3)
+	assert.Contains(t, pairs, authPair{
+		User:  "bar",
+		Value: "Basic YmFyOmZvbw==",
+	})
+	assert.Contains(t, pairs, authPair{
+		User:  "foo",
+		Value: "Basic Zm9vOmJhcg==",
+	})
+	assert.Contains(t, pairs, authPair{
+		User:  "admin",
+		Value: "Basic YWRtaW46cGFzc3dvcmQ=",
+	})
+}
 
-	r.GET("/login", func(c *Context) {
-		c.String(200, "autorized")
+func TestBasicAuthFails(t *testing.T) {
+	assert.Panics(t, func() { processAccounts(nil) })
+	assert.Panics(t, func() {
+		processAccounts(Accounts{
+			"":    "password",
+			"foo": "bar",
+		})
 	})
+}
 
-	req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
-	r.ServeHTTP(w, req)
+func TestBasicAuthSearchCredential(t *testing.T) {
+	pairs := processAccounts(Accounts{
+		"admin": "password",
+		"foo":   "bar",
+		"bar":   "foo",
+	})
+
+	user, found := pairs.searchCredential(authorizationHeader("admin", "password"))
+	assert.Equal(t, user, "admin")
+	assert.True(t, found)
+
+	user, found = pairs.searchCredential(authorizationHeader("foo", "bar"))
+	assert.Equal(t, user, "foo")
+	assert.True(t, found)
+
+	user, found = pairs.searchCredential(authorizationHeader("bar", "foo"))
+	assert.Equal(t, user, "bar")
+	assert.True(t, found)
 
-	if w.Code != 200 {
-		t.Errorf("Response code should be Ok, was: %s", w.Code)
-	}
-	bodyAsString := w.Body.String()
+	user, found = pairs.searchCredential(authorizationHeader("admins", "password"))
+	assert.Empty(t, user)
+	assert.False(t, found)
 
-	if bodyAsString != "autorized" {
-		t.Errorf("Response body should be `autorized`, was  %s", bodyAsString)
-	}
+	user, found = pairs.searchCredential(authorizationHeader("foo", "bar "))
+	assert.Empty(t, user)
+	assert.False(t, found)
+
+	user, found = pairs.searchCredential("")
+	assert.Empty(t, user)
+	assert.False(t, found)
 }
 
-func TestBasicAuth401(t *testing.T) {
-	req, _ := http.NewRequest("GET", "/login", nil)
+func TestBasicAuthAuthorizationHeader(t *testing.T) {
+	assert.Equal(t, authorizationHeader("admin", "password"), "Basic YWRtaW46cGFzc3dvcmQ=")
+}
+
+func TestBasicAuthSecureCompare(t *testing.T) {
+	assert.True(t, secureCompare("1234567890", "1234567890"))
+	assert.False(t, secureCompare("123456789", "1234567890"))
+	assert.False(t, secureCompare("12345678900", "1234567890"))
+	assert.False(t, secureCompare("1234567891", "1234567890"))
+}
+
+func TestBasicAuthSucceed(t *testing.T) {
+	accounts := Accounts{"admin": "password"}
+	router := New()
+	router.Use(BasicAuth(accounts))
+	router.GET("/login", func(c *Context) {
+		c.String(200, c.MustGet(AuthUserKey).(string))
+	})
+
 	w := httptest.NewRecorder()
+	req, _ := http.NewRequest("GET", "/login", nil)
+	req.Header.Set("Authorization", authorizationHeader("admin", "password"))
+	router.ServeHTTP(w, req)
 
-	r := New()
-	accounts := Accounts{"foo": "bar"}
-	r.Use(BasicAuth(accounts))
+	assert.Equal(t, w.Code, 200)
+	assert.Equal(t, w.Body.String(), "admin")
+}
 
-	r.GET("/login", func(c *Context) {
-		c.String(200, "autorized")
+func TestBasicAuth401(t *testing.T) {
+	called := false
+	accounts := Accounts{"foo": "bar"}
+	router := New()
+	router.Use(BasicAuth(accounts))
+	router.GET("/login", func(c *Context) {
+		called = true
+		c.String(200, c.MustGet(AuthUserKey).(string))
 	})
 
+	w := httptest.NewRecorder()
+	req, _ := http.NewRequest("GET", "/login", nil)
 	req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
-	r.ServeHTTP(w, req)
-
-	if w.Code != 401 {
-		t.Errorf("Response code should be Not autorized, was: %s", w.Code)
-	}
+	router.ServeHTTP(w, req)
 
-	if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"Authorization Required\"" {
-		t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type"))
-	}
+	assert.False(t, called)
+	assert.Equal(t, w.Code, 401)
+	assert.Equal(t, w.HeaderMap.Get("WWW-Authenticate"), "Basic realm=\"Authorization Required\"")
 }
 
 func TestBasicAuth401WithCustomRealm(t *testing.T) {
-	req, _ := http.NewRequest("GET", "/login", nil)
-	w := httptest.NewRecorder()
-
-	r := New()
+	called := false
 	accounts := Accounts{"foo": "bar"}
-	r.Use(BasicAuthForRealm(accounts, "My Custom Realm"))
-
-	r.GET("/login", func(c *Context) {
-		c.String(200, "autorized")
+	router := New()
+	router.Use(BasicAuthForRealm(accounts, "My Custom \"Realm\""))
+	router.GET("/login", func(c *Context) {
+		called = true
+		c.String(200, c.MustGet(AuthUserKey).(string))
 	})
 
+	w := httptest.NewRecorder()
+	req, _ := http.NewRequest("GET", "/login", nil)
 	req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
-	r.ServeHTTP(w, req)
-
-	if w.Code != 401 {
-		t.Errorf("Response code should be Not autorized, was: %s", w.Code)
-	}
+	router.ServeHTTP(w, req)
 
-	if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"My Custom Realm\"" {
-		t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type"))
-	}
+	assert.False(t, called)
+	assert.Equal(t, w.Code, 401)
+	assert.Equal(t, w.HeaderMap.Get("WWW-Authenticate"), "Basic realm=\"My Custom \\\"Realm\\\"\"")
 }

+ 35 - 256
binding/binding.go

@@ -5,280 +5,59 @@
 package binding
 
 import (
-	"encoding/json"
-	"encoding/xml"
-	"errors"
 	"net/http"
-	"reflect"
-	"strconv"
-	"strings"
-)
-
-type (
-	Binding interface {
-		Bind(*http.Request, interface{}) error
-	}
-
-	// JSON binding
-	jsonBinding struct{}
 
-	// XML binding
-	xmlBinding struct{}
-
-	// form binding
-	formBinding struct{}
-
-	// multipart form binding
-	multipartFormBinding struct{}
+	"gopkg.in/bluesuncorp/validator.v5"
 )
 
-const MAX_MEMORY = 1 * 1024 * 1024
-
-var (
-	JSON          = jsonBinding{}
-	XML           = xmlBinding{}
-	Form          = formBinding{} // todo
-	MultipartForm = multipartFormBinding{}
+const (
+	MIMEJSON              = "application/json"
+	MIMEHTML              = "text/html"
+	MIMEXML               = "application/xml"
+	MIMEXML2              = "text/xml"
+	MIMEPlain             = "text/plain"
+	MIMEPOSTForm          = "application/x-www-form-urlencoded"
+	MIMEMultipartPOSTForm = "multipart/form-data"
 )
 
-func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error {
-	decoder := json.NewDecoder(req.Body)
-	if err := decoder.Decode(obj); err == nil {
-		return Validate(obj)
-	} else {
-		return err
-	}
+type Binding interface {
+	Name() string
+	Bind(*http.Request, interface{}) error
 }
 
-func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error {
-	decoder := xml.NewDecoder(req.Body)
-	if err := decoder.Decode(obj); err == nil {
-		return Validate(obj)
-	} else {
-		return err
-	}
-}
+var validate = validator.New("binding", validator.BakedInValidators)
 
-func (_ formBinding) Bind(req *http.Request, obj interface{}) error {
-	if err := req.ParseForm(); err != nil {
-		return err
-	}
-	if err := mapForm(obj, req.Form); err != nil {
-		return err
-	}
-	return Validate(obj)
-}
-
-func (_ multipartFormBinding) Bind(req *http.Request, obj interface{}) error {
-	if err := req.ParseMultipartForm(MAX_MEMORY); err != nil {
-		return err
-	}
-	if err := mapForm(obj, req.Form); err != nil {
-		return err
-	}
-	return Validate(obj)
-}
-
-func mapForm(ptr interface{}, form map[string][]string) error {
-	typ := reflect.TypeOf(ptr).Elem()
-	formStruct := reflect.ValueOf(ptr).Elem()
-	for i := 0; i < typ.NumField(); i++ {
-		typeField := typ.Field(i)
-		if inputFieldName := typeField.Tag.Get("form"); inputFieldName != "" {
-			structField := formStruct.Field(i)
-			if !structField.CanSet() {
-				continue
-			}
+var (
+	JSON = jsonBinding{}
+	XML  = xmlBinding{}
+	Form = formBinding{}
+)
 
-			inputValue, exists := form[inputFieldName]
-			if !exists {
-				continue
-			}
-			numElems := len(inputValue)
-			if structField.Kind() == reflect.Slice && numElems > 0 {
-				sliceOf := structField.Type().Elem().Kind()
-				slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
-				for i := 0; i < numElems; i++ {
-					if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil {
-						return err
-					}
-				}
-				formStruct.Field(i).Set(slice)
-			} else {
-				if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
-					return err
-				}
-			}
+func Default(method, contentType string) Binding {
+	if method == "GET" {
+		return Form
+	} else {
+		switch contentType {
+		case MIMEJSON:
+			return JSON
+		case MIMEXML, MIMEXML2:
+			return XML
+		default:
+			return Form
 		}
 	}
-	return nil
-}
-
-func setIntField(val string, bitSize int, structField reflect.Value) error {
-	if val == "" {
-		val = "0"
-	}
-
-	intVal, err := strconv.ParseInt(val, 10, bitSize)
-	if err == nil {
-		structField.SetInt(intVal)
-	}
-
-	return err
 }
 
-func setUintField(val string, bitSize int, structField reflect.Value) error {
-	if val == "" {
-		val = "0"
-	}
-
-	uintVal, err := strconv.ParseUint(val, 10, bitSize)
-	if err == nil {
-		structField.SetUint(uintVal)
-	}
-
-	return err
-}
-
-func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
-	switch valueKind {
-	case reflect.Int:
-		return setIntField(val, 0, structField)
-	case reflect.Int8:
-		return setIntField(val, 8, structField)
-	case reflect.Int16:
-		return setIntField(val, 16, structField)
-	case reflect.Int32:
-		return setIntField(val, 32, structField)
-	case reflect.Int64:
-		return setIntField(val, 64, structField)
-	case reflect.Uint:
-		return setUintField(val, 0, structField)
-	case reflect.Uint8:
-		return setUintField(val, 8, structField)
-	case reflect.Uint16:
-		return setUintField(val, 16, structField)
-	case reflect.Uint32:
-		return setUintField(val, 32, structField)
-	case reflect.Uint64:
-		return setUintField(val, 64, structField)
-	case reflect.Bool:
-		if val == "" {
-			val = "false"
-		}
-		boolVal, err := strconv.ParseBool(val)
-		if err != nil {
-			return err
-		} else {
-			structField.SetBool(boolVal)
-		}
-	case reflect.Float32:
-		if val == "" {
-			val = "0.0"
-		}
-		floatVal, err := strconv.ParseFloat(val, 32)
-		if err != nil {
-			return err
-		} else {
-			structField.SetFloat(floatVal)
-		}
-	case reflect.Float64:
-		if val == "" {
-			val = "0.0"
-		}
-		floatVal, err := strconv.ParseFloat(val, 64)
-		if err != nil {
-			return err
-		} else {
-			structField.SetFloat(floatVal)
-		}
-	case reflect.String:
-		structField.SetString(val)
+func ValidateField(f interface{}, tag string) error {
+	if err := validate.Field(f, tag); err != nil {
+		return error(err)
 	}
 	return nil
 }
 
-// Don't pass in pointers to bind to. Can lead to bugs. See:
-// https://github.com/codegangsta/martini-contrib/issues/40
-// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659
-func ensureNotPointer(obj interface{}) {
-	if reflect.TypeOf(obj).Kind() == reflect.Ptr {
-		panic("Pointers are not accepted as binding models")
-	}
-}
-
-func Validate(obj interface{}, parents ...string) error {
-	typ := reflect.TypeOf(obj)
-	val := reflect.ValueOf(obj)
-
-	if typ.Kind() == reflect.Ptr {
-		typ = typ.Elem()
-		val = val.Elem()
-	}
-
-	switch typ.Kind() {
-	case reflect.Struct:
-		for i := 0; i < typ.NumField(); i++ {
-			field := typ.Field(i)
-
-			// Allow ignored and unexported fields in the struct
-			if len(field.PkgPath) > 0 || field.Tag.Get("form") == "-" {
-				continue
-			}
-
-			fieldValue := val.Field(i).Interface()
-			zero := reflect.Zero(field.Type).Interface()
-
-			if strings.Index(field.Tag.Get("binding"), "required") > -1 {
-				fieldType := field.Type.Kind()
-				if fieldType == reflect.Struct {
-					if reflect.DeepEqual(zero, fieldValue) {
-						return errors.New("Required " + field.Name)
-					}
-					err := Validate(fieldValue, field.Name)
-					if err != nil {
-						return err
-					}
-				} else if reflect.DeepEqual(zero, fieldValue) {
-					if len(parents) > 0 {
-						return errors.New("Required " + field.Name + " on " + parents[0])
-					} else {
-						return errors.New("Required " + field.Name)
-					}
-				} else if fieldType == reflect.Slice && field.Type.Elem().Kind() == reflect.Struct {
-					err := Validate(fieldValue)
-					if err != nil {
-						return err
-					}
-				}
-			} else {
-				fieldType := field.Type.Kind()
-				if fieldType == reflect.Struct {
-					if reflect.DeepEqual(zero, fieldValue) {
-						continue
-					}
-					err := Validate(fieldValue, field.Name)
-					if err != nil {
-						return err
-					}
-				} else if fieldType == reflect.Slice && field.Type.Elem().Kind() == reflect.Struct {
-					err := Validate(fieldValue, field.Name)
-					if err != nil {
-						return err
-					}
-				}
-			}
-		}
-	case reflect.Slice:
-		for i := 0; i < val.Len(); i++ {
-			fieldValue := val.Index(i).Interface()
-			err := Validate(fieldValue)
-			if err != nil {
-				return err
-			}
-		}
-	default:
-		return nil
+func Validate(obj interface{}) error {
+	if err := validate.Struct(obj); err != nil {
+		return error(err)
 	}
 	return nil
 }

+ 96 - 0
binding/binding_test.go

@@ -0,0 +1,96 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"bytes"
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type FooStruct struct {
+	Foo string `json:"foo" form:"foo" xml:"foo" binding:"required"`
+}
+
+func TestBindingDefault(t *testing.T) {
+	assert.Equal(t, Default("GET", ""), Form)
+	assert.Equal(t, Default("GET", MIMEJSON), Form)
+
+	assert.Equal(t, Default("POST", MIMEJSON), JSON)
+	assert.Equal(t, Default("PUT", MIMEJSON), JSON)
+
+	assert.Equal(t, Default("POST", MIMEXML), XML)
+	assert.Equal(t, Default("PUT", MIMEXML2), XML)
+
+	assert.Equal(t, Default("POST", MIMEPOSTForm), Form)
+	assert.Equal(t, Default("DELETE", MIMEPOSTForm), Form)
+}
+
+func TestBindingJSON(t *testing.T) {
+	testBodyBinding(t,
+		JSON, "json",
+		"/", "/",
+		`{"foo": "bar"}`, `{"bar": "foo"}`)
+}
+
+func TestBindingForm(t *testing.T) {
+	testFormBinding(t, "POST",
+		"/", "/",
+		"foo=bar", "bar=foo")
+}
+
+func TestBindingForm2(t *testing.T) {
+	testFormBinding(t, "GET",
+		"/?foo=bar", "/?bar=foo",
+		"", "")
+}
+
+func TestBindingXML(t *testing.T) {
+	testBodyBinding(t,
+		XML, "xml",
+		"/", "/",
+		"<map><foo>bar</foo></map>", "<map><bar>foo</bar></map>")
+}
+
+func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
+	b := Form
+	assert.Equal(t, b.Name(), "query")
+
+	obj := FooStruct{}
+	req := requestWithBody(method, path, body)
+	if method == "POST" {
+		req.Header.Add("Content-Type", MIMEPOSTForm)
+	}
+	err := b.Bind(req, &obj)
+	assert.NoError(t, err)
+	assert.Equal(t, obj.Foo, "bar")
+
+	obj = FooStruct{}
+	req = requestWithBody(method, badPath, badBody)
+	err = JSON.Bind(req, &obj)
+	assert.Error(t, err)
+}
+
+func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
+	assert.Equal(t, b.Name(), name)
+
+	obj := FooStruct{}
+	req := requestWithBody("POST", path, body)
+	err := b.Bind(req, &obj)
+	assert.NoError(t, err)
+	assert.Equal(t, obj.Foo, "bar")
+
+	obj = FooStruct{}
+	req = requestWithBody("POST", badPath, badBody)
+	err = JSON.Bind(req, &obj)
+	assert.Error(t, err)
+}
+
+func requestWithBody(method, path, body string) (req *http.Request) {
+	req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
+	return
+}

+ 23 - 0
binding/form.go

@@ -0,0 +1,23 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import "net/http"
+
+type formBinding struct{}
+
+func (_ formBinding) Name() string {
+	return "query"
+}
+
+func (_ formBinding) Bind(req *http.Request, obj interface{}) error {
+	if err := req.ParseForm(); err != nil {
+		return err
+	}
+	if err := mapForm(obj, req.Form); err != nil {
+		return err
+	}
+	return Validate(obj)
+}

+ 139 - 0
binding/form_mapping.go

@@ -0,0 +1,139 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"errors"
+	"reflect"
+	"strconv"
+)
+
+func mapForm(ptr interface{}, form map[string][]string) error {
+	typ := reflect.TypeOf(ptr).Elem()
+	val := reflect.ValueOf(ptr).Elem()
+	for i := 0; i < typ.NumField(); i++ {
+		typeField := typ.Field(i)
+		structField := val.Field(i)
+		if !structField.CanSet() {
+			continue
+		}
+
+		inputFieldName := typeField.Tag.Get("form")
+		if inputFieldName == "" {
+			inputFieldName = typeField.Name
+		}
+		inputValue, exists := form[inputFieldName]
+		if !exists {
+			continue
+		}
+
+		numElems := len(inputValue)
+		if structField.Kind() == reflect.Slice && numElems > 0 {
+			sliceOf := structField.Type().Elem().Kind()
+			slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
+			for i := 0; i < numElems; i++ {
+				if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil {
+					return err
+				}
+			}
+			val.Field(i).Set(slice)
+		} else {
+			if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
+				return err
+			}
+		}
+
+	}
+	return nil
+}
+
+func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
+	switch valueKind {
+	case reflect.Int:
+		return setIntField(val, 0, structField)
+	case reflect.Int8:
+		return setIntField(val, 8, structField)
+	case reflect.Int16:
+		return setIntField(val, 16, structField)
+	case reflect.Int32:
+		return setIntField(val, 32, structField)
+	case reflect.Int64:
+		return setIntField(val, 64, structField)
+	case reflect.Uint:
+		return setUintField(val, 0, structField)
+	case reflect.Uint8:
+		return setUintField(val, 8, structField)
+	case reflect.Uint16:
+		return setUintField(val, 16, structField)
+	case reflect.Uint32:
+		return setUintField(val, 32, structField)
+	case reflect.Uint64:
+		return setUintField(val, 64, structField)
+	case reflect.Bool:
+		return setBoolField(val, structField)
+	case reflect.Float32:
+		return setFloatField(val, 32, structField)
+	case reflect.Float64:
+		return setFloatField(val, 64, structField)
+	case reflect.String:
+		structField.SetString(val)
+	default:
+		return errors.New("Unknown type")
+	}
+	return nil
+}
+
+func setIntField(val string, bitSize int, field reflect.Value) error {
+	if val == "" {
+		val = "0"
+	}
+	intVal, err := strconv.ParseInt(val, 10, bitSize)
+	if err == nil {
+		field.SetInt(intVal)
+	}
+	return err
+}
+
+func setUintField(val string, bitSize int, field reflect.Value) error {
+	if val == "" {
+		val = "0"
+	}
+	uintVal, err := strconv.ParseUint(val, 10, bitSize)
+	if err == nil {
+		field.SetUint(uintVal)
+	}
+	return err
+}
+
+func setBoolField(val string, field reflect.Value) error {
+	if val == "" {
+		val = "false"
+	}
+	boolVal, err := strconv.ParseBool(val)
+	if err == nil {
+		field.SetBool(boolVal)
+	}
+	return nil
+}
+
+func setFloatField(val string, bitSize int, field reflect.Value) error {
+	if val == "" {
+		val = "0.0"
+	}
+	floatVal, err := strconv.ParseFloat(val, bitSize)
+	if err == nil {
+		field.SetFloat(floatVal)
+	}
+	return err
+}
+
+// Don't pass in pointers to bind to. Can lead to bugs. See:
+// https://github.com/codegangsta/martini-contrib/issues/40
+// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659
+func ensureNotPointer(obj interface{}) {
+	if reflect.TypeOf(obj).Kind() == reflect.Ptr {
+		panic("Pointers are not accepted as binding models")
+	}
+}

+ 25 - 0
binding/json.go

@@ -0,0 +1,25 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"encoding/json"
+
+	"net/http"
+)
+
+type jsonBinding struct{}
+
+func (_ jsonBinding) Name() string {
+	return "json"
+}
+
+func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error {
+	decoder := json.NewDecoder(req.Body)
+	if err := decoder.Decode(obj); err != nil {
+		return err
+	}
+	return Validate(obj)
+}

+ 53 - 0
binding/validate_test.go

@@ -0,0 +1,53 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type struct1 struct {
+	Value float64 `binding:"required"`
+}
+
+type struct2 struct {
+	RequiredValue string `binding:"required"`
+	Value         float64
+}
+
+type struct3 struct {
+	Integer    int
+	String     string
+	BasicSlice []int
+	Boolean    bool
+
+	RequiredInteger       int       `binding:"required"`
+	RequiredString        string    `binding:"required"`
+	RequiredAnotherStruct struct1   `binding:"required"`
+	RequiredBasicSlice    []int     `binding:"required"`
+	RequiredComplexSlice  []struct2 `binding:"required"`
+	RequiredBoolean       bool      `binding:"required"`
+}
+
+func createStruct() struct3 {
+	return struct3{
+		RequiredInteger:       2,
+		RequiredString:        "hello",
+		RequiredAnotherStruct: struct1{1.5},
+		RequiredBasicSlice:    []int{1, 2, 3, 4},
+		RequiredComplexSlice: []struct2{
+			{RequiredValue: "A"},
+			{RequiredValue: "B"},
+		},
+		RequiredBoolean: true,
+	}
+}
+
+func TestValidateGoodObject(t *testing.T) {
+	test := createStruct()
+	assert.Nil(t, Validate(&test))
+}

+ 24 - 0
binding/xml.go

@@ -0,0 +1,24 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"encoding/xml"
+	"net/http"
+)
+
+type xmlBinding struct{}
+
+func (_ xmlBinding) Name() string {
+	return "xml"
+}
+
+func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error {
+	decoder := xml.NewDecoder(req.Body)
+	if err := decoder.Decode(obj); err != nil {
+		return err
+	}
+	return Validate(obj)
+}

+ 267 - 235
context.go

@@ -5,56 +5,58 @@
 package gin
 
 import (
-	"bytes"
 	"errors"
-	"fmt"
-	"github.com/gin-gonic/gin/binding"
-	"github.com/gin-gonic/gin/render"
-	"github.com/julienschmidt/httprouter"
-	"log"
-	"net"
+	"io"
+	"math"
 	"net/http"
 	"strings"
+	"time"
+
+	"github.com/gin-gonic/gin/binding"
+	"github.com/gin-gonic/gin/render"
+	"github.com/manucorporat/sse"
+	"golang.org/x/net/context"
 )
 
 const (
-	ErrorTypeInternal = 1 << iota
-	ErrorTypeExternal = 1 << iota
-	ErrorTypeAll      = 0xffffffff
+	MIMEJSON              = binding.MIMEJSON
+	MIMEHTML              = binding.MIMEHTML
+	MIMEXML               = binding.MIMEXML
+	MIMEXML2              = binding.MIMEXML2
+	MIMEPlain             = binding.MIMEPlain
+	MIMEPOSTForm          = binding.MIMEPOSTForm
+	MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
 )
 
-// Used internally to collect errors that occurred during an http request.
-type errorMsg struct {
-	Err  string      `json:"error"`
-	Type uint32      `json:"-"`
-	Meta interface{} `json:"meta"`
+const AbortIndex = math.MaxInt8 / 2
+
+var _ context.Context = &Context{}
+
+// Param is a single URL parameter, consisting of a key and a value.
+type Param struct {
+	Key   string
+	Value string
 }
 
-type errorMsgs []errorMsg
+// Params is a Param-slice, as returned by the router.
+// The slice is ordered, the first URL parameter is also the first slice value.
+// It is therefore safe to read values by the index.
+type Params []Param
 
-func (a errorMsgs) ByType(typ uint32) errorMsgs {
-	if len(a) == 0 {
-		return a
-	}
-	result := make(errorMsgs, 0, len(a))
-	for _, msg := range a {
-		if msg.Type&typ > 0 {
-			result = append(result, msg)
+// ByName returns the value of the first Param which key matches the given name.
+// If no matching Param is found, an empty string is returned.
+func (ps Params) Get(name string) (string, bool) {
+	for _, entry := range ps {
+		if entry.Key == name {
+			return entry.Value, true
 		}
 	}
-	return result
+	return "", false
 }
 
-func (a errorMsgs) String() string {
-	if len(a) == 0 {
-		return ""
-	}
-	var buffer bytes.Buffer
-	for i, msg := range a {
-		text := fmt.Sprintf("Error #%02d: %s \n     Meta: %v\n", (i + 1), msg.Err, msg.Meta)
-		buffer.WriteString(text)
-	}
-	return buffer.String()
+func (ps Params) ByName(name string) (va string) {
+	va, _ = ps.Get(name)
+	return
 }
 
 // Context is the most important part of gin. It allows us to pass variables between middleware,
@@ -63,38 +65,35 @@ type Context struct {
 	writermem responseWriter
 	Request   *http.Request
 	Writer    ResponseWriter
-	Keys      map[string]interface{}
-	Errors    errorMsgs
-	Params    httprouter.Params
-	Engine    *Engine
-	handlers  []HandlerFunc
-	index     int8
-	accepted  []string
+
+	Params   Params
+	handlers HandlersChain
+	index    int8
+
+	engine   *Engine
+	Keys     map[string]interface{}
+	Errors   errorMsgs
+	Accepted []string
 }
 
 /************************************/
 /********** CONTEXT CREATION ********/
 /************************************/
 
-func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context {
-	c := engine.pool.Get().(*Context)
-	c.writermem.reset(w)
-	c.Request = req
-	c.Params = params
-	c.handlers = handlers
-	c.Keys = nil
+func (c *Context) reset() {
+	c.Writer = &c.writermem
+	c.Params = c.Params[0:0]
+	c.handlers = nil
 	c.index = -1
-	c.accepted = nil
+	c.Keys = nil
 	c.Errors = c.Errors[0:0]
-	return c
-}
-
-func (engine *Engine) reuseContext(c *Context) {
-	engine.pool.Put(c)
+	c.Accepted = nil
 }
 
 func (c *Context) Copy() *Context {
 	var cp Context = *c
+	cp.writermem.ResponseWriter = nil
+	cp.Writer = &cp.writermem
 	cp.index = AbortIndex
 	cp.handlers = nil
 	return &cp
@@ -115,7 +114,7 @@ func (c *Context) Next() {
 	}
 }
 
-// Forces the system to do not continue calling the pending handlers in the chain.
+// Forces the system to not continue calling the pending handlers in the chain.
 func (c *Context) Abort() {
 	c.index = AbortIndex
 }
@@ -127,43 +126,35 @@ func (c *Context) AbortWithStatus(code int) {
 	c.Abort()
 }
 
-/************************************/
-/********* ERROR MANAGEMENT *********/
-/************************************/
-
-// Fail is the same as Abort plus an error message.
-// Calling `context.Fail(500, err)` is equivalent to:
-// ```
-// context.Error("Operation aborted", err)
-// context.AbortWithStatus(500)
-// ```
-func (c *Context) Fail(code int, err error) {
-	c.Error(err, "Operation aborted")
+func (c *Context) AbortWithError(code int, err error) *Error {
 	c.AbortWithStatus(code)
+	return c.Error(err)
 }
 
-func (c *Context) ErrorTyped(err error, typ uint32, meta interface{}) {
-	c.Errors = append(c.Errors, errorMsg{
-		Err:  err.Error(),
-		Type: typ,
-		Meta: meta,
-	})
+func (c *Context) IsAborted() bool {
+	return c.index == AbortIndex
 }
 
+/************************************/
+/********* ERROR MANAGEMENT *********/
+/************************************/
+
 // Attaches an error to the current context. The error is pushed to a list of errors.
 // It's a good idea to call Error for each error that occurred during the resolution of a request.
 // A middleware can be used to collect all the errors and push them to a database together, print a log, or append it in the HTTP response.
-func (c *Context) Error(err error, meta interface{}) {
-	c.ErrorTyped(err, ErrorTypeExternal, meta)
-}
-
-func (c *Context) LastError() error {
-	nuErrors := len(c.Errors)
-	if nuErrors > 0 {
-		return errors.New(c.Errors[nuErrors-1].Err)
-	} else {
-		return nil
+func (c *Context) Error(err error) *Error {
+	var parsedError *Error
+	switch err.(type) {
+	case *Error:
+		parsedError = err.(*Error)
+	default:
+		parsedError = &Error{
+			Err:  err,
+			Type: ErrorTypePrivate,
+		}
 	}
+	c.Errors = append(c.Errors, parsedError)
+	return parsedError
 }
 
 /************************************/
@@ -172,206 +163,201 @@ func (c *Context) LastError() error {
 
 // Sets a new pair key/value just for the specified context.
 // It also lazy initializes the hashmap.
-func (c *Context) Set(key string, item interface{}) {
+func (c *Context) Set(key string, value interface{}) {
 	if c.Keys == nil {
 		c.Keys = make(map[string]interface{})
 	}
-	c.Keys[key] = item
+	c.Keys[key] = value
 }
 
 // Get returns the value for the given key or an error if the key does not exist.
-func (c *Context) Get(key string) (interface{}, error) {
+func (c *Context) Get(key string) (value interface{}, exists bool) {
 	if c.Keys != nil {
-		value, ok := c.Keys[key]
-		if ok {
-			return value, nil
-		}
+		value, exists = c.Keys[key]
 	}
-	return nil, errors.New("Key does not exist.")
+	return
 }
 
 // MustGet returns the value for the given key or panics if the value doesn't exist.
 func (c *Context) MustGet(key string) interface{} {
-	value, err := c.Get(key)
-	if err != nil || value == nil {
-		log.Panicf("Key %s doesn't exist", value)
+	if value, exists := c.Get(key); exists {
+		return value
 	}
-	return value
+	panic("Key \"" + key + "\" does not exist")
 }
 
-func ipInMasks(ip net.IP, masks []interface{}) bool {
-	for _, proxy := range masks {
-		var mask *net.IPNet
-		var err error
-
-		switch t := proxy.(type) {
-		case string:
-			if _, mask, err = net.ParseCIDR(t); err != nil {
-				panic(err)
-			}
-		case net.IP:
-			mask = &net.IPNet{IP: t, Mask: net.CIDRMask(len(t)*8, len(t)*8)}
-		case net.IPNet:
-			mask = &t
-		}
-
-		if mask.Contains(ip) {
-			return true
-		}
-	}
+/************************************/
+/************ INPUT DATA ************/
+/************************************/
 
-	return false
+/** Shortcut for c.Request.FormValue(key) */
+func (c *Context) FormValue(key string) (va string) {
+	va, _ = c.formValue(key)
+	return
 }
 
-// the ForwardedFor middleware unwraps the X-Forwarded-For headers, be careful to only use this
-// middleware if you've got servers in front of this server. The list with (known) proxies and
-// local ips are being filtered out of the forwarded for list, giving the last not local ip being
-// the real client ip.
-func ForwardedFor(proxies ...interface{}) HandlerFunc {
-	if len(proxies) == 0 {
-		// default to local ips
-		var reservedLocalIps = []string{"10.0.0.0/8", "127.0.0.1/32", "172.16.0.0/12", "192.168.0.0/16"}
+/** Shortcut for c.Request.PostFormValue(key) */
+func (c *Context) PostFormValue(key string) (va string) {
+	va, _ = c.postFormValue(key)
+	return
+}
 
-		proxies = make([]interface{}, len(reservedLocalIps))
+/** Shortcut for c.Params.ByName(key) */
+func (c *Context) ParamValue(key string) (va string) {
+	va, _ = c.paramValue(key)
+	return
+}
 
-		for i, v := range reservedLocalIps {
-			proxies[i] = v
-		}
+func (c *Context) DefaultPostFormValue(key, defaultValue string) string {
+	if va, ok := c.postFormValue(key); ok {
+		return va
 	}
+	return defaultValue
+}
 
-	return func(c *Context) {
-		// the X-Forwarded-For header contains an array with left most the client ip, then
-		// comma separated, all proxies the request passed. The last proxy appears
-		// as the remote address of the request. Returning the client
-		// ip to comply with default RemoteAddr response.
-
-		// check if remoteaddr is local ip or in list of defined proxies
-		remoteIp := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0])
-
-		if !ipInMasks(remoteIp, proxies) {
-			return
-		}
-
-		if forwardedFor := c.Request.Header.Get("X-Forwarded-For"); forwardedFor != "" {
-			parts := strings.Split(forwardedFor, ",")
-
-			for i := len(parts) - 1; i >= 0; i-- {
-				part := parts[i]
-
-				ip := net.ParseIP(strings.TrimSpace(part))
+func (c *Context) DefaultFormValue(key, defaultValue string) string {
+	if va, ok := c.formValue(key); ok {
+		return va
+	}
+	return defaultValue
+}
 
-				if ipInMasks(ip, proxies) {
-					continue
-				}
+func (c *Context) DefaultParamValue(key, defaultValue string) string {
+	if va, ok := c.paramValue(key); ok {
+		return va
+	}
+	return defaultValue
+}
 
-				// returning remote addr conform the original remote addr format
-				c.Request.RemoteAddr = ip.String() + ":0"
+func (c *Context) paramValue(key string) (string, bool) {
+	return c.Params.Get(key)
+}
 
-				// remove forwarded for address
-				c.Request.Header.Set("X-Forwarded-For", "")
-				return
-			}
-		}
+func (c *Context) formValue(key string) (string, bool) {
+	req := c.Request
+	req.ParseForm()
+	if values, ok := req.Form[key]; ok && len(values) > 0 {
+		return values[0], true
 	}
+	return "", false
 }
 
-func (c *Context) ClientIP() string {
-	return c.Request.RemoteAddr
+func (c *Context) postFormValue(key string) (string, bool) {
+	req := c.Request
+	req.ParseForm()
+	if values, ok := req.PostForm[key]; ok && len(values) > 0 {
+		return values[0], true
+	}
+	return "", false
 }
 
-/************************************/
-/********* PARSING REQUEST **********/
-/************************************/
-
 // This function checks the Content-Type to select a binding engine automatically,
 // Depending the "Content-Type" header different bindings are used:
 // "application/json" --> JSON binding
 // "application/xml"  --> XML binding
 // else --> returns an error
 // if Parses the request's body as JSON if Content-Type == "application/json"  using JSON or XML  as a JSON input. It decodes the json payload into the struct specified as a pointer.Like ParseBody() but this method also writes a 400 error if the json is not valid.
-func (c *Context) Bind(obj interface{}) bool {
-	var b binding.Binding
-	ctype := filterFlags(c.Request.Header.Get("Content-Type"))
-	switch {
-	case c.Request.Method == "GET" || ctype == MIMEPOSTForm:
-		b = binding.Form
-	case ctype == MIMEMultipartPOSTForm:
-		b = binding.MultipartForm
-	case ctype == MIMEJSON:
-		b = binding.JSON
-	case ctype == MIMEXML || ctype == MIMEXML2:
-		b = binding.XML
-	default:
-		c.Fail(400, errors.New("unknown content-type: "+ctype))
-		return false
-	}
+func (c *Context) Bind(obj interface{}) error {
+	b := binding.Default(c.Request.Method, c.ContentType())
 	return c.BindWith(obj, b)
 }
 
-func (c *Context) BindWith(obj interface{}, b binding.Binding) bool {
+func (c *Context) BindJSON(obj interface{}) error {
+	return c.BindWith(obj, binding.JSON)
+}
+
+func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
 	if err := b.Bind(c.Request, obj); err != nil {
-		c.Fail(400, err)
-		return false
+		c.AbortWithError(400, err).SetType(ErrorTypeBind)
+		return err
 	}
-	return true
+	return nil
+}
+
+func (c *Context) ClientIP() string {
+	clientIP := c.Request.Header.Get("X-Real-IP")
+	if len(clientIP) > 0 {
+		return clientIP
+	}
+	clientIP = c.Request.Header.Get("X-Forwarded-For")
+	clientIP = strings.Split(clientIP, ",")[0]
+	if len(clientIP) > 0 {
+		return strings.TrimSpace(clientIP)
+	}
+	return c.Request.RemoteAddr
+}
+
+func (c *Context) ContentType() string {
+	return filterFlags(c.Request.Header.Get("Content-Type"))
 }
 
 /************************************/
 /******** RESPONSE RENDERING ********/
 /************************************/
 
-func (c *Context) Render(code int, render render.Render, obj ...interface{}) {
-	if err := render.Render(c.Writer, code, obj...); err != nil {
-		c.ErrorTyped(err, ErrorTypeInternal, obj)
-		c.AbortWithStatus(500)
+func (c *Context) Header(key, value string) {
+	if len(value) == 0 {
+		c.Writer.Header().Del(key)
+	} else {
+		c.Writer.Header().Set(key, value)
+	}
+}
+
+func (c *Context) Render(code int, r render.Render) {
+	c.Writer.WriteHeader(code)
+	if err := r.Write(c.Writer); err != nil {
+		debugPrintError(err)
+		c.AbortWithError(500, err).SetType(ErrorTypeRender)
 	}
 }
 
+// Renders the HTTP template specified by its file name.
+// It also updates the HTTP code and sets the Content-Type as "text/html".
+// See http://golang.org/doc/articles/wiki/
+func (c *Context) HTML(code int, name string, obj interface{}) {
+	instance := c.engine.HTMLRender.Instance(name, obj)
+	c.Render(code, instance)
+}
+
+func (c *Context) IndentedJSON(code int, obj interface{}) {
+	c.Render(code, render.IndentedJSON{Data: obj})
+}
+
 // Serializes the given struct as JSON into the response body in a fast and efficient way.
 // It also sets the Content-Type as "application/json".
 func (c *Context) JSON(code int, obj interface{}) {
-	c.Render(code, render.JSON, obj)
+	c.Render(code, render.JSON{Data: obj})
 }
 
 // Serializes the given struct as XML into the response body in a fast and efficient way.
 // It also sets the Content-Type as "application/xml".
 func (c *Context) XML(code int, obj interface{}) {
-	c.Render(code, render.XML, obj)
-}
-
-// Renders the HTTP template specified by its file name.
-// It also updates the HTTP code and sets the Content-Type as "text/html".
-// See http://golang.org/doc/articles/wiki/
-func (c *Context) HTML(code int, name string, obj interface{}) {
-	c.Render(code, c.Engine.HTMLRender, name, obj)
+	c.Render(code, render.XML{Data: obj})
 }
 
 // Writes the given string into the response body and sets the Content-Type to "text/plain".
 func (c *Context) String(code int, format string, values ...interface{}) {
-	c.Render(code, render.Plain, format, values)
-}
-
-// Writes the given string into the response body and sets the Content-Type to "text/html" without template.
-func (c *Context) HTMLString(code int, format string, values ...interface{}) {
-	c.Render(code, render.HTMLPlain, format, values)
+	c.Render(code, render.String{
+		Format: format,
+		Data:   values},
+	)
 }
 
 // Returns a HTTP redirect to the specific location.
 func (c *Context) Redirect(code int, location string) {
-	if code >= 300 && code <= 308 {
-		c.Render(code, render.Redirect, location)
-	} else {
-		panic(fmt.Sprintf("Cannot send a redirect with status code %d", code))
-	}
+	c.Render(-1, render.Redirect{
+		Code:     code,
+		Location: location,
+		Request:  c.Request,
+	})
 }
 
 // Writes some data into the body stream and updates the HTTP code.
 func (c *Context) Data(code int, contentType string, data []byte) {
-	if len(contentType) > 0 {
-		c.Writer.Header().Set("Content-Type", contentType)
-	}
-	c.Writer.WriteHeader(code)
-	c.Writer.Write(data)
+	c.Render(code, render.Data{
+		ContentType: contentType,
+		Data:        data,
+	})
 }
 
 // Writes the specified file into the body stream
@@ -379,13 +365,37 @@ func (c *Context) File(filepath string) {
 	http.ServeFile(c.Writer, c.Request, filepath)
 }
 
+func (c *Context) SSEvent(name string, message interface{}) {
+	c.Render(-1, sse.Event{
+		Event: name,
+		Data:  message,
+	})
+}
+
+func (c *Context) Stream(step func(w io.Writer) bool) {
+	w := c.Writer
+	clientGone := w.CloseNotify()
+	for {
+		select {
+		case <-clientGone:
+			return
+		default:
+			keepopen := step(w)
+			w.Flush()
+			if !keepopen {
+				return
+			}
+		}
+	}
+}
+
 /************************************/
 /******** CONTENT NEGOTIATION *******/
 /************************************/
 
 type Negotiate struct {
 	Offered  []string
-	HTMLPath string
+	HTMLName string
 	HTMLData interface{}
 	JSONData interface{}
 	XMLData  interface{}
@@ -394,23 +404,20 @@ type Negotiate struct {
 
 func (c *Context) Negotiate(code int, config Negotiate) {
 	switch c.NegotiateFormat(config.Offered...) {
-	case MIMEJSON:
+	case binding.MIMEJSON:
 		data := chooseData(config.JSONData, config.Data)
 		c.JSON(code, data)
 
-	case MIMEHTML:
+	case binding.MIMEHTML:
 		data := chooseData(config.HTMLData, config.Data)
-		if len(config.HTMLPath) == 0 {
-			panic("negotiate config is wrong. html path is needed")
-		}
-		c.HTML(code, config.HTMLPath, data)
+		c.HTML(code, config.HTMLName, data)
 
-	case MIMEXML:
+	case binding.MIMEXML:
 		data := chooseData(config.XMLData, config.Data)
 		c.XML(code, data)
 
 	default:
-		c.Fail(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server"))
+		c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server"))
 	}
 }
 
@@ -418,24 +425,49 @@ func (c *Context) NegotiateFormat(offered ...string) string {
 	if len(offered) == 0 {
 		panic("you must provide at least one offer")
 	}
-	if c.accepted == nil {
-		c.accepted = parseAccept(c.Request.Header.Get("Accept"))
+	if c.Accepted == nil {
+		c.Accepted = parseAccept(c.Request.Header.Get("Accept"))
 	}
-	if len(c.accepted) == 0 {
+	if len(c.Accepted) == 0 {
 		return offered[0]
-
-	} else {
-		for _, accepted := range c.accepted {
-			for _, offert := range offered {
-				if accepted == offert {
-					return offert
-				}
+	}
+	for _, accepted := range c.Accepted {
+		for _, offert := range offered {
+			if accepted == offert {
+				return offert
 			}
 		}
-		return ""
 	}
+	return ""
 }
 
 func (c *Context) SetAccepted(formats ...string) {
-	c.accepted = formats
+	c.Accepted = formats
+}
+
+/************************************/
+/******** CONTENT NEGOTIATION *******/
+/************************************/
+
+func (c *Context) Deadline() (deadline time.Time, ok bool) {
+	return
+}
+
+func (c *Context) Done() <-chan struct{} {
+	return nil
+}
+
+func (c *Context) Err() error {
+	return nil
+}
+
+func (c *Context) Value(key interface{}) interface{} {
+	if key == 0 {
+		return c.Request
+	}
+	if keyAsString, ok := key.(string); ok {
+		val, _ := c.Get(keyAsString)
+		return val
+	}
+	return nil
 }

+ 371 - 406
context_test.go

@@ -11,509 +11,474 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"testing"
-)
-
-// TestContextParamsGet tests that a parameter can be parsed from the URL.
-func TestContextParamsByName(t *testing.T) {
-	req, _ := http.NewRequest("GET", "/test/alexandernyquist", nil)
-	w := httptest.NewRecorder()
-	name := ""
+	"time"
 
-	r := New()
-	r.GET("/test/:name", func(c *Context) {
-		name = c.Params.ByName("name")
-	})
+	"github.com/gin-gonic/gin/binding"
+	"github.com/manucorporat/sse"
+	"github.com/stretchr/testify/assert"
+)
 
-	r.ServeHTTP(w, req)
+// Unit tests TODO
+// func (c *Context) File(filepath string) {
+// func (c *Context) Negotiate(code int, config Negotiate) {
+// BAD case: func (c *Context) Render(code int, render render.Render, obj ...interface{}) {
+// test that information is not leaked when reusing Contexts (using the Pool)
+
+func createTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) {
+	w = httptest.NewRecorder()
+	r = New()
+	c = r.allocateContext()
+	c.reset()
+	c.writermem.reset(w)
+	return
+}
 
-	if name != "alexandernyquist" {
-		t.Errorf("Url parameter was not correctly parsed. Should be alexandernyquist, was %s.", name)
-	}
+func TestContextReset(t *testing.T) {
+	router := New()
+	c := router.allocateContext()
+	assert.Equal(t, c.engine, router)
+
+	c.index = 2
+	c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()}
+	c.Params = Params{Param{}}
+	c.Error(errors.New("test"))
+	c.Set("foo", "bar")
+	c.reset()
+
+	assert.False(t, c.IsAborted())
+	assert.Nil(t, c.Keys)
+	assert.Nil(t, c.Accepted)
+	assert.Len(t, c.Errors, 0)
+	assert.Empty(t, c.Errors.Errors())
+	assert.Empty(t, c.Errors.ByType(ErrorTypeAny))
+	assert.Len(t, c.Params, 0)
+	assert.Equal(t, c.index, -1)
+	assert.Equal(t, c.Writer.(*responseWriter), &c.writermem)
 }
 
 // TestContextSetGet tests that a parameter is set correctly on the
 // current context and can be retrieved using Get.
 func TestContextSetGet(t *testing.T) {
-	req, _ := http.NewRequest("GET", "/test", nil)
-	w := httptest.NewRecorder()
-
-	r := New()
-	r.GET("/test", func(c *Context) {
-		// Key should be lazily created
-		if c.Keys != nil {
-			t.Error("Keys should be nil")
-		}
-
-		// Set
-		c.Set("foo", "bar")
-
-		v, err := c.Get("foo")
-		if err != nil {
-			t.Errorf("Error on exist key")
-		}
-		if v != "bar" {
-			t.Errorf("Value should be bar, was %s", v)
-		}
-	})
+	c, _, _ := createTestContext()
+	c.Set("foo", "bar")
+
+	value, err := c.Get("foo")
+	assert.Equal(t, value, "bar")
+	assert.True(t, err)
 
-	r.ServeHTTP(w, req)
+	value, err = c.Get("foo2")
+	assert.Nil(t, value)
+	assert.False(t, err)
+
+	assert.Equal(t, c.MustGet("foo"), "bar")
+	assert.Panics(t, func() { c.MustGet("no_exist") })
 }
 
-// TestContextJSON tests that the response is serialized as JSON
-// and Content-Type is set to application/json
-func TestContextJSON(t *testing.T) {
-	req, _ := http.NewRequest("GET", "/test", nil)
-	w := httptest.NewRecorder()
+func TestContextSetGetValues(t *testing.T) {
+	c, _, _ := createTestContext()
+	c.Set("string", "this is a string")
+	c.Set("int32", int32(-42))
+	c.Set("int64", int64(42424242424242))
+	c.Set("uint64", uint64(42))
+	c.Set("float32", float32(4.2))
+	c.Set("float64", 4.2)
+	var a interface{} = 1
+	c.Set("intInterface", a)
+
+	assert.Exactly(t, c.MustGet("string").(string), "this is a string")
+	assert.Exactly(t, c.MustGet("int32").(int32), int32(-42))
+	assert.Exactly(t, c.MustGet("int64").(int64), int64(42424242424242))
+	assert.Exactly(t, c.MustGet("uint64").(uint64), uint64(42))
+	assert.Exactly(t, c.MustGet("float32").(float32), float32(4.2))
+	assert.Exactly(t, c.MustGet("float64").(float64), 4.2)
+	assert.Exactly(t, c.MustGet("intInterface").(int), 1)
 
-	r := New()
-	r.GET("/test", func(c *Context) {
-		c.JSON(200, H{"foo": "bar"})
-	})
+}
 
-	r.ServeHTTP(w, req)
+func TestContextCopy(t *testing.T) {
+	c, _, _ := createTestContext()
+	c.index = 2
+	c.Request, _ = http.NewRequest("POST", "/hola", nil)
+	c.handlers = HandlersChain{func(c *Context) {}}
+	c.Params = Params{Param{Key: "foo", Value: "bar"}}
+	c.Set("foo", "bar")
+
+	cp := c.Copy()
+	assert.Nil(t, cp.handlers)
+	assert.Nil(t, cp.writermem.ResponseWriter)
+	assert.Equal(t, &cp.writermem, cp.Writer.(*responseWriter))
+	assert.Equal(t, cp.Request, c.Request)
+	assert.Equal(t, cp.index, AbortIndex)
+	assert.Equal(t, cp.Keys, c.Keys)
+	assert.Equal(t, cp.engine, c.engine)
+	assert.Equal(t, cp.Params, c.Params)
+}
 
-	if w.Body.String() != "{\"foo\":\"bar\"}\n" {
-		t.Errorf("Response should be {\"foo\":\"bar\"}, was: %s", w.Body.String())
-	}
+func TestContextFormParse(t *testing.T) {
+	c, _, _ := createTestContext()
+	c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10", nil)
 
-	if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
-		t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
-	}
+	assert.Equal(t, c.DefaultFormValue("foo", "none"), "bar")
+	assert.Equal(t, c.FormValue("foo"), "bar")
+	assert.Empty(t, c.PostFormValue("foo"))
+
+	assert.Equal(t, c.DefaultFormValue("page", "0"), "10")
+	assert.Equal(t, c.FormValue("page"), "10")
+	assert.Empty(t, c.PostFormValue("page"))
+
+	assert.Equal(t, c.DefaultFormValue("NoKey", "nada"), "nada")
+	assert.Empty(t, c.FormValue("NoKey"))
+	assert.Empty(t, c.PostFormValue("NoKey"))
 }
 
-// TestContextHTML tests that the response executes the templates
-// and responds with Content-Type set to text/html
-func TestContextHTML(t *testing.T) {
-	req, _ := http.NewRequest("GET", "/test", nil)
-	w := httptest.NewRecorder()
+func TestContextPostFormParse(t *testing.T) {
+	c, _, _ := createTestContext()
+	body := bytes.NewBufferString("foo=bar&page=11&both=POST")
+	c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main", body)
+	c.Request.Header.Add("Content-Type", MIMEPOSTForm)
 
-	r := New()
-	templ, _ := template.New("t").Parse(`Hello {{.Name}}`)
-	r.SetHTMLTemplate(templ)
+	assert.Equal(t, c.DefaultPostFormValue("foo", "none"), "bar")
+	assert.Equal(t, c.PostFormValue("foo"), "bar")
+	assert.Equal(t, c.FormValue("foo"), "bar")
 
-	type TestData struct{ Name string }
+	assert.Equal(t, c.DefaultPostFormValue("page", "0"), "11")
+	assert.Equal(t, c.PostFormValue("page"), "11")
+	assert.Equal(t, c.FormValue("page"), "11")
 
-	r.GET("/test", func(c *Context) {
-		c.HTML(200, "t", TestData{"alexandernyquist"})
-	})
+	assert.Equal(t, c.PostFormValue("both"), "POST")
+	assert.Equal(t, c.FormValue("both"), "POST")
 
-	r.ServeHTTP(w, req)
+	assert.Equal(t, c.FormValue("id"), "main")
+	assert.Empty(t, c.PostFormValue("id"))
 
-	if w.Body.String() != "Hello alexandernyquist" {
-		t.Errorf("Response should be Hello alexandernyquist, was: %s", w.Body.String())
-	}
+	assert.Equal(t, c.DefaultPostFormValue("NoKey", "nada"), "nada")
+	assert.Empty(t, c.PostFormValue("NoKey"))
+	assert.Empty(t, c.FormValue("NoKey"))
+}
 
-	if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" {
-		t.Errorf("Content-Type should be text/html, was %s", w.HeaderMap.Get("Content-Type"))
-	}
+// Tests that the response is serialized as JSON
+// and Content-Type is set to application/json
+func TestContextRenderJSON(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.JSON(201, H{"foo": "bar"})
+
+	assert.Equal(t, w.Code, 201)
+	assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n")
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
 }
 
-// TestContextString tests that the response is returned
-// with Content-Type set to text/plain
-func TestContextString(t *testing.T) {
-	req, _ := http.NewRequest("GET", "/test", nil)
-	w := httptest.NewRecorder()
+// Tests that the response is serialized as JSON
+// and Content-Type is set to application/json
+func TestContextRenderIndentedJSON(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
 
-	r := New()
-	r.GET("/test", func(c *Context) {
-		c.String(200, "test")
-	})
+	assert.Equal(t, w.Code, 201)
+	assert.Equal(t, w.Body.String(), "{\n    \"bar\": \"foo\",\n    \"foo\": \"bar\",\n    \"nested\": {\n        \"foo\": \"bar\"\n    }\n}")
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
+}
 
-	r.ServeHTTP(w, req)
+// Tests that the response executes the templates
+// and responds with Content-Type set to text/html
+func TestContextRenderHTML(t *testing.T) {
+	c, w, router := createTestContext()
+	templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
+	router.SetHTMLTemplate(templ)
 
-	if w.Body.String() != "test" {
-		t.Errorf("Response should be test, was: %s", w.Body.String())
-	}
+	c.HTML(201, "t", H{"name": "alexandernyquist"})
 
-	if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" {
-		t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
-	}
+	assert.Equal(t, w.Code, 201)
+	assert.Equal(t, w.Body.String(), "Hello alexandernyquist")
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
 }
 
 // TestContextXML tests that the response is serialized as XML
 // and Content-Type is set to application/xml
-func TestContextXML(t *testing.T) {
-	req, _ := http.NewRequest("GET", "/test", nil)
-	w := httptest.NewRecorder()
+func TestContextRenderXML(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.XML(201, H{"foo": "bar"})
 
-	r := New()
-	r.GET("/test", func(c *Context) {
-		c.XML(200, H{"foo": "bar"})
-	})
+	assert.Equal(t, w.Code, 201)
+	assert.Equal(t, w.Body.String(), "<map><foo>bar</foo></map>")
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8")
+}
 
-	r.ServeHTTP(w, req)
+// TestContextString tests that the response is returned
+// with Content-Type set to text/plain
+func TestContextRenderString(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.String(201, "test %s %d", "string", 2)
 
-	if w.Body.String() != "<map><foo>bar</foo></map>" {
-		t.Errorf("Response should be <map><foo>bar</foo></map>, was: %s", w.Body.String())
-	}
+	assert.Equal(t, w.Code, 201)
+	assert.Equal(t, w.Body.String(), "test string 2")
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
+}
 
-	if w.HeaderMap.Get("Content-Type") != "application/xml; charset=utf-8" {
-		t.Errorf("Content-Type should be application/xml, was %s", w.HeaderMap.Get("Content-Type"))
-	}
+// TestContextString tests that the response is returned
+// with Content-Type set to text/html
+func TestContextRenderHTMLString(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.Header("Content-Type", "text/html; charset=utf-8")
+	c.String(201, "<html>%s %d</html>", "string", 3)
+
+	assert.Equal(t, w.Code, 201)
+	assert.Equal(t, w.Body.String(), "<html>string 3</html>")
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
 }
 
 // TestContextData tests that the response can be written from `bytesting`
 // with specified MIME type
-func TestContextData(t *testing.T) {
-	req, _ := http.NewRequest("GET", "/test/csv", nil)
-	w := httptest.NewRecorder()
-
-	r := New()
-	r.GET("/test/csv", func(c *Context) {
-		c.Data(200, "text/csv", []byte(`foo,bar`))
-	})
-
-	r.ServeHTTP(w, req)
+func TestContextRenderData(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.Data(201, "text/csv", []byte(`foo,bar`))
 
-	if w.Body.String() != "foo,bar" {
-		t.Errorf("Response should be foo&bar, was: %s", w.Body.String())
-	}
-
-	if w.HeaderMap.Get("Content-Type") != "text/csv" {
-		t.Errorf("Content-Type should be text/csv, was %s", w.HeaderMap.Get("Content-Type"))
-	}
+	assert.Equal(t, w.Code, 201)
+	assert.Equal(t, w.Body.String(), "foo,bar")
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv")
 }
 
-func TestContextFile(t *testing.T) {
-	req, _ := http.NewRequest("GET", "/test/file", nil)
-	w := httptest.NewRecorder()
-
-	r := New()
-	r.GET("/test/file", func(c *Context) {
-		c.File("./gin.go")
+func TestContextRenderSSE(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.SSEvent("float", 1.5)
+	c.Render(-1, sse.Event{
+		Id:   "123",
+		Data: "text",
+	})
+	c.SSEvent("chat", H{
+		"foo": "bar",
+		"bar": "foo",
 	})
 
-	r.ServeHTTP(w, req)
-
-	bodyAsString := w.Body.String()
-
-	if len(bodyAsString) == 0 {
-		t.Errorf("Got empty body instead of file data")
-	}
-
-	if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" {
-		t.Errorf("Content-Type should be text/plain; charset=utf-8, was %s", w.HeaderMap.Get("Content-Type"))
-	}
+	assert.Equal(t, w.Body.String(), "event: float\ndata: 1.5\n\nid: 123\ndata: text\n\nevent: chat\ndata: {\"bar\":\"foo\",\"foo\":\"bar\"}\n\n")
 }
 
-// TestHandlerFunc - ensure that custom middleware works properly
-func TestHandlerFunc(t *testing.T) {
+func TestContextRenderFile(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.Request, _ = http.NewRequest("GET", "/", nil)
+	c.File("./gin.go")
 
-	req, _ := http.NewRequest("GET", "/", nil)
-	w := httptest.NewRecorder()
+	assert.Equal(t, w.Code, 200)
+	assert.Contains(t, w.Body.String(), "func New() *Engine {")
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
+}
 
-	r := New()
-	var stepsPassed int = 0
+func TestContextHeaders(t *testing.T) {
+	c, _, _ := createTestContext()
+	c.Header("Content-Type", "text/plain")
+	c.Header("X-Custom", "value")
 
-	r.Use(func(context *Context) {
-		stepsPassed += 1
-		context.Next()
-		stepsPassed += 1
-	})
+	assert.Equal(t, c.Writer.Header().Get("Content-Type"), "text/plain")
+	assert.Equal(t, c.Writer.Header().Get("X-Custom"), "value")
 
-	r.ServeHTTP(w, req)
+	c.Header("Content-Type", "text/html")
+	c.Header("X-Custom", "")
 
-	if w.Code != 404 {
-		t.Errorf("Response code should be Not found, was: %s", w.Code)
-	}
-
-	if stepsPassed != 2 {
-		t.Errorf("Falied to switch context in handler function: %s", stepsPassed)
-	}
+	assert.Equal(t, c.Writer.Header().Get("Content-Type"), "text/html")
+	_, exist := c.Writer.Header()["X-Custom"]
+	assert.False(t, exist)
 }
 
-// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers
-func TestBadAbortHandlersChain(t *testing.T) {
-	// SETUP
-	var stepsPassed int = 0
-	r := New()
-	r.Use(func(c *Context) {
-		stepsPassed += 1
-		c.Next()
-		stepsPassed += 1
-		// after check and abort
-		c.AbortWithStatus(409)
-	})
-	r.Use(func(c *Context) {
-		stepsPassed += 1
-		c.Next()
-		stepsPassed += 1
-		c.AbortWithStatus(403)
-	})
-
-	// RUN
-	w := PerformRequest(r, "GET", "/")
-
-	// TEST
-	if w.Code != 409 {
-		t.Errorf("Response code should be Forbiden, was: %d", w.Code)
-	}
-	if stepsPassed != 4 {
-		t.Errorf("Falied to switch context in handler function: %d", stepsPassed)
-	}
+// TODO
+func TestContextRenderRedirectWithRelativePath(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
+	assert.Panics(t, func() { c.Redirect(299, "/new_path") })
+	assert.Panics(t, func() { c.Redirect(309, "/new_path") })
+
+	c.Redirect(302, "/path")
+	c.Writer.WriteHeaderNow()
+	assert.Equal(t, w.Code, 302)
+	assert.Equal(t, w.Header().Get("Location"), "/path")
 }
 
-// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order
-func TestAbortHandlersChain(t *testing.T) {
-	// SETUP
-	var stepsPassed int = 0
-	r := New()
-	r.Use(func(context *Context) {
-		stepsPassed += 1
-		context.AbortWithStatus(409)
-	})
-	r.Use(func(context *Context) {
-		stepsPassed += 1
-		context.Next()
-		stepsPassed += 1
-	})
-
-	// RUN
-	w := PerformRequest(r, "GET", "/")
+func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
+	c.Redirect(302, "http://google.com")
+	c.Writer.WriteHeaderNow()
 
-	// TEST
-	if w.Code != 409 {
-		t.Errorf("Response code should be Conflict, was: %d", w.Code)
-	}
-	if stepsPassed != 1 {
-		t.Errorf("Falied to switch context in handler function: %d", stepsPassed)
-	}
+	assert.Equal(t, w.Code, 302)
+	assert.Equal(t, w.Header().Get("Location"), "http://google.com")
 }
 
-// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as
-// as well as Abort
-func TestFailHandlersChain(t *testing.T) {
-	// SETUP
-	var stepsPassed int = 0
-	r := New()
-	r.Use(func(context *Context) {
-		stepsPassed += 1
-		context.Fail(500, errors.New("foo"))
-	})
-	r.Use(func(context *Context) {
-		stepsPassed += 1
-		context.Next()
-		stepsPassed += 1
-	})
-
-	// RUN
-	w := PerformRequest(r, "GET", "/")
+func TestContextNegotiationFormat(t *testing.T) {
+	c, _, _ := createTestContext()
+	c.Request, _ = http.NewRequest("POST", "", nil)
 
-	// TEST
-	if w.Code != 500 {
-		t.Errorf("Response code should be Server error, was: %d", w.Code)
-	}
-	if stepsPassed != 1 {
-		t.Errorf("Falied to switch context in handler function: %d", stepsPassed)
-	}
+	assert.Panics(t, func() { c.NegotiateFormat() })
+	assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON)
+	assert.Equal(t, c.NegotiateFormat(MIMEHTML, MIMEJSON), MIMEHTML)
 }
 
-func TestBindingJSON(t *testing.T) {
+func TestContextNegotiationFormatWithAccept(t *testing.T) {
+	c, _, _ := createTestContext()
+	c.Request, _ = http.NewRequest("POST", "/", nil)
+	c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
 
-	body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}"))
-
-	r := New()
-	r.POST("/binding/json", func(c *Context) {
-		var body struct {
-			Foo string `json:"foo"`
-		}
-		if c.Bind(&body) {
-			c.JSON(200, H{"parsed": body.Foo})
-		}
-	})
+	assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEXML)
+	assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEHTML)
+	assert.Equal(t, c.NegotiateFormat(MIMEJSON), "")
+}
 
-	req, _ := http.NewRequest("POST", "/binding/json", body)
-	req.Header.Set("Content-Type", "application/json")
-	w := httptest.NewRecorder()
+func TestContextNegotiationFormatCustum(t *testing.T) {
+	c, _, _ := createTestContext()
+	c.Request, _ = http.NewRequest("POST", "/", nil)
+	c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
 
-	r.ServeHTTP(w, req)
+	c.Accepted = nil
+	c.SetAccepted(MIMEJSON, MIMEXML)
 
-	if w.Code != 200 {
-		t.Errorf("Response code should be Ok, was: %s", w.Code)
-	}
-
-	if w.Body.String() != "{\"parsed\":\"bar\"}\n" {
-		t.Errorf("Response should be {\"parsed\":\"bar\"}, was: %s", w.Body.String())
-	}
+	assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON)
+	assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEXML)
+	assert.Equal(t, c.NegotiateFormat(MIMEJSON), MIMEJSON)
+}
 
-	if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
-		t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
-	}
+// TestContextData tests that the response can be written from `bytesting`
+// with specified MIME type
+func TestContextAbortWithStatus(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.index = 4
+	c.AbortWithStatus(401)
+	c.Writer.WriteHeaderNow()
+
+	assert.Equal(t, c.index, AbortIndex)
+	assert.Equal(t, c.Writer.Status(), 401)
+	assert.Equal(t, w.Code, 401)
+	assert.True(t, c.IsAborted())
 }
 
-func TestBindingJSONEncoding(t *testing.T) {
+func TestContextError(t *testing.T) {
+	c, _, _ := createTestContext()
+	assert.Empty(t, c.Errors)
 
-	body := bytes.NewBuffer([]byte("{\"foo\":\"嘉\"}"))
+	c.Error(errors.New("first error"))
+	assert.Len(t, c.Errors, 1)
+	assert.Equal(t, c.Errors.String(), "Error #01: first error\n")
 
-	r := New()
-	r.POST("/binding/json", func(c *Context) {
-		var body struct {
-			Foo string `json:"foo"`
-		}
-		if c.Bind(&body) {
-			c.JSON(200, H{"parsed": body.Foo})
-		}
+	c.Error(&Error{
+		Err:  errors.New("second error"),
+		Meta: "some data 2",
+		Type: ErrorTypePublic,
 	})
+	assert.Len(t, c.Errors, 2)
 
-	req, _ := http.NewRequest("POST", "/binding/json", body)
-	req.Header.Set("Content-Type", "application/json; charset=utf-8")
-	w := httptest.NewRecorder()
+	assert.Equal(t, c.Errors[0].Err, errors.New("first error"))
+	assert.Nil(t, c.Errors[0].Meta)
+	assert.Equal(t, c.Errors[0].Type, ErrorTypePrivate)
 
-	r.ServeHTTP(w, req)
-
-	if w.Code != 200 {
-		t.Errorf("Response code should be Ok, was: %s", w.Code)
-	}
+	assert.Equal(t, c.Errors[1].Err, errors.New("second error"))
+	assert.Equal(t, c.Errors[1].Meta, "some data 2")
+	assert.Equal(t, c.Errors[1].Type, ErrorTypePublic)
 
-	if w.Body.String() != "{\"parsed\":\"嘉\"}\n" {
-		t.Errorf("Response should be {\"parsed\":\"嘉\"}, was: %s", w.Body.String())
-	}
-
-	if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
-		t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
-	}
+	assert.Equal(t, c.Errors.Last(), c.Errors[1])
 }
 
-func TestBindingJSONNoContentType(t *testing.T) {
-
-	body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}"))
-
-	r := New()
-	r.POST("/binding/json", func(c *Context) {
-		var body struct {
-			Foo string `json:"foo"`
-		}
-		if c.Bind(&body) {
-			c.JSON(200, H{"parsed": body.Foo})
-		}
-
-	})
-
-	req, _ := http.NewRequest("POST", "/binding/json", body)
-	w := httptest.NewRecorder()
-
-	r.ServeHTTP(w, req)
-
-	if w.Code != 400 {
-		t.Errorf("Response code should be Bad request, was: %s", w.Code)
-	}
+func TestContextTypedError(t *testing.T) {
+	c, _, _ := createTestContext()
+	c.Error(errors.New("externo 0")).SetType(ErrorTypePublic)
+	c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate)
 
-	if w.Body.String() == "{\"parsed\":\"bar\"}\n" {
-		t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String())
+	for _, err := range c.Errors.ByType(ErrorTypePublic) {
+		assert.Equal(t, err.Type, ErrorTypePublic)
 	}
-
-	if w.HeaderMap.Get("Content-Type") == "application/json" {
-		t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type"))
+	for _, err := range c.Errors.ByType(ErrorTypePrivate) {
+		assert.Equal(t, err.Type, ErrorTypePrivate)
 	}
+	assert.Equal(t, c.Errors.Errors(), []string{"externo 0", "interno 0"})
 }
 
-func TestBindingJSONMalformed(t *testing.T) {
-
-	body := bytes.NewBuffer([]byte("\"foo\":\"bar\"\n"))
-
-	r := New()
-	r.POST("/binding/json", func(c *Context) {
-		var body struct {
-			Foo string `json:"foo"`
-		}
-		if c.Bind(&body) {
-			c.JSON(200, H{"parsed": body.Foo})
-		}
-
-	})
-
-	req, _ := http.NewRequest("POST", "/binding/json", body)
-	req.Header.Set("Content-Type", "application/json")
+func TestContextAbortWithError(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.AbortWithError(401, errors.New("bad input")).SetMeta("some input")
+	c.Writer.WriteHeaderNow()
 
-	w := httptest.NewRecorder()
+	assert.Equal(t, w.Code, 401)
+	assert.Equal(t, c.index, AbortIndex)
+	assert.True(t, c.IsAborted())
+}
 
-	r.ServeHTTP(w, req)
+func TestContextClientIP(t *testing.T) {
+	c, _, _ := createTestContext()
+	c.Request, _ = http.NewRequest("POST", "/", nil)
 
-	if w.Code != 400 {
-		t.Errorf("Response code should be Bad request, was: %s", w.Code)
-	}
-	if w.Body.String() == "{\"parsed\":\"bar\"}\n" {
-		t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String())
-	}
+	c.Request.Header.Set("X-Real-IP", "10.10.10.10")
+	c.Request.Header.Set("X-Forwarded-For", "20.20.20.20 , 30.30.30.30")
+	c.Request.RemoteAddr = "40.40.40.40"
 
-	if w.HeaderMap.Get("Content-Type") == "application/json" {
-		t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type"))
-	}
+	assert.Equal(t, c.ClientIP(), "10.10.10.10")
+	c.Request.Header.Del("X-Real-IP")
+	assert.Equal(t, c.ClientIP(), "20.20.20.20")
+	c.Request.Header.Del("X-Forwarded-For")
+	assert.Equal(t, c.ClientIP(), "40.40.40.40")
 }
 
-func TestBindingForm(t *testing.T) {
+func TestContextContentType(t *testing.T) {
+	c, _, _ := createTestContext()
+	c.Request, _ = http.NewRequest("POST", "/", nil)
+	c.Request.Header.Set("Content-Type", "application/json; charset=utf-8")
 
-	body := bytes.NewBuffer([]byte("foo=bar&num=123&unum=1234567890"))
-
-	r := New()
-	r.POST("/binding/form", func(c *Context) {
-		var body struct {
-			Foo  string `form:"foo"`
-			Num  int    `form:"num"`
-			Unum uint   `form:"unum"`
-		}
-		if c.Bind(&body) {
-			c.JSON(200, H{"foo": body.Foo, "num": body.Num, "unum": body.Unum})
-		}
-	})
-
-	req, _ := http.NewRequest("POST", "/binding/form", body)
-	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-	w := httptest.NewRecorder()
+	assert.Equal(t, c.ContentType(), "application/json")
+}
 
-	r.ServeHTTP(w, req)
+func TestContextAutoBind(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
+	c.Request.Header.Add("Content-Type", MIMEJSON)
+	var obj struct {
+		Foo string `json:"foo"`
+		Bar string `json:"bar"`
+	}
+	assert.NoError(t, c.Bind(&obj))
+	assert.Equal(t, obj.Bar, "foo")
+	assert.Equal(t, obj.Foo, "bar")
+	assert.Equal(t, w.Body.Len(), 0)
+}
 
-	if w.Code != 200 {
-		t.Errorf("Response code should be Ok, was: %d", w.Code)
+func TestContextBadAutoBind(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
+	c.Request.Header.Add("Content-Type", MIMEJSON)
+	var obj struct {
+		Foo string `json:"foo"`
+		Bar string `json:"bar"`
 	}
 
-	expected := "{\"foo\":\"bar\",\"num\":123,\"unum\":1234567890}\n"
-	if w.Body.String() != expected {
-		t.Errorf("Response should be %s, was %s", expected, w.Body.String())
-	}
+	assert.False(t, c.IsAborted())
+	assert.Error(t, c.Bind(&obj))
+	c.Writer.WriteHeaderNow()
 
-	if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
-		t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
-	}
+	assert.Empty(t, obj.Bar)
+	assert.Empty(t, obj.Foo)
+	assert.Equal(t, w.Code, 400)
+	assert.True(t, c.IsAborted())
 }
 
-func TestClientIP(t *testing.T) {
-	r := New()
-
-	var clientIP string = ""
-	r.GET("/", func(c *Context) {
-		clientIP = c.ClientIP()
-	})
-
-	body := bytes.NewBuffer([]byte(""))
-	req, _ := http.NewRequest("GET", "/", body)
-	req.RemoteAddr = "clientip:1234"
-	w := httptest.NewRecorder()
-	r.ServeHTTP(w, req)
-
-	if clientIP != "clientip:1234" {
-		t.Errorf("ClientIP should not be %s, but clientip:1234", clientIP)
-	}
+func TestContextBindWith(t *testing.T) {
+	c, w, _ := createTestContext()
+	c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
+	c.Request.Header.Add("Content-Type", MIMEXML)
+	var obj struct {
+		Foo string `json:"foo"`
+		Bar string `json:"bar"`
+	}
+	assert.NoError(t, c.BindWith(&obj, binding.JSON))
+	assert.Equal(t, obj.Bar, "foo")
+	assert.Equal(t, obj.Foo, "bar")
+	assert.Equal(t, w.Body.Len(), 0)
 }
 
-func TestClientIPWithXForwardedForWithProxy(t *testing.T) {
-	r := New()
-	r.Use(ForwardedFor())
-
-	var clientIP string = ""
-	r.GET("/", func(c *Context) {
-		clientIP = c.ClientIP()
-	})
-
-	body := bytes.NewBuffer([]byte(""))
-	req, _ := http.NewRequest("GET", "/", body)
-	req.RemoteAddr = "172.16.8.3:1234"
-	req.Header.Set("X-Real-Ip", "realip")
-	req.Header.Set("X-Forwarded-For", "1.2.3.4, 10.10.0.4, 192.168.0.43, 172.16.8.4")
-	w := httptest.NewRecorder()
-	r.ServeHTTP(w, req)
-
-	if clientIP != "1.2.3.4:0" {
-		t.Errorf("ClientIP should not be %s, but 1.2.3.4:0", clientIP)
-	}
+func TestContextGolangContext(t *testing.T) {
+	c, _, _ := createTestContext()
+	c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
+	assert.NoError(t, c.Err())
+	assert.Nil(t, c.Done())
+	ti, ok := c.Deadline()
+	assert.Equal(t, ti, time.Time{})
+	assert.False(t, ok)
+	assert.Equal(t, c.Value(0), c.Request)
+	assert.Nil(t, c.Value("foo"))
+
+	c.Set("foo", "bar")
+	assert.Equal(t, c.Value("foo"), "bar")
+	assert.Nil(t, c.Value(1))
 }

+ 40 - 0
debug.go

@@ -0,0 +1,40 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+import (
+	"log"
+	"os"
+)
+
+var debugLogger = log.New(os.Stdout, "[GIN-debug] ", 0)
+
+func IsDebugging() bool {
+	return ginMode == debugCode
+}
+
+func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
+	if IsDebugging() {
+		nuHandlers := len(handlers)
+		handlerName := nameOfFunction(handlers[nuHandlers-1])
+		debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
+	}
+}
+
+func debugPrint(format string, values ...interface{}) {
+	if IsDebugging() {
+		debugLogger.Printf(format, values...)
+	}
+}
+
+func debugPrintWARNING() {
+	debugPrint("[WARNING] Running in DEBUG mode! Disable it before going production\n")
+}
+
+func debugPrintError(err error) {
+	if err != nil {
+		debugPrint("[ERROR] %v\n", err)
+	}
+}

+ 79 - 0
debug_test.go

@@ -0,0 +1,79 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+import (
+	"bytes"
+	"errors"
+	"io"
+	"log"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var cachedDebugLogger *log.Logger = nil
+
+// TODO
+// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
+// func debugPrint(format string, values ...interface{}) {
+
+func TestIsDebugging(t *testing.T) {
+	SetMode(DebugMode)
+	assert.True(t, IsDebugging())
+	SetMode(ReleaseMode)
+	assert.False(t, IsDebugging())
+	SetMode(TestMode)
+	assert.False(t, IsDebugging())
+}
+
+func TestDebugPrint(t *testing.T) {
+	var w bytes.Buffer
+	setup(&w)
+	defer teardown()
+
+	SetMode(ReleaseMode)
+	debugPrint("DEBUG this!")
+	SetMode(TestMode)
+	debugPrint("DEBUG this!")
+	assert.Empty(t, w.String())
+
+	SetMode(DebugMode)
+	debugPrint("these are %d %s\n", 2, "error messages")
+	assert.Equal(t, w.String(), "[GIN-debug] these are 2 error messages\n")
+}
+
+func TestDebugPrintError(t *testing.T) {
+	var w bytes.Buffer
+	setup(&w)
+	defer teardown()
+
+	SetMode(DebugMode)
+	debugPrintError(nil)
+	assert.Empty(t, w.String())
+
+	debugPrintError(errors.New("this is an error"))
+	assert.Equal(t, w.String(), "[GIN-debug] [ERROR] this is an error\n")
+}
+
+func setup(w io.Writer) {
+	SetMode(DebugMode)
+	if cachedDebugLogger == nil {
+		cachedDebugLogger = debugLogger
+		debugLogger = log.New(w, debugLogger.Prefix(), 0)
+	} else {
+		panic("setup failed")
+	}
+}
+
+func teardown() {
+	SetMode(TestMode)
+	if cachedDebugLogger != nil {
+		debugLogger = cachedDebugLogger
+		cachedDebugLogger = nil
+	} else {
+		panic("teardown failed")
+	}
+}

+ 0 - 42
deprecated.go

@@ -3,45 +3,3 @@
 // license that can be found in the LICENSE file.
 
 package gin
-
-import (
-	"github.com/gin-gonic/gin/binding"
-	"net/http"
-)
-
-// DEPRECATED, use Bind() instead.
-// Like ParseBody() but this method also writes a 400 error if the json is not valid.
-func (c *Context) EnsureBody(item interface{}) bool {
-	return c.Bind(item)
-}
-
-// DEPRECATED use bindings directly
-// Parses the body content as a JSON input. It decodes the json payload into the struct specified as a pointer.
-func (c *Context) ParseBody(item interface{}) error {
-	return binding.JSON.Bind(c.Request, item)
-}
-
-// DEPRECATED use gin.Static() instead
-// ServeFiles serves files from the given file system root.
-// The path must end with "/*filepath", files are then served from the local
-// path /defined/root/dir/*filepath.
-// For example if root is "/etc" and *filepath is "passwd", the local file
-// "/etc/passwd" would be served.
-// Internally a http.FileServer is used, therefore http.NotFound is used instead
-// of the Router's NotFound handler.
-// To use the operating system's file system implementation,
-// use http.Dir:
-//     router.ServeFiles("/src/*filepath", http.Dir("/var/www"))
-func (engine *Engine) ServeFiles(path string, root http.FileSystem) {
-	engine.router.ServeFiles(path, root)
-}
-
-// DEPRECATED use gin.LoadHTMLGlob() or gin.LoadHTMLFiles() instead
-func (engine *Engine) LoadHTMLTemplates(pattern string) {
-	engine.LoadHTMLGlob(pattern)
-}
-
-// DEPRECATED. Use NoRoute() instead
-func (engine *Engine) NotFound404(handlers ...HandlerFunc) {
-	engine.NoRoute(handlers...)
-}

+ 128 - 0
errors.go

@@ -0,0 +1,128 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+import (
+	"bytes"
+	"fmt"
+	"reflect"
+)
+
+const (
+	ErrorTypeBind    = 1 << 31
+	ErrorTypeRender  = 1 << 30
+	ErrorTypePrivate = 1 << 0
+	ErrorTypePublic  = 1 << 1
+
+	ErrorTypeAny = 0xffffffff
+	ErrorTypeNu  = 2
+)
+
+// Used internally to collect errors that occurred during an http request.
+type Error struct {
+	Err  error       `json:"error"`
+	Type int         `json:"-"`
+	Meta interface{} `json:"meta"`
+}
+
+var _ error = &Error{}
+
+func (msg *Error) SetType(flags int) *Error {
+	msg.Type = flags
+	return msg
+}
+
+func (msg *Error) SetMeta(data interface{}) *Error {
+	msg.Meta = data
+	return msg
+}
+
+func (msg *Error) JSON() interface{} {
+	json := H{}
+	if msg.Meta != nil {
+		value := reflect.ValueOf(msg.Meta)
+		switch value.Kind() {
+		case reflect.Struct:
+			return msg.Meta
+		case reflect.Map:
+			for _, key := range value.MapKeys() {
+				json[key.String()] = value.MapIndex(key).Interface()
+			}
+		default:
+			json["meta"] = msg.Meta
+		}
+	}
+	if _, ok := json["error"]; !ok {
+		json["error"] = msg.Error()
+	}
+	return json
+}
+
+func (msg *Error) Error() string {
+	return msg.Err.Error()
+}
+
+type errorMsgs []*Error
+
+func (a errorMsgs) ByType(typ int) errorMsgs {
+	if len(a) == 0 {
+		return a
+	}
+	result := make(errorMsgs, 0, len(a))
+	for _, msg := range a {
+		if msg.Type&typ > 0 {
+			result = append(result, msg)
+		}
+	}
+	return result
+}
+
+func (a errorMsgs) Last() *Error {
+	length := len(a)
+	if length == 0 {
+		return nil
+	}
+	return a[length-1]
+}
+
+func (a errorMsgs) Errors() []string {
+	if len(a) == 0 {
+		return []string{}
+	}
+	errorStrings := make([]string, len(a))
+	for i, err := range a {
+		errorStrings[i] = err.Error()
+	}
+	return errorStrings
+}
+
+func (a errorMsgs) JSON() interface{} {
+	switch len(a) {
+	case 0:
+		return nil
+	case 1:
+		return a.Last().JSON()
+	default:
+		json := make([]interface{}, len(a))
+		for i, err := range a {
+			json[i] = err.JSON()
+		}
+		return json
+	}
+}
+
+func (a errorMsgs) String() string {
+	if len(a) == 0 {
+		return ""
+	}
+	var buffer bytes.Buffer
+	for i, msg := range a {
+		fmt.Fprintf(&buffer, "Error #%02d: %s\n", (i + 1), msg.Err)
+		if msg.Meta != nil {
+			fmt.Fprintf(&buffer, "     Meta: %v\n", msg.Meta)
+		}
+	}
+	return buffer.String()
+}

+ 90 - 0
errors_test.go

@@ -0,0 +1,90 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestError(t *testing.T) {
+	baseError := errors.New("test error")
+	err := &Error{
+		Err:  baseError,
+		Type: ErrorTypePrivate,
+	}
+	assert.Equal(t, err.Error(), baseError.Error())
+	assert.Equal(t, err.JSON(), H{"error": baseError.Error()})
+
+	assert.Equal(t, err.SetType(ErrorTypePublic), err)
+	assert.Equal(t, err.Type, ErrorTypePublic)
+
+	assert.Equal(t, err.SetMeta("some data"), err)
+	assert.Equal(t, err.Meta, "some data")
+	assert.Equal(t, err.JSON(), H{
+		"error": baseError.Error(),
+		"meta":  "some data",
+	})
+
+	err.SetMeta(H{
+		"status": "200",
+		"data":   "some data",
+	})
+	assert.Equal(t, err.JSON(), H{
+		"error":  baseError.Error(),
+		"status": "200",
+		"data":   "some data",
+	})
+
+	err.SetMeta(H{
+		"error":  "custom error",
+		"status": "200",
+		"data":   "some data",
+	})
+	assert.Equal(t, err.JSON(), H{
+		"error":  "custom error",
+		"status": "200",
+		"data":   "some data",
+	})
+}
+
+func TestErrorSlice(t *testing.T) {
+	errs := errorMsgs{
+		{Err: errors.New("first"), Type: ErrorTypePrivate},
+		{Err: errors.New("second"), Type: ErrorTypePrivate, Meta: "some data"},
+		{Err: errors.New("third"), Type: ErrorTypePublic, Meta: H{"status": "400"}},
+	}
+
+	assert.Equal(t, errs.Last().Error(), "third")
+	assert.Equal(t, errs.Errors(), []string{"first", "second", "third"})
+	assert.Equal(t, errs.ByType(ErrorTypePublic).Errors(), []string{"third"})
+	assert.Equal(t, errs.ByType(ErrorTypePrivate).Errors(), []string{"first", "second"})
+	assert.Equal(t, errs.ByType(ErrorTypePublic|ErrorTypePrivate).Errors(), []string{"first", "second", "third"})
+	assert.Empty(t, errs.ByType(ErrorTypeBind))
+
+	assert.Equal(t, errs.String(), `Error #01: first
+Error #02: second
+     Meta: some data
+Error #03: third
+     Meta: map[status:400]
+`)
+	assert.Equal(t, errs.JSON(), []interface{}{
+		H{"error": "first"},
+		H{"error": "second", "meta": "some data"},
+		H{"error": "third", "status": "400"},
+	})
+
+	errs = errorMsgs{
+		{Err: errors.New("first"), Type: ErrorTypePrivate},
+	}
+	assert.Equal(t, errs.JSON(), H{"error": "first"})
+
+	errs = errorMsgs{}
+	assert.Nil(t, errs.Last())
+	assert.Nil(t, errs.JSON())
+	assert.Empty(t, errs.String())
+}

+ 1 - 1
examples/example_basic.go → examples/basic/main.go

@@ -45,7 +45,7 @@ func main() {
 			Value string `json:"value" binding:"required"`
 		}
 
-		if c.Bind(&json) {
+		if c.Bind(&json) == nil {
 			DB[user] = json.Value
 			c.JSON(200, gin.H{"status": "ok"})
 		}

+ 0 - 58
examples/pluggable_renderer/example_pongo2.go

@@ -1,58 +0,0 @@
-package main
-
-import (
-	"github.com/flosch/pongo2"
-	"github.com/gin-gonic/gin"
-	"net/http"
-)
-
-type pongoRender struct {
-	cache map[string]*pongo2.Template
-}
-
-func newPongoRender() *pongoRender {
-	return &pongoRender{map[string]*pongo2.Template{}}
-}
-
-func writeHeader(w http.ResponseWriter, code int, contentType string) {
-	if code >= 0 {
-		w.Header().Set("Content-Type", contentType)
-		w.WriteHeader(code)
-	}
-}
-
-func (p *pongoRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
-	file := data[0].(string)
-	ctx := data[1].(pongo2.Context)
-	var t *pongo2.Template
-
-	if tmpl, ok := p.cache[file]; ok {
-		t = tmpl
-	} else {
-		tmpl, err := pongo2.FromFile(file)
-		if err != nil {
-			return err
-		}
-		p.cache[file] = tmpl
-		t = tmpl
-	}
-	writeHeader(w, code, "text/html")
-	return t.ExecuteWriter(ctx, w)
-}
-
-func main() {
-	r := gin.Default()
-	r.HTMLRender = newPongoRender()
-
-	r.GET("/index", func(c *gin.Context) {
-		name := c.Request.FormValue("name")
-		ctx := pongo2.Context{
-			"title": "Gin meets pongo2 !",
-			"name":  name,
-		}
-		c.HTML(200, "index.html", ctx)
-	})
-
-	// Listen and server on 0.0.0.0:8080
-	r.Run(":8080")
-}

+ 0 - 12
examples/pluggable_renderer/index.html

@@ -1,12 +0,0 @@
-<!DOCTYPE html>
-<html lang="ja">
-  <head>
-    <meta charset="utf-8">
-    <title>{{ title }}</title>
-    <meta name="keywords" content="">
-    <meta name="description" content="">
-  </head>
-  <body>
-    Hello {{ name }} ! 
-  </body>
-</html>

+ 39 - 0
examples/realtime-advanced/main.go

@@ -0,0 +1,39 @@
+package main
+
+import (
+	"fmt"
+	"runtime"
+
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	ConfigRuntime()
+	StartWorkers()
+	StartGin()
+}
+
+func ConfigRuntime() {
+	nuCPU := runtime.NumCPU()
+	runtime.GOMAXPROCS(nuCPU)
+	fmt.Printf("Running with %d CPUs\n", nuCPU)
+}
+
+func StartWorkers() {
+	go statsWorker()
+}
+
+func StartGin() {
+	gin.SetMode(gin.ReleaseMode)
+
+	router := gin.New()
+	router.Use(rateLimit, gin.Recovery())
+	router.LoadHTMLGlob("resources/*.templ.html")
+	router.Static("/static", "resources/static")
+	router.GET("/", index)
+	router.GET("/room/:roomid", roomGET)
+	router.POST("/room-post/:roomid", roomPOST)
+	router.GET("/stream/:roomid", streamRoom)
+
+	router.Run(":80")
+}

+ 208 - 0
examples/realtime-advanced/resources/room_login.templ.html

@@ -0,0 +1,208 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8">
+        <meta http-equiv="X-UA-Compatible" content="IE=edge">
+        <meta name="viewport" content="width=device-width, initial-scale=1">
+        <title>Server-Sent Events. Room "{{.roomid}}"</title>
+        <!-- jQuery -->
+        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
+        <script src="http://malsup.github.com/jquery.form.js"></script> 
+        <!-- EPOCH -->
+        <script src="http://d3js.org/d3.v3.min.js"></script>
+        <script src="/static/epoch.min.js"></script>
+        <link rel="stylesheet" href="/static/epoch.min.css">
+        <script src="/static/realtime.js"></script>
+        <!-- Latest compiled and minified CSS -->
+        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
+        <!-- Optional theme -->
+        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap-theme.min.css">
+        <!-- Latest compiled and minified JavaScript -->
+        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
+        <!-- Primjs -->
+        <link href="/static/prismjs.min.css" rel="stylesheet" />
+
+        <script type="text/javascript">
+            $(document).ready(function() { 
+              StartRealtime({{.roomid}}, {{.timestamp}});
+            });
+        </script>
+        <style>
+        body { padding-top: 50px; }
+        </style>
+    </head>
+    <body>
+    <nav class="navbar navbar-fixed-top navbar-inverse">
+      <div class="container">
+        <div class="navbar-header">
+          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
+            <span class="sr-only">Toggle navigation</span>
+            <span class="icon-bar"></span>
+            <span class="icon-bar"></span>
+            <span class="icon-bar"></span>
+          </button>
+          <a class="navbar-brand" href="#">Server-Sent Events</a>
+        </div>
+        <div id="navbar" class="collapse navbar-collapse">
+          <ul class="nav navbar-nav">
+            <li class="active"><a href="#">Demo</a></li>
+            <li><a href="http://www.w3.org/TR/2009/WD-eventsource-20091029/">W3 Standard</a></li>
+            <li><a href="http://caniuse.com/#feat=eventsource">Browser Support</a></li>
+            <li><a href="http://gin-gonic.github.io/gin/">Gin Framework</a></li>
+            <li><a href="https://github.com/gin-gonic/gin/tree/develop/examples/realtime-advanced">Github</a></li>
+          </ul>
+        </div><!-- /.nav-collapse -->
+      </div><!-- /.container -->
+    </nav><!-- /.navbar -->
+        <!-- Main jumbotron for a primary marketing message or call to action -->
+        <div class="jumbotron">
+            <div class="container">
+                <h1>Server-Sent Events in Go</h1>
+                <p>Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection. It is not websockets. <a href="http://www.html5rocks.com/en/tutorials/eventsource/basics/">Learn more.</a></p>
+                <p>The chat and the charts data is provided in realtime using the SSE implemention of <a href="https://github.com/gin-gonic/gin/blob/15b0c49da556d58a3d934b86e3aa552ff224026d/examples/realtime-chat/main.go#L23-L32">Gin Framework</a>.</p>
+                <div class="row">
+                    <div class="col-md-8">
+                        <div id="chat-scroll" style="overflow-y:scroll; overflow-x:scroll; height:290px">
+                            <table id="table-style" class="table" data-show-header="false">
+                                <thead>
+                                    <tr>
+                                        <th data-field="nick" class="col-md-2">Nick</th>
+                                        <th data-field="message" class="col-md-8">Message</th>
+                                    </tr>
+                                </thead>
+                                <tbody id="chat"></tbody>
+                            </table>
+                        </div>
+                        {{if .nick}}
+                        <form autocomplete="off" class="form-inline" id="chat-form" action="/room-post/{{.roomid}}?nick={{.nick}}" method="post">
+                            <div class="form-group">
+                                <label class="sr-only" for="chat-message">Message</label>
+                                <div class="input-group">
+                                    <div class="input-group-addon">{{.nick}}</div>
+                                    <input type="text" name="message" id="chat-message" class="form-control" placeholder="a message" value="" />
+                                </div>
+                            </div>
+                            <input type="submit" class="btn btn-primary" value="Send" />
+                        </form>
+                        {{else}}
+                        <form action="" method="get" class="form-inline">
+                            <legend>Join the SSE real-time chat</legend>
+                            <div class="form-group">
+                                <input value='' name="nick" id="nick" placeholder="Your Name" type="text" class="form-control" />
+                            </div>
+                            <div class="form-group text-center">
+                                <input type="submit" class="btn btn-success btn-login-submit" value="Join" />
+                            </div>
+                        </form>
+                        {{end}}
+                    </div>
+                    <div class="col-md-4">
+                        <div id="messagesChart" class="epoch category10"></div>
+                        <p>
+                        <span style="font-size:20px; color:#1f77b4">◼︎</span> Users<br>
+                        <span style="font-size:20px; color:#ff7f0e">◼︎</span> Inbound messages / sec<br>
+                        <span style="font-size:20px; color:#2ca02c">◼︎</span> Outbound messages / sec<br>
+                        </p>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="container">
+            <div class="row">
+                <h2>Realtime server Go stats</h2>
+                <div class="col-md-6">
+                    <h3>Memory usage</h3>
+                    <p>
+                    <div id="heapChart" class="epoch category20c"></div>
+                    </p>
+                    <p>
+                    <span style="font-size:20px; color:#1f77b4">◼︎</span> Heap bytes<br>
+                    <span style="font-size:20px; color:#aec7e8">◼︎</span> Stack bytes<br>
+                    </p>
+                </div>
+                <div class="col-md-6">
+                    <h3>Allocations per second</h3>
+                    <p>
+                    <div id="mallocsChart" class="epoch category20b"></div>
+                    </p>
+                    <p>
+                    <span style="font-size:20px; color:#393b79">◼︎</span> Mallocs / sec<br>
+                    <span style="font-size:20px; color:#5254a3">◼︎</span> Frees / sec<br>
+                    </p>
+                </div>
+            </div>
+            <div class="row">
+                <h2>MIT Open Sourced</h2>
+                <ul>
+                    <li><a href="https://github.com/gin-gonic/gin/tree/develop/examples/realtime-advanced">This demo website (JS and Go)</a></li>
+                    <li><a href="https://github.com/manucorporat/sse">The SSE implementation in Go</a></li>
+                    <li><a href="https://github.com/gin-gonic/gin">The Web Framework (Gin)</a></li>
+                </ul>
+                <div class="col-md-6">
+                <script src="/static/prismjs.min.js"></script>
+                    <h3>Server-side (Go)</h3>
+                    <pre><code class="language-go">func streamRoom(c *gin.Context) {
+    roomid := c.ParamValue(&quot;roomid&quot;)
+    listener := openListener(roomid)
+    statsTicker := time.NewTicker(1 * time.Second)
+    defer closeListener(roomid, listener)
+    defer statsTicker.Stop()
+
+    c.Stream(func(w io.Writer) bool {
+        select {
+        case msg := &lt;-listener:
+            c.SSEvent(&quot;message&quot;, msg)
+        case &lt;-statsTicker.C:
+            c.SSEvent(&quot;stats&quot;, Stats())
+        }
+        return true
+    })
+}</code></pre>
+                </div>
+                <div class="col-md-6">
+                    <h3>Client-side (JS)</h3>
+                    <pre><code class="language-javascript">function StartSSE(roomid) {
+    var source = new EventSource('/stream/'+roomid);
+    source.addEventListener('message', newChatMessage, false);
+    source.addEventListener('stats', stats, false);
+}</code></pre>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-12">
+                    <h3>SSE package</h3>
+                    <pre><code class="language-go">import &quot;github.com/manucorporat/sse&quot;
+
+func httpHandler(w http.ResponseWriter, req *http.Request) {
+    // data can be a primitive like a string, an integer or a float
+    sse.Encode(w, sse.Event{
+        Event: &quot;message&quot;,
+        Data:  &quot;some data\nmore data&quot;,
+    })
+
+    // also a complex type, like a map, a struct or a slice
+    sse.Encode(w, sse.Event{
+        Id:    &quot;124&quot;,
+        Event: &quot;message&quot;,
+        Data: map[string]interface{}{
+            &quot;user&quot;:    &quot;manu&quot;,
+            &quot;date&quot;:    time.Now().Unix(),
+            &quot;content&quot;: &quot;hi!&quot;,
+        },
+    })
+}</code></pre>
+<pre>event: message
+data: some data\\nmore data
+
+id: 124
+event: message
+data: {&quot;content&quot;:&quot;hi!&quot;,&quot;date&quot;:1431540810,&quot;user&quot;:&quot;manu&quot;}</pre>
+                </div>
+            </div>
+            <hr>
+            <footer>
+                <p>Created with <span class="glyphicon glyphicon-heart"></span> by <a href="https://github.com/manucorporat">Manu Martinez-Almeida</a></p>
+            </footer>
+        </div>
+    </body>
+</html>

File diff ditekan karena terlalu besar
+ 0 - 0
examples/realtime-advanced/resources/static/epoch.min.css


+ 114 - 0
examples/realtime-advanced/resources/static/epoch.min.js

@@ -0,0 +1,114 @@
+(function(){var e;null==window.Epoch&&(window.Epoch={});null==(e=window.Epoch).Chart&&(e.Chart={});null==(e=window.Epoch).Time&&(e.Time={});null==(e=window.Epoch).Util&&(e.Util={});null==(e=window.Epoch).Formats&&(e.Formats={});Epoch.warn=function(g){return(console.warn||console.log)("Epoch Warning: "+g)};Epoch.exception=function(g){throw"Epoch Error: "+g;}}).call(this);
+(function(){Epoch.TestContext=function(){function e(){var c,a,d;this._log=[];a=0;for(d=g.length;a<d;a++)c=g[a],this._makeFauxMethod(c)}var g;g="arc arcTo beginPath bezierCurveTo clearRect clip closePath drawImage fill fillRect fillText moveTo quadraticCurveTo rect restore rotate save scale scrollPathIntoView setLineDash setTransform stroke strokeRect strokeText transform translate".split(" ");e.prototype._makeFauxMethod=function(c){return this[c]=function(){var a;return this._log.push(""+c+"("+function(){var d,
+b,h;h=[];d=0;for(b=arguments.length;d<b;d++)a=arguments[d],h.push(a.toString());return h}.apply(this,arguments).join(",")+")")}};e.prototype.getImageData=function(){var c;this._log.push("getImageData("+function(){var a,d,b;b=[];a=0;for(d=arguments.length;a<d;a++)c=arguments[a],b.push(c.toString());return b}.apply(this,arguments).join(",")+")");return{width:0,height:0,resolution:1,data:[]}};return e}()}).call(this);
+(function(){var e,g;e=function(c){return function(a){return Object.prototype.toString.call(a)==="[object "+c+"]"}};Epoch.isArray=null!=(g=Array.isArray)?g:e("Array");Epoch.isObject=e("Object");Epoch.isString=e("String");Epoch.isFunction=e("Function");Epoch.isNumber=e("Number");Epoch.isElement=function(c){return"undefined"!==typeof HTMLElement&&null!==HTMLElement?c instanceof HTMLElement:null!=c&&Epoch.isObject(c)&&1===c.nodeType&&Epoch.isString(c.nodeName)};Epoch.Util.copy=function(c){var a,d,b;if(null==
+c)return null;a={};for(d in c)b=c[d],a[d]=b;return a};Epoch.Util.defaults=function(c,a){var d,b,h,k,f;f=Epoch.Util.copy(c);for(h in a)k=c[h],b=a[h],d=Epoch.isObject(k)&&Epoch.isObject(b),null!=k&&null!=b?d&&!Epoch.isArray(k)?f[h]=Epoch.Util.defaults(k,b):f[h]=k:f[h]=null!=k?k:b;return f};Epoch.Util.formatSI=function(c,a,d){var b,h,k,f;null==a&&(a=1);null==d&&(d=!1);if(1E3>c){if((c|0)!==c||d)c=c.toFixed(a);return c}f="KMGTPEZY".split("");for(h in f)if(k=f[h],b=Math.pow(10,3*((h|0)+1)),c>=b&&c<Math.pow(10,
+3*((h|0)+2))){c/=b;if(0!==c%1||d)c=c.toFixed(a);return""+c+" "+k}};Epoch.Util.formatBytes=function(c,a,d){var b,h,k,f;null==a&&(a=1);null==d&&(d=!1);if(1024>c){if(0!==c%1||d)c=c.toFixed(a);return""+c+" B"}f="KB MB GB TB PB EB ZB YB".split(" ");for(h in f)if(k=f[h],b=Math.pow(1024,(h|0)+1),c>=b&&c<Math.pow(1024,(h|0)+2)){c/=b;if(0!==c%1||d)c=c.toFixed(a);return""+c+" "+k}};Epoch.Util.dasherize=function(c){return Epoch.Util.trim(c).replace("\n","").replace(/\s+/g,"-").toLowerCase()};Epoch.Util.domain=
+function(c,a){var d,b,h,k,f,q,u,m;null==a&&(a="x");h={};d=[];k=0;for(q=c.length;k<q;k++)for(b=c[k],m=b.values,f=0,u=m.length;f<u;f++)b=m[f],null==h[b[a]]&&(d.push(b[a]),h[b[a]]=!0);return d};Epoch.Util.trim=function(c){return Epoch.isString(c)?c.replace(/^\s+/g,"").replace(/\s+$/g,""):null};Epoch.Util.getComputedStyle=function(c,a){if(Epoch.isFunction(window.getComputedStyle))return window.getComputedStyle(c,a);if(null!=c.currentStyle)return c.currentStyle};Epoch.Util.toRGBA=function(c,a){var d,b,
+h;if(d=c.match(/^rgba\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*[0-9\.]+\)/))h=d[1],b=d[2],d=d[3],b="rgba("+h+","+b+","+d+","+a+")";else if(d=d3.rgb(c))b="rgba("+d.r+","+d.g+","+d.b+","+a+")";return b};Epoch.Util.getContext=function(c,a){null==a&&(a="2d");return null!=c.getContext?c.getContext(a):new Epoch.TestContext}}).call(this);
+(function(){d3.selection.prototype.width=function(e){return null!=e&&Epoch.isString(e)?this.style("width",e):null!=e&&Epoch.isNumber(e)?this.style("width",""+e+"px"):+Epoch.Util.getComputedStyle(this.node(),null).width.replace("px","")};d3.selection.prototype.height=function(e){return null!=e&&Epoch.isString(e)?this.style("height",e):null!=e&&Epoch.isNumber(e)?this.style("height",""+e+"px"):+Epoch.Util.getComputedStyle(this.node(),null).height.replace("px","")}}).call(this);
+(function(){var e;Epoch.Formats.regular=function(g){return g};Epoch.Formats.si=function(g){return Epoch.Util.formatSI(g)};Epoch.Formats.percent=function(g){return(100*g).toFixed(1)+"%"};Epoch.Formats.seconds=function(g){return e(new Date(1E3*g))};e=d3.time.format("%I:%M:%S %p");Epoch.Formats.bytes=function(g){return Epoch.Util.formatBytes(g)}}).call(this);
+(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Events=function(){function c(){this._events={}}c.prototype.on=function(a,d){var b;if(null!=d)return null==(b=this._events)[a]&&(b[a]=[]),this._events[a].push(d)};c.prototype.onAll=function(a){var d,b,h;if(Epoch.isObject(a)){h=[];for(b in a)d=a[b],h.push(this.on(b,d));return h}};c.prototype.off=
+function(a,d){var b,h;if(Epoch.isArray(this._events[a])){if(null==d)return delete this._events[a];for(h=[];0<=(b=this._events[a].indexOf(d));)h.push(this._events[a].splice(b,1));return h}};c.prototype.offAll=function(a){var d,b,h,k;if(Epoch.isArray(a)){k=[];d=0;for(h=a.length;d<h;d++)b=a[d],k.push(this.off(b));return k}if(Epoch.isObject(a)){h=[];for(b in a)d=a[b],h.push(this.off(b,d));return h}};c.prototype.trigger=function(a){var d,b,h,k,f,q,c,m;if(null!=this._events[a]){d=function(){var a,f,q;q=
+[];k=a=1;for(f=arguments.length;1<=f?a<f:a>f;k=1<=f?++a:--a)q.push(arguments[k]);return q}.apply(this,arguments);c=this._events[a];m=[];f=0;for(q=c.length;f<q;f++)b=c[f],h=null,Epoch.isString(b)?h=this[b]:Epoch.isFunction(b)&&(h=b),null==h&&Epoch.exception("Callback for event '"+a+"' is not a function or reference to a method."),m.push(h.apply(this,d));return m}};return c}();Epoch.Chart.Base=function(c){function a(h){this.options=null!=h?h:{};a.__super__.constructor.call(this);this.setData(this.options.data||
+[]);null!=this.options.el&&(this.el=d3.select(this.options.el));this.width=this.options.width;this.height=this.options.height;null!=this.el?(null==this.width&&(this.width=this.el.width()),null==this.height&&(this.height=this.el.height())):(null==this.width&&(this.width=d.width),null==this.height&&(this.height=d.height));this.onAll(b)}var d,b;g(a,c);d={width:320,height:240};b={"option:width":"dimensionsChanged","option:height":"dimensionsChanged"};a.prototype._getAllOptions=function(){return Epoch.Util.defaults({},
+this.options)};a.prototype._getOption=function(a){var k,f;a=a.split(".");for(k=this.options;a.length&&null!=k;)f=a.shift(),k=k[f];return k};a.prototype._setOption=function(a,k){var f,q,b;f=a.split(".");for(q=this.options;f.length;){b=f.shift();if(0===f.length){q[b]=k;this.trigger("option:"+a);break}null==q[b]&&(q[b]={});q=q[b]}};a.prototype._setManyOptions=function(a,k){var f,q,b;null==k&&(k="");b=[];for(f in a)q=a[f],Epoch.isObject(q)?b.push(this._setManyOptions(q,""+(k+f)+".")):b.push(this._setOption(k+
+f,q));return b};a.prototype.option=function(){if(0===arguments.length)return this._getAllOptions();if(1===arguments.length&&Epoch.isString(arguments[0]))return this._getOption(arguments[0]);if(2===arguments.length&&Epoch.isString(arguments[0]))return this._setOption(arguments[0],arguments[1]);if(1===arguments.length&&Epoch.isObject(arguments[0]))return this._setManyOptions(arguments[0])};a.prototype.setData=function(a){var k,f,b,d,c;k=1;d=0;for(c=a.length;d<c;d++)b=a[d],f=["layer"],f.push("category"+
+k),b.category=k,null!=b.label&&f.push(Epoch.Util.dasherize(b.label)),b.className=f.join(" "),k++;return this.data=a};a.prototype.update=function(a,k){null==k&&(k=!0);this.setData(a);if(k)return this.draw()};a.prototype.draw=function(){return this.trigger("draw")};a.prototype.extent=function(a){return[d3.min(this.data,function(k){return d3.min(k.values,a)}),d3.max(this.data,function(k){return d3.max(k.values,a)})]};a.prototype.dimensionsChanged=function(){this.width=this.option("width")||this.width;
+this.height=this.option("height")||this.height;this.el.width(this.width);return this.el.height(this.height)};return a}(Epoch.Events);Epoch.Chart.SVG=function(c){function a(d){this.options=null!=d?d:{};a.__super__.constructor.call(this,this.options);this.svg=null!=this.el?this.el.append("svg"):d3.select(document.createElement("svg"));this.svg.attr({xmlns:"http://www.w3.org/2000/svg",width:this.width,height:this.height})}g(a,c);a.prototype.dimensionsChanged=function(){a.__super__.dimensionsChanged.call(this);
+return this.svg.attr("width",this.width).attr("height",this.height)};return a}(Epoch.Chart.Base);Epoch.Chart.Canvas=function(c){function a(d){this.options=null!=d?d:{};a.__super__.constructor.call(this,this.options);this.pixelRatio=null!=this.options.pixelRatio?this.options.pixelRatio:null!=window.devicePixelRatio?window.devicePixelRatio:1;this.canvas=d3.select(document.createElement("CANVAS"));this.canvas.style({width:""+this.width+"px",height:""+this.height+"px"});this.canvas.attr({width:this.getWidth(),
+height:this.getHeight()});null!=this.el&&this.el.node().appendChild(this.canvas.node());this.ctx=Epoch.Util.getContext(this.canvas.node())}g(a,c);a.prototype.getWidth=function(){return this.width*this.pixelRatio};a.prototype.getHeight=function(){return this.height*this.pixelRatio};a.prototype.clear=function(){return this.ctx.clearRect(0,0,this.getWidth(),this.getHeight())};a.prototype.getStyles=function(a){return Epoch.QueryCSS.getStyles(a,this.el)};a.prototype.dimensionsChanged=function(){a.__super__.dimensionsChanged.call(this);
+this.canvas.style({width:""+this.width+"px",height:""+this.height+"px"});return this.canvas.attr({width:this.getWidth(),height:this.getHeight()})};return a}(Epoch.Chart.Base)}).call(this);
+(function(){var e;e=function(){function g(){}var c,a,d,b,h;a=0;b=function(){return"epoch-container-"+a++};c=/^([^#. ]+)?(#[^. ]+)?(\.[^# ]+)?$/;d=!1;h=function(a){var f,b;f=a.match(c);if(null==f)return Epoch.error("Query CSS cannot match given selector: "+a);b=f[1];a=f[2];f=f[3];b=(null!=b?b:"div").toUpperCase();b=document.createElement(b);null!=a&&(b.id=a.substr(1));null!=f&&(b.className=f.substr(1).replace(/\./g," "));return b};g.log=function(a){return d=a};g.cache={};g.styleList=["fill","stroke",
+"stroke-width"];g.container=null;g.purge=function(){return g.cache={}};g.getContainer=function(){var a;if(null!=g.container)return g.container;a=document.createElement("DIV");a.id="_canvas_css_reference";document.body.appendChild(a);return g.container=d3.select(a)};g.hash=function(a,f){var d;d=f.attr("data-epoch-container-id");null==d&&(d=b(),f.attr("data-epoch-container-id",d));return""+d+"__"+a};g.getStyles=function(a,f){var b,c,m,l,n,e,r;c=g.hash(a,f);b=g.cache[c];if(null!=b)return b;m=[];for(b=
+f.node().parentNode;null!=b&&"body"!==b.nodeName.toLowerCase();)m.unshift(b),b=b.parentNode;m.push(f.node());b=[];e=0;for(r=m.length;e<r;e++)l=m[e],n=l.nodeName.toLowerCase(),null!=l.id&&0<l.id.length&&(n+="#"+l.id),null!=l.className&&0<l.className.length&&(n+="."+Epoch.Util.trim(l.className).replace(/\s+/g,".")),b.push(n);b.push("svg");e=Epoch.Util.trim(a).split(/\s+/);l=0;for(n=e.length;l<n;l++)m=e[l],b.push(m);d&&console.log(b);for(l=n=h(b.shift());b.length;)m=h(b.shift()),l.appendChild(m),l=m;
+d&&console.log(n);g.getContainer().node().appendChild(n);m=d3.select("#_canvas_css_reference "+a);l={};r=g.styleList;n=0;for(e=r.length;n<e;n++)b=r[n],l[b]=m.style(b);g.cache[c]=l;g.getContainer().html("");return l};return g}();Epoch.QueryCSS=e}).call(this);
+(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Chart.Plot=function(c){function a(k){var f,c,u;this.options=null!=k?k:{};Epoch.Util.copy(this.options.margins);a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,b));this.margins={};u=["top","right","bottom","left"];f=0;for(c=u.length;f<c;f++)k=u[f],this.margins[k]=
+null!=this.options.margins&&null!=this.options.margins[k]?this.options.margins[k]:this.hasAxis(k)?d[k]:6;this.g=this.svg.append("g").attr("transform","translate("+this.margins.left+", "+this.margins.top+")");this.onAll(h)}var d,b,h;g(a,c);b={domain:null,range:null,axes:["left","bottom"],ticks:{top:14,bottom:14,left:5,right:5},tickFormats:{top:Epoch.Formats.regular,bottom:Epoch.Formats.regular,left:Epoch.Formats.si,right:Epoch.Formats.si}};d={top:25,right:50,bottom:25,left:50};h={"option:margins.top":"marginsChanged",
+"option:margins.right":"marginsChanged","option:margins.bottom":"marginsChanged","option:margins.left":"marginsChanged","option:axes":"axesChanged","option:ticks.top":"ticksChanged","option:ticks.right":"ticksChanged","option:ticks.bottom":"ticksChanged","option:ticks.left":"ticksChanged","option:tickFormats.top":"tickFormatsChanged","option:tickFormats.right":"tickFormatsChanged","option:tickFormats.bottom":"tickFormatsChanged","option:tickFormats.left":"tickFormatsChanged","option:domain":"domainChanged",
+"option:range":"rangeChanged"};a.prototype.setTickFormat=function(a,f){return this.options.tickFormats[a]=f};a.prototype.hasAxis=function(a){return-1<this.options.axes.indexOf(a)};a.prototype.innerWidth=function(){return this.width-(this.margins.left+this.margins.right)};a.prototype.innerHeight=function(){return this.height-(this.margins.top+this.margins.bottom)};a.prototype.x=function(){var a,f;a=null!=(f=this.options.domain)?f:this.extent(function(a){return a.x});return d3.scale.linear().domain(a).range([0,
+this.innerWidth()])};a.prototype.y=function(){var a,f;a=null!=(f=this.options.range)?f:this.extent(function(a){return a.y});return d3.scale.linear().domain(a).range([this.innerHeight(),0])};a.prototype.bottomAxis=function(){return d3.svg.axis().scale(this.x()).orient("bottom").ticks(this.options.ticks.bottom).tickFormat(this.options.tickFormats.bottom)};a.prototype.topAxis=function(){return d3.svg.axis().scale(this.x()).orient("top").ticks(this.options.ticks.top).tickFormat(this.options.tickFormats.top)};
+a.prototype.leftAxis=function(){return d3.svg.axis().scale(this.y()).orient("left").ticks(this.options.ticks.left).tickFormat(this.options.tickFormats.left)};a.prototype.rightAxis=function(){return d3.svg.axis().scale(this.y()).orient("right").ticks(this.options.ticks.right).tickFormat(this.options.tickFormats.right)};a.prototype.draw=function(){this._axesDrawn?this._redrawAxes():this._drawAxes();return a.__super__.draw.call(this)};a.prototype._redrawAxes=function(){this.hasAxis("bottom")&&this.g.selectAll(".x.axis.bottom").transition().duration(500).ease("linear").call(this.bottomAxis());
+this.hasAxis("top")&&this.g.selectAll(".x.axis.top").transition().duration(500).ease("linear").call(this.topAxis());this.hasAxis("left")&&this.g.selectAll(".y.axis.left").transition().duration(500).ease("linear").call(this.leftAxis());if(this.hasAxis("right"))return this.g.selectAll(".y.axis.right").transition().duration(500).ease("linear").call(this.rightAxis())};a.prototype._drawAxes=function(){this.hasAxis("bottom")&&this.g.append("g").attr("class","x axis bottom").attr("transform","translate(0, "+
+this.innerHeight()+")").call(this.bottomAxis());this.hasAxis("top")&&this.g.append("g").attr("class","x axis top").call(this.topAxis());this.hasAxis("left")&&this.g.append("g").attr("class","y axis left").call(this.leftAxis());this.hasAxis("right")&&this.g.append("g").attr("class","y axis right").attr("transform","translate("+this.innerWidth()+", 0)").call(this.rightAxis());return this._axesDrawn=!0};a.prototype.dimensionsChanged=function(){a.__super__.dimensionsChanged.call(this);this.g.selectAll(".axis").remove();
+this._axesDrawn=!1;return this.draw()};a.prototype.marginsChanged=function(){var a,f,b;if(null!=this.options.margins){b=this.options.margins;for(a in b)f=b[a],this.margins[a]=null==f?6:f;this.g.transition().duration(750).attr("transform","translate("+this.margins.left+", "+this.margins.top+")");return this.draw()}};a.prototype.axesChanged=function(){var a,f,b,c;c=["top","right","bottom","left"];f=0;for(b=c.length;f<b;f++)if(a=c[f],null==this.options.margins||null==this.options.margins[a])this.hasAxis(a)?
+this.margins[a]=d[a]:this.margins[a]=6;this.g.transition().duration(750).attr("transform","translate("+this.margins.left+", "+this.margins.top+")");this.g.selectAll(".axis").remove();this._axesDrawn=!1;return this.draw()};a.prototype.ticksChanged=function(){return this.draw()};a.prototype.tickFormatsChanged=function(){return this.draw()};a.prototype.domainChanged=function(){return this.draw()};a.prototype.rangeChanged=function(){return this.draw()};return a}(Epoch.Chart.SVG)}).call(this);
+(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Chart.Area=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype.y=function(){var a,b,c,k,f,q,u,m;a=[];q=this.data;k=0;for(f=q.length;k<f;k++)for(b in c=q[k],u=c.values,u)c=u[b],null!=a[b]&&(a[b]+=c.y),null==a[b]&&(a[b]=c.y);return d3.scale.linear().domain(null!=
+(m=this.options.range)?m:[0,d3.max(a)]).range([this.height-this.margins.top-this.margins.bottom,0])};a.prototype.draw=function(){var d,b,c,k;b=[this.x(),this.y()];c=b[0];k=b[1];d=d3.svg.area().x(function(a){return c(a.x)}).y0(function(a){return k(a.y0)}).y1(function(a){return k(a.y0+a.y)});d3.layout.stack().values(function(a){return a.values})(this.data);this.g.selectAll(".layer").remove();b=this.g.selectAll(".layer").data(this.data,function(a){return a.category});b.select(".area").attr("d",function(a){return d(a.values)});
+b.enter().append("g").attr("class",function(a){return a.className});b.append("path").attr("class","area").attr("d",function(a){return d(a.values)});return a.__super__.draw.call(this)};return a}(Epoch.Chart.Plot)}).call(this);
+(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Chart.Bar=function(c){function a(k){this.options=null!=k?k:{};this.options="horizontal"===this.options.orientation?Epoch.Util.defaults(this.options,b):Epoch.Util.defaults(this.options,d);a.__super__.constructor.call(this,this.options);this.onAll(h)}var d,b,h;g(a,c);d={style:"grouped",orientation:"vertical",
+padding:{bar:0.08,group:0.1},outerPadding:{bar:0.08,group:0.1}};b=Epoch.Util.defaults({tickFormats:{top:Epoch.Formats.si,bottom:Epoch.Formats.si,left:Epoch.Formats.regular,right:Epoch.Formats.regular}},d);h={"option:orientation":"orientationChanged","option:padding":"paddingChanged","option:outerPadding":"paddingChanged","option:padding:bar":"paddingChanged","option:padding:group":"paddingChanged","option:outerPadding:bar":"paddingChanged","option:outerPadding:group":"paddingChanged"};a.prototype.x=
+function(){var a;if("vertical"===this.options.orientation)return d3.scale.ordinal().domain(Epoch.Util.domain(this.data)).rangeRoundBands([0,this.innerWidth()],this.options.padding.group,this.options.outerPadding.group);a=this.extent(function(a){return a.y});a[0]=Math.min(0,a[0]);return d3.scale.linear().domain(a).range([0,this.width-this.margins.left-this.margins.right])};a.prototype.x1=function(a){var f;return d3.scale.ordinal().domain(function(){var a,k,b,d;b=this.data;d=[];a=0;for(k=b.length;a<
+k;a++)f=b[a],d.push(f.category);return d}.call(this)).rangeRoundBands([0,a.rangeBand()],this.options.padding.bar,this.options.outerPadding.bar)};a.prototype.y=function(){var a;return"vertical"===this.options.orientation?(a=this.extent(function(a){return a.y}),a[0]=Math.min(0,a[0]),d3.scale.linear().domain(a).range([this.height-this.margins.top-this.margins.bottom,0])):d3.scale.ordinal().domain(Epoch.Util.domain(this.data)).rangeRoundBands([0,this.innerHeight()],this.options.padding.group,this.options.outerPadding.group)};
+a.prototype.y1=function(a){var f;return d3.scale.ordinal().domain(function(){var a,k,b,d;b=this.data;d=[];a=0;for(k=b.length;a<k;a++)f=b[a],d.push(f.category);return d}.call(this)).rangeRoundBands([0,a.rangeBand()],this.options.padding.bar,this.options.outerPadding.bar)};a.prototype._remapData=function(){var a,f,b,d,c,h,n,e,g,s,t,v;c={};t=this.data;h=0;for(e=t.length;h<e;h++)for(d=t[h],a="bar "+d.className.replace(/\s*layer\s*/,""),v=d.values,n=0,g=v.length;n<g;n++)f=v[n],null==c[s=f.x]&&(c[s]=[]),
+c[f.x].push({label:d.category,y:f.y,className:a});f=[];for(b in c)a=c[b],f.push({group:b,values:a});return f};a.prototype.draw=function(){"horizontal"===this.options.orientation?this._drawHorizontal():this._drawVertical();return a.__super__.draw.call(this)};a.prototype._drawVertical=function(){var a,b,d,c,h,l;a=[this.x(),this.y()];c=a[0];l=a[1];h=this.x1(c);b=this.height-this.margins.top-this.margins.bottom;a=this._remapData();a=this.g.selectAll(".layer").data(a,function(a){return a.group});a.transition().duration(750).attr("transform",
+function(a){return"translate("+c(a.group)+", 0)"});a.enter().append("g").attr("class","layer").attr("transform",function(a){return"translate("+c(a.group)+", 0)"});d=a.selectAll("rect").data(function(a){return a.values});d.transition().duration(600).attr("x",function(a){return h(a.label)}).attr("y",function(a){return l(a.y)}).attr("width",h.rangeBand()).attr("height",function(a){return b-l(a.y)});d.enter().append("rect").attr("class",function(a){return a.className}).attr("x",function(a){return h(a.label)}).attr("y",
+function(a){return l(a.y)}).attr("width",h.rangeBand()).attr("height",function(a){return b-l(a.y)});d.exit().transition().duration(150).style("opacity","0").remove();return a.exit().transition().duration(750).style("opacity","0").remove()};a.prototype._drawHorizontal=function(){var a,b,d,c,h;a=[this.x(),this.y()];d=a[0];c=a[1];h=this.y1(c);a=this._remapData();a=this.g.selectAll(".layer").data(a,function(a){return a.group});a.transition().duration(750).attr("transform",function(a){return"translate(0, "+
+c(a.group)+")"});a.enter().append("g").attr("class","layer").attr("transform",function(a){return"translate(0, "+c(a.group)+")"});b=a.selectAll("rect").data(function(a){return a.values});b.transition().duration(600).attr("x",function(a){return 0}).attr("y",function(a){return h(a.label)}).attr("height",h.rangeBand()).attr("width",function(a){return d(a.y)});b.enter().append("rect").attr("class",function(a){return a.className}).attr("x",function(a){return 0}).attr("y",function(a){return h(a.label)}).attr("height",
+h.rangeBand()).attr("width",function(a){return d(a.y)});b.exit().transition().duration(150).style("opacity","0").remove();return a.exit().transition().duration(750).style("opacity","0").remove()};a.prototype.orientationChanged=function(){var a,b,d,c;c=this.options.tickFormats.top;a=this.options.tickFormats.bottom;b=this.options.tickFormats.left;d=this.options.tickFormats.right;this.options.tickFormats.left=c;this.options.tickFormats.right=a;this.options.tickFormats.top=b;this.options.tickFormats.bottom=
+d;return this.draw()};a.prototype.paddingChanged=function(){return this.draw()};return a}(Epoch.Chart.Plot)}).call(this);
+(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Chart.Line=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype.line=function(){var a,b,c;c=[this.x(),this.y()];a=c[0];b=c[1];return d3.svg.line().x(function(b){return function(b){return a(b.x)}}(this)).y(function(a){return function(a){return b(a.y)}}(this))};a.prototype.draw=
+function(){var c,b;b=[this.x(),this.y(),this.line()][2];c=this.g.selectAll(".layer").data(this.data,function(a){return a.category});c.select(".line").transition().duration(500).attr("d",function(a){return b(a.values)});c.enter().append("g").attr("class",function(a){return a.className}).append("path").attr("class","line").attr("d",function(a){return b(a.values)});c.exit().transition().duration(750).style("opacity","0").remove();return a.__super__.draw.call(this)};return a}(Epoch.Chart.Plot)}).call(this);
+(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Chart.Pie=function(c){function a(b){this.options=null!=b?b:{};a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,d));this.pie=d3.layout.pie().sort(null).value(function(a){return a.value});this.arc=d3.svg.arc().outerRadius(function(a){return function(){return Math.max(a.width,
+a.height)/2-a.options.margin}}(this)).innerRadius(function(a){return function(){return a.options.inner}}(this));this.g=this.svg.append("g").attr("transform","translate("+this.width/2+", "+this.height/2+")");this.on("option:margin","marginChanged");this.on("option:inner","innerChanged")}var d;g(a,c);d={margin:10,inner:0};a.prototype.draw=function(){var b;this.g.selectAll(".arc").remove();b=this.g.selectAll(".arc").data(this.pie(this.data),function(a){return a.data.category});b.enter().append("g").attr("class",
+function(a){return"arc pie "+a.data.className});b.select("path").attr("d",this.arc);b.select("text").attr("transform",function(a){return function(b){return"translate("+a.arc.centroid(b)+")"}}(this)).text(function(a){return a.data.label||a.data.category});b.append("path").attr("d",this.arc).each(function(a){return this._current=a});b.append("text").attr("transform",function(a){return function(b){return"translate("+a.arc.centroid(b)+")"}}(this)).attr("dy",".35em").style("text-anchor","middle").text(function(a){return a.data.label||
+a.data.category});return a.__super__.draw.call(this)};a.prototype.marginChanged=function(){return this.draw()};a.prototype.innerChanged=function(){return this.draw()};return a}(Epoch.Chart.SVG)}).call(this);
+(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Chart.Scatter=function(c){function a(b){this.options=null!=b?b:{};a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,d));this.on("option:radius","radiusChanged")}var d;g(a,c);d={radius:3.5,axes:["top","bottom","left","right"]};a.prototype.draw=function(){var b,c,k,f,
+d;b=[this.x(),this.y()];f=b[0];d=b[1];k=this.options.radius;c=this.g.selectAll(".layer").data(this.data,function(a){return a.category});c.enter().append("g").attr("class",function(a){return a.className});b=c.selectAll(".dot").data(function(a){return a.values});b.transition().duration(500).attr("r",function(a){var b;return null!=(b=a.r)?b:k}).attr("cx",function(a){return f(a.x)}).attr("cy",function(a){return d(a.y)});b.enter().append("circle").attr("class","dot").attr("r",function(a){var b;return null!=
+(b=a.r)?b:k}).attr("cx",function(a){return f(a.x)}).attr("cy",function(a){return d(a.y)});b.exit().transition().duration(750).style("opacity",0).remove();c.exit().transition().duration(750).style("opacity",0).remove();return a.__super__.draw.call(this)};a.prototype.radiusChanged=function(){return this.draw()};return a}(Epoch.Chart.Plot)}).call(this);
+(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Plot=function(c){function a(k){var f,c,u;this.options=k;Epoch.Util.copy(this.options.margins);a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,b));this._queue=[];this.margins={};u=["top","right","bottom","left"];f=0;for(c=u.length;f<c;f++)k=u[f],this.margins[k]=
+null!=this.options.margins&&null!=this.options.margins[k]?this.options.margins[k]:this.hasAxis(k)?d[k]:6;this.svg=this.el.insert("svg",":first-child").attr("width",this.width).attr("height",this.height).style("z-index","1000");"absolute"!==this.el.style("position")&&"relative"!==this.el.style("position")&&this.el.style("position","relative");this.canvas.style({position:"absolute","z-index":"999"});this._sizeCanvas();this.animation={interval:null,active:!1,delta:function(a){return function(){return-(a.w()/
+a.options.fps)}}(this),tickDelta:function(a){return function(){return-(a.w()/a.pixelRatio/a.options.fps)}}(this),frame:0,duration:this.options.fps};this._buildAxes();this.animationCallback=function(a){return function(){return a._animate()}}(this);this.onAll(h)}var d,b,h;g(a,c);b={fps:24,historySize:120,windowSize:40,queueSize:10,axes:["bottom"],ticks:{time:15,left:5,right:5},tickFormats:{top:Epoch.Formats.seconds,bottom:Epoch.Formats.seconds,left:Epoch.Formats.si,right:Epoch.Formats.si}};d={top:25,
+right:50,bottom:25,left:50};h={"option:margins":"marginsChanged","option:margins.top":"marginsChanged","option:margins.right":"marginsChanged","option:margins.bottom":"marginsChanged","option:margins.left":"marginsChanged","option:axes":"axesChanged","option:ticks":"ticksChanged","option:ticks.top":"ticksChanged","option:ticks.right":"ticksChanged","option:ticks.bottom":"ticksChanged","option:ticks.left":"ticksChanged","option:tickFormats":"tickFormatsChanged","option:tickFormats.top":"tickFormatsChanged",
+"option:tickFormats.right":"tickFormatsChanged","option:tickFormats.bottom":"tickFormatsChanged","option:tickFormats.left":"tickFormatsChanged"};a.prototype._sizeCanvas=function(){this.canvas.attr({width:this.innerWidth(),height:this.innerHeight()});return this.canvas.style({width:""+this.innerWidth()/this.pixelRatio+"px",height:""+this.innerHeight()/this.pixelRatio+"px",top:""+this.margins.top+"px",left:""+this.margins.left+"px"})};a.prototype._buildAxes=function(){this.svg.selectAll(".axis").remove();
+this._prepareTimeAxes();return this._prepareRangeAxes()};a.prototype.setData=function(a){var b,c,d,h,e;this.data=[];e=[];for(d in a)h=a[d],c=Epoch.Util.copy(h),b=Math.max(0,h.values.length-this.options.historySize),c.values=h.values.slice(b),b=["layer"],b.push("category"+((d|0)+1)),null!=h.label&&b.push(Epoch.Util.dasherize(h.label)),c.className=b.join(" "),e.push(this.data.push(c));return e};a.prototype._offsetX=function(){return 0};a.prototype._prepareTimeAxes=function(){var a;this.hasAxis("bottom")&&
+(a=this.bottomAxis=this.svg.append("g").attr("class","x axis bottom canvas").attr("transform","translate("+(this.margins.left-1)+", "+(this.innerHeight()/this.pixelRatio+this.margins.top)+")"),a.append("path").attr("class","domain").attr("d","M0,0H"+(this.innerWidth()/this.pixelRatio+1)));this.hasAxis("top")&&(a=this.topAxis=this.svg.append("g").attr("class","x axis top canvas").attr("transform","translate("+(this.margins.left-1)+", "+this.margins.top+")"),a.append("path").attr("class","domain").attr("d",
+"M0,0H"+(this.innerWidth()/this.pixelRatio+1)));return this._resetInitialTimeTicks()};a.prototype._resetInitialTimeTicks=function(){var a,b,c,d,h;d=this.options.ticks.time;this._ticks=[];this._tickTimer=d;null!=this.bottomAxis&&this.bottomAxis.selectAll(".tick").remove();null!=this.topAxis&&this.topAxis.selectAll(".tick").remove();h=this.data;a=0;for(b=h.length;a<b;a++)if(c=h[a],null!=c.values&&0<c.values.length){b=[this.options.windowSize-1,c.values.length-1];a=b[0];for(b=b[1];0<=a&&0<=b;)this._pushTick(a,
+c.values[b].time,!1,!0),a-=d,b-=d;break}return[]};a.prototype._prepareRangeAxes=function(){this.hasAxis("left")&&this.svg.append("g").attr("class","y axis left").attr("transform","translate("+(this.margins.left-1)+", "+this.margins.top+")").call(this.leftAxis());if(this.hasAxis("right"))return this.svg.append("g").attr("class","y axis right").attr("transform","translate("+(this.width-this.margins.right)+", "+this.margins.top+")").call(this.rightAxis())};a.prototype.leftAxis=function(){var a,b;b=this.options.ticks.left;
+a=d3.svg.axis().scale(this.ySvg()).orient("left").tickFormat(this.options.tickFormats.left);return 2===b?a.tickValues(this.extent(function(a){return a.y})):a.ticks(b)};a.prototype.rightAxis=function(){var a,b;this.extent(function(a){return a.y});b=this.options.ticks.right;a=d3.svg.axis().scale(this.ySvg()).orient("right").tickFormat(this.options.tickFormats.left);return 2===b?a.tickValues(this.extent(function(a){return a.y})):a.ticks(b)};a.prototype.hasAxis=function(a){return-1<this.options.axes.indexOf(a)};
+a.prototype.innerWidth=function(){return(this.width-(this.margins.left+this.margins.right))*this.pixelRatio};a.prototype.innerHeight=function(){return(this.height-(this.margins.top+this.margins.bottom))*this.pixelRatio};a.prototype._prepareEntry=function(a){return a};a.prototype._prepareLayers=function(a){return a};a.prototype._startTransition=function(){if(!0!==this.animation.active&&0!==this._queue.length)return this.trigger("transition:start"),this._shift(),this.animation.active=!0,this.animation.interval=
+setInterval(this.animationCallback,1E3/this.options.fps)};a.prototype._stopTransition=function(){var a,b,c,d;if(this.inTransition()){d=this.data;b=0;for(c=d.length;b<c;b++)a=d[b],a.values.length>this.options.windowSize+1&&a.values.shift();b=[this._ticks[0],this._ticks[this._ticks.length-1]];a=b[0];b=b[1];null!=b&&b.enter&&(b.enter=!1,b.opacity=1);null!=a&&a.exit&&this._shiftTick();this.animation.frame=0;this.trigger("transition:end");if(0<this._queue.length)return this._shift();this.animation.active=
+!1;return clearInterval(this.animation.interval)}};a.prototype.inTransition=function(){return this.animation.active};a.prototype.push=function(a){a=this._prepareLayers(a);this._queue.length>this.options.queueSize&&this._queue.splice(this.options.queueSize,this._queue.length-this.options.queueSize);if(this._queue.length===this.options.queueSize)return!1;this._queue.push(a.map(function(a){return function(b){return a._prepareEntry(b)}}(this)));this.trigger("push");if(!this.inTransition())return this._startTransition()};
+a.prototype._shift=function(){var a,b,c,d;this.trigger("before:shift");a=this._queue.shift();d=this.data;for(b in d)c=d[b],c.values.push(a[b]);this._updateTicks(a[0].time);this._transitionRangeAxes();return this.trigger("after:shift")};a.prototype._transitionRangeAxes=function(){this.hasAxis("left")&&this.svg.selectAll(".y.axis.left").transition().duration(500).ease("linear").call(this.leftAxis());if(this.hasAxis("right"))return this.svg.selectAll(".y.axis.right").transition().duration(500).ease("linear").call(this.rightAxis())};
+a.prototype._animate=function(){if(this.inTransition())return++this.animation.frame===this.animation.duration&&this._stopTransition(),this.draw(this.animation.frame*this.animation.delta()),this._updateTimeAxes()};a.prototype.y=function(){return d3.scale.linear().domain(this.extent(function(a){return a.y})).range([this.innerHeight(),0])};a.prototype.ySvg=function(){return d3.scale.linear().domain(this.extent(function(a){return a.y})).range([this.innerHeight()/this.pixelRatio,0])};a.prototype.w=function(){return this.innerWidth()/
+this.options.windowSize};a.prototype._updateTicks=function(a){if(this.hasAxis("top")||this.hasAxis("bottom"))if(++this._tickTimer%this.options.ticks.time||this._pushTick(this.options.windowSize,a,!0),!(0<=this._ticks[0].x-this.w()/this.pixelRatio))return this._ticks[0].exit=!0};a.prototype._pushTick=function(a,b,c,d){null==c&&(c=!1);null==d&&(d=!1);if(this.hasAxis("top")||this.hasAxis("bottom"))return b={time:b,x:a*(this.w()/this.pixelRatio)+this._offsetX(),opacity:c?0:1,enter:c?!0:!1,exit:!1},this.hasAxis("bottom")&&
+(a=this.bottomAxis.append("g").attr("class","tick major").attr("transform","translate("+(b.x+1)+",0)").style("opacity",b.opacity),a.append("line").attr("y2",6),a.append("text").attr("text-anchor","middle").attr("dy",19).text(this.options.tickFormats.bottom(b.time)),b.bottomEl=a),this.hasAxis("top")&&(a=this.topAxis.append("g").attr("class","tick major").attr("transform","translate("+(b.x+1)+",0)").style("opacity",b.opacity),a.append("line").attr("y2",-6),a.append("text").attr("text-anchor","middle").attr("dy",
+-10).text(this.options.tickFormats.top(b.time)),b.topEl=a),d?this._ticks.unshift(b):this._ticks.push(b),b};a.prototype._shiftTick=function(){var a;if(0<this._ticks.length&&(a=this._ticks.shift(),null!=a.topEl&&a.topEl.remove(),null!=a.bottomEl))return a.bottomEl.remove()};a.prototype._updateTimeAxes=function(){var a,b,c,d,h,e,g;if(this.hasAxis("top")||this.hasAxis("bottom")){a=[this.animation.tickDelta(),1/this.options.fps];b=a[0];a=a[1];e=this._ticks;g=[];d=0;for(h=e.length;d<h;d++)c=e[d],c.x+=b,
+this.hasAxis("bottom")&&c.bottomEl.attr("transform","translate("+(c.x+1)+",0)"),this.hasAxis("top")&&c.topEl.attr("transform","translate("+(c.x+1)+",0)"),c.enter?c.opacity+=a:c.exit&&(c.opacity-=a),c.enter||c.exit?(this.hasAxis("bottom")&&c.bottomEl.style("opacity",c.opacity),this.hasAxis("top")?g.push(c.topEl.style("opacity",c.opacity)):g.push(void 0)):g.push(void 0);return g}};a.prototype.draw=function(b){return a.__super__.draw.call(this)};a.prototype.dimensionsChanged=function(){a.__super__.dimensionsChanged.call(this);
+this.svg.attr("width",this.width).attr("height",this.height);this._sizeCanvas();this._buildAxes();return this.draw(this.animation.frame*this.animation.delta())};a.prototype.axesChanged=function(){var a,b,c,h;h=["top","right","bottom","left"];b=0;for(c=h.length;b<c;b++)if(a=h[b],null==this.options.margins||null==this.options.margins[a])this.hasAxis(a)?this.margins[a]=d[a]:this.margins[a]=6;this._sizeCanvas();this._buildAxes();return this.draw(this.animation.frame*this.animation.delta())};a.prototype.ticksChanged=
+function(){this._resetInitialTimeTicks();this._transitionRangeAxes();return this.draw(this.animation.frame*this.animation.delta())};a.prototype.tickFormatsChanged=function(){this._resetInitialTimeTicks();this._transitionRangeAxes();return this.draw(this.animation.frame*this.animation.delta())};a.prototype.marginsChanged=function(){var a,b,c;if(null!=this.options.margins){c=this.options.margins;for(a in c)b=c[a],this.margins[a]=null==b?6:b;this._sizeCanvas();return this.draw(this.animation.frame*this.animation.delta())}};
+return a}(Epoch.Chart.Canvas);Epoch.Time.Stack=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype._prepareLayers=function(a){var b,c,k,f;k=c=0;for(f=a.length;k<f;k++)b=a[k],b.y0=c,c+=b.y;return a};a.prototype.setData=function(c){var b,h,k,f,e;a.__super__.setData.call(this,c);e=[];b=c=0;for(f=this.data[0].values.length;0<=f?c<f:c>f;b=0<=f?++c:--c)k=0,e.push(function(){var a,c,d,f;d=this.data;f=[];a=0;for(c=d.length;a<c;a++)h=d[a],h.values[b].y0=k,f.push(k+=
+h.values[b].y);return f}.call(this));return e};a.prototype.extent=function(){var a,b,c,k,f,e,g,m;a=f=c=0;for(g=this.data[0].values.length;0<=g?f<g:f>g;a=0<=g?++f:--f){b=e=k=0;for(m=this.data.length;0<=m?e<m:e>m;b=0<=m?++e:--e)k+=this.data[b].values[a].y;k>c&&(c=k)}return[0,c]};return a}(Epoch.Time.Plot)}).call(this);
+(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Area=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype.setStyles=function(a){a=null!=a.className?this.getStyles("g."+a.className.replace(/\s/g,".")+" path.area"):this.getStyles("g path.area");this.ctx.fillStyle=a.fill;null!=a.stroke&&(this.ctx.strokeStyle=
+a.stroke);if(null!=a["stroke-width"])return this.ctx.lineWidth=a["stroke-width"].replace("px","")};a.prototype._drawAreas=function(a){var b,c,k,f,e,g,m,l,n,p;null==a&&(a=0);g=[this.y(),this.w()];m=g[0];g=g[1];p=[];for(c=l=n=this.data.length-1;0>=n?0>=l:0<=l;c=0>=n?++l:--l){f=this.data[c];this.setStyles(f);this.ctx.beginPath();e=[this.options.windowSize,f.values.length,this.inTransition()];c=e[0];k=e[1];for(e=e[2];-2<=--c&&0<=--k;)b=f.values[k],b=[(c+1)*g+a,m(b.y+b.y0)],e&&(b[0]+=g),c===this.options.windowSize-
+1?this.ctx.moveTo.apply(this.ctx,b):this.ctx.lineTo.apply(this.ctx,b);c=e?(c+3)*g+a:(c+2)*g+a;this.ctx.lineTo(c,this.innerHeight());this.ctx.lineTo(this.width*this.pixelRatio+g+a,this.innerHeight());this.ctx.closePath();p.push(this.ctx.fill())}return p};a.prototype._drawStrokes=function(a){var b,c,k,f,e,g,m,l,n,p;null==a&&(a=0);c=[this.y(),this.w()];m=c[0];g=c[1];p=[];for(c=l=n=this.data.length-1;0>=n?0>=l:0<=l;c=0>=n?++l:--l){f=this.data[c];this.setStyles(f);this.ctx.beginPath();e=[this.options.windowSize,
+f.values.length,this.inTransition()];c=e[0];k=e[1];for(e=e[2];-2<=--c&&0<=--k;)b=f.values[k],b=[(c+1)*g+a,m(b.y+b.y0)],e&&(b[0]+=g),c===this.options.windowSize-1?this.ctx.moveTo.apply(this.ctx,b):this.ctx.lineTo.apply(this.ctx,b);p.push(this.ctx.stroke())}return p};a.prototype.draw=function(c){null==c&&(c=0);this.clear();this._drawAreas(c);this._drawStrokes(c);return a.__super__.draw.call(this)};return a}(Epoch.Time.Stack)}).call(this);
+(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Bar=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype._offsetX=function(){return 0.5*this.w()/this.pixelRatio};a.prototype.setStyles=function(a){a=this.getStyles("rect.bar."+a.replace(/\s/g,"."));this.ctx.fillStyle=a.fill;this.ctx.strokeStyle=
+null==a.stroke||"none"===a.stroke?"transparent":a.stroke;if(null!=a["stroke-width"])return this.ctx.lineWidth=a["stroke-width"].replace("px","")};a.prototype.draw=function(c){var b,h,k,f,e,g,m,l,n,p,r,s,t;null==c&&(c=0);this.clear();f=[this.y(),this.w()];p=f[0];n=f[1];t=this.data;r=0;for(s=t.length;r<s;r++)if(m=t[r],0<m.values.length)for(this.setStyles(m.className),e=[this.options.windowSize,m.values.length,this.inTransition()],f=e[0],g=e[1],e=(l=e[2])?-1:0;--f>=e&&0<=--g;)b=m.values[g],k=[f*n+c,
+b.y,b.y0],b=k[0],h=k[1],k=k[2],l&&(b+=n),b=[b+1,p(h+k),n-2,this.innerHeight()-p(h)+0.5*this.pixelRatio],this.ctx.fillRect.apply(this.ctx,b),this.ctx.strokeRect.apply(this.ctx,b);return a.__super__.draw.call(this)};return a}(Epoch.Time.Stack)}).call(this);
+(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Gauge=function(c){function a(c){this.options=null!=c?c:{};a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,d));this.value=this.options.value||0;"absolute"!==this.el.style("position")&&"relative"!==this.el.style("position")&&this.el.style("position","relative");
+this.svg=this.el.insert("svg",":first-child").attr("width",this.width).attr("height",this.height).attr("class","gauge-labels");this.svg.style({position:"absolute","z-index":"1"});this.svg.append("g").attr("transform","translate("+this.textX()+", "+this.textY()+")").append("text").attr("class","value").text(this.options.format(this.value));this.animation={interval:null,active:!1,delta:0,target:0};this._animate=function(a){return function(){Math.abs(a.animation.target-a.value)<Math.abs(a.animation.delta)?
+(a.value=a.animation.target,clearInterval(a.animation.interval),a.animation.active=!1):a.value+=a.animation.delta;a.svg.select("text.value").text(a.options.format(a.value));return a.draw()}}(this);this.onAll(b)}var d,b;g(a,c);d={domain:[0,1],ticks:10,tickSize:5,tickOffset:5,fps:34,format:Epoch.Formats.percent};b={"option:domain":"domainChanged","option:ticks":"ticksChanged","option:tickSize":"tickSizeChanged","option:tickOffset":"tickOffsetChanged","option:format":"formatChanged"};a.prototype.update=
+function(a){this.animation.target=a;this.animation.delta=(a-this.value)/this.options.fps;if(!this.animation.active)return this.animation.interval=setInterval(this._animate,1E3/this.options.fps),this.animation.active=!0};a.prototype.push=function(a){return this.update(a)};a.prototype.radius=function(){return this.getHeight()/1.58};a.prototype.centerX=function(){return this.getWidth()/2};a.prototype.centerY=function(){return 0.68*this.getHeight()};a.prototype.textX=function(){return this.width/2};a.prototype.textY=
+function(){return 0.48*this.height};a.prototype.getAngle=function(a){var b,c;c=this.options.domain;b=c[0];return(a-b)/(c[1]-b)*(Math.PI+2*Math.PI/8)-Math.PI/2-Math.PI/8};a.prototype.setStyles=function(a){a=this.getStyles(a);this.ctx.fillStyle=a.fill;this.ctx.strokeStyle=a.stroke;if(null!=a["stroke-width"])return this.ctx.lineWidth=a["stroke-width"].replace("px","")};a.prototype.draw=function(){var b,c,d,e,g,m,l,n,p,r,s,t;g=[this.centerX(),this.centerY(),this.radius()];d=g[0];e=g[1];g=g[2];l=[this.options.tickOffset,
+this.options.tickSize];n=l[0];p=l[1];this.clear();l=d3.scale.linear().domain([0,this.options.ticks]).range([-1.125*Math.PI,Math.PI/8]);this.setStyles(".epoch .gauge .tick");this.ctx.beginPath();b=s=0;for(t=this.options.ticks;0<=t?s<=t:s>=t;b=0<=t?++s:--s)b=l(b),b=[Math.cos(b),Math.sin(b)],c=b[0],m=b[1],b=c*(g-n)+d,r=m*(g-n)+e,c=c*(g-n-p)+d,m=m*(g-n-p)+e,this.ctx.moveTo(b,r),this.ctx.lineTo(c,m);this.ctx.stroke();this.setStyles(".epoch .gauge .arc.outer");this.ctx.beginPath();this.ctx.arc(d,e,g,-1.125*
+Math.PI,0.125*Math.PI,!1);this.ctx.stroke();this.setStyles(".epoch .gauge .arc.inner");this.ctx.beginPath();this.ctx.arc(d,e,g-10,-1.125*Math.PI,0.125*Math.PI,!1);this.ctx.stroke();this.drawNeedle();return a.__super__.draw.call(this)};a.prototype.drawNeedle=function(){var a,b,c;c=[this.centerX(),this.centerY(),this.radius()];a=c[0];b=c[1];c=c[2];this.setStyles(".epoch .gauge .needle");this.ctx.beginPath();this.ctx.save();this.ctx.translate(a,b);this.ctx.rotate(this.getAngle(this.value));this.ctx.moveTo(4*
+this.pixelRatio,0);this.ctx.lineTo(-4*this.pixelRatio,0);this.ctx.lineTo(-1*this.pixelRatio,19-c);this.ctx.lineTo(1,19-c);this.ctx.fill();this.setStyles(".epoch .gauge .needle-base");this.ctx.beginPath();this.ctx.arc(0,0,this.getWidth()/25,0,2*Math.PI);this.ctx.fill();return this.ctx.restore()};a.prototype.domainChanged=function(){return this.draw()};a.prototype.ticksChanged=function(){return this.draw()};a.prototype.tickSizeChanged=function(){return this.draw()};a.prototype.tickOffsetChanged=function(){return this.draw()};
+a.prototype.formatChanged=function(){return this.svg.select("text.value").text(this.options.format(this.value))};return a}(Epoch.Chart.Canvas)}).call(this);
+(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Heatmap=function(c){function a(c){this.options=c;a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,b));this._setOpacityFunction();this._setupPaintCanvas();this.onAll(e)}var d,b,e;g(a,c);b={buckets:10,bucketRange:[0,100],opacity:"linear",bucketPadding:2,paintZeroValues:!1,
+cutOutliers:!1};d={root:function(a,b){return Math.pow(a/b,0.5)},linear:function(a,b){return a/b},quadratic:function(a,b){return Math.pow(a/b,2)},cubic:function(a,b){return Math.pow(a/b,3)},quartic:function(a,b){return Math.pow(a/b,4)},quintic:function(a,b){return Math.pow(a/b,5)}};e={"option:buckets":"bucketsChanged","option:bucketRange":"bucketRangeChanged","option:opacity":"opacityChanged","option:bucketPadding":"bucketPaddingChanged","option:paintZeroValues":"paintZeroValuesChanged","option:cutOutliers":"cutOutliersChanged"};
+a.prototype._setOpacityFunction=function(){if(Epoch.isString(this.options.opacity)){if(this._opacityFn=d[this.options.opacity],null==this._opacityFn)return Epoch.exception("Unknown coloring function provided '"+this.options.opacity+"'")}else return Epoch.isFunction(this.options.opacity)?this._opacityFn=this.options.opacity:Epoch.exception("Unknown type for provided coloring function.")};a.prototype.setData=function(b){var c,d,e,g;a.__super__.setData.call(this,b);e=this.data;g=[];c=0;for(d=e.length;c<
+d;c++)b=e[c],g.push(b.values=b.values.map(function(a){return function(b){return a._prepareEntry(b)}}(this)));return g};a.prototype._getBuckets=function(a){var b,c,d,e,g;e=a.time;g=[];b=0;for(d=this.options.buckets;0<=d?b<d:b>d;0<=d?++b:--b)g.push(0);e={time:e,max:0,buckets:g};b=(this.options.bucketRange[1]-this.options.bucketRange[0])/this.options.buckets;g=a.histogram;for(c in g)a=g[c],d=parseInt((c-this.options.bucketRange[0])/b),this.options.cutOutliers&&(0>d||d>=this.options.buckets)||(0>d?d=
+0:d>=this.options.buckets&&(d=this.options.buckets-1),e.buckets[d]+=parseInt(a));c=a=0;for(b=e.buckets.length;0<=b?a<b:a>b;c=0<=b?++a:--a)e.max=Math.max(e.max,e.buckets[c]);return e};a.prototype.y=function(){return d3.scale.linear().domain(this.options.bucketRange).range([this.innerHeight(),0])};a.prototype.ySvg=function(){return d3.scale.linear().domain(this.options.bucketRange).range([this.innerHeight()/this.pixelRatio,0])};a.prototype.h=function(){return this.innerHeight()/this.options.buckets};
+a.prototype._offsetX=function(){return 0.5*this.w()/this.pixelRatio};a.prototype._setupPaintCanvas=function(){this.paintWidth=(this.options.windowSize+1)*this.w();this.paintHeight=this.height*this.pixelRatio;this.paint=document.createElement("CANVAS");this.paint.width=this.paintWidth;this.paint.height=this.paintHeight;this.p=Epoch.Util.getContext(this.paint);this.redraw();this.on("after:shift","_paintEntry");this.on("transition:end","_shiftPaintCanvas");return this.on("transition:end",function(a){return function(){return a.draw(a.animation.frame*
+a.animation.delta())}}(this))};a.prototype.redraw=function(){var a,b;b=this.data[0].values.length;a=this.options.windowSize;for(this.inTransition()&&a++;0<=--b&&0<=--a;)this._paintEntry(b,a);return this.draw(this.animation.frame*this.animation.delta())};a.prototype._computeColor=function(a,b,c){return Epoch.Util.toRGBA(c,this._opacityFn(a,b))};a.prototype._paintEntry=function(a,b){var c,d,e,g,h,p,r,s,t,v,y,w,A,z;null==a&&(a=null);null==b&&(b=null);g=[this.w(),this.h()];y=g[0];p=g[1];null==a&&(a=this.data[0].values.length-
+1);null==b&&(b=this.options.windowSize);g=[];var x;x=[];h=0;for(v=this.options.buckets;0<=v?h<v:h>v;0<=v?++h:--h)x.push(0);v=0;t=this.data;d=0;for(r=t.length;d<r;d++){s=t[d];h=this._getBuckets(s.values[a]);w=h.buckets;for(c in w)e=w[c],x[c]+=e;v+=h.max;e=this.getStyles("."+s.className.split(" ").join(".")+" rect.bucket");h.color=e.fill;g.push(h)}s=b*y;this.p.clearRect(s,0,y,this.paintHeight);r=this.options.buckets;z=[];for(c in x){e=x[c];d=this._avgLab(g,c);w=t=0;for(A=g.length;w<A;w++)h=g[w],t+=
+h.buckets[c]/e*v;if(0<e||this.options.paintZeroValues)this.p.fillStyle=this._computeColor(e,t,d),this.p.fillRect(s,(r-1)*p,y-this.options.bucketPadding,p-this.options.bucketPadding);z.push(r--)}return z};a.prototype._shiftPaintCanvas=function(){var a;a=this.p.getImageData(this.w(),0,this.paintWidth-this.w(),this.paintHeight);return this.p.putImageData(a,0,0)};a.prototype._avgLab=function(a,b){var c,d,e,g,h,p,r,s;r=[0,0,0,0];h=r[0];c=r[1];d=r[2];r=r[3];p=0;for(s=a.length;p<s;p++)e=a[p],null!=e.buckets[b]&&
+(r+=e.buckets[b]);for(g in a)e=a[g],p=null!=e.buckets[b]?e.buckets[b]|0:0,p/=r,e=d3.lab(e.color),h+=p*e.l,c+=p*e.a,d+=p*e.b;return d3.lab(h,c,d).toString()};a.prototype.draw=function(b){null==b&&(b=0);this.clear();this.ctx.drawImage(this.paint,b,0);return a.__super__.draw.call(this)};a.prototype.bucketsChanged=function(){return this.redraw()};a.prototype.bucketRangeChanged=function(){this._transitionRangeAxes();return this.redraw()};a.prototype.opacityChanged=function(){this._setOpacityFunction();
+return this.redraw()};a.prototype.bucketPaddingChanged=function(){return this.redraw()};a.prototype.paintZeroValuesChanged=function(){return this.redraw()};a.prototype.cutOutliersChanged=function(){return this.redraw()};return a}(Epoch.Time.Plot)}).call(this);
+(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Line=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype.setStyles=function(a){a=this.getStyles("g."+a.replace(/\s/g,".")+" path.line");this.ctx.fillStyle=a.fill;this.ctx.strokeStyle=a.stroke;return this.ctx.lineWidth=this.pixelRatio*a["stroke-width"].replace("px",
+"")};a.prototype.y=function(){return d3.scale.linear().domain(this.extent(function(a){return a.y})).range([this.innerHeight()-this.pixelRatio/2,this.pixelRatio])};a.prototype.draw=function(c){var b,e,g,f,q,u,m,l,n,p;null==c&&(c=0);this.clear();e=[this.y(),this.w()];m=e[0];u=e[1];p=this.data;l=0;for(n=p.length;l<n;l++)if(f=p[l],0<f.values.length){this.setStyles(f.className);this.ctx.beginPath();q=[this.options.windowSize,f.values.length,this.inTransition()];e=q[0];g=q[1];for(q=q[2];-2<=--e&&0<=--g;)b=
+f.values[g],b=[(e+1)*u+c,m(b.y)],q&&(b[0]+=u),e===this.options.windowSize-1?this.ctx.moveTo.apply(this.ctx,b):this.ctx.lineTo.apply(this.ctx,b);this.ctx.stroke()}return a.__super__.draw.call(this)};return a}(Epoch.Time.Plot)}).call(this);(function(){Epoch._typeMap={area:Epoch.Chart.Area,bar:Epoch.Chart.Bar,line:Epoch.Chart.Line,pie:Epoch.Chart.Pie,scatter:Epoch.Chart.Scatter,"time.area":Epoch.Time.Area,"time.bar":Epoch.Time.Bar,"time.line":Epoch.Time.Line,"time.gauge":Epoch.Time.Gauge,"time.heatmap":Epoch.Time.Heatmap}}).call(this);
+(function(){null!=window.MooTools&&function(){return Element.implement("epoch",function(e){var g,c;c=$$(this);null==(g=c.retrieve("epoch-chart")[0])&&(e.el=this,g=Epoch._typeMap[e.type],null==g&&Epoch.exception("Unknown chart type '"+e.type+"'"),c.store("epoch-chart",g=new g(e)),g.draw());return g})}()}).call(this);
+(function(){var e;e=function(e){return e.fn.epoch=function(c){var a;c.el=this.get(0);null==(a=this.data("epoch-chart"))&&(a=Epoch._typeMap[c.type],null==a&&Epoch.exception("Unknown chart type '"+c.type+"'"),this.data("epoch-chart",a=new a(c)),a.draw());return a}};null!=window.jQuery&&e(jQuery)}).call(this);
+(function(){var e;e=function(e){var c,a,d;a={};c=0;d=function(){return"epoch-chart-"+ ++c};return e.extend(e.fn,{epoch:function(b){var c,e;if(null!=(c=this.data("epoch-chart")))return a[c];b.el=this.get(0);e=Epoch._typeMap[b.type];null==e&&Epoch.exception("Unknown chart type '"+b.type+"'");this.data("epoch-chart",c=d());b=new e(b);a[c]=b;b.draw();return b}})};null!=window.Zepto&&e(Zepto)}).call(this);

+ 137 - 0
examples/realtime-advanced/resources/static/prismjs.min.css

@@ -0,0 +1,137 @@
+/* http://prismjs.com/download.html?themes=prism&languages=clike+javascript+go */
+/**
+ * prism.js default theme for JavaScript, CSS and HTML
+ * Based on dabblet (http://dabblet.com)
+ * @author Lea Verou
+ */
+
+code[class*="language-"],
+pre[class*="language-"] {
+	color: black;
+	text-shadow: 0 1px white;
+	font-family: Consolas, Monaco, 'Andale Mono', monospace;
+	direction: ltr;
+	text-align: left;
+	white-space: pre;
+	word-spacing: normal;
+	word-break: normal;
+	line-height: 1.5;
+
+	-moz-tab-size: 4;
+	-o-tab-size: 4;
+	tab-size: 4;
+
+	-webkit-hyphens: none;
+	-moz-hyphens: none;
+	-ms-hyphens: none;
+	hyphens: none;
+}
+
+pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
+code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
+	text-shadow: none;
+	background: #b3d4fc;
+}
+
+pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
+code[class*="language-"]::selection, code[class*="language-"] ::selection {
+	text-shadow: none;
+	background: #b3d4fc;
+}
+
+@media print {
+	code[class*="language-"],
+	pre[class*="language-"] {
+		text-shadow: none;
+	}
+}
+
+/* Code blocks */
+pre[class*="language-"] {
+	padding: 1em;
+	margin: .5em 0;
+	overflow: auto;
+}
+
+:not(pre) > code[class*="language-"],
+pre[class*="language-"] {
+	background: #f5f2f0;
+}
+
+/* Inline code */
+:not(pre) > code[class*="language-"] {
+	padding: .1em;
+	border-radius: .3em;
+}
+
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+	color: slategray;
+}
+
+.token.punctuation {
+	color: #999;
+}
+
+.namespace {
+	opacity: .7;
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted {
+	color: #905;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.inserted {
+	color: #690;
+}
+
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+	color: #a67f59;
+	background: hsla(0, 0%, 100%, .5);
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+	color: #07a;
+}
+
+.token.function {
+	color: #DD4A68;
+}
+
+.token.regex,
+.token.important,
+.token.variable {
+	color: #e90;
+}
+
+.token.important,
+.token.bold {
+	font-weight: bold;
+}
+.token.italic {
+	font-style: italic;
+}
+
+.token.entity {
+	cursor: help;
+}
+

File diff ditekan karena terlalu besar
+ 1 - 0
examples/realtime-advanced/resources/static/prismjs.min.js


+ 144 - 0
examples/realtime-advanced/resources/static/realtime.js

@@ -0,0 +1,144 @@
+
+
+function StartRealtime(roomid, timestamp) {
+    StartEpoch(timestamp);
+    StartSSE(roomid);
+    StartForm();
+}
+
+function StartForm() {
+    $('#chat-message').focus();
+    $('#chat-form').ajaxForm(function() {
+        $('#chat-message').val('');
+        $('#chat-message').focus();
+    });
+}
+
+function StartEpoch(timestamp) {
+    var windowSize = 60;
+    var height = 200;
+    var defaultData = histogram(windowSize, timestamp);
+
+    window.heapChart = $('#heapChart').epoch({
+        type: 'time.area',
+        axes: ['bottom', 'left'],
+        height: height,
+        historySize: 10,
+        data: [
+            {values: defaultData},
+            {values: defaultData}
+        ]
+    });
+
+    window.mallocsChart = $('#mallocsChart').epoch({
+        type: 'time.area',
+        axes: ['bottom', 'left'],
+        height: height,
+        historySize: 10,
+        data: [
+            {values: defaultData},
+            {values: defaultData}
+        ]
+    });
+
+    window.messagesChart = $('#messagesChart').epoch({
+        type: 'time.line',
+        axes: ['bottom', 'left'],
+        height: 240,
+        historySize: 10,
+        data: [
+            {values: defaultData},
+            {values: defaultData},
+            {values: defaultData}
+        ]
+    });
+}
+
+function StartSSE(roomid) {
+    if (!window.EventSource) {
+        alert("EventSource is not enabled in this browser");
+        return;
+    }
+    var source = new EventSource('/stream/'+roomid);
+    source.addEventListener('message', newChatMessage, false);
+    source.addEventListener('stats', stats, false);
+}
+
+function stats(e) {
+    var data = parseJSONStats(e.data);
+    heapChart.push(data.heap);
+    mallocsChart.push(data.mallocs);
+    messagesChart.push(data.messages);
+}
+
+function parseJSONStats(e) {
+    var data = jQuery.parseJSON(e);
+    var timestamp = data.timestamp;
+
+    var heap = [
+        {time: timestamp, y: data.HeapInuse},
+        {time: timestamp, y: data.StackInuse}
+    ];
+
+    var mallocs = [
+        {time: timestamp, y: data.Mallocs},
+        {time: timestamp, y: data.Frees}
+    ];
+    var messages = [
+        {time: timestamp, y: data.Connected},
+        {time: timestamp, y: data.Inbound},
+        {time: timestamp, y: data.Outbound}
+    ];
+
+    return {
+        heap: heap,
+        mallocs: mallocs,
+        messages: messages
+    }
+}
+
+function newChatMessage(e) {
+    var data = jQuery.parseJSON(e.data);
+    var nick = data.nick;
+    var message = data.message;
+    var style = rowStyle(nick);
+    var html = "<tr class=\""+style+"\"><td>"+nick+"</td><td>"+message+"</td></tr>";
+    $('#chat').append(html);
+
+    $("#chat-scroll").scrollTop($("#chat-scroll")[0].scrollHeight);
+}
+
+function histogram(windowSize, timestamp) {
+    var entries = new Array(windowSize);
+    for(var i = 0; i < windowSize; i++) {
+        entries[i] = {time: (timestamp-windowSize+i-1), y:0};
+    }
+    return entries;
+}
+
+var entityMap = {
+    "&": "&amp;",
+    "<": "&lt;",
+    ">": "&gt;",
+    '"': '&quot;',
+    "'": '&#39;',
+    "/": '&#x2F;'
+};
+
+function rowStyle(nick) {
+    var classes = ['active', 'success', 'info', 'warning', 'danger'];
+    var index = hashCode(nick)%5;
+    return classes[index];
+}
+
+function hashCode(s){
+  return Math.abs(s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0));             
+}
+
+function escapeHtml(string) {
+    return String(string).replace(/[&<>"'\/]/g, function (s) {
+      return entityMap[s];
+    });
+}
+
+window.StartRealtime = StartRealtime

+ 25 - 0
examples/realtime-advanced/rooms.go

@@ -0,0 +1,25 @@
+package main
+
+import "github.com/dustin/go-broadcast"
+
+var roomChannels = make(map[string]broadcast.Broadcaster)
+
+func openListener(roomid string) chan interface{} {
+	listener := make(chan interface{})
+	room(roomid).Register(listener)
+	return listener
+}
+
+func closeListener(roomid string, listener chan interface{}) {
+	room(roomid).Unregister(listener)
+	close(listener)
+}
+
+func room(roomid string) broadcast.Broadcaster {
+	b, ok := roomChannels[roomid]
+	if !ok {
+		b = broadcast.NewBroadcaster(10)
+		roomChannels[roomid] = b
+	}
+	return b
+}

+ 96 - 0
examples/realtime-advanced/routes.go

@@ -0,0 +1,96 @@
+package main
+
+import (
+	"fmt"
+	"html"
+	"io"
+	"strings"
+	"time"
+
+	"github.com/gin-gonic/gin"
+)
+
+func rateLimit(c *gin.Context) {
+
+	ip := c.ClientIP()
+	value := int(ips.Add(ip, 1))
+	if value%50 == 0 {
+		fmt.Printf("ip: %s, count: %d\n", ip, value)
+	}
+	if value >= 200 {
+		if value%200 == 0 {
+			fmt.Println("ip blocked")
+		}
+		c.Abort()
+		c.String(503, "you were automatically banned :)")
+	}
+}
+
+func index(c *gin.Context) {
+	c.Redirect(301, "/room/hn")
+}
+
+func roomGET(c *gin.Context) {
+	roomid := c.ParamValue("roomid")
+	nick := c.FormValue("nick")
+	if len(nick) < 2 {
+		nick = ""
+	}
+	if len(nick) > 13 {
+		nick = nick[0:12] + "..."
+	}
+	c.HTML(200, "room_login.templ.html", gin.H{
+		"roomid":    roomid,
+		"nick":      nick,
+		"timestamp": time.Now().Unix(),
+	})
+
+}
+
+func roomPOST(c *gin.Context) {
+	roomid := c.ParamValue("roomid")
+	nick := c.FormValue("nick")
+	message := c.PostFormValue("message")
+	message = strings.TrimSpace(message)
+
+	validMessage := len(message) > 1 && len(message) < 200
+	validNick := len(nick) > 1 && len(nick) < 14
+	if !validMessage || !validNick {
+		c.JSON(400, gin.H{
+			"status": "failed",
+			"error":  "the message or nickname is too long",
+		})
+		return
+	}
+
+	post := gin.H{
+		"nick":    html.EscapeString(nick),
+		"message": html.EscapeString(message),
+	}
+	messages.Add("inbound", 1)
+	room(roomid).Submit(post)
+	c.JSON(200, post)
+}
+
+func streamRoom(c *gin.Context) {
+	roomid := c.ParamValue("roomid")
+	listener := openListener(roomid)
+	ticker := time.NewTicker(1 * time.Second)
+	users.Add("connected", 1)
+	defer func() {
+		closeListener(roomid, listener)
+		ticker.Stop()
+		users.Add("disconnected", 1)
+	}()
+
+	c.Stream(func(w io.Writer) bool {
+		select {
+		case msg := <-listener:
+			messages.Add("outbound", 1)
+			c.SSEvent("message", msg)
+		case <-ticker.C:
+			c.SSEvent("stats", Stats())
+		}
+		return true
+	})
+}

+ 56 - 0
examples/realtime-advanced/stats.go

@@ -0,0 +1,56 @@
+package main
+
+import (
+	"runtime"
+	"sync"
+	"time"
+
+	"github.com/manucorporat/stats"
+)
+
+var ips = stats.New()
+var messages = stats.New()
+var users = stats.New()
+var mutexStats sync.RWMutex
+var savedStats map[string]uint64
+
+func statsWorker() {
+	c := time.Tick(1 * time.Second)
+	var lastMallocs uint64 = 0
+	var lastFrees uint64 = 0
+	for range c {
+		var stats runtime.MemStats
+		runtime.ReadMemStats(&stats)
+
+		mutexStats.Lock()
+		savedStats = map[string]uint64{
+			"timestamp":  uint64(time.Now().Unix()),
+			"HeapInuse":  stats.HeapInuse,
+			"StackInuse": stats.StackInuse,
+			"Mallocs":    (stats.Mallocs - lastMallocs),
+			"Frees":      (stats.Frees - lastFrees),
+			"Inbound":    uint64(messages.Get("inbound")),
+			"Outbound":   uint64(messages.Get("outbound")),
+			"Connected":  connectedUsers(),
+		}
+		lastMallocs = stats.Mallocs
+		lastFrees = stats.Frees
+		messages.Reset()
+		mutexStats.Unlock()
+	}
+}
+
+func connectedUsers() uint64 {
+	connected := users.Get("connected") - users.Get("disconnected")
+	if connected < 0 {
+		return 0
+	}
+	return uint64(connected)
+}
+
+func Stats() map[string]uint64 {
+	mutexStats.RLock()
+	defer mutexStats.RUnlock()
+
+	return savedStats
+}

+ 58 - 0
examples/realtime-chat/main.go

@@ -0,0 +1,58 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"math/rand"
+
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	router := gin.Default()
+	router.SetHTMLTemplate(html)
+
+	router.GET("/room/:roomid", roomGET)
+	router.POST("/room/:roomid", roomPOST)
+	router.DELETE("/room/:roomid", roomDELETE)
+	router.GET("/stream/:roomid", stream)
+
+	router.Run(":8080")
+}
+
+func stream(c *gin.Context) {
+	roomid := c.ParamValue("roomid")
+	listener := openListener(roomid)
+	defer closeListener(roomid, listener)
+
+	c.Stream(func(w io.Writer) bool {
+		c.SSEvent("message", <-listener)
+		return true
+	})
+}
+
+func roomGET(c *gin.Context) {
+	roomid := c.ParamValue("roomid")
+	userid := fmt.Sprint(rand.Int31())
+	c.HTML(200, "chat_room", gin.H{
+		"roomid": roomid,
+		"userid": userid,
+	})
+}
+
+func roomPOST(c *gin.Context) {
+	roomid := c.ParamValue("roomid")
+	userid := c.PostFormValue("user")
+	message := c.PostFormValue("message")
+	room(roomid).Submit(userid + ": " + message)
+
+	c.JSON(200, gin.H{
+		"status":  "success",
+		"message": message,
+	})
+}
+
+func roomDELETE(c *gin.Context) {
+	roomid := c.ParamValue("roomid")
+	deleteBroadcast(roomid)
+}

+ 33 - 0
examples/realtime-chat/rooms.go

@@ -0,0 +1,33 @@
+package main
+
+import "github.com/dustin/go-broadcast"
+
+var roomChannels = make(map[string]broadcast.Broadcaster)
+
+func openListener(roomid string) chan interface{} {
+	listener := make(chan interface{})
+	room(roomid).Register(listener)
+	return listener
+}
+
+func closeListener(roomid string, listener chan interface{}) {
+	room(roomid).Unregister(listener)
+	close(listener)
+}
+
+func deleteBroadcast(roomid string) {
+	b, ok := roomChannels[roomid]
+	if ok {
+		b.Close()
+		delete(roomChannels, roomid)
+	}
+}
+
+func room(roomid string) broadcast.Broadcaster {
+	b, ok := roomChannels[roomid]
+	if !ok {
+		b = broadcast.NewBroadcaster(10)
+		roomChannels[roomid] = b
+	}
+	return b
+}

+ 44 - 0
examples/realtime-chat/template.go

@@ -0,0 +1,44 @@
+package main
+
+import "html/template"
+
+var html = template.Must(template.New("chat_room").Parse(`
+<html> 
+<head> 
+    <title>{{.roomid}}</title>
+    <link rel="stylesheet" type="text/css" href="http://meyerweb.com/eric/tools/css/reset/reset.css"/>
+    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.js"></script> 
+    <script src="http://malsup.github.com/jquery.form.js"></script> 
+    <script> 
+        $('#message_form').focus();
+        $(document).ready(function() { 
+            // bind 'myForm' and provide a simple callback function 
+            $('#myForm').ajaxForm(function() {
+                $('#message_form').val('');
+                $('#message_form').focus();
+            });
+
+            if (!!window.EventSource) {
+                var source = new EventSource('/stream/{{.roomid}}');
+                source.addEventListener('message', function(e) {
+                    $('#messages').append(e.data + "</br>");
+                    $('html, body').animate({scrollTop:$(document).height()}, 'slow');
+
+                }, false);
+            } else {
+                alert("NOT SUPPORTED");
+            }
+        });
+    </script> 
+    </head>
+    <body>
+    <h1>Welcome to {{.roomid}} room</h1>
+    <div id="messages"></div>
+    <form id="myForm" action="/room/{{.roomid}}" method="post"> 
+    User: <input id="user_form" name="user" value="{{.userid}}"></input> 
+    Message: <input id="message_form" name="message"></input> 
+    <input type="submit" value="Submit" /> 
+    </form>
+</body>
+</html>
+`))

+ 208 - 82
gin.go

@@ -5,60 +5,81 @@
 package gin
 
 import (
-	"github.com/gin-gonic/gin/render"
-	"github.com/julienschmidt/httprouter"
 	"html/template"
-	"math"
+	"net"
 	"net/http"
+	"os"
 	"sync"
-)
 
-const (
-	AbortIndex            = math.MaxInt8 / 2
-	MIMEJSON              = "application/json"
-	MIMEHTML              = "text/html"
-	MIMEXML               = "application/xml"
-	MIMEXML2              = "text/xml"
-	MIMEPlain             = "text/plain"
-	MIMEPOSTForm          = "application/x-www-form-urlencoded"
-	MIMEMultipartPOSTForm = "multipart/form-data"
+	"github.com/gin-gonic/gin/binding"
+	"github.com/gin-gonic/gin/render"
 )
 
+const Version = "v1.0rc1"
+
+var default404Body = []byte("404 page not found")
+var default405Body = []byte("405 method not allowed")
+
 type (
-	HandlerFunc func(*Context)
+	HandlerFunc   func(*Context)
+	HandlersChain []HandlerFunc
 
 	// Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares.
 	Engine struct {
-		*RouterGroup
-		HTMLRender         render.Render
-		Default404Body     []byte
-		Default405Body     []byte
-		pool               sync.Pool
-		allNoRouteNoMethod []HandlerFunc
-		noRoute            []HandlerFunc
-		noMethod           []HandlerFunc
-		router             *httprouter.Router
+		RouterGroup
+		HTMLRender  render.HTMLRender
+		pool        sync.Pool
+		allNoRoute  HandlersChain
+		allNoMethod HandlersChain
+		noRoute     HandlersChain
+		noMethod    HandlersChain
+		trees       map[string]*node
+
+		// Enables automatic redirection if the current route can't be matched but a
+		// handler for the path with (without) the trailing slash exists.
+		// For example if /foo/ is requested but a route only exists for /foo, the
+		// client is redirected to /foo with http status code 301 for GET requests
+		// and 307 for all other request methods.
+		RedirectTrailingSlash bool
+
+		// If enabled, the router tries to fix the current request path, if no
+		// handle is registered for it.
+		// First superfluous path elements like ../ or // are removed.
+		// Afterwards the router does a case-insensitive lookup of the cleaned path.
+		// If a handle can be found for this route, the router makes a redirection
+		// to the corrected path with status code 301 for GET requests and 307 for
+		// all other request methods.
+		// For example /FOO and /..//Foo could be redirected to /foo.
+		// RedirectTrailingSlash is independent of this option.
+		RedirectFixedPath bool
+
+		// If enabled, the router checks if another method is allowed for the
+		// current route, if the current request can not be routed.
+		// If this is the case, the request is answered with 'Method Not Allowed'
+		// and HTTP status code 405.
+		// If no other Method is allowed, the request is delegated to the NotFound
+		// handler.
+		HandleMethodNotAllowed bool
 	}
 )
 
 // Returns a new blank Engine instance without any middleware attached.
 // The most basic configuration
 func New() *Engine {
-	engine := &Engine{}
-	engine.RouterGroup = &RouterGroup{
-		Handlers:     nil,
-		absolutePath: "/",
-		engine:       engine,
-	}
-	engine.router = httprouter.New()
-	engine.Default404Body = []byte("404 page not found")
-	engine.Default405Body = []byte("405 method not allowed")
-	engine.router.NotFound = engine.handle404
-	engine.router.MethodNotAllowed = engine.handle405
+	debugPrintWARNING()
+	engine := &Engine{
+		RouterGroup: RouterGroup{
+			Handlers: nil,
+			BasePath: "/",
+		},
+		RedirectTrailingSlash:  true,
+		RedirectFixedPath:      true,
+		HandleMethodNotAllowed: true,
+		trees: make(map[string]*node),
+	}
+	engine.RouterGroup.engine = engine
 	engine.pool.New = func() interface{} {
-		c := &Context{Engine: engine}
-		c.Writer = &c.writermem
-		return c
+		return engine.allocateContext()
 	}
 	return engine
 }
@@ -70,10 +91,13 @@ func Default() *Engine {
 	return engine
 }
 
+func (engine *Engine) allocateContext() (context *Context) {
+	return &Context{engine: engine}
+}
+
 func (engine *Engine) LoadHTMLGlob(pattern string) {
 	if IsDebugging() {
-		render.HTMLDebug.AddGlob(pattern)
-		engine.HTMLRender = render.HTMLDebug
+		engine.HTMLRender = render.HTMLDebug{Glob: pattern}
 	} else {
 		templ := template.Must(template.ParseGlob(pattern))
 		engine.SetHTMLTemplate(templ)
@@ -82,8 +106,7 @@ func (engine *Engine) LoadHTMLGlob(pattern string) {
 
 func (engine *Engine) LoadHTMLFiles(files ...string) {
 	if IsDebugging() {
-		render.HTMLDebug.AddFiles(files...)
-		engine.HTMLRender = render.HTMLDebug
+		engine.HTMLRender = render.HTMLDebug{Files: files}
 	} else {
 		templ := template.Must(template.ParseFiles(files...))
 		engine.SetHTMLTemplate(templ)
@@ -91,9 +114,7 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
 }
 
 func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
-	engine.HTMLRender = render.HTMLRender{
-		Template: templ,
-	}
+	engine.HTMLRender = render.HTMLProduction{Template: templ}
 }
 
 // Adds handlers for NoRoute. It return a 404 code by default.
@@ -114,60 +135,165 @@ func (engine *Engine) Use(middlewares ...HandlerFunc) {
 }
 
 func (engine *Engine) rebuild404Handlers() {
-	engine.allNoRouteNoMethod = engine.combineHandlers(engine.noRoute)
+	engine.allNoRoute = engine.combineHandlers(engine.noRoute)
 }
 
 func (engine *Engine) rebuild405Handlers() {
-	engine.allNoRouteNoMethod = engine.combineHandlers(engine.noMethod)
+	engine.allNoMethod = engine.combineHandlers(engine.noMethod)
 }
 
-func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) {
-	c := engine.createContext(w, req, nil, engine.allNoRouteNoMethod)
-	// set 404 by default, useful for logging
-	c.Writer.WriteHeader(404)
-	c.Next()
-	if !c.Writer.Written() {
-		if c.Writer.Status() == 404 {
-			c.Data(-1, MIMEPlain, engine.Default404Body)
-		} else {
-			c.Writer.WriteHeaderNow()
-		}
+func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
+	debugPrintRoute(method, path, handlers)
+
+	if path[0] != '/' {
+		panic("path must begin with '/'")
+	}
+	if method == "" {
+		panic("HTTP method can not be empty")
+	}
+	if len(handlers) == 0 {
+		panic("there must be at least one handler")
 	}
-	engine.reuseContext(c)
+
+	root := engine.trees[method]
+	if root == nil {
+		root = new(node)
+		engine.trees[method] = root
+	}
+	root.addRoute(path, handlers)
 }
 
-func (engine *Engine) handle405(w http.ResponseWriter, req *http.Request) {
-	c := engine.createContext(w, req, nil, engine.allNoRouteNoMethod)
-	// set 405 by default, useful for logging
-	c.Writer.WriteHeader(405)
-	c.Next()
-	if !c.Writer.Written() {
-		if c.Writer.Status() == 405 {
-			c.Data(-1, MIMEPlain, engine.Default405Body)
-		} else {
-			c.Writer.WriteHeaderNow()
-		}
+func (engine *Engine) Run(addr string) (err error) {
+	debugPrint("Listening and serving HTTP on %s\n", addr)
+	defer func() { debugPrintError(err) }()
+
+	err = http.ListenAndServe(addr, engine)
+	return
+}
+
+func (engine *Engine) RunTLS(addr string, cert string, key string) (err error) {
+	debugPrint("Listening and serving HTTPS on %s\n", addr)
+	defer func() { debugPrintError(err) }()
+
+	err = http.ListenAndServe(addr, engine)
+	return
+}
+
+func (engine *Engine) RunUnix(file string) (err error) {
+	debugPrint("Listening and serving HTTP on unix:/%s", file)
+	defer func() { debugPrintError(err) }()
+
+	os.Remove(file)
+	listener, err := net.Listen("unix", file)
+	if err != nil {
+		return
 	}
-	engine.reuseContext(c)
+	defer listener.Close()
+	err = http.Serve(listener, engine)
+	return
 }
 
 // ServeHTTP makes the router implement the http.Handler interface.
-func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
-	engine.router.ServeHTTP(writer, request)
+func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	c := engine.getcontext(w, req)
+	engine.handleHTTPRequest(c)
+	engine.putcontext(c)
 }
 
-func (engine *Engine) Run(addr string) error {
-	debugPrint("Listening and serving HTTP on %s\n", addr)
-	if err := http.ListenAndServe(addr, engine); err != nil {
-		return err
+func (engine *Engine) getcontext(w http.ResponseWriter, req *http.Request) *Context {
+	c := engine.pool.Get().(*Context)
+	c.writermem.reset(w)
+	c.Request = req
+	c.reset()
+	return c
+}
+
+func (engine *Engine) putcontext(c *Context) {
+	engine.pool.Put(c)
+}
+
+func (engine *Engine) handleHTTPRequest(context *Context) {
+	httpMethod := context.Request.Method
+	path := context.Request.URL.Path
+
+	// Find root of the tree for the given HTTP method
+	if root := engine.trees[httpMethod]; root != nil {
+		// Find route in tree
+		handlers, params, tsr := root.getValue(path, context.Params)
+		if handlers != nil {
+			context.handlers = handlers
+			context.Params = params
+			context.Next()
+			context.writermem.WriteHeaderNow()
+			return
+
+		} else if httpMethod != "CONNECT" && path != "/" {
+			if engine.serveAutoRedirect(context, root, tsr) {
+				return
+			}
+		}
 	}
-	return nil
+
+	if engine.HandleMethodNotAllowed {
+		for method, root := range engine.trees {
+			if method != httpMethod {
+				if handlers, _, _ := root.getValue(path, nil); handlers != nil {
+					context.handlers = engine.allNoMethod
+					serveError(context, 405, default405Body)
+					return
+				}
+			}
+		}
+	}
+	context.handlers = engine.allNoRoute
+	serveError(context, 404, default404Body)
 }
 
-func (engine *Engine) RunTLS(addr string, cert string, key string) error {
-	debugPrint("Listening and serving HTTPS on %s\n", addr)
-	if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil {
-		return err
+func (engine *Engine) serveAutoRedirect(c *Context, root *node, tsr bool) bool {
+	req := c.Request
+	path := req.URL.Path
+	code := 301 // Permanent redirect, request with GET method
+	if req.Method != "GET" {
+		code = 307
+	}
+
+	if tsr && engine.RedirectTrailingSlash {
+		if len(path) > 1 && path[len(path)-1] == '/' {
+			req.URL.Path = path[:len(path)-1]
+		} else {
+			req.URL.Path = path + "/"
+		}
+		debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String())
+		http.Redirect(c.Writer, req, req.URL.String(), code)
+		c.writermem.WriteHeaderNow()
+		return true
+	}
+
+	// Try to fix the request path
+	if engine.RedirectFixedPath {
+		fixedPath, found := root.findCaseInsensitivePath(
+			CleanPath(path),
+			engine.RedirectTrailingSlash,
+		)
+		if found {
+			req.URL.Path = string(fixedPath)
+			debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String())
+			http.Redirect(c.Writer, req, req.URL.String(), code)
+			c.writermem.WriteHeaderNow()
+			return true
+		}
+	}
+	return false
+}
+
+func serveError(c *Context, code int, defaultMessage []byte) {
+	c.writermem.status = code
+	c.Next()
+	if !c.Writer.Written() {
+		if c.Writer.Status() == code {
+			c.Data(-1, binding.MIMEPlain, defaultMessage)
+		} else {
+			c.Writer.WriteHeaderNow()
+		}
 	}
-	return nil
 }

+ 122 - 178
gin_test.go

@@ -5,202 +5,146 @@
 package gin
 
 import (
-	"io/ioutil"
-	"net/http"
-	"net/http/httptest"
-	"os"
-	"path"
-	"strings"
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
+//TODO
+// func (engine *Engine) LoadHTMLGlob(pattern string) {
+// func (engine *Engine) LoadHTMLFiles(files ...string) {
+// func (engine *Engine) Run(addr string) error {
+// func (engine *Engine) RunTLS(addr string, cert string, key string) error {
+
 func init() {
 	SetMode(TestMode)
 }
 
-func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
-	req, _ := http.NewRequest(method, path, nil)
-	w := httptest.NewRecorder()
-	r.ServeHTTP(w, req)
-	return w
-}
-
-// TestSingleRouteOK tests that POST route is correctly invoked.
-func testRouteOK(method string, t *testing.T) {
-	// SETUP
-	passed := false
-	r := New()
-	r.Handle(method, "/test", []HandlerFunc{func(c *Context) {
-		passed = true
-	}})
-
-	// RUN
-	w := PerformRequest(r, method, "/test")
-
-	// TEST
-	if passed == false {
-		t.Errorf(method + " route handler was not invoked.")
-	}
-	if w.Code != http.StatusOK {
-		t.Errorf("Status code should be %v, was %d", http.StatusOK, w.Code)
-	}
-}
-func TestRouterGroupRouteOK(t *testing.T) {
-	testRouteOK("POST", t)
-	testRouteOK("DELETE", t)
-	testRouteOK("PATCH", t)
-	testRouteOK("PUT", t)
-	testRouteOK("OPTIONS", t)
-	testRouteOK("HEAD", t)
+func TestCreateEngine(t *testing.T) {
+	router := New()
+	assert.Equal(t, "/", router.BasePath)
+	assert.Equal(t, router.engine, router)
+	assert.Empty(t, router.Handlers)
+	assert.True(t, router.RedirectTrailingSlash)
+	assert.True(t, router.RedirectFixedPath)
+	assert.True(t, router.HandleMethodNotAllowed)
+
+	assert.Panics(t, func() { router.addRoute("", "/", HandlersChain{func(_ *Context) {}}) })
+	assert.Panics(t, func() { router.addRoute("GET", "a", HandlersChain{func(_ *Context) {}}) })
+	assert.Panics(t, func() { router.addRoute("GET", "/", HandlersChain{}) })
 }
 
-// TestSingleRouteOK tests that POST route is correctly invoked.
-func testRouteNotOK(method string, t *testing.T) {
-	// SETUP
-	passed := false
-	r := New()
-	r.Handle(method, "/test_2", []HandlerFunc{func(c *Context) {
-		passed = true
-	}})
-
-	// RUN
-	w := PerformRequest(r, method, "/test")
-
-	// TEST
-	if passed == true {
-		t.Errorf(method + " route handler was invoked, when it should not")
-	}
-	if w.Code != http.StatusNotFound {
-		// If this fails, it's because httprouter needs to be updated to at least f78f58a0db
-		t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusNotFound, w.Code, w.HeaderMap.Get("Location"))
-	}
+func TestCreateDefaultRouter(t *testing.T) {
+	router := Default()
+	assert.Len(t, router.Handlers, 2)
 }
 
-// TestSingleRouteOK tests that POST route is correctly invoked.
-func TestRouteNotOK(t *testing.T) {
-	testRouteNotOK("POST", t)
-	testRouteNotOK("DELETE", t)
-	testRouteNotOK("PATCH", t)
-	testRouteNotOK("PUT", t)
-	testRouteNotOK("OPTIONS", t)
-	testRouteNotOK("HEAD", t)
+func TestNoRouteWithoutGlobalHandlers(t *testing.T) {
+	middleware0 := func(c *Context) {}
+	middleware1 := func(c *Context) {}
+
+	router := New()
+
+	router.NoRoute(middleware0)
+	assert.Nil(t, router.Handlers)
+	assert.Len(t, router.noRoute, 1)
+	assert.Len(t, router.allNoRoute, 1)
+	assert.Equal(t, router.noRoute[0], middleware0)
+	assert.Equal(t, router.allNoRoute[0], middleware0)
+
+	router.NoRoute(middleware1, middleware0)
+	assert.Len(t, router.noRoute, 2)
+	assert.Len(t, router.allNoRoute, 2)
+	assert.Equal(t, router.noRoute[0], middleware1)
+	assert.Equal(t, router.allNoRoute[0], middleware1)
+	assert.Equal(t, router.noRoute[1], middleware0)
+	assert.Equal(t, router.allNoRoute[1], middleware0)
 }
 
-// TestSingleRouteOK tests that POST route is correctly invoked.
-func testRouteNotOK2(method string, t *testing.T) {
-	// SETUP
-	passed := false
-	r := New()
-	var methodRoute string
-	if method == "POST" {
-		methodRoute = "GET"
-	} else {
-		methodRoute = "POST"
-	}
-	r.Handle(methodRoute, "/test", []HandlerFunc{func(c *Context) {
-		passed = true
-	}})
-
-	// RUN
-	w := PerformRequest(r, method, "/test")
-
-	// TEST
-	if passed == true {
-		t.Errorf(method + " route handler was invoked, when it should not")
-	}
-	if w.Code != http.StatusMethodNotAllowed {
-		t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusMethodNotAllowed, w.Code, w.HeaderMap.Get("Location"))
-	}
+func TestNoRouteWithGlobalHandlers(t *testing.T) {
+	middleware0 := func(c *Context) {}
+	middleware1 := func(c *Context) {}
+	middleware2 := func(c *Context) {}
+
+	router := New()
+	router.Use(middleware2)
+
+	router.NoRoute(middleware0)
+	assert.Len(t, router.allNoRoute, 2)
+	assert.Len(t, router.Handlers, 1)
+	assert.Len(t, router.noRoute, 1)
+
+	assert.Equal(t, router.Handlers[0], middleware2)
+	assert.Equal(t, router.noRoute[0], middleware0)
+	assert.Equal(t, router.allNoRoute[0], middleware2)
+	assert.Equal(t, router.allNoRoute[1], middleware0)
+
+	router.Use(middleware1)
+	assert.Len(t, router.allNoRoute, 3)
+	assert.Len(t, router.Handlers, 2)
+	assert.Len(t, router.noRoute, 1)
+
+	assert.Equal(t, router.Handlers[0], middleware2)
+	assert.Equal(t, router.Handlers[1], middleware1)
+	assert.Equal(t, router.noRoute[0], middleware0)
+	assert.Equal(t, router.allNoRoute[0], middleware2)
+	assert.Equal(t, router.allNoRoute[1], middleware1)
+	assert.Equal(t, router.allNoRoute[2], middleware0)
 }
 
-// TestSingleRouteOK tests that POST route is correctly invoked.
-func TestRouteNotOK2(t *testing.T) {
-	testRouteNotOK2("POST", t)
-	testRouteNotOK2("DELETE", t)
-	testRouteNotOK2("PATCH", t)
-	testRouteNotOK2("PUT", t)
-	testRouteNotOK2("OPTIONS", t)
-	testRouteNotOK2("HEAD", t)
+func TestNoMethodWithoutGlobalHandlers(t *testing.T) {
+	middleware0 := func(c *Context) {}
+	middleware1 := func(c *Context) {}
+
+	router := New()
+
+	router.NoMethod(middleware0)
+	assert.Empty(t, router.Handlers)
+	assert.Len(t, router.noMethod, 1)
+	assert.Len(t, router.allNoMethod, 1)
+	assert.Equal(t, router.noMethod[0], middleware0)
+	assert.Equal(t, router.allNoMethod[0], middleware0)
+
+	router.NoMethod(middleware1, middleware0)
+	assert.Len(t, router.noMethod, 2)
+	assert.Len(t, router.allNoMethod, 2)
+	assert.Equal(t, router.noMethod[0], middleware1)
+	assert.Equal(t, router.allNoMethod[0], middleware1)
+	assert.Equal(t, router.noMethod[1], middleware0)
+	assert.Equal(t, router.allNoMethod[1], middleware0)
 }
 
-// TestHandleStaticFile - ensure the static file handles properly
-func TestHandleStaticFile(t *testing.T) {
-	// SETUP file
-	testRoot, _ := os.Getwd()
-	f, err := ioutil.TempFile(testRoot, "")
-	if err != nil {
-		t.Error(err)
-	}
-	defer os.Remove(f.Name())
-	filePath := path.Join("/", path.Base(f.Name()))
-	f.WriteString("Gin Web Framework")
-	f.Close()
-
-	// SETUP gin
-	r := New()
-	r.Static("./", testRoot)
-
-	// RUN
-	w := PerformRequest(r, "GET", filePath)
-
-	// TEST
-	if w.Code != 200 {
-		t.Errorf("Response code should be 200, was: %d", w.Code)
-	}
-	if w.Body.String() != "Gin Web Framework" {
-		t.Errorf("Response should be test, was: %s", w.Body.String())
-	}
-	if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" {
-		t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
-	}
-}
+func TestRebuild404Handlers(t *testing.T) {
 
-// TestHandleStaticDir - ensure the root/sub dir handles properly
-func TestHandleStaticDir(t *testing.T) {
-	// SETUP
-	r := New()
-	r.Static("/", "./")
-
-	// RUN
-	w := PerformRequest(r, "GET", "/")
-
-	// TEST
-	bodyAsString := w.Body.String()
-	if w.Code != 200 {
-		t.Errorf("Response code should be 200, was: %d", w.Code)
-	}
-	if len(bodyAsString) == 0 {
-		t.Errorf("Got empty body instead of file tree")
-	}
-	if !strings.Contains(bodyAsString, "gin.go") {
-		t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString)
-	}
-	if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" {
-		t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
-	}
 }
 
-// TestHandleHeadToDir - ensure the root/sub dir handles properly
-func TestHandleHeadToDir(t *testing.T) {
-	// SETUP
-	r := New()
-	r.Static("/", "./")
-
-	// RUN
-	w := PerformRequest(r, "HEAD", "/")
-
-	// TEST
-	bodyAsString := w.Body.String()
-	if w.Code != 200 {
-		t.Errorf("Response code should be Ok, was: %s", w.Code)
-	}
-	if len(bodyAsString) == 0 {
-		t.Errorf("Got empty body instead of file tree")
-	}
-	if !strings.Contains(bodyAsString, "gin.go") {
-		t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString)
-	}
-	if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" {
-		t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
-	}
+func TestNoMethodWithGlobalHandlers(t *testing.T) {
+	middleware0 := func(c *Context) {}
+	middleware1 := func(c *Context) {}
+	middleware2 := func(c *Context) {}
+
+	router := New()
+	router.Use(middleware2)
+
+	router.NoMethod(middleware0)
+	assert.Len(t, router.allNoMethod, 2)
+	assert.Len(t, router.Handlers, 1)
+	assert.Len(t, router.noMethod, 1)
+
+	assert.Equal(t, router.Handlers[0], middleware2)
+	assert.Equal(t, router.noMethod[0], middleware0)
+	assert.Equal(t, router.allNoMethod[0], middleware2)
+	assert.Equal(t, router.allNoMethod[1], middleware0)
+
+	router.Use(middleware1)
+	assert.Len(t, router.allNoMethod, 3)
+	assert.Len(t, router.Handlers, 2)
+	assert.Len(t, router.noMethod, 1)
+
+	assert.Equal(t, router.Handlers[0], middleware2)
+	assert.Equal(t, router.Handlers[1], middleware1)
+	assert.Equal(t, router.noMethod[0], middleware0)
+	assert.Equal(t, router.allNoMethod[0], middleware2)
+	assert.Equal(t, router.allNoMethod[1], middleware1)
+	assert.Equal(t, router.allNoMethod[2], middleware0)
 }

+ 344 - 0
githubapi_test.go

@@ -0,0 +1,344 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+import (
+	"bytes"
+	"fmt"
+	"math/rand"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type route struct {
+	method string
+	path   string
+}
+
+// http://developer.github.com/v3/
+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"},
+}
+
+func TestGithubAPI(t *testing.T) {
+	router := New()
+
+	for _, route := range githubAPI {
+		router.Handle(route.method, route.path, func(c *Context) {
+			output := H{"status": "good"}
+			for _, param := range c.Params {
+				output[param.Key] = param.Value
+			}
+			c.JSON(200, output)
+		})
+	}
+
+	for _, route := range githubAPI {
+		path, values := exampleFromPath(route.path)
+		w := performRequest(router, route.method, path)
+
+		// TEST
+		assert.Contains(t, w.Body.String(), "\"status\":\"good\"")
+		for _, value := range values {
+			str := fmt.Sprintf("\"%s\":\"%s\"", value.Key, value.Value)
+			assert.Contains(t, w.Body.String(), str)
+		}
+	}
+}
+
+func exampleFromPath(path string) (string, Params) {
+	output := new(bytes.Buffer)
+	params := make(Params, 0, 6)
+	start := -1
+	for i, c := range path {
+		if c == ':' {
+			start = i + 1
+		}
+		if start >= 0 {
+			if c == '/' {
+				value := fmt.Sprint(rand.Intn(100000))
+				params = append(params, Param{
+					Key:   path[start:i],
+					Value: value,
+				})
+				output.WriteString(value)
+				output.WriteRune(c)
+				start = -1
+			}
+		} else {
+			output.WriteRune(c)
+		}
+	}
+	if start >= 0 {
+		value := fmt.Sprint(rand.Intn(100000))
+		params = append(params, Param{
+			Key:   path[start:len(path)],
+			Value: value,
+		})
+		output.WriteString(value)
+	}
+
+	return output.String(), params
+}

+ 28 - 24
logger.go

@@ -5,8 +5,8 @@
 package gin
 
 import (
-	"github.com/mattn/go-colorable"
-	"log"
+	"fmt"
+	"io"
 	"time"
 )
 
@@ -22,28 +22,31 @@ var (
 )
 
 func ErrorLogger() HandlerFunc {
-	return ErrorLoggerT(ErrorTypeAll)
+	return ErrorLoggerT(ErrorTypeAny)
 }
 
-func ErrorLoggerT(typ uint32) HandlerFunc {
+func ErrorLoggerT(typ int) HandlerFunc {
 	return func(c *Context) {
 		c.Next()
 
-		errs := c.Errors.ByType(typ)
-		if len(errs) > 0 {
-			// -1 status code = do not change current one
-			c.JSON(-1, c.Errors)
+		if !c.Writer.Written() {
+			json := c.Errors.ByType(typ).JSON()
+			if json != nil {
+				c.JSON(-1, json)
+			}
 		}
 	}
 }
 
 func Logger() HandlerFunc {
-	stdlogger := log.New(colorable.NewColorableStdout(), "", 0)
-	//errlogger := log.New(os.Stderr, "", 0)
+	return LoggerWithWriter(DefaultWriter)
+}
 
+func LoggerWithWriter(out io.Writer) HandlerFunc {
 	return func(c *Context) {
 		// Start timer
 		start := time.Now()
+		path := c.Request.URL.Path
 
 		// Process request
 		c.Next()
@@ -57,26 +60,27 @@ func Logger() HandlerFunc {
 		statusCode := c.Writer.Status()
 		statusColor := colorForStatus(statusCode)
 		methodColor := colorForMethod(method)
+		comment := c.Errors.String()
 
-		stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s |%s  %s %-7s %s\n%s",
+		fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %s |%s  %s %-7s %s\n%s",
 			end.Format("2006/01/02 - 15:04:05"),
 			statusColor, statusCode, reset,
 			latency,
 			clientIP,
 			methodColor, reset, method,
-			c.Request.URL.Path,
-			c.Errors.String(),
+			path,
+			comment,
 		)
 	}
 }
 
 func colorForStatus(code int) string {
 	switch {
-	case code >= 200 && code <= 299:
+	case code >= 200 && code < 300:
 		return green
-	case code >= 300 && code <= 399:
+	case code >= 300 && code < 400:
 		return white
-	case code >= 400 && code <= 499:
+	case code >= 400 && code < 500:
 		return yellow
 	default:
 		return red
@@ -84,20 +88,20 @@ func colorForStatus(code int) string {
 }
 
 func colorForMethod(method string) string {
-	switch {
-	case method == "GET":
+	switch method {
+	case "GET":
 		return blue
-	case method == "POST":
+	case "POST":
 		return cyan
-	case method == "PUT":
+	case "PUT":
 		return yellow
-	case method == "DELETE":
+	case "DELETE":
 		return red
-	case method == "PATCH":
+	case "PATCH":
 		return green
-	case method == "HEAD":
+	case "HEAD":
 		return magenta
-	case method == "OPTIONS":
+	case "OPTIONS":
 		return white
 	default:
 		return reset

+ 35 - 0
logger_test.go

@@ -0,0 +1,35 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+//TODO
+// func (engine *Engine) LoadHTMLGlob(pattern string) {
+// func (engine *Engine) LoadHTMLFiles(files ...string) {
+// func (engine *Engine) Run(addr string) error {
+// func (engine *Engine) RunTLS(addr string, cert string, key string) error {
+
+func init() {
+	SetMode(TestMode)
+}
+
+func TestLogger(t *testing.T) {
+	buffer := new(bytes.Buffer)
+	router := New()
+	router.Use(LoggerWithWriter(buffer))
+	router.GET("/example", func(c *Context) {})
+
+	performRequest(router, "GET", "/example")
+
+	assert.Contains(t, buffer.String(), "200")
+	assert.Contains(t, buffer.String(), "GET")
+	assert.Contains(t, buffer.String(), "/example")
+}

+ 217 - 0
middleware_test.go

@@ -0,0 +1,217 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+import (
+	"errors"
+
+	"testing"
+
+	"github.com/manucorporat/sse"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMiddlewareGeneralCase(t *testing.T) {
+	signature := ""
+	router := New()
+	router.Use(func(c *Context) {
+		signature += "A"
+		c.Next()
+		signature += "B"
+	})
+	router.Use(func(c *Context) {
+		signature += "C"
+	})
+	router.GET("/", func(c *Context) {
+		signature += "D"
+	})
+	router.NoRoute(func(c *Context) {
+		signature += " X "
+	})
+	router.NoMethod(func(c *Context) {
+		signature += " XX "
+	})
+	// RUN
+	w := performRequest(router, "GET", "/")
+
+	// TEST
+	assert.Equal(t, w.Code, 200)
+	assert.Equal(t, signature, "ACDB")
+}
+
+func TestMiddlewareNoRoute(t *testing.T) {
+	signature := ""
+	router := New()
+	router.Use(func(c *Context) {
+		signature += "A"
+		c.Next()
+		signature += "B"
+	})
+	router.Use(func(c *Context) {
+		signature += "C"
+		c.Next()
+		c.Next()
+		c.Next()
+		c.Next()
+		signature += "D"
+	})
+	router.NoRoute(func(c *Context) {
+		signature += "E"
+		c.Next()
+		signature += "F"
+	}, func(c *Context) {
+		signature += "G"
+		c.Next()
+		signature += "H"
+	})
+	router.NoMethod(func(c *Context) {
+		signature += " X "
+	})
+	// RUN
+	w := performRequest(router, "GET", "/")
+
+	// TEST
+	assert.Equal(t, w.Code, 404)
+	assert.Equal(t, signature, "ACEGHFDB")
+}
+
+func TestMiddlewareNoMethod(t *testing.T) {
+	signature := ""
+	router := New()
+	router.Use(func(c *Context) {
+		signature += "A"
+		c.Next()
+		signature += "B"
+	})
+	router.Use(func(c *Context) {
+		signature += "C"
+		c.Next()
+		signature += "D"
+	})
+	router.NoMethod(func(c *Context) {
+		signature += "E"
+		c.Next()
+		signature += "F"
+	}, func(c *Context) {
+		signature += "G"
+		c.Next()
+		signature += "H"
+	})
+	router.NoRoute(func(c *Context) {
+		signature += " X "
+	})
+	router.POST("/", func(c *Context) {
+		signature += " XX "
+	})
+	// RUN
+	w := performRequest(router, "GET", "/")
+
+	// TEST
+	assert.Equal(t, w.Code, 405)
+	assert.Equal(t, signature, "ACEGHFDB")
+}
+
+func TestMiddlewareAbort(t *testing.T) {
+	signature := ""
+	router := New()
+	router.Use(func(c *Context) {
+		signature += "A"
+	})
+	router.Use(func(c *Context) {
+		signature += "C"
+		c.AbortWithStatus(401)
+		c.Next()
+		signature += "D"
+	})
+	router.GET("/", func(c *Context) {
+		signature += " X "
+		c.Next()
+		signature += " XX "
+	})
+
+	// RUN
+	w := performRequest(router, "GET", "/")
+
+	// TEST
+	assert.Equal(t, w.Code, 401)
+	assert.Equal(t, signature, "ACD")
+}
+
+func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
+	signature := ""
+	router := New()
+	router.Use(func(c *Context) {
+		signature += "A"
+		c.Next()
+		c.AbortWithStatus(410)
+		signature += "B"
+
+	})
+	router.GET("/", func(c *Context) {
+		signature += "C"
+		c.Next()
+	})
+	// RUN
+	w := performRequest(router, "GET", "/")
+
+	// TEST
+	assert.Equal(t, w.Code, 410)
+	assert.Equal(t, signature, "ACB")
+}
+
+// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as
+// as well as Abort
+func TestMiddlewareFailHandlersChain(t *testing.T) {
+	// SETUP
+	signature := ""
+	router := New()
+	router.Use(func(context *Context) {
+		signature += "A"
+		context.AbortWithError(500, errors.New("foo"))
+	})
+	router.Use(func(context *Context) {
+		signature += "B"
+		context.Next()
+		signature += "C"
+	})
+	// RUN
+	w := performRequest(router, "GET", "/")
+
+	// TEST
+	assert.Equal(t, w.Code, 500)
+	assert.Equal(t, signature, "A")
+}
+
+func TestMiddlewareWrite(t *testing.T) {
+	router := New()
+	router.Use(func(c *Context) {
+		c.String(400, "hola\n")
+	})
+	router.Use(func(c *Context) {
+		c.XML(400, H{"foo": "bar"})
+	})
+	router.Use(func(c *Context) {
+		c.JSON(400, H{"foo": "bar"})
+	})
+	router.GET("/", func(c *Context) {
+		c.JSON(400, H{"foo": "bar"})
+	}, func(c *Context) {
+		c.Render(400, sse.Event{
+			Event: "test",
+			Data:  "message",
+		})
+	})
+
+	w := performRequest(router, "GET", "/")
+
+	assert.Equal(t, w.Code, 400)
+	assert.Equal(t, w.Body.String(), `hola
+<map><foo>bar</foo></map>{"foo":"bar"}
+{"foo":"bar"}
+event: test
+data: message
+
+`)
+}

+ 15 - 22
mode.go

@@ -5,11 +5,13 @@
 package gin
 
 import (
-	"fmt"
+	"io"
 	"os"
+
+	"github.com/mattn/go-colorable"
 )
 
-const GIN_MODE = "GIN_MODE"
+const ENV_GIN_MODE = "GIN_MODE"
 
 const (
 	DebugMode   string = "debug"
@@ -22,42 +24,33 @@ const (
 	testCode    = iota
 )
 
-var gin_mode int = debugCode
-var mode_name string = DebugMode
+var DefaultWriter io.Writer = colorable.NewColorableStdout()
+var ginMode int = debugCode
+var modeName string = DebugMode
 
 func init() {
-	value := os.Getenv(GIN_MODE)
-	if len(value) == 0 {
+	mode := os.Getenv(ENV_GIN_MODE)
+	if len(mode) == 0 {
 		SetMode(DebugMode)
 	} else {
-		SetMode(value)
+		SetMode(mode)
 	}
 }
 
 func SetMode(value string) {
 	switch value {
 	case DebugMode:
-		gin_mode = debugCode
+		ginMode = debugCode
 	case ReleaseMode:
-		gin_mode = releaseCode
+		ginMode = releaseCode
 	case TestMode:
-		gin_mode = testCode
+		ginMode = testCode
 	default:
 		panic("gin mode unknown: " + value)
 	}
-	mode_name = value
+	modeName = value
 }
 
 func Mode() string {
-	return mode_name
-}
-
-func IsDebugging() bool {
-	return gin_mode == debugCode
-}
-
-func debugPrint(format string, values ...interface{}) {
-	if IsDebugging() {
-		fmt.Printf("[GIN-debug] "+format, values...)
-	}
+	return modeName
 }

+ 31 - 0
mode_test.go

@@ -0,0 +1,31 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func init() {
+	SetMode(TestMode)
+}
+
+func TestSetMode(t *testing.T) {
+	SetMode(DebugMode)
+	assert.Equal(t, ginMode, debugCode)
+	assert.Equal(t, Mode(), DebugMode)
+
+	SetMode(ReleaseMode)
+	assert.Equal(t, ginMode, releaseCode)
+	assert.Equal(t, Mode(), ReleaseMode)
+
+	SetMode(TestMode)
+	assert.Equal(t, ginMode, testCode)
+	assert.Equal(t, Mode(), TestMode)
+
+	assert.Panics(t, func() { SetMode("unknown") })
+}

+ 123 - 0
path.go

@@ -0,0 +1,123 @@
+// Copyright 2013 Julien Schmidt. All rights reserved.
+// Based on the path package, Copyright 2009 The Go Authors.
+// Use of this source code is governed by a BSD-style license that can be found
+// in the LICENSE file.
+
+package gin
+
+// CleanPath is the URL version of path.Clean, it returns a canonical URL path
+// for p, eliminating . and .. elements.
+//
+// The following rules are applied iteratively until no further processing can
+// be done:
+//	1. Replace multiple slashes with a single slash.
+//	2. Eliminate each . path name element (the current directory).
+//	3. Eliminate each inner .. path name element (the parent directory)
+//	   along with the non-.. element that precedes it.
+//	4. Eliminate .. elements that begin a rooted path:
+//	   that is, replace "/.." by "/" at the beginning of a path.
+//
+// If the result of this process is an empty string, "/" is returned
+func CleanPath(p string) string {
+	// Turn empty string into "/"
+	if p == "" {
+		return "/"
+	}
+
+	n := len(p)
+	var buf []byte
+
+	// Invariants:
+	//      reading from path; r is index of next byte to process.
+	//      writing to buf; w is index of next byte to write.
+
+	// path must start with '/'
+	r := 1
+	w := 1
+
+	if p[0] != '/' {
+		r = 0
+		buf = make([]byte, n+1)
+		buf[0] = '/'
+	}
+
+	trailing := n > 2 && p[n-1] == '/'
+
+	// A bit more clunky without a 'lazybuf' like the path package, but the loop
+	// gets completely inlined (bufApp). So in contrast to the path package this
+	// loop has no expensive function calls (except 1x make)
+
+	for r < n {
+		switch {
+		case p[r] == '/':
+			// empty path element, trailing slash is added after the end
+			r++
+
+		case p[r] == '.' && r+1 == n:
+			trailing = true
+			r++
+
+		case p[r] == '.' && p[r+1] == '/':
+			// . element
+			r++
+
+		case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
+			// .. element: remove to last /
+			r += 2
+
+			if w > 1 {
+				// can backtrack
+				w--
+
+				if buf == nil {
+					for w > 1 && p[w] != '/' {
+						w--
+					}
+				} else {
+					for w > 1 && buf[w] != '/' {
+						w--
+					}
+				}
+			}
+
+		default:
+			// real path element.
+			// add slash if needed
+			if w > 1 {
+				bufApp(&buf, p, w, '/')
+				w++
+			}
+
+			// copy element
+			for r < n && p[r] != '/' {
+				bufApp(&buf, p, w, p[r])
+				w++
+				r++
+			}
+		}
+	}
+
+	// re-append trailing slash
+	if trailing && w > 1 {
+		bufApp(&buf, p, w, '/')
+		w++
+	}
+
+	if buf == nil {
+		return p[:w]
+	}
+	return string(buf[:w])
+}
+
+// internal helper to lazily create a buffer if necessary
+func bufApp(buf *[]byte, s string, w int, c byte) {
+	if *buf == nil {
+		if s[w] == c {
+			return
+		}
+
+		*buf = make([]byte, len(s))
+		copy(*buf, s[:w])
+	}
+	(*buf)[w] = c
+}

+ 88 - 0
path_test.go

@@ -0,0 +1,88 @@
+// Copyright 2013 Julien Schmidt. All rights reserved.
+// Based on the path package, Copyright 2009 The Go Authors.
+// Use of this source code is governed by a BSD-style license that can be found
+// in the LICENSE file.
+
+package gin
+
+import (
+	"runtime"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var cleanTests = []struct {
+	path, result string
+}{
+	// Already clean
+	{"/", "/"},
+	{"/abc", "/abc"},
+	{"/a/b/c", "/a/b/c"},
+	{"/abc/", "/abc/"},
+	{"/a/b/c/", "/a/b/c/"},
+
+	// missing root
+	{"", "/"},
+	{"abc", "/abc"},
+	{"abc/def", "/abc/def"},
+	{"a/b/c", "/a/b/c"},
+
+	// Remove doubled slash
+	{"//", "/"},
+	{"/abc//", "/abc/"},
+	{"/abc/def//", "/abc/def/"},
+	{"/a/b/c//", "/a/b/c/"},
+	{"/abc//def//ghi", "/abc/def/ghi"},
+	{"//abc", "/abc"},
+	{"///abc", "/abc"},
+	{"//abc//", "/abc/"},
+
+	// Remove . elements
+	{".", "/"},
+	{"./", "/"},
+	{"/abc/./def", "/abc/def"},
+	{"/./abc/def", "/abc/def"},
+	{"/abc/.", "/abc/"},
+
+	// Remove .. elements
+	{"..", "/"},
+	{"../", "/"},
+	{"../../", "/"},
+	{"../..", "/"},
+	{"../../abc", "/abc"},
+	{"/abc/def/ghi/../jkl", "/abc/def/jkl"},
+	{"/abc/def/../ghi/../jkl", "/abc/jkl"},
+	{"/abc/def/..", "/abc"},
+	{"/abc/def/../..", "/"},
+	{"/abc/def/../../..", "/"},
+	{"/abc/def/../../..", "/"},
+	{"/abc/def/../../../ghi/jkl/../../../mno", "/mno"},
+
+	// Combinations
+	{"abc/./../def", "/def"},
+	{"abc//./../def", "/def"},
+	{"abc/../../././../def", "/def"},
+}
+
+func TestPathClean(t *testing.T) {
+	for _, test := range cleanTests {
+		assert.Equal(t, CleanPath(test.path), test.result)
+		assert.Equal(t, CleanPath(test.result), test.result)
+	}
+}
+
+func TestPathCleanMallocs(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping malloc count in short mode")
+	}
+	if runtime.GOMAXPROCS(0) > 1 {
+		t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1")
+		return
+	}
+
+	for _, test := range cleanTests {
+		allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) })
+		assert.Equal(t, allocs, 0)
+	}
+}

+ 25 - 17
recovery.go

@@ -7,9 +7,9 @@ package gin
 import (
 	"bytes"
 	"fmt"
+	"io"
 	"io/ioutil"
 	"log"
-	"net/http"
 	"runtime"
 )
 
@@ -20,6 +20,30 @@ var (
 	slash     = []byte("/")
 )
 
+// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
+func Recovery() HandlerFunc {
+	return RecoveryWithWriter(DefaultWriter)
+}
+
+func RecoveryWithWriter(out io.Writer) HandlerFunc {
+	var logger *log.Logger
+	if out != nil {
+		logger = log.New(out, "", log.LstdFlags)
+	}
+	return func(c *Context) {
+		defer func() {
+			if err := recover(); err != nil {
+				if logger != nil {
+					stack := stack(3)
+					logger.Printf("Panic recovery -> %s\n%s\n", err, stack)
+				}
+				c.AbortWithStatus(500)
+			}
+		}()
+		c.Next()
+	}
+}
+
 // stack returns a nicely formated stack frame, skipping skip frames
 func stack(skip int) []byte {
 	buf := new(bytes.Buffer) // the returned data
@@ -80,19 +104,3 @@ func function(pc uintptr) []byte {
 	name = bytes.Replace(name, centerDot, dot, -1)
 	return name
 }
-
-// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
-// While Gin is in development mode, Recovery will also output the panic as HTML.
-func Recovery() HandlerFunc {
-	return func(c *Context) {
-		defer func() {
-			if err := recover(); err != nil {
-				stack := stack(3)
-				log.Printf("PANIC: %s\n%s", err, stack)
-				c.Writer.WriteHeader(http.StatusInternalServerError)
-			}
-		}()
-
-		c.Next()
-	}
-}

+ 16 - 30
recovery_test.go

@@ -6,51 +6,37 @@ package gin
 
 import (
 	"bytes"
-	"log"
-	"os"
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
 // TestPanicInHandler assert that panic has been recovered.
 func TestPanicInHandler(t *testing.T) {
-	// SETUP
-	log.SetOutput(bytes.NewBuffer(nil)) // Disable panic logs for testing
-	r := New()
-	r.Use(Recovery())
-	r.GET("/recovery", func(_ *Context) {
+	buffer := new(bytes.Buffer)
+	router := New()
+	router.Use(RecoveryWithWriter(buffer))
+	router.GET("/recovery", func(_ *Context) {
 		panic("Oupps, Houston, we have a problem")
 	})
-
 	// RUN
-	w := PerformRequest(r, "GET", "/recovery")
-
-	// restore logging
-	log.SetOutput(os.Stderr)
-
-	if w.Code != 500 {
-		t.Errorf("Response code should be Internal Server Error, was: %s", w.Code)
-	}
+	w := performRequest(router, "GET", "/recovery")
+	// TEST
+	assert.Equal(t, w.Code, 500)
+	assert.Contains(t, buffer.String(), "Panic recovery -> Oupps, Houston, we have a problem")
+	assert.Contains(t, buffer.String(), "TestPanicInHandler")
 }
 
 // TestPanicWithAbort assert that panic has been recovered even if context.Abort was used.
 func TestPanicWithAbort(t *testing.T) {
-	// SETUP
-	log.SetOutput(bytes.NewBuffer(nil))
-	r := New()
-	r.Use(Recovery())
-	r.GET("/recovery", func(c *Context) {
+	router := New()
+	router.Use(RecoveryWithWriter(nil))
+	router.GET("/recovery", func(c *Context) {
 		c.AbortWithStatus(400)
 		panic("Oupps, Houston, we have a problem")
 	})
-
 	// RUN
-	w := PerformRequest(r, "GET", "/recovery")
-
-	// restore logging
-	log.SetOutput(os.Stderr)
-
+	w := performRequest(router, "GET", "/recovery")
 	// TEST
-	if w.Code != 500 {
-		t.Errorf("Response code should be Bad request, was: %s", w.Code)
-	}
+	assert.Equal(t, w.Code, 500) // NOT SURE
 }

+ 16 - 0
render/data.go

@@ -0,0 +1,16 @@
+package render
+
+import "net/http"
+
+type Data struct {
+	ContentType string
+	Data        []byte
+}
+
+func (r Data) Write(w http.ResponseWriter) error {
+	if len(r.ContentType) > 0 {
+		w.Header().Set("Content-Type", r.ContentType)
+	}
+	w.Write(r.Data)
+	return nil
+}

+ 59 - 0
render/html.go

@@ -0,0 +1,59 @@
+package render
+
+import (
+	"html/template"
+	"net/http"
+)
+
+type (
+	HTMLRender interface {
+		Instance(string, interface{}) Render
+	}
+
+	HTMLProduction struct {
+		Template *template.Template
+	}
+
+	HTMLDebug struct {
+		Files []string
+		Glob  string
+	}
+
+	HTML struct {
+		Template *template.Template
+		Name     string
+		Data     interface{}
+	}
+)
+
+const htmlContentType = "text/html; charset=utf-8"
+
+func (r HTMLProduction) Instance(name string, data interface{}) Render {
+	return HTML{
+		Template: r.Template,
+		Name:     name,
+		Data:     data,
+	}
+}
+
+func (r HTMLDebug) Instance(name string, data interface{}) Render {
+	return HTML{
+		Template: r.loadTemplate(),
+		Name:     name,
+		Data:     data,
+	}
+}
+func (r HTMLDebug) loadTemplate() *template.Template {
+	if len(r.Files) > 0 {
+		return template.Must(template.ParseFiles(r.Files...))
+	}
+	if len(r.Glob) > 0 {
+		return template.Must(template.ParseGlob(r.Glob))
+	}
+	panic("the HTML debug render was created without files or glob pattern")
+}
+
+func (r HTML) Write(w http.ResponseWriter) error {
+	w.Header().Set("Content-Type", htmlContentType)
+	return r.Template.ExecuteTemplate(w, r.Name, r.Data)
+}

+ 33 - 0
render/json.go

@@ -0,0 +1,33 @@
+package render
+
+import (
+	"encoding/json"
+	"net/http"
+)
+
+type (
+	JSON struct {
+		Data interface{}
+	}
+
+	IndentedJSON struct {
+		Data interface{}
+	}
+)
+
+const jsonContentType = "application/json; charset=utf-8"
+
+func (r JSON) Write(w http.ResponseWriter) error {
+	w.Header().Set("Content-Type", jsonContentType)
+	return json.NewEncoder(w).Encode(r.Data)
+}
+
+func (r IndentedJSON) Write(w http.ResponseWriter) error {
+	w.Header().Set("Content-Type", jsonContentType)
+	jsonBytes, err := json.MarshalIndent(r.Data, "", "    ")
+	if err != nil {
+		return err
+	}
+	w.Write(jsonBytes)
+	return nil
+}

+ 20 - 0
render/redirect.go

@@ -0,0 +1,20 @@
+package render
+
+import (
+	"fmt"
+	"net/http"
+)
+
+type Redirect struct {
+	Code     int
+	Request  *http.Request
+	Location string
+}
+
+func (r Redirect) Write(w http.ResponseWriter) error {
+	if r.Code < 300 || r.Code > 308 {
+		panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code))
+	}
+	http.Redirect(w, r.Request, r.Location, r.Code)
+	return nil
+}

+ 13 - 130
render/render.go

@@ -4,137 +4,20 @@
 
 package render
 
-import (
-	"encoding/json"
-	"encoding/xml"
-	"fmt"
-	"html/template"
-	"net/http"
-)
-
-type (
-	Render interface {
-		Render(http.ResponseWriter, int, ...interface{}) error
-	}
-
-	// JSON binding
-	jsonRender struct{}
-
-	// XML binding
-	xmlRender struct{}
-
-	// Plain text
-	plainRender struct{}
+import "net/http"
 
-	// HTML Plain text
-	htmlPlainRender struct{}
-
-	// Redirects
-	redirectRender struct{}
-
-	// Redirects
-	htmlDebugRender struct {
-		files []string
-		globs []string
-	}
-
-	// form binding
-	HTMLRender struct {
-		Template *template.Template
-	}
-)
+type Render interface {
+	Write(http.ResponseWriter) error
+}
 
 var (
-	JSON      = jsonRender{}
-	XML       = xmlRender{}
-	Plain     = plainRender{}
-	HTMLPlain = htmlPlainRender{}
-	Redirect  = redirectRender{}
-	HTMLDebug = &htmlDebugRender{}
+	_ Render     = JSON{}
+	_ Render     = IndentedJSON{}
+	_ Render     = XML{}
+	_ Render     = String{}
+	_ Render     = Redirect{}
+	_ Render     = Data{}
+	_ Render     = HTML{}
+	_ HTMLRender = HTMLDebug{}
+	_ HTMLRender = HTMLProduction{}
 )
-
-func writeHeader(w http.ResponseWriter, code int, contentType string) {
-	w.Header().Set("Content-Type", contentType+"; charset=utf-8")
-	w.WriteHeader(code)
-}
-
-func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
-	writeHeader(w, code, "application/json")
-	encoder := json.NewEncoder(w)
-	return encoder.Encode(data[0])
-}
-
-func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
-	w.Header().Set("Location", data[0].(string))
-	w.WriteHeader(code)
-	return nil
-}
-
-func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
-	writeHeader(w, code, "application/xml")
-	encoder := xml.NewEncoder(w)
-	return encoder.Encode(data[0])
-}
-
-func (_ plainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
-	writeHeader(w, code, "text/plain")
-	format := data[0].(string)
-	args := data[1].([]interface{})
-	var err error
-	if len(args) > 0 {
-		_, err = w.Write([]byte(fmt.Sprintf(format, args...)))
-	} else {
-		_, err = w.Write([]byte(format))
-	}
-	return err
-}
-
-func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
-	writeHeader(w, code, "text/html")
-	format := data[0].(string)
-	args := data[1].([]interface{})
-	var err error
-	if len(args) > 0 {
-		_, err = w.Write([]byte(fmt.Sprintf(format, args...)))
-	} else {
-		_, err = w.Write([]byte(format))
-	}
-	return err
-}
-
-func (r *htmlDebugRender) AddGlob(pattern string) {
-	r.globs = append(r.globs, pattern)
-}
-
-func (r *htmlDebugRender) AddFiles(files ...string) {
-	r.files = append(r.files, files...)
-}
-
-func (r *htmlDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
-	writeHeader(w, code, "text/html")
-	file := data[0].(string)
-	obj := data[1]
-
-	t := template.New("")
-
-	if len(r.files) > 0 {
-		if _, err := t.ParseFiles(r.files...); err != nil {
-			return err
-		}
-	}
-
-	for _, glob := range r.globs {
-		if _, err := t.ParseGlob(glob); err != nil {
-			return err
-		}
-	}
-
-	return t.ExecuteTemplate(w, file, obj)
-}
-
-func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
-	writeHeader(w, code, "text/html")
-	file := data[0].(string)
-	obj := data[1]
-	return html.Template.ExecuteTemplate(w, file, obj)
-}

+ 130 - 0
render/render_test.go

@@ -0,0 +1,130 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package render
+
+import (
+	"encoding/xml"
+	"html/template"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// TODO unit tests
+// test errors
+
+func TestRenderJSON(t *testing.T) {
+	w := httptest.NewRecorder()
+	data := map[string]interface{}{
+		"foo": "bar",
+	}
+
+	err := (JSON{data}).Write(w)
+
+	assert.NoError(t, err)
+	assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n")
+	assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8")
+}
+
+func TestRenderIndentedJSON(t *testing.T) {
+	w := httptest.NewRecorder()
+	data := map[string]interface{}{
+		"foo": "bar",
+		"bar": "foo",
+	}
+
+	err := (IndentedJSON{data}).Write(w)
+
+	assert.NoError(t, err)
+	assert.Equal(t, w.Body.String(), "{\n    \"bar\": \"foo\",\n    \"foo\": \"bar\"\n}")
+	assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8")
+}
+
+type xmlmap map[string]interface{}
+
+// Allows type H to be used with xml.Marshal
+func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+	start.Name = xml.Name{
+		Space: "",
+		Local: "map",
+	}
+	if err := e.EncodeToken(start); err != nil {
+		return err
+	}
+	for key, value := range h {
+		elem := xml.StartElement{
+			Name: xml.Name{Space: "", Local: key},
+			Attr: []xml.Attr{},
+		}
+		if err := e.EncodeElement(value, elem); err != nil {
+			return err
+		}
+	}
+	if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
+		return err
+	}
+	return nil
+}
+
+func TestRenderXML(t *testing.T) {
+	w := httptest.NewRecorder()
+	data := xmlmap{
+		"foo": "bar",
+	}
+
+	err := (XML{data}).Write(w)
+
+	assert.NoError(t, err)
+	assert.Equal(t, w.Body.String(), "<map><foo>bar</foo></map>")
+	assert.Equal(t, w.Header().Get("Content-Type"), "application/xml; charset=utf-8")
+}
+
+func TestRenderRedirect(t *testing.T) {
+	// TODO
+}
+
+func TestRenderData(t *testing.T) {
+	w := httptest.NewRecorder()
+	data := []byte("#!PNG some raw data")
+
+	err := (Data{
+		ContentType: "image/png",
+		Data:        data,
+	}).Write(w)
+
+	assert.NoError(t, err)
+	assert.Equal(t, w.Body.String(), "#!PNG some raw data")
+	assert.Equal(t, w.Header().Get("Content-Type"), "image/png")
+}
+
+func TestRenderString(t *testing.T) {
+	w := httptest.NewRecorder()
+
+	err := (String{
+		Format: "hola %s %d",
+		Data:   []interface{}{"manu", 2},
+	}).Write(w)
+
+	assert.NoError(t, err)
+	assert.Equal(t, w.Body.String(), "hola manu 2")
+	assert.Equal(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8")
+}
+
+func TestRenderHTMLTemplate(t *testing.T) {
+	w := httptest.NewRecorder()
+	templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
+
+	htmlRender := HTMLProduction{Template: templ}
+	instance := htmlRender.Instance("t", map[string]interface{}{
+		"name": "alexandernyquist",
+	})
+
+	err := instance.Write(w)
+
+	assert.NoError(t, err)
+	assert.Equal(t, w.Body.String(), "Hello alexandernyquist")
+	assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8")
+}

+ 26 - 0
render/text.go

@@ -0,0 +1,26 @@
+package render
+
+import (
+	"fmt"
+	"net/http"
+)
+
+type String struct {
+	Format string
+	Data   []interface{}
+}
+
+const plainContentType = "text/plain; charset=utf-8"
+
+func (r String) Write(w http.ResponseWriter) error {
+	header := w.Header()
+	if _, exist := header["Content-Type"]; !exist {
+		header.Set("Content-Type", plainContentType)
+	}
+	if len(r.Data) > 0 {
+		fmt.Fprintf(w, r.Format, r.Data...)
+	} else {
+		w.Write([]byte(r.Format))
+	}
+	return nil
+}

+ 17 - 0
render/xml.go

@@ -0,0 +1,17 @@
+package render
+
+import (
+	"encoding/xml"
+	"net/http"
+)
+
+type XML struct {
+	Data interface{}
+}
+
+const xmlContentType = "application/xml; charset=utf-8"
+
+func (r XML) Write(w http.ResponseWriter) error {
+	w.Header().Set("Content-Type", xmlContentType)
+	return xml.NewEncoder(w).Encode(r.Data)
+}

+ 13 - 18
response_writer.go

@@ -6,14 +6,13 @@ package gin
 
 import (
 	"bufio"
-	"errors"
-	"log"
 	"net"
 	"net/http"
 )
 
 const (
-	NoWritten = -1
+	noWritten     = -1
+	defaultStatus = 200
 )
 
 type (
@@ -31,23 +30,23 @@ type (
 
 	responseWriter struct {
 		http.ResponseWriter
-		status int
 		size   int
+		status int
 	}
 )
 
 func (w *responseWriter) reset(writer http.ResponseWriter) {
 	w.ResponseWriter = writer
-	w.status = 200
-	w.size = NoWritten
+	w.size = noWritten
+	w.status = defaultStatus
 }
 
 func (w *responseWriter) WriteHeader(code int) {
-	if code > 0 {
-		w.status = code
+	if code > 0 && w.status != code {
 		if w.Written() {
-			log.Println("[GIN] WARNING. Headers were already written!")
+			debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)
 		}
+		w.status = code
 	}
 }
 
@@ -74,16 +73,15 @@ func (w *responseWriter) Size() int {
 }
 
 func (w *responseWriter) Written() bool {
-	return w.size != NoWritten
+	return w.size != noWritten
 }
 
 // Implements the http.Hijacker interface
 func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
-	hijacker, ok := w.ResponseWriter.(http.Hijacker)
-	if !ok {
-		return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface")
+	if w.size < 0 {
+		w.size = 0
 	}
-	return hijacker.Hijack()
+	return w.ResponseWriter.(http.Hijacker).Hijack()
 }
 
 // Implements the http.CloseNotify interface
@@ -93,8 +91,5 @@ func (w *responseWriter) CloseNotify() <-chan bool {
 
 // Implements the http.Flush interface
 func (w *responseWriter) Flush() {
-	flusher, ok := w.ResponseWriter.(http.Flusher)
-	if ok {
-		flusher.Flush()
-	}
+	w.ResponseWriter.(http.Flusher).Flush()
 }

+ 115 - 0
response_writer_test.go

@@ -0,0 +1,115 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// TODO
+// func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+// func (w *responseWriter) CloseNotify() <-chan bool {
+// func (w *responseWriter) Flush() {
+
+var _ ResponseWriter = &responseWriter{}
+var _ http.ResponseWriter = &responseWriter{}
+var _ http.ResponseWriter = ResponseWriter(&responseWriter{})
+var _ http.Hijacker = ResponseWriter(&responseWriter{})
+var _ http.Flusher = ResponseWriter(&responseWriter{})
+var _ http.CloseNotifier = ResponseWriter(&responseWriter{})
+
+func init() {
+	SetMode(TestMode)
+}
+
+func TestResponseWriterReset(t *testing.T) {
+	testWritter := httptest.NewRecorder()
+	writer := &responseWriter{}
+	var w ResponseWriter = writer
+
+	writer.reset(testWritter)
+	assert.Equal(t, writer.size, -1)
+	assert.Equal(t, writer.status, 200)
+	assert.Equal(t, writer.ResponseWriter, testWritter)
+	assert.Equal(t, w.Size(), -1)
+	assert.Equal(t, w.Status(), 200)
+	assert.False(t, w.Written())
+}
+
+func TestResponseWriterWriteHeader(t *testing.T) {
+	testWritter := httptest.NewRecorder()
+	writer := &responseWriter{}
+	writer.reset(testWritter)
+	w := ResponseWriter(writer)
+
+	w.WriteHeader(300)
+	assert.False(t, w.Written())
+	assert.Equal(t, w.Status(), 300)
+	assert.NotEqual(t, testWritter.Code, 300)
+
+	w.WriteHeader(-1)
+	assert.Equal(t, w.Status(), 300)
+}
+
+func TestResponseWriterWriteHeadersNow(t *testing.T) {
+	testWritter := httptest.NewRecorder()
+	writer := &responseWriter{}
+	writer.reset(testWritter)
+	w := ResponseWriter(writer)
+
+	w.WriteHeader(300)
+	w.WriteHeaderNow()
+
+	assert.True(t, w.Written())
+	assert.Equal(t, w.Size(), 0)
+	assert.Equal(t, testWritter.Code, 300)
+
+	writer.size = 10
+	w.WriteHeaderNow()
+	assert.Equal(t, w.Size(), 10)
+}
+
+func TestResponseWriterWrite(t *testing.T) {
+	testWritter := httptest.NewRecorder()
+	writer := &responseWriter{}
+	writer.reset(testWritter)
+	w := ResponseWriter(writer)
+
+	n, err := w.Write([]byte("hola"))
+	assert.Equal(t, n, 4)
+	assert.Equal(t, w.Size(), 4)
+	assert.Equal(t, w.Status(), 200)
+	assert.Equal(t, testWritter.Code, 200)
+	assert.Equal(t, testWritter.Body.String(), "hola")
+	assert.NoError(t, err)
+
+	n, err = w.Write([]byte(" adios"))
+	assert.Equal(t, n, 6)
+	assert.Equal(t, w.Size(), 10)
+	assert.Equal(t, testWritter.Body.String(), "hola adios")
+	assert.NoError(t, err)
+}
+
+func TestResponseWriterHijack(t *testing.T) {
+	testWritter := httptest.NewRecorder()
+	writer := &responseWriter{}
+	writer.reset(testWritter)
+	w := ResponseWriter(writer)
+
+	assert.Panics(t, func() {
+		w.Hijack()
+	})
+	assert.True(t, w.Written())
+
+	assert.Panics(t, func() {
+		w.CloseNotify()
+	})
+
+	w.Flush()
+}

+ 63 - 52
routergroup.go

@@ -5,17 +5,17 @@
 package gin
 
 import (
-	"github.com/julienschmidt/httprouter"
 	"net/http"
 	"path"
+	"strings"
 )
 
 // Used internally to configure router, a RouterGroup is associated with a prefix
 // and an array of handlers (middlewares)
 type RouterGroup struct {
-	Handlers     []HandlerFunc
-	absolutePath string
-	engine       *Engine
+	Handlers HandlersChain
+	BasePath string
+	engine   *Engine
 }
 
 // Adds middlewares to the group, see example code in github.
@@ -27,9 +27,9 @@ func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
 // For example, all the routes that use a common middlware for authorization could be grouped.
 func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
 	return &RouterGroup{
-		Handlers:     group.combineHandlers(handlers),
-		absolutePath: group.calculateAbsolutePath(relativePath),
-		engine:       group.engine,
+		Handlers: group.combineHandlers(handlers),
+		BasePath: group.calculateAbsolutePath(relativePath),
+		engine:   group.engine,
 	}
 }
 
@@ -43,66 +43,73 @@ func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *R
 // This function is intended for bulk loading and to allow the usage of less
 // frequently used, non-standardized or custom methods (e.g. for internal
 // communication with a proxy).
-func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []HandlerFunc) {
+func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) {
 	absolutePath := group.calculateAbsolutePath(relativePath)
 	handlers = group.combineHandlers(handlers)
-	if IsDebugging() {
-		nuHandlers := len(handlers)
-		handlerName := nameOfFunction(handlers[nuHandlers-1])
-		debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
-	}
+	group.engine.addRoute(httpMethod, absolutePath, handlers)
+}
 
-	group.engine.router.Handle(httpMethod, absolutePath, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
-		context := group.engine.createContext(w, req, params, handlers)
-		context.Next()
-		context.Writer.WriteHeaderNow()
-		group.engine.reuseContext(context)
-	})
+func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) {
+	group.handle(httpMethod, relativePath, handlers)
 }
 
 // POST is a shortcut for router.Handle("POST", path, handle)
 func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) {
-	group.Handle("POST", relativePath, handlers)
+	group.handle("POST", relativePath, handlers)
 }
 
 // GET is a shortcut for router.Handle("GET", path, handle)
 func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) {
-	group.Handle("GET", relativePath, handlers)
+	group.handle("GET", relativePath, handlers)
 }
 
 // DELETE is a shortcut for router.Handle("DELETE", path, handle)
 func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) {
-	group.Handle("DELETE", relativePath, handlers)
+	group.handle("DELETE", relativePath, handlers)
 }
 
 // PATCH is a shortcut for router.Handle("PATCH", path, handle)
 func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) {
-	group.Handle("PATCH", relativePath, handlers)
+	group.handle("PATCH", relativePath, handlers)
 }
 
 // PUT is a shortcut for router.Handle("PUT", path, handle)
 func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) {
-	group.Handle("PUT", relativePath, handlers)
+	group.handle("PUT", relativePath, handlers)
 }
 
 // OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle)
 func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) {
-	group.Handle("OPTIONS", relativePath, handlers)
+	group.handle("OPTIONS", relativePath, handlers)
 }
 
 // HEAD is a shortcut for router.Handle("HEAD", path, handle)
 func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) {
-	group.Handle("HEAD", relativePath, handlers)
+	group.handle("HEAD", relativePath, handlers)
 }
 
-// LINK is a shortcut for router.Handle("LINK", path, handle)
-func (group *RouterGroup) LINK(relativePath string, handlers ...HandlerFunc) {
-	group.Handle("LINK", relativePath, handlers)
+func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) {
+	// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE
+	group.handle("GET", relativePath, handlers)
+	group.handle("POST", relativePath, handlers)
+	group.handle("PUT", relativePath, handlers)
+	group.handle("PATCH", relativePath, handlers)
+	group.handle("HEAD", relativePath, handlers)
+	group.handle("OPTIONS", relativePath, handlers)
+	group.handle("DELETE", relativePath, handlers)
+	group.handle("CONNECT", relativePath, handlers)
+	group.handle("TRACE", relativePath, handlers)
 }
 
-// UNLINK is a shortcut for router.Handle("UNLINK", path, handle)
-func (group *RouterGroup) UNLINK(relativePath string, handlers ...HandlerFunc) {
-	group.Handle("UNLINK", relativePath, handlers)
+func (group *RouterGroup) StaticFile(relativePath, filepath string) {
+	if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
+		panic("URL parameters can not be used when serving a static file")
+	}
+	handler := func(c *Context) {
+		c.File(filepath)
+	}
+	group.GET(relativePath, handler)
+	group.HEAD(relativePath, handler)
 }
 
 // Static serves files from the given file system root.
@@ -112,37 +119,41 @@ func (group *RouterGroup) UNLINK(relativePath string, handlers ...HandlerFunc) {
 // use :
 //     router.Static("/static", "/var/www")
 func (group *RouterGroup) Static(relativePath, root string) {
-	absolutePath := group.calculateAbsolutePath(relativePath)
-	handler := group.createStaticHandler(absolutePath, root)
-	absolutePath = path.Join(absolutePath, "/*filepath")
+	group.StaticFS(relativePath, http.Dir(root), false)
+}
+
+func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem, listDirectory bool) {
+	if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
+		panic("URL parameters can not be used when serving a static folder")
+	}
+	handler := group.createStaticHandler(relativePath, fs, listDirectory)
+	relativePath = path.Join(relativePath, "/*filepath")
 
 	// Register GET and HEAD handlers
-	group.GET(absolutePath, handler)
-	group.HEAD(absolutePath, handler)
+	group.GET(relativePath, handler)
+	group.HEAD(relativePath, handler)
 }
 
-func (group *RouterGroup) createStaticHandler(absolutePath, root string) func(*Context) {
-	fileServer := http.StripPrefix(absolutePath, http.FileServer(http.Dir(root)))
+func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem, listDirectory bool) HandlerFunc {
+	absolutePath := group.calculateAbsolutePath(relativePath)
+	fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
 	return func(c *Context) {
+		if !listDirectory && lastChar(c.Request.URL.Path) == '/' {
+			http.NotFound(c.Writer, c.Request)
+			return
+		}
 		fileServer.ServeHTTP(c.Writer, c.Request)
 	}
 }
 
-func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc {
+func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
 	finalSize := len(group.Handlers) + len(handlers)
-	mergedHandlers := make([]HandlerFunc, 0, finalSize)
-	mergedHandlers = append(mergedHandlers, group.Handlers...)
-	return append(mergedHandlers, handlers...)
+	mergedHandlers := make(HandlersChain, finalSize)
+	copy(mergedHandlers, group.Handlers)
+	copy(mergedHandlers[len(group.Handlers):], handlers)
+	return mergedHandlers
 }
 
 func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
-	if len(relativePath) == 0 {
-		return group.absolutePath
-	}
-	absolutePath := path.Join(group.absolutePath, relativePath)
-	appendSlash := lastChar(relativePath) == '/' && lastChar(absolutePath) != '/'
-	if appendSlash {
-		return absolutePath + "/"
-	}
-	return absolutePath
+	return joinPaths(group.BasePath, relativePath)
 }

+ 111 - 0
routergroup_test.go

@@ -0,0 +1,111 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func init() {
+	SetMode(TestMode)
+}
+
+func TestRouterGroupBasic(t *testing.T) {
+	router := New()
+	group := router.Group("/hola", func(c *Context) {})
+	group.Use(func(c *Context) {})
+
+	assert.Len(t, group.Handlers, 2)
+	assert.Equal(t, group.BasePath, "/hola")
+	assert.Equal(t, group.engine, router)
+
+	group2 := group.Group("manu")
+	group2.Use(func(c *Context) {}, func(c *Context) {})
+
+	assert.Len(t, group2.Handlers, 4)
+	assert.Equal(t, group2.BasePath, "/hola/manu")
+	assert.Equal(t, group2.engine, router)
+}
+
+func TestRouterGroupBasicHandle(t *testing.T) {
+	performRequestInGroup(t, "GET")
+	performRequestInGroup(t, "POST")
+	performRequestInGroup(t, "PUT")
+	performRequestInGroup(t, "PATCH")
+	performRequestInGroup(t, "DELETE")
+	performRequestInGroup(t, "HEAD")
+	performRequestInGroup(t, "OPTIONS")
+}
+
+func performRequestInGroup(t *testing.T, method string) {
+	router := New()
+	v1 := router.Group("v1", func(c *Context) {})
+	assert.Equal(t, v1.BasePath, "/v1")
+
+	login := v1.Group("/login/", func(c *Context) {}, func(c *Context) {})
+	assert.Equal(t, login.BasePath, "/v1/login/")
+
+	handler := func(c *Context) {
+		c.String(400, "the method was %s and index %d", c.Request.Method, c.index)
+	}
+
+	switch method {
+	case "GET":
+		v1.GET("/test", handler)
+		login.GET("/test", handler)
+	case "POST":
+		v1.POST("/test", handler)
+		login.POST("/test", handler)
+	case "PUT":
+		v1.PUT("/test", handler)
+		login.PUT("/test", handler)
+	case "PATCH":
+		v1.PATCH("/test", handler)
+		login.PATCH("/test", handler)
+	case "DELETE":
+		v1.DELETE("/test", handler)
+		login.DELETE("/test", handler)
+	case "HEAD":
+		v1.HEAD("/test", handler)
+		login.HEAD("/test", handler)
+	case "OPTIONS":
+		v1.OPTIONS("/test", handler)
+		login.OPTIONS("/test", handler)
+	default:
+		panic("unknown method")
+	}
+
+	w := performRequest(router, method, "/v1/login/test")
+	assert.Equal(t, w.Code, 400)
+	assert.Equal(t, w.Body.String(), "the method was "+method+" and index 3")
+
+	w = performRequest(router, method, "/v1/test")
+	assert.Equal(t, w.Code, 400)
+	assert.Equal(t, w.Body.String(), "the method was "+method+" and index 1")
+}
+
+func TestRouterGroupInvalidStatic(t *testing.T) {
+	router := New()
+	assert.Panics(t, func() {
+		router.Static("/path/:param", "/")
+	})
+
+	assert.Panics(t, func() {
+		router.Static("/path/*param", "/")
+	})
+}
+
+func TestRouterGroupInvalidStaticFile(t *testing.T) {
+	router := New()
+	assert.Panics(t, func() {
+		router.StaticFile("/path/:param", "favicon.ico")
+	})
+
+	assert.Panics(t, func() {
+		router.StaticFile("/path/*param", "favicon.ico")
+	})
+}

+ 286 - 0
routes_test.go

@@ -0,0 +1,286 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"path"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func performRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
+	req, _ := http.NewRequest(method, path, nil)
+	w := httptest.NewRecorder()
+	r.ServeHTTP(w, req)
+	return w
+}
+
+func testRouteOK(method string, t *testing.T) {
+	passed := false
+	passedAny := false
+	r := New()
+	r.Any("/test2", func(c *Context) {
+		passedAny = true
+	})
+	r.Handle(method, "/test", func(c *Context) {
+		passed = true
+	})
+
+	w := performRequest(r, method, "/test")
+	assert.True(t, passed)
+	assert.Equal(t, w.Code, http.StatusOK)
+
+	performRequest(r, method, "/test2")
+	assert.True(t, passedAny)
+
+}
+
+// TestSingleRouteOK tests that POST route is correctly invoked.
+func testRouteNotOK(method string, t *testing.T) {
+	passed := false
+	router := New()
+	router.Handle(method, "/test_2", func(c *Context) {
+		passed = true
+	})
+
+	w := performRequest(router, method, "/test")
+
+	assert.False(t, passed)
+	assert.Equal(t, w.Code, http.StatusNotFound)
+}
+
+// TestSingleRouteOK tests that POST route is correctly invoked.
+func testRouteNotOK2(method string, t *testing.T) {
+	passed := false
+	router := New()
+	var methodRoute string
+	if method == "POST" {
+		methodRoute = "GET"
+	} else {
+		methodRoute = "POST"
+	}
+	router.Handle(methodRoute, "/test", func(c *Context) {
+		passed = true
+	})
+
+	w := performRequest(router, method, "/test")
+
+	assert.False(t, passed)
+	assert.Equal(t, w.Code, http.StatusMethodNotAllowed)
+}
+
+func TestRouterGroupRouteOK(t *testing.T) {
+	testRouteOK("GET", t)
+	testRouteOK("POST", t)
+	testRouteOK("PUT", t)
+	testRouteOK("PATCH", t)
+	testRouteOK("HEAD", t)
+	testRouteOK("OPTIONS", t)
+	testRouteOK("DELETE", t)
+	testRouteOK("CONNECT", t)
+	testRouteOK("TRACE", t)
+}
+
+// TestSingleRouteOK tests that POST route is correctly invoked.
+func TestRouteNotOK(t *testing.T) {
+	testRouteNotOK("GET", t)
+	testRouteNotOK("POST", t)
+	testRouteNotOK("PUT", t)
+	testRouteNotOK("PATCH", t)
+	testRouteNotOK("HEAD", t)
+	testRouteNotOK("OPTIONS", t)
+	testRouteNotOK("DELETE", t)
+	testRouteNotOK("CONNECT", t)
+	testRouteNotOK("TRACE", t)
+}
+
+// TestSingleRouteOK tests that POST route is correctly invoked.
+func TestRouteNotOK2(t *testing.T) {
+	testRouteNotOK2("GET", t)
+	testRouteNotOK2("POST", t)
+	testRouteNotOK2("PUT", t)
+	testRouteNotOK2("PATCH", t)
+	testRouteNotOK2("HEAD", t)
+	testRouteNotOK2("OPTIONS", t)
+	testRouteNotOK2("DELETE", t)
+	testRouteNotOK2("CONNECT", t)
+	testRouteNotOK2("TRACE", t)
+}
+
+// TestContextParamsGet tests that a parameter can be parsed from the URL.
+func TestRouteParamsByName(t *testing.T) {
+	name := ""
+	lastName := ""
+	wild := ""
+	router := New()
+	router.GET("/test/:name/:last_name/*wild", func(c *Context) {
+		name = c.Params.ByName("name")
+		lastName = c.Params.ByName("last_name")
+		wild = c.Params.ByName("wild")
+
+		assert.Equal(t, name, c.ParamValue("name"))
+		assert.Equal(t, lastName, c.ParamValue("last_name"))
+
+		assert.Equal(t, name, c.DefaultParamValue("name", "nothing"))
+		assert.Equal(t, lastName, c.DefaultParamValue("last_name", "nothing"))
+		assert.Equal(t, c.DefaultParamValue("noKey", "default"), "default")
+	})
+
+	w := performRequest(router, "GET", "/test/john/smith/is/super/great")
+
+	assert.Equal(t, w.Code, 200)
+	assert.Equal(t, name, "john")
+	assert.Equal(t, lastName, "smith")
+	assert.Equal(t, wild, "/is/super/great")
+}
+
+// TestHandleStaticFile - ensure the static file handles properly
+func TestRouteStaticFile(t *testing.T) {
+	// SETUP file
+	testRoot, _ := os.Getwd()
+	f, err := ioutil.TempFile(testRoot, "")
+	if err != nil {
+		t.Error(err)
+	}
+	defer os.Remove(f.Name())
+	f.WriteString("Gin Web Framework")
+	f.Close()
+
+	dir, filename := path.Split(f.Name())
+
+	// SETUP gin
+	router := New()
+	router.Static("/using_static", dir)
+	router.StaticFile("/result", f.Name())
+
+	w := performRequest(router, "GET", "/using_static/"+filename)
+	w2 := performRequest(router, "GET", "/result")
+
+	assert.Equal(t, w, w2)
+	assert.Equal(t, w.Code, 200)
+	assert.Equal(t, w.Body.String(), "Gin Web Framework")
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
+
+	w3 := performRequest(router, "HEAD", "/using_static/"+filename)
+	w4 := performRequest(router, "HEAD", "/result")
+
+	assert.Equal(t, w3, w4)
+	assert.Equal(t, w3.Code, 200)
+}
+
+// TestHandleStaticDir - ensure the root/sub dir handles properly
+func TestRouteStaticListingDir(t *testing.T) {
+	router := New()
+	router.StaticFS("/", http.Dir("./"), true)
+
+	w := performRequest(router, "GET", "/")
+
+	assert.Equal(t, w.Code, 200)
+	assert.Contains(t, w.Body.String(), "gin.go")
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
+}
+
+// TestHandleHeadToDir - ensure the root/sub dir handles properly
+func TestRouteStaticNoListing(t *testing.T) {
+	router := New()
+	router.Static("/", "./")
+
+	w := performRequest(router, "GET", "/")
+
+	assert.Equal(t, w.Code, 404)
+	assert.NotContains(t, w.Body.String(), "gin.go")
+}
+
+func TestRouterMiddlewareAndStatic(t *testing.T) {
+	router := New()
+	static := router.Group("/", func(c *Context) {
+		c.Writer.Header().Add("Last-Modified", "Mon, 02 Jan 2006 15:04:05 MST")
+		c.Writer.Header().Add("Expires", "Mon, 02 Jan 2006 15:04:05 MST")
+		c.Writer.Header().Add("X-GIN", "Gin Framework")
+	})
+	static.Static("/", "./")
+
+	w := performRequest(router, "GET", "/gin.go")
+
+	assert.Equal(t, w.Code, 200)
+	assert.Contains(t, w.Body.String(), "package gin")
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
+	assert.NotEqual(t, w.HeaderMap.Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
+	assert.Equal(t, w.HeaderMap.Get("Expires"), "Mon, 02 Jan 2006 15:04:05 MST")
+	assert.Equal(t, w.HeaderMap.Get("x-GIN"), "Gin Framework")
+}
+
+func TestRouteNotAllowed(t *testing.T) {
+	router := New()
+
+	router.POST("/path", func(c *Context) {})
+	w := performRequest(router, "GET", "/path")
+	assert.Equal(t, w.Code, http.StatusMethodNotAllowed)
+
+	router.NoMethod(func(c *Context) {
+		c.String(http.StatusTeapot, "responseText")
+	})
+	w = performRequest(router, "GET", "/path")
+	assert.Equal(t, w.Body.String(), "responseText")
+	assert.Equal(t, w.Code, http.StatusTeapot)
+}
+
+func TestRouterNotFound(t *testing.T) {
+	router := New()
+	router.GET("/path", func(c *Context) {})
+	router.GET("/dir/", func(c *Context) {})
+	router.GET("/", func(c *Context) {})
+
+	testRoutes := []struct {
+		route  string
+		code   int
+		header string
+	}{
+		{"/path/", 301, "map[Location:[/path]]"},   // TSR -/
+		{"/dir", 301, "map[Location:[/dir/]]"},     // TSR +/
+		{"", 301, "map[Location:[/]]"},             // TSR +/
+		{"/PATH", 301, "map[Location:[/path]]"},    // Fixed Case
+		{"/DIR/", 301, "map[Location:[/dir/]]"},    // Fixed Case
+		{"/PATH/", 301, "map[Location:[/path]]"},   // Fixed Case -/
+		{"/DIR", 301, "map[Location:[/dir/]]"},     // Fixed Case +/
+		{"/../path", 301, "map[Location:[/path]]"}, // CleanPath
+		{"/nope", 404, ""},                         // NotFound
+	}
+	for _, tr := range testRoutes {
+		w := performRequest(router, "GET", tr.route)
+		assert.Equal(t, w.Code, tr.code)
+		if w.Code != 404 {
+			assert.Equal(t, fmt.Sprint(w.Header()), tr.header)
+		}
+	}
+
+	// Test custom not found handler
+	var notFound bool
+	router.NoRoute(func(c *Context) {
+		c.AbortWithStatus(404)
+		notFound = true
+	})
+	w := performRequest(router, "GET", "/nope")
+	assert.Equal(t, w.Code, 404)
+	assert.True(t, notFound)
+
+	// Test other method than GET (want 307 instead of 301)
+	router.PATCH("/path", func(c *Context) {})
+	w = performRequest(router, "PATCH", "/path/")
+	assert.Equal(t, w.Code, 307)
+	assert.Equal(t, fmt.Sprint(w.Header()), "map[Location:[/path]]")
+
+	// Test special case where no node for the prefix "/" exists
+	router = New()
+	router.GET("/a", func(c *Context) {})
+	w = performRequest(router, "GET", "/")
+	assert.Equal(t, w.Code, 404)
+}

+ 553 - 0
tree.go

@@ -0,0 +1,553 @@
+// Copyright 2013 Julien Schmidt. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be found
+// in the LICENSE file.
+
+package gin
+
+import (
+	"strings"
+	"unicode"
+)
+
+func min(a, b int) int {
+	if a <= b {
+		return a
+	}
+	return b
+}
+
+func countParams(path string) uint8 {
+	var n uint
+	for i := 0; i < len(path); i++ {
+		if path[i] != ':' && path[i] != '*' {
+			continue
+		}
+		n++
+	}
+	if n >= 255 {
+		return 255
+	}
+	return uint8(n)
+}
+
+type nodeType uint8
+
+const (
+	static   nodeType = 0
+	param    nodeType = 1
+	catchAll nodeType = 2
+)
+
+type node struct {
+	path      string
+	wildChild bool
+	nType     nodeType
+	maxParams uint8
+	indices   string
+	children  []*node
+	handlers  HandlersChain
+	priority  uint32
+}
+
+// increments priority of the given child and reorders if necessary
+func (n *node) incrementChildPrio(pos int) int {
+	n.children[pos].priority++
+	prio := n.children[pos].priority
+
+	// adjust position (move to front)
+	newPos := pos
+	for newPos > 0 && n.children[newPos-1].priority < prio {
+		// swap node positions
+		tmpN := n.children[newPos-1]
+		n.children[newPos-1] = n.children[newPos]
+		n.children[newPos] = tmpN
+
+		newPos--
+	}
+
+	// build new index char string
+	if newPos != pos {
+		n.indices = n.indices[:newPos] + // unchanged prefix, might be empty
+			n.indices[pos:pos+1] + // the index char we move
+			n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos'
+	}
+
+	return newPos
+}
+
+// addRoute adds a node with the given handle to the path.
+// Not concurrency-safe!
+func (n *node) addRoute(path string, handlers HandlersChain) {
+	fullPath := path
+	n.priority++
+	numParams := countParams(path)
+
+	// non-empty tree
+	if len(n.path) > 0 || len(n.children) > 0 {
+	walk:
+		for {
+			// Update maxParams of the current node
+			if numParams > n.maxParams {
+				n.maxParams = numParams
+			}
+
+			// Find the longest common prefix.
+			// This also implies that the common prefix contains no ':' or '*'
+			// since the existing key can't contain those chars.
+			i := 0
+			max := min(len(path), len(n.path))
+			for i < max && path[i] == n.path[i] {
+				i++
+			}
+
+			// Split edge
+			if i < len(n.path) {
+				child := node{
+					path:      n.path[i:],
+					wildChild: n.wildChild,
+					indices:   n.indices,
+					children:  n.children,
+					handlers:  n.handlers,
+					priority:  n.priority - 1,
+				}
+
+				// Update maxParams (max of all children)
+				for i := range child.children {
+					if child.children[i].maxParams > child.maxParams {
+						child.maxParams = child.children[i].maxParams
+					}
+				}
+
+				n.children = []*node{&child}
+				// []byte for proper unicode char conversion, see #65
+				n.indices = string([]byte{n.path[i]})
+				n.path = path[:i]
+				n.handlers = nil
+				n.wildChild = false
+			}
+
+			// Make new node a child of this node
+			if i < len(path) {
+				path = path[i:]
+
+				if n.wildChild {
+					n = n.children[0]
+					n.priority++
+
+					// Update maxParams of the child node
+					if numParams > n.maxParams {
+						n.maxParams = numParams
+					}
+					numParams--
+
+					// Check if the wildcard matches
+					if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
+						// check for longer wildcard, e.g. :name and :names
+						if len(n.path) >= len(path) || path[len(n.path)] == '/' {
+							continue walk
+						}
+					}
+
+					panic("path segment '" + path +
+						"' conflicts with existing wildcard '" + n.path +
+						"' in path '" + fullPath + "'")
+				}
+
+				c := path[0]
+
+				// slash after param
+				if n.nType == param && c == '/' && len(n.children) == 1 {
+					n = n.children[0]
+					n.priority++
+					continue walk
+				}
+
+				// Check if a child with the next path byte exists
+				for i := 0; i < len(n.indices); i++ {
+					if c == n.indices[i] {
+						i = n.incrementChildPrio(i)
+						n = n.children[i]
+						continue walk
+					}
+				}
+
+				// Otherwise insert it
+				if c != ':' && c != '*' {
+					// []byte for proper unicode char conversion, see #65
+					n.indices += string([]byte{c})
+					child := &node{
+						maxParams: numParams,
+					}
+					n.children = append(n.children, child)
+					n.incrementChildPrio(len(n.indices) - 1)
+					n = child
+				}
+				n.insertChild(numParams, path, fullPath, handlers)
+				return
+
+			} else if i == len(path) { // Make node a (in-path) leaf
+				if n.handlers != nil {
+					panic("handlers are already registered for path ''" + fullPath + "'")
+				}
+				n.handlers = handlers
+			}
+			return
+		}
+	} else { // Empty tree
+		n.insertChild(numParams, path, fullPath, handlers)
+	}
+}
+
+func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
+	var offset int // already handled bytes of the path
+
+	// find prefix until first wildcard (beginning with ':'' or '*'')
+	for i, max := 0, len(path); numParams > 0; i++ {
+		c := path[i]
+		if c != ':' && c != '*' {
+			continue
+		}
+
+		// find wildcard end (either '/' or path end)
+		end := i + 1
+		for end < max && path[end] != '/' {
+			switch path[end] {
+			// the wildcard name must not contain ':' and '*'
+			case ':', '*':
+				panic("only one wildcard per path segment is allowed, has: '" +
+					path[i:] + "' in path '" + fullPath + "'")
+			default:
+				end++
+			}
+		}
+
+		// check if this Node existing children which would be
+		// unreachable if we insert the wildcard here
+		if len(n.children) > 0 {
+			panic("wildcard route '" + path[i:end] +
+				"' conflicts with existing children in path '" + fullPath + "'")
+		}
+
+		// check if the wildcard has a name
+		if end-i < 2 {
+			panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
+		}
+
+		if c == ':' { // param
+			// split path at the beginning of the wildcard
+			if i > 0 {
+				n.path = path[offset:i]
+				offset = i
+			}
+
+			child := &node{
+				nType:     param,
+				maxParams: numParams,
+			}
+			n.children = []*node{child}
+			n.wildChild = true
+			n = child
+			n.priority++
+			numParams--
+
+			// if the path doesn't end with the wildcard, then there
+			// will be another non-wildcard subpath starting with '/'
+			if end < max {
+				n.path = path[offset:end]
+				offset = end
+
+				child := &node{
+					maxParams: numParams,
+					priority:  1,
+				}
+				n.children = []*node{child}
+				n = child
+			}
+
+		} else { // catchAll
+			if end != max || numParams > 1 {
+				panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
+			}
+
+			if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
+				panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
+			}
+
+			// currently fixed width 1 for '/'
+			i--
+			if path[i] != '/' {
+				panic("no / before catch-all in path '" + fullPath + "'")
+			}
+
+			n.path = path[offset:i]
+
+			// first node: catchAll node with empty path
+			child := &node{
+				wildChild: true,
+				nType:     catchAll,
+				maxParams: 1,
+			}
+			n.children = []*node{child}
+			n.indices = string(path[i])
+			n = child
+			n.priority++
+
+			// second node: node holding the variable
+			child = &node{
+				path:      path[i:],
+				nType:     catchAll,
+				maxParams: 1,
+				handlers:  handlers,
+				priority:  1,
+			}
+			n.children = []*node{child}
+
+			return
+		}
+	}
+
+	// insert remaining path part and handle to the leaf
+	n.path = path[offset:]
+	n.handlers = handlers
+}
+
+// Returns the handle registered with the given path (key). The values of
+// wildcards are saved to a map.
+// If no handle can be found, a TSR (trailing slash redirect) recommendation is
+// made if a handle exists with an extra (without the) trailing slash for the
+// given path.
+func (n *node) getValue(path string, po Params) (handlers HandlersChain, p Params, tsr bool) {
+	p = po
+walk: // Outer loop for walking the tree
+	for {
+		if len(path) > len(n.path) {
+			if path[:len(n.path)] == n.path {
+				path = path[len(n.path):]
+				// If this node does not have a wildcard (param or catchAll)
+				// child,  we can just look up the next child node and continue
+				// to walk down the tree
+				if !n.wildChild {
+					c := path[0]
+					for i := 0; i < len(n.indices); i++ {
+						if c == n.indices[i] {
+							n = n.children[i]
+							continue walk
+						}
+					}
+
+					// Nothing found.
+					// We can recommend to redirect to the same URL without a
+					// trailing slash if a leaf exists for that path.
+					tsr = (path == "/" && n.handlers != nil)
+					return
+				}
+
+				// handle wildcard child
+				n = n.children[0]
+				switch n.nType {
+				case param:
+					// find param end (either '/' or path end)
+					end := 0
+					for end < len(path) && path[end] != '/' {
+						end++
+					}
+
+					// save param value
+					if cap(p) < int(n.maxParams) {
+						p = make(Params, 0, n.maxParams)
+					}
+					i := len(p)
+					p = p[:i+1] // expand slice within preallocated capacity
+					p[i].Key = n.path[1:]
+					p[i].Value = path[:end]
+
+					// we need to go deeper!
+					if end < len(path) {
+						if len(n.children) > 0 {
+							path = path[end:]
+							n = n.children[0]
+							continue walk
+						}
+
+						// ... but we can't
+						tsr = (len(path) == end+1)
+						return
+					}
+
+					if handlers = n.handlers; handlers != nil {
+						return
+					} else if len(n.children) == 1 {
+						// No handle found. Check if a handle for this path + a
+						// trailing slash exists for TSR recommendation
+						n = n.children[0]
+						tsr = (n.path == "/" && n.handlers != nil)
+					}
+
+					return
+
+				case catchAll:
+					// save param value
+					if cap(p) < int(n.maxParams) {
+						p = make(Params, 0, n.maxParams)
+					}
+					i := len(p)
+					p = p[:i+1] // expand slice within preallocated capacity
+					p[i].Key = n.path[2:]
+					p[i].Value = path
+
+					handlers = n.handlers
+					return
+
+				default:
+					panic("invalid node type")
+				}
+			}
+		} else if path == n.path {
+			// We should have reached the node containing the handle.
+			// Check if this node has a handle registered.
+			if handlers = n.handlers; handlers != nil {
+				return
+			}
+
+			// No handle found. Check if a handle for this path + a
+			// trailing slash exists for trailing slash recommendation
+			for i := 0; i < len(n.indices); i++ {
+				if n.indices[i] == '/' {
+					n = n.children[i]
+					tsr = (len(n.path) == 1 && n.handlers != nil) ||
+						(n.nType == catchAll && n.children[0].handlers != nil)
+					return
+				}
+			}
+
+			return
+		}
+
+		// Nothing found. We can recommend to redirect to the same URL with an
+		// extra trailing slash if a leaf exists for that path
+		tsr = (path == "/") ||
+			(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
+				path == n.path[:len(n.path)-1] && n.handlers != nil)
+		return
+	}
+}
+
+// Makes a case-insensitive lookup of the given path and tries to find a handler.
+// It can optionally also fix trailing slashes.
+// It returns the case-corrected path and a bool indicating whether the lookup
+// was successful.
+func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) {
+	ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory
+
+	// Outer loop for walking the tree
+	for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) {
+		path = path[len(n.path):]
+		ciPath = append(ciPath, n.path...)
+
+		if len(path) > 0 {
+			// If this node does not have a wildcard (param or catchAll) child,
+			// we can just look up the next child node and continue to walk down
+			// the tree
+			if !n.wildChild {
+				r := unicode.ToLower(rune(path[0]))
+				for i, index := range n.indices {
+					// must use recursive approach since both index and
+					// ToLower(index) could exist. We must check both.
+					if r == unicode.ToLower(index) {
+						out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash)
+						if found {
+							return append(ciPath, out...), true
+						}
+					}
+				}
+
+				// Nothing found. We can recommend to redirect to the same URL
+				// without a trailing slash if a leaf exists for that path
+				found = (fixTrailingSlash && path == "/" && n.handlers != nil)
+				return
+			}
+
+			n = n.children[0]
+			switch n.nType {
+			case param:
+				// find param end (either '/' or path end)
+				k := 0
+				for k < len(path) && path[k] != '/' {
+					k++
+				}
+
+				// add param value to case insensitive path
+				ciPath = append(ciPath, path[:k]...)
+
+				// we need to go deeper!
+				if k < len(path) {
+					if len(n.children) > 0 {
+						path = path[k:]
+						n = n.children[0]
+						continue
+					}
+
+					// ... but we can't
+					if fixTrailingSlash && len(path) == k+1 {
+						return ciPath, true
+					}
+					return
+				}
+
+				if n.handlers != nil {
+					return ciPath, true
+				} else if fixTrailingSlash && len(n.children) == 1 {
+					// No handle found. Check if a handle for this path + a
+					// trailing slash exists
+					n = n.children[0]
+					if n.path == "/" && n.handlers != nil {
+						return append(ciPath, '/'), true
+					}
+				}
+				return
+
+			case catchAll:
+				return append(ciPath, path...), true
+
+			default:
+				panic("invalid node type")
+			}
+		} else {
+			// We should have reached the node containing the handle.
+			// Check if this node has a handle registered.
+			if n.handlers != nil {
+				return ciPath, true
+			}
+
+			// No handle found.
+			// Try to fix the path by adding a trailing slash
+			if fixTrailingSlash {
+				for i := 0; i < len(n.indices); i++ {
+					if n.indices[i] == '/' {
+						n = n.children[i]
+						if (len(n.path) == 1 && n.handlers != nil) ||
+							(n.nType == catchAll && n.children[0].handlers != nil) {
+							return append(ciPath, '/'), true
+						}
+						return
+					}
+				}
+			}
+			return
+		}
+	}
+
+	// Nothing found.
+	// Try to fix the path by adding / removing a trailing slash
+	if fixTrailingSlash {
+		if path == "/" {
+			return ciPath, true
+		}
+		if len(path)+1 == len(n.path) && n.path[len(path)] == '/' &&
+			strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) &&
+			n.handlers != nil {
+			return append(ciPath, n.path...), true
+		}
+	}
+	return
+}

+ 608 - 0
tree_test.go

@@ -0,0 +1,608 @@
+// Copyright 2013 Julien Schmidt. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be found
+// in the LICENSE file.
+
+package gin
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+	"testing"
+)
+
+func printChildren(n *node, prefix string) {
+	fmt.Printf(" %02d:%02d %s%s[%d] %v %t %d \r\n", n.priority, n.maxParams, prefix, n.path, len(n.children), n.handlers, n.wildChild, n.nType)
+	for l := len(n.path); l > 0; l-- {
+		prefix += " "
+	}
+	for _, child := range n.children {
+		printChildren(child, prefix)
+	}
+}
+
+// Used as a workaround since we can't compare functions or their adresses
+var fakeHandlerValue string
+
+func fakeHandler(val string) HandlersChain {
+	return HandlersChain{func(c *Context) {
+		fakeHandlerValue = val
+	}}
+}
+
+type testRequests []struct {
+	path       string
+	nilHandler bool
+	route      string
+	ps         Params
+}
+
+func checkRequests(t *testing.T, tree *node, requests testRequests) {
+	for _, request := range requests {
+		handler, ps, _ := tree.getValue(request.path, nil)
+
+		if handler == nil {
+			if !request.nilHandler {
+				t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
+			}
+		} else if request.nilHandler {
+			t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
+		} else {
+			handler[0](nil)
+			if fakeHandlerValue != request.route {
+				t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route)
+			}
+		}
+
+		if !reflect.DeepEqual(ps, request.ps) {
+			t.Errorf("Params mismatch for route '%s'", request.path)
+		}
+	}
+}
+
+func checkPriorities(t *testing.T, n *node) uint32 {
+	var prio uint32
+	for i := range n.children {
+		prio += checkPriorities(t, n.children[i])
+	}
+
+	if n.handlers != nil {
+		prio++
+	}
+
+	if n.priority != prio {
+		t.Errorf(
+			"priority mismatch for node '%s': is %d, should be %d",
+			n.path, n.priority, prio,
+		)
+	}
+
+	return prio
+}
+
+func checkMaxParams(t *testing.T, n *node) uint8 {
+	var maxParams uint8
+	for i := range n.children {
+		params := checkMaxParams(t, n.children[i])
+		if params > maxParams {
+			maxParams = params
+		}
+	}
+	if n.nType != static && !n.wildChild {
+		maxParams++
+	}
+
+	if n.maxParams != maxParams {
+		t.Errorf(
+			"maxParams mismatch for node '%s': is %d, should be %d",
+			n.path, n.maxParams, maxParams,
+		)
+	}
+
+	return maxParams
+}
+
+func TestCountParams(t *testing.T) {
+	if countParams("/path/:param1/static/*catch-all") != 2 {
+		t.Fail()
+	}
+	if countParams(strings.Repeat("/:param", 256)) != 255 {
+		t.Fail()
+	}
+}
+
+func TestTreeAddAndGet(t *testing.T) {
+	tree := &node{}
+
+	routes := [...]string{
+		"/hi",
+		"/contact",
+		"/co",
+		"/c",
+		"/a",
+		"/ab",
+		"/doc/",
+		"/doc/go_faq.html",
+		"/doc/go1.html",
+		"/α",
+		"/β",
+	}
+	for _, route := range routes {
+		tree.addRoute(route, fakeHandler(route))
+	}
+
+	//printChildren(tree, "")
+
+	checkRequests(t, tree, testRequests{
+		{"/a", false, "/a", nil},
+		{"/", true, "", nil},
+		{"/hi", false, "/hi", nil},
+		{"/contact", false, "/contact", nil},
+		{"/co", false, "/co", nil},
+		{"/con", true, "", nil},  // key mismatch
+		{"/cona", true, "", nil}, // key mismatch
+		{"/no", true, "", nil},   // no matching child
+		{"/ab", false, "/ab", nil},
+		{"/α", false, "/α", nil},
+		{"/β", false, "/β", nil},
+	})
+
+	checkPriorities(t, tree)
+	checkMaxParams(t, tree)
+}
+
+func TestTreeWildcard(t *testing.T) {
+	tree := &node{}
+
+	routes := [...]string{
+		"/",
+		"/cmd/:tool/:sub",
+		"/cmd/:tool/",
+		"/src/*filepath",
+		"/search/",
+		"/search/:query",
+		"/user_:name",
+		"/user_:name/about",
+		"/files/:dir/*filepath",
+		"/doc/",
+		"/doc/go_faq.html",
+		"/doc/go1.html",
+		"/info/:user/public",
+		"/info/:user/project/:project",
+	}
+	for _, route := range routes {
+		tree.addRoute(route, fakeHandler(route))
+	}
+
+	//printChildren(tree, "")
+
+	checkRequests(t, tree, testRequests{
+		{"/", false, "/", nil},
+		{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
+		{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
+		{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}},
+		{"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}},
+		{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
+		{"/search/", false, "/search/", nil},
+		{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
+		{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
+		{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
+		{"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}},
+		{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}},
+		{"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}},
+		{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
+	})
+
+	checkPriorities(t, tree)
+	checkMaxParams(t, tree)
+}
+
+func catchPanic(testFunc func()) (recv interface{}) {
+	defer func() {
+		recv = recover()
+	}()
+
+	testFunc()
+	return
+}
+
+type testRoute struct {
+	path     string
+	conflict bool
+}
+
+func testRoutes(t *testing.T, routes []testRoute) {
+	tree := &node{}
+
+	for _, route := range routes {
+		recv := catchPanic(func() {
+			tree.addRoute(route.path, nil)
+		})
+
+		if route.conflict {
+			if recv == nil {
+				t.Errorf("no panic for conflicting route '%s'", route.path)
+			}
+		} else if recv != nil {
+			t.Errorf("unexpected panic for route '%s': %v", route.path, recv)
+		}
+	}
+
+	//printChildren(tree, "")
+}
+
+func TestTreeWildcardConflict(t *testing.T) {
+	routes := []testRoute{
+		{"/cmd/:tool/:sub", false},
+		{"/cmd/vet", true},
+		{"/src/*filepath", false},
+		{"/src/*filepathx", true},
+		{"/src/", true},
+		{"/src1/", false},
+		{"/src1/*filepath", true},
+		{"/src2*filepath", true},
+		{"/search/:query", false},
+		{"/search/invalid", true},
+		{"/user_:name", false},
+		{"/user_x", true},
+		{"/user_:name", false},
+		{"/id:id", false},
+		{"/id/:id", true},
+	}
+	testRoutes(t, routes)
+}
+
+func TestTreeChildConflict(t *testing.T) {
+	routes := []testRoute{
+		{"/cmd/vet", false},
+		{"/cmd/:tool/:sub", true},
+		{"/src/AUTHORS", false},
+		{"/src/*filepath", true},
+		{"/user_x", false},
+		{"/user_:name", true},
+		{"/id/:id", false},
+		{"/id:id", true},
+		{"/:id", true},
+		{"/*filepath", true},
+	}
+	testRoutes(t, routes)
+}
+
+func TestTreeDupliatePath(t *testing.T) {
+	tree := &node{}
+
+	routes := [...]string{
+		"/",
+		"/doc/",
+		"/src/*filepath",
+		"/search/:query",
+		"/user_:name",
+	}
+	for _, route := range routes {
+		recv := catchPanic(func() {
+			tree.addRoute(route, fakeHandler(route))
+		})
+		if recv != nil {
+			t.Fatalf("panic inserting route '%s': %v", route, recv)
+		}
+
+		// Add again
+		recv = catchPanic(func() {
+			tree.addRoute(route, nil)
+		})
+		if recv == nil {
+			t.Fatalf("no panic while inserting duplicate route '%s", route)
+		}
+	}
+
+	//printChildren(tree, "")
+
+	checkRequests(t, tree, testRequests{
+		{"/", false, "/", nil},
+		{"/doc/", false, "/doc/", nil},
+		{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
+		{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
+		{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
+	})
+}
+
+func TestEmptyWildcardName(t *testing.T) {
+	tree := &node{}
+
+	routes := [...]string{
+		"/user:",
+		"/user:/",
+		"/cmd/:/",
+		"/src/*",
+	}
+	for _, route := range routes {
+		recv := catchPanic(func() {
+			tree.addRoute(route, nil)
+		})
+		if recv == nil {
+			t.Fatalf("no panic while inserting route with empty wildcard name '%s", route)
+		}
+	}
+}
+
+func TestTreeCatchAllConflict(t *testing.T) {
+	routes := []testRoute{
+		{"/src/*filepath/x", true},
+		{"/src2/", false},
+		{"/src2/*filepath/x", true},
+	}
+	testRoutes(t, routes)
+}
+
+func TestTreeCatchAllConflictRoot(t *testing.T) {
+	routes := []testRoute{
+		{"/", false},
+		{"/*filepath", true},
+	}
+	testRoutes(t, routes)
+}
+
+func TestTreeDoubleWildcard(t *testing.T) {
+	const panicMsg = "only one wildcard per path segment is allowed"
+
+	routes := [...]string{
+		"/:foo:bar",
+		"/:foo:bar/",
+		"/:foo*bar",
+	}
+
+	for _, route := range routes {
+		tree := &node{}
+		recv := catchPanic(func() {
+			tree.addRoute(route, nil)
+		})
+
+		if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) {
+			t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv)
+		}
+	}
+}
+
+/*func TestTreeDuplicateWildcard(t *testing.T) {
+    tree := &node{}
+
+    routes := [...]string{
+        "/:id/:name/:id",
+    }
+    for _, route := range routes {
+        ...
+    }
+}*/
+
+func TestTreeTrailingSlashRedirect(t *testing.T) {
+	tree := &node{}
+
+	routes := [...]string{
+		"/hi",
+		"/b/",
+		"/search/:query",
+		"/cmd/:tool/",
+		"/src/*filepath",
+		"/x",
+		"/x/y",
+		"/y/",
+		"/y/z",
+		"/0/:id",
+		"/0/:id/1",
+		"/1/:id/",
+		"/1/:id/2",
+		"/aa",
+		"/a/",
+		"/doc",
+		"/doc/go_faq.html",
+		"/doc/go1.html",
+		"/no/a",
+		"/no/b",
+		"/api/hello/:name",
+	}
+	for _, route := range routes {
+		recv := catchPanic(func() {
+			tree.addRoute(route, fakeHandler(route))
+		})
+		if recv != nil {
+			t.Fatalf("panic inserting route '%s': %v", route, recv)
+		}
+	}
+
+	//printChildren(tree, "")
+
+	tsrRoutes := [...]string{
+		"/hi/",
+		"/b",
+		"/search/gopher/",
+		"/cmd/vet",
+		"/src",
+		"/x/",
+		"/y",
+		"/0/go/",
+		"/1/go",
+		"/a",
+		"/doc/",
+	}
+	for _, route := range tsrRoutes {
+		handler, _, tsr := tree.getValue(route, nil)
+		if handler != nil {
+			t.Fatalf("non-nil handler for TSR route '%s", route)
+		} else if !tsr {
+			t.Errorf("expected TSR recommendation for route '%s'", route)
+		}
+	}
+
+	noTsrRoutes := [...]string{
+		"/",
+		"/no",
+		"/no/",
+		"/_",
+		"/_/",
+		"/api/world/abc",
+	}
+	for _, route := range noTsrRoutes {
+		handler, _, tsr := tree.getValue(route, nil)
+		if handler != nil {
+			t.Fatalf("non-nil handler for No-TSR route '%s", route)
+		} else if tsr {
+			t.Errorf("expected no TSR recommendation for route '%s'", route)
+		}
+	}
+}
+
+func TestTreeFindCaseInsensitivePath(t *testing.T) {
+	tree := &node{}
+
+	routes := [...]string{
+		"/hi",
+		"/b/",
+		"/ABC/",
+		"/search/:query",
+		"/cmd/:tool/",
+		"/src/*filepath",
+		"/x",
+		"/x/y",
+		"/y/",
+		"/y/z",
+		"/0/:id",
+		"/0/:id/1",
+		"/1/:id/",
+		"/1/:id/2",
+		"/aa",
+		"/a/",
+		"/doc",
+		"/doc/go_faq.html",
+		"/doc/go1.html",
+		"/doc/go/away",
+		"/no/a",
+		"/no/b",
+	}
+
+	for _, route := range routes {
+		recv := catchPanic(func() {
+			tree.addRoute(route, fakeHandler(route))
+		})
+		if recv != nil {
+			t.Fatalf("panic inserting route '%s': %v", route, recv)
+		}
+	}
+
+	// Check out == in for all registered routes
+	// With fixTrailingSlash = true
+	for _, route := range routes {
+		out, found := tree.findCaseInsensitivePath(route, true)
+		if !found {
+			t.Errorf("Route '%s' not found!", route)
+		} else if string(out) != route {
+			t.Errorf("Wrong result for route '%s': %s", route, string(out))
+		}
+	}
+	// With fixTrailingSlash = false
+	for _, route := range routes {
+		out, found := tree.findCaseInsensitivePath(route, false)
+		if !found {
+			t.Errorf("Route '%s' not found!", route)
+		} else if string(out) != route {
+			t.Errorf("Wrong result for route '%s': %s", route, string(out))
+		}
+	}
+
+	tests := []struct {
+		in    string
+		out   string
+		found bool
+		slash bool
+	}{
+		{"/HI", "/hi", true, false},
+		{"/HI/", "/hi", true, true},
+		{"/B", "/b/", true, true},
+		{"/B/", "/b/", true, false},
+		{"/abc", "/ABC/", true, true},
+		{"/abc/", "/ABC/", true, false},
+		{"/aBc", "/ABC/", true, true},
+		{"/aBc/", "/ABC/", true, false},
+		{"/abC", "/ABC/", true, true},
+		{"/abC/", "/ABC/", true, false},
+		{"/SEARCH/QUERY", "/search/QUERY", true, false},
+		{"/SEARCH/QUERY/", "/search/QUERY", true, true},
+		{"/CMD/TOOL/", "/cmd/TOOL/", true, false},
+		{"/CMD/TOOL", "/cmd/TOOL/", true, true},
+		{"/SRC/FILE/PATH", "/src/FILE/PATH", true, false},
+		{"/x/Y", "/x/y", true, false},
+		{"/x/Y/", "/x/y", true, true},
+		{"/X/y", "/x/y", true, false},
+		{"/X/y/", "/x/y", true, true},
+		{"/X/Y", "/x/y", true, false},
+		{"/X/Y/", "/x/y", true, true},
+		{"/Y/", "/y/", true, false},
+		{"/Y", "/y/", true, true},
+		{"/Y/z", "/y/z", true, false},
+		{"/Y/z/", "/y/z", true, true},
+		{"/Y/Z", "/y/z", true, false},
+		{"/Y/Z/", "/y/z", true, true},
+		{"/y/Z", "/y/z", true, false},
+		{"/y/Z/", "/y/z", true, true},
+		{"/Aa", "/aa", true, false},
+		{"/Aa/", "/aa", true, true},
+		{"/AA", "/aa", true, false},
+		{"/AA/", "/aa", true, true},
+		{"/aA", "/aa", true, false},
+		{"/aA/", "/aa", true, true},
+		{"/A/", "/a/", true, false},
+		{"/A", "/a/", true, true},
+		{"/DOC", "/doc", true, false},
+		{"/DOC/", "/doc", true, true},
+		{"/NO", "", false, true},
+		{"/DOC/GO", "", false, true},
+	}
+	// With fixTrailingSlash = true
+	for _, test := range tests {
+		out, found := tree.findCaseInsensitivePath(test.in, true)
+		if found != test.found || (found && (string(out) != test.out)) {
+			t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
+				test.in, string(out), found, test.out, test.found)
+			return
+		}
+	}
+	// With fixTrailingSlash = false
+	for _, test := range tests {
+		out, found := tree.findCaseInsensitivePath(test.in, false)
+		if test.slash {
+			if found { // test needs a trailingSlash fix. It must not be found!
+				t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out))
+			}
+		} else {
+			if found != test.found || (found && (string(out) != test.out)) {
+				t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
+					test.in, string(out), found, test.out, test.found)
+				return
+			}
+		}
+	}
+}
+
+func TestTreeInvalidNodeType(t *testing.T) {
+	tree := &node{}
+	tree.addRoute("/", fakeHandler("/"))
+	tree.addRoute("/:page", fakeHandler("/:page"))
+
+	// set invalid node type
+	tree.children[0].nType = 42
+
+	// normal lookup
+	recv := catchPanic(func() {
+		tree.getValue("/test", nil)
+	})
+	if rs, ok := recv.(string); !ok || rs != "invalid node type" {
+		t.Fatalf(`Expected panic "invalid node type", got "%v"`, recv)
+	}
+
+	// case-insensitive lookup
+	recv = catchPanic(func() {
+		tree.findCaseInsensitivePath("/test", true)
+	})
+	if rs, ok := recv.(string); !ok || rs != "invalid node type" {
+		t.Fatalf(`Expected panic "invalid node type", got "%v"`, recv)
+	}
+}

+ 35 - 5
utils.go

@@ -6,11 +6,25 @@ package gin
 
 import (
 	"encoding/xml"
+	"net/http"
+	"path"
 	"reflect"
 	"runtime"
 	"strings"
 )
 
+func WrapF(f http.HandlerFunc) HandlerFunc {
+	return func(c *Context) {
+		f(c.Writer, c.Request)
+	}
+}
+
+func WrapH(h http.Handler) HandlerFunc {
+	return func(c *Context) {
+		h.ServeHTTP(c.Writer, c.Request)
+	}
+}
+
 type H map[string]interface{}
 
 // Allows type H to be used with xml.Marshal
@@ -56,17 +70,20 @@ func chooseData(custom, wildcard interface{}) interface{} {
 	return custom
 }
 
-func parseAccept(accept string) []string {
-	parts := strings.Split(accept, ",")
-	for i, part := range parts {
+func parseAccept(acceptHeader string) []string {
+	parts := strings.Split(acceptHeader, ",")
+	out := make([]string, 0, len(parts))
+	for _, part := range parts {
 		index := strings.IndexByte(part, ';')
 		if index >= 0 {
 			part = part[0:index]
 		}
 		part = strings.TrimSpace(part)
-		parts[i] = part
+		if len(part) > 0 {
+			out = append(out, part)
+		}
 	}
-	return parts
+	return out
 }
 
 func lastChar(str string) uint8 {
@@ -80,3 +97,16 @@ func lastChar(str string) uint8 {
 func nameOfFunction(f interface{}) string {
 	return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
 }
+
+func joinPaths(absolutePath, relativePath string) string {
+	if len(relativePath) == 0 {
+		return absolutePath
+	}
+
+	finalPath := path.Join(absolutePath, relativePath)
+	appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/'
+	if appendSlash {
+		return finalPath + "/"
+	}
+	return finalPath
+}

+ 99 - 0
utils_test.go

@@ -0,0 +1,99 @@
+// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func init() {
+	SetMode(TestMode)
+}
+
+type testStruct struct {
+	T *testing.T
+}
+
+func (t *testStruct) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	assert.Equal(t.T, req.Method, "POST")
+	assert.Equal(t.T, req.URL.Path, "/path")
+	w.WriteHeader(500)
+	fmt.Fprint(w, "hello")
+}
+
+func TestWrap(t *testing.T) {
+	router := New()
+	router.POST("/path", WrapH(&testStruct{t}))
+	router.GET("/path2", WrapF(func(w http.ResponseWriter, req *http.Request) {
+		assert.Equal(t, req.Method, "GET")
+		assert.Equal(t, req.URL.Path, "/path2")
+		w.WriteHeader(400)
+		fmt.Fprint(w, "hola!")
+	}))
+
+	w := performRequest(router, "POST", "/path")
+	assert.Equal(t, w.Code, 500)
+	assert.Equal(t, w.Body.String(), "hello")
+
+	w = performRequest(router, "GET", "/path2")
+	assert.Equal(t, w.Code, 400)
+	assert.Equal(t, w.Body.String(), "hola!")
+}
+
+func TestLastChar(t *testing.T) {
+	assert.Equal(t, lastChar("hola"), uint8('a'))
+	assert.Equal(t, lastChar("adios"), uint8('s'))
+	assert.Panics(t, func() { lastChar("") })
+}
+
+func TestParseAccept(t *testing.T) {
+	parts := parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9,  */* ;q=0.8")
+	assert.Len(t, parts, 4)
+	assert.Equal(t, parts[0], "text/html")
+	assert.Equal(t, parts[1], "application/xhtml+xml")
+	assert.Equal(t, parts[2], "application/xml")
+	assert.Equal(t, parts[3], "*/*")
+}
+
+func TestChooseData(t *testing.T) {
+	A := "a"
+	B := "b"
+	assert.Equal(t, chooseData(A, B), A)
+	assert.Equal(t, chooseData(nil, B), B)
+	assert.Panics(t, func() { chooseData(nil, nil) })
+}
+
+func TestFilterFlags(t *testing.T) {
+	result := filterFlags("text/html ")
+	assert.Equal(t, result, "text/html")
+
+	result = filterFlags("text/html;")
+	assert.Equal(t, result, "text/html")
+}
+
+func TestFunctionName(t *testing.T) {
+	assert.Equal(t, nameOfFunction(somefunction), "github.com/gin-gonic/gin.somefunction")
+}
+
+func somefunction() {
+	// this empty function is used by TestFunctionName()
+}
+
+func TestJoinPaths(t *testing.T) {
+	assert.Equal(t, joinPaths("", ""), "")
+	assert.Equal(t, joinPaths("", "/"), "/")
+	assert.Equal(t, joinPaths("/a", ""), "/a")
+	assert.Equal(t, joinPaths("/a/", ""), "/a/")
+	assert.Equal(t, joinPaths("/a/", "/"), "/a/")
+	assert.Equal(t, joinPaths("/a", "/"), "/a/")
+	assert.Equal(t, joinPaths("/a", "/hola"), "/a/hola")
+	assert.Equal(t, joinPaths("/a/", "/hola"), "/a/hola")
+	assert.Equal(t, joinPaths("/a/", "/hola/"), "/a/hola/")
+	assert.Equal(t, joinPaths("/a/", "/hola//"), "/a/hola/")
+}

+ 1 - 0
wercker.yml

@@ -0,0 +1 @@
+box: wercker/default

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini