Browse Source

Merge branch 'master' into develop

Conflicts:
	README.md
	gin.go
	routergroup.go
Manu Mtz-Almeida 10 years ago
parent
commit
e8bc8f48e9
27 changed files with 794 additions and 249 deletions
  1. 10 1
      CHANGELOG.md
  2. 102 70
      README.md
  3. 7 9
      auth.go
  4. 5 3
      binding/binding.go
  5. 39 0
      binding/binding_test.go
  6. 32 2
      binding/form.go
  7. 0 1
      binding/form_mapping.go
  8. 2 2
      binding/json.go
  9. 2 2
      binding/xml.go
  10. 58 30
      context.go
  11. 65 13
      context_test.go
  12. 32 3
      debug.go
  13. 9 0
      debug_test.go
  14. 3 3
      errors.go
  15. 1 0
      errors_test.go
  16. 2 3
      fs.go
  17. 68 23
      gin.go
  18. 53 16
      gin_integration_test.go
  19. 65 2
      gin_test.go
  20. 28 0
      logger_test.go
  21. 2 2
      middleware_test.go
  22. 10 0
      response_writer.go
  23. 73 56
      routergroup.go
  24. 5 5
      routergroup_test.go
  25. 76 3
      routes_test.go
  26. 18 0
      utils.go
  27. 27 0
      utils_test.go

+ 10 - 1
CHANGELOG.md

@@ -12,11 +12,20 @@
 - [NEW] Benchmarks suite
 - [NEW] Bind validation can be disabled and replaced with custom validators.
 - [NEW] More flexible HTML render
+- [NEW] Multipart and PostForm bindings
+- [NEW] Adds method to return all the registered routes
+- [NEW] Context.HandlerName() returns the main handler's name
+- [NEW] Adds Error.IsType() helper
 - [FIX] Binding multipart form
 - [FIX] Integration tests
 - [FIX] Crash when binding non struct object in Context.
 - [FIX] RunTLS() implementation
 - [FIX] Logger() unit tests
+- [FIX] Adds SetHTMLTemplate() warning
+- [FIX] Context.IsAborted()
+- [FIX] More unit tests
+- [FIX] JSON, XML, HTML renders accept custom content-types
+- [FIX] gin.AbortIndex is unexported
 - [FIX] Better approach to avoid directory listing in StaticFS()
 - [FIX] Context.ClientIP() always returns the IP with trimmed spaces.
 - [FIX] Better warning when running in debug mode.
@@ -62,7 +71,7 @@
 - [FIX] Better debugging messages
 - [FIX] ErrorLogger
 - [FIX] Debug HTTP render
-- [FIX] Refactored binding and render modules 
+- [FIX] Refactored binding and render modules
 - [FIX] Refactored Context initialization
 - [FIX] Refactored BasicAuth()
 - [FIX] NoMethod/NoRoute handlers

+ 102 - 70
README.md

@@ -1,9 +1,15 @@
-#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=master)](https://coveralls.io/r/gin-gonic/gin?branch=master)  
 
- [![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 Web Framework
+<img align="right" src="https://s3.amazonaws.com/uploads.hipchat.com/36744/1498287/JVR32LgyEGCiy01/path4201%20copy%202.png">
+[![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=master)](https://coveralls.io/r/gin-gonic/gin?branch=master)
+[![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.
 
+
+
 ![Gin console logger](https://gin-gonic.github.io/gin/other/console.png)
 
 ```
@@ -30,36 +36,40 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
 [See all benchmarks](/BENCHMARKS.md)
 
 
-```
-BenchmarkAce_GithubAll     10000        109482 ns/op       13792 B/op        167 allocs/op
-BenchmarkBear_GithubAll    10000        287490 ns/op       79952 B/op        943 allocs/op
-BenchmarkBeego_GithubAll        3000        562184 ns/op      146272 B/op       2092 allocs/op
-BenchmarkBone_GithubAll      500       2578716 ns/op      648016 B/op       8119 allocs/op
-BenchmarkDenco_GithubAll       20000         94955 ns/op       20224 B/op        167 allocs/op
-BenchmarkEcho_GithubAll    30000         58705 ns/op           0 B/op          0 allocs/op
-BenchmarkGin_GithubAll     30000         50991 ns/op           0 B/op          0 allocs/op
-BenchmarkGocraftWeb_GithubAll       5000        449648 ns/op      133280 B/op       1889 allocs/op
-BenchmarkGoji_GithubAll     2000        689748 ns/op       56113 B/op        334 allocs/op
-BenchmarkGoJsonRest_GithubAll       5000        537769 ns/op      135995 B/op       2940 allocs/op
-BenchmarkGoRestful_GithubAll         100      18410628 ns/op      797236 B/op       7725 allocs/op
-BenchmarkGorillaMux_GithubAll        200       8036360 ns/op      153137 B/op       1791 allocs/op
-BenchmarkHttpRouter_GithubAll      20000         63506 ns/op       13792 B/op        167 allocs/op
-BenchmarkHttpTreeMux_GithubAll     10000        165927 ns/op       56112 B/op        334 allocs/op
-BenchmarkKocha_GithubAll       10000        171362 ns/op       23304 B/op        843 allocs/op
-BenchmarkMacaron_GithubAll      2000        817008 ns/op      224960 B/op       2315 allocs/op
-BenchmarkMartini_GithubAll       100      12609209 ns/op      237952 B/op       2686 allocs/op
-BenchmarkPat_GithubAll       300       4830398 ns/op     1504101 B/op      32222 allocs/op
-BenchmarkPossum_GithubAll      10000        301716 ns/op       97440 B/op        812 allocs/op
-BenchmarkR2router_GithubAll    10000        270691 ns/op       77328 B/op       1182 allocs/op
-BenchmarkRevel_GithubAll        1000       1491919 ns/op      345553 B/op       5918 allocs/op
-BenchmarkRivet_GithubAll       10000        283860 ns/op       84272 B/op       1079 allocs/op
-BenchmarkTango_GithubAll        5000        473821 ns/op       87078 B/op       2470 allocs/op
-BenchmarkTigerTonic_GithubAll       2000       1120131 ns/op      241088 B/op       6052 allocs/op
-BenchmarkTraffic_GithubAll       200       8708979 ns/op     2664762 B/op      22390 allocs/op
-BenchmarkVulcan_GithubAll       5000        353392 ns/op       19894 B/op        609 allocs/op
-BenchmarkZeus_GithubAll     2000        944234 ns/op      300688 B/op       2648 allocs/op
-```
-
+Benchmark name 					| (1) 		| (2) 		| (3) 		| (4)
+--------------------------------|----------:|----------:|----------:|------:
+BenchmarkAce_GithubAll 			| 10000 	| 109482 	| 13792 	| 167
+BenchmarkBear_GithubAll 		| 10000 	| 287490 	| 79952 	| 943
+BenchmarkBeego_GithubAll 		| 3000 		| 562184 	| 146272 	| 2092
+BenchmarkBone_GithubAll 		| 500 		| 2578716 	| 648016 	| 8119
+BenchmarkDenco_GithubAll 		| 20000 	| 94955 	| 20224 	| 167
+BenchmarkEcho_GithubAll 		| 30000 	| 58705 	| 0 		| 0
+**BenchmarkGin_GithubAll** 		| **30000** | **50991** | **0** 	| **0**
+BenchmarkGocraftWeb_GithubAll 	| 5000 		| 449648 	| 133280 	| 1889
+BenchmarkGoji_GithubAll 		| 2000 		| 689748 	| 56113 	| 334
+BenchmarkGoJsonRest_GithubAll 	| 5000 		| 537769 	| 135995 	| 2940
+BenchmarkGoRestful_GithubAll 	| 100 		| 18410628 	| 797236 	| 7725
+BenchmarkGorillaMux_GithubAll 	| 200 		| 8036360 	| 153137 	| 1791
+BenchmarkHttpRouter_GithubAll 	| 20000 	| 63506 	| 13792 	| 167
+BenchmarkHttpTreeMux_GithubAll 	| 10000 	| 165927 	| 56112 	| 334
+BenchmarkKocha_GithubAll 		| 10000 	| 171362 	| 23304 	| 843
+BenchmarkMacaron_GithubAll 		| 2000 		| 817008 	| 224960 	| 2315
+BenchmarkMartini_GithubAll 		| 100 		| 12609209 	| 237952 	| 2686
+BenchmarkPat_GithubAll 			| 300 		| 4830398 	| 1504101 	| 32222
+BenchmarkPossum_GithubAll 		| 10000 	| 301716 	| 97440 	| 812
+BenchmarkR2router_GithubAll 	| 10000 	| 270691 	| 77328 	| 1182
+BenchmarkRevel_GithubAll 		| 1000 		| 1491919 	| 345553 	| 5918
+BenchmarkRivet_GithubAll 		| 10000 	| 283860 	| 84272 	| 1079
+BenchmarkTango_GithubAll 		| 5000 		| 473821 	| 87078 	| 2470
+BenchmarkTigerTonic_GithubAll 	| 2000 		| 1120131 	| 241088 	| 6052
+BenchmarkTraffic_GithubAll 		| 200 		| 8708979 	| 2664762 	| 22390
+BenchmarkVulcan_GithubAll 		| 5000 		| 353392 	| 19894 	| 609
+BenchmarkZeus_GithubAll 		| 2000 		| 944234 	| 300688 	| 2648
+
+(1): Total Repetitions  
+(2): Single Repetition Duration (ns/op)  
+(3): Heap Memory (B/op)  
+(4): Average Allocations per Repetition (allocs/op)  
 
 ##Gin v1. stable
 
@@ -166,6 +176,36 @@ func main() {
 }
 ```
 
+### Another example: query + post form
+
+```
+POST /post?id=1234&page=1 HTTP/1.1
+Content-Type: application/x-www-form-urlencoded
+
+name=manu&message=this_is_great
+```
+
+```go
+func main() {
+	router := gin.Default()
+
+	router.POST("/post", func(c *gin.Context) {
+        id := c.Query("id")
+        page := c.DefaultQuery("id", "0")
+        name := c.PostForm("name")
+        message := c.PostForm("message")
+
+        fmt.Println("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
+	})
+	router.Run(":8080")
+}
+```
+
+```
+id: 1234; page: 0; name: manu; message: this_is_great
+```
+
+
 #### Grouping routes
 ```go
 func main() {
@@ -253,46 +293,41 @@ You can also specify that specific fields are required. If a field is decorated
 
 ```go
 // Binding from JSON
-type LoginJSON struct {
-	User     string `json:"user" binding:"required"`
-	Password string `json:"password" binding:"required"`
-}
-
-// Binding from form values
-type LoginForm struct {
-    User     string `form:"user" binding:"required"`
-    Password string `form:"password" binding:"required"`
+type Login struct {
+	User     string `form:"user" json:"user" binding:"required"`
+	Password string `form:"password" json:"password" binding:"required"`
 }
 
 func main() {
-	r := gin.Default()
+	router := gin.Default()
 
     // Example for binding JSON ({"user": "manu", "password": "123"})
-	r.POST("/loginJSON", func(c *gin.Context) {
-		var json LoginJSON
-
-        c.Bind(&json) // This will infer what binder to use depending on the content-type header.
-        if json.User == "manu" && json.Password == "123" {
-            c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
-        } else {
-            c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
+	router.POST("/loginJSON", func(c *gin.Context) {
+		var json Login
+        if c.BindJSON(&json) == nil {
+            if json.User == "manu" && json.Password == "123" {
+                c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
+            } else {
+                c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
+            }
         }
 	})
 
     // Example for binding a HTML form (user=manu&password=123)
-    r.POST("/loginHTML", func(c *gin.Context) {
-        var form LoginForm
-
-        c.BindWith(&form, binding.Form) // You can also specify which binder to use. We support binding.Form, binding.JSON and binding.XML.
-        if form.User == "manu" && form.Password == "123" {
-            c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
-        } else {
-            c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
+    router.POST("/loginForm", func(c *gin.Context) {
+        var form Login
+        // This will infer what binder to use depending on the content-type header.
+        if c.Bind(&form) == nil {
+            if form.User == "manu" && form.Password == "123" {
+                c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
+            } else {
+                c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
+            }
         }
     })
 
 	// Listen and server on 0.0.0.0:8080
-	r.Run(":8080")
+	router.Run(":8080")
 }
 ```
 
@@ -312,25 +347,22 @@ type LoginForm struct {
 }
 
 func main() {
-
 	router := gin.Default()
-
 	router.POST("/login", func(c *gin.Context) {
 		// you can bind multipart form with explicit binding declaration:
 		// c.BindWith(&form, binding.Form)
 		// or you can simply use autobinding with Bind method:
 		var form LoginForm
-		c.Bind(&form) // in this case proper binding will be automatically selected
-
-		if form.User == "user" && form.Password == "password" {
-			c.JSON(200, gin.H{"status": "you are logged in"})
-		} else {
-			c.JSON(401, gin.H{"status": "unauthorized"})
-		}
+        // in this case proper binding will be automatically selected
+		if c.Bind(&form) == nil {
+            if form.User == "user" && form.Password == "password" {
+			    c.JSON(200, gin.H{"status": "you are logged in"})
+            } else {
+			    c.JSON(401, gin.H{"status": "unauthorized"})
+            }
+        }
 	})
-
 	router.Run(":8080")
-
 }
 ```
 

+ 7 - 9
auth.go

@@ -10,9 +10,7 @@ import (
 	"strconv"
 )
 
-const (
-	AuthUserKey = "user"
-)
+const AuthUserKey = "user"
 
 type (
 	Accounts map[string]string
@@ -35,8 +33,9 @@ func (a authPairs) searchCredential(authValue string) (string, bool) {
 	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
+// BasicAuthForRealm returns a Basic HTTP Authorization middleware. 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.
+// If the realm is empty, "Authorization Required" will be used by default.
 // (see http://tools.ietf.org/html/rfc2617#section-1.2)
 func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
 	if realm == "" {
@@ -59,7 +58,7 @@ func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
 	}
 }
 
-// Implements a basic Basic HTTP Authorization. It takes as argument a map[string]string where
+// BasicAuth returns a Basic HTTP Authorization middleware. It takes as argument a map[string]string where
 // the key is the user name and the value is the password.
 func BasicAuth(accounts Accounts) HandlerFunc {
 	return BasicAuthForRealm(accounts, "")
@@ -91,8 +90,7 @@ func authorizationHeader(user, password string) string {
 func secureCompare(given, actual string) bool {
 	if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
 		return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1
-	} else {
-		/* Securely compare actual to itself to keep constant time, but always return false */
-		return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false
 	}
+	/* Securely compare actual to itself to keep constant time, but always return false */
+	return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false
 }

+ 5 - 3
binding/binding.go

@@ -33,9 +33,11 @@ type StructValidator interface {
 var Validator StructValidator = &defaultValidator{}
 
 var (
-	JSON = jsonBinding{}
-	XML  = xmlBinding{}
-	Form = formBinding{}
+	JSON          = jsonBinding{}
+	XML           = xmlBinding{}
+	Form          = formBinding{}
+	FormPost      = formPostBinding{}
+	FormMultipart = formMultipartBinding{}
 )
 
 func Default(method, contentType string) Binding {

+ 39 - 0
binding/binding_test.go

@@ -6,6 +6,7 @@ package binding
 
 import (
 	"bytes"
+	"mime/multipart"
 	"net/http"
 	"testing"
 
@@ -64,6 +65,44 @@ func TestBindingXML(t *testing.T) {
 		"<map><foo>bar</foo></map>", "<map><bar>foo</bar></map>")
 }
 
+func createFormPostRequest() *http.Request {
+	req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo"))
+	req.Header.Set("Content-Type", MIMEPOSTForm)
+	return req
+}
+
+func createFormMultipartRequest() *http.Request {
+	boundary := "--testboundary"
+	body := new(bytes.Buffer)
+	mw := multipart.NewWriter(body)
+	defer mw.Close()
+
+	mw.SetBoundary(boundary)
+	mw.WriteField("foo", "bar")
+	mw.WriteField("bar", "foo")
+	req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
+	req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
+	return req
+}
+
+func TestBindingFormPost(t *testing.T) {
+	req := createFormPostRequest()
+	var obj FooBarStruct
+	FormPost.Bind(req, &obj)
+
+	assert.Equal(t, obj.Foo, "bar")
+	assert.Equal(t, obj.Bar, "foo")
+}
+
+func TestBindingFormMultipart(t *testing.T) {
+	req := createFormMultipartRequest()
+	var obj FooBarStruct
+	FormMultipart.Bind(req, &obj)
+
+	assert.Equal(t, obj.Foo, "bar")
+	assert.Equal(t, obj.Bar, "foo")
+}
+
 func TestValidationFails(t *testing.T) {
 	var obj FooStruct
 	req := requestWithBody("POST", "/", `{"bar": "foo"}`)

+ 32 - 2
binding/form.go

@@ -7,12 +7,14 @@ package binding
 import "net/http"
 
 type formBinding struct{}
+type formPostBinding struct{}
+type formMultipartBinding struct{}
 
-func (_ formBinding) Name() string {
+func (formBinding) Name() string {
 	return "form"
 }
 
-func (_ formBinding) Bind(req *http.Request, obj interface{}) error {
+func (formBinding) Bind(req *http.Request, obj interface{}) error {
 	if err := req.ParseForm(); err != nil {
 		return err
 	}
@@ -22,3 +24,31 @@ func (_ formBinding) Bind(req *http.Request, obj interface{}) error {
 	}
 	return validate(obj)
 }
+
+func (formPostBinding) Name() string {
+	return "form-urlencoded"
+}
+
+func (formPostBinding) Bind(req *http.Request, obj interface{}) error {
+	if err := req.ParseForm(); err != nil {
+		return err
+	}
+	if err := mapForm(obj, req.PostForm); err != nil {
+		return err
+	}
+	return validate(obj)
+}
+
+func (formMultipartBinding) Name() string {
+	return "multipart/form-data"
+}
+
+func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
+	if err := req.ParseMultipartForm(32 << 10); err != nil {
+		return err
+	}
+	if err := mapForm(obj, req.MultipartForm.Value); err != nil {
+		return err
+	}
+	return validate(obj)
+}

+ 0 - 1
binding/form_mapping.go

@@ -56,7 +56,6 @@ func mapForm(ptr interface{}, form map[string][]string) error {
 				return err
 			}
 		}
-
 	}
 	return nil
 }

+ 2 - 2
binding/json.go

@@ -12,11 +12,11 @@ import (
 
 type jsonBinding struct{}
 
-func (_ jsonBinding) Name() string {
+func (jsonBinding) Name() string {
 	return "json"
 }
 
-func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error {
+func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
 	decoder := json.NewDecoder(req.Body)
 	if err := decoder.Decode(obj); err != nil {
 		return err

+ 2 - 2
binding/xml.go

@@ -11,11 +11,11 @@ import (
 
 type xmlBinding struct{}
 
-func (_ xmlBinding) Name() string {
+func (xmlBinding) Name() string {
 	return "xml"
 }
 
-func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error {
+func (xmlBinding) Bind(req *http.Request, obj interface{}) error {
 	decoder := xml.NewDecoder(req.Body)
 	if err := decoder.Decode(obj); err != nil {
 		return err

+ 58 - 30
context.go

@@ -18,6 +18,7 @@ import (
 	"golang.org/x/net/context"
 )
 
+// Content-Type MIME of the most common data formats
 const (
 	MIMEJSON              = binding.MIMEJSON
 	MIMEHTML              = binding.MIMEHTML
@@ -28,7 +29,7 @@ const (
 	MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
 )
 
-const AbortIndex int8 = math.MaxInt8 / 2
+const abortIndex int8 = math.MaxInt8 / 2
 
 // Context is the most important part of gin. It allows us to pass variables between middleware,
 // manage the flow, validate the JSON of a request and render a JSON response for example.
@@ -63,15 +64,23 @@ func (c *Context) reset() {
 	c.Accepted = nil
 }
 
+// Copy returns a copy of the current context that can be safely used outside the request's scope.
+// This have to be used then the context has to be passed to a goroutine.
 func (c *Context) Copy() *Context {
 	var cp Context = *c
 	cp.writermem.ResponseWriter = nil
 	cp.Writer = &cp.writermem
-	cp.index = AbortIndex
+	cp.index = abortIndex
 	cp.handlers = nil
 	return &cp
 }
 
+// HandlerName returns the main handle's name. For example if the handler is "handleGetUsers()", this
+// function will return "main.handleGetUsers"
+func (c *Context) HandlerName() string {
+	return nameOfFunction(c.handlers.Last())
+}
+
 /************************************/
 /*********** FLOW CONTROL ***********/
 /************************************/
@@ -87,27 +96,27 @@ func (c *Context) Next() {
 	}
 }
 
-// Returns if the currect context was aborted.
+// IsAborted returns true if the currect context was aborted.
 func (c *Context) IsAborted() bool {
-	return c.index == AbortIndex
+	return c.index >= abortIndex
 }
 
-// Stops the system to continue calling the pending handlers in the chain.
+// Abort stops the system to continue calling the pending handlers in the chain.
 // Let's say you have an authorization middleware that validates if the request is authorized
 // if the authorization fails (the password does not match). This method (Abort()) should be called
 // in order to stop the execution of the actual handler.
 func (c *Context) Abort() {
-	c.index = AbortIndex
+	c.index = abortIndex
 }
 
-// It calls Abort() and writes the headers with the specified status code.
+// AbortWithStatus calls `Abort()` and writes the headers with the specified status code.
 // For example, a failed attempt to authentificate a request could use: context.AbortWithStatus(401).
 func (c *Context) AbortWithStatus(code int) {
 	c.Writer.WriteHeader(code)
 	c.Abort()
 }
 
-// It calls AbortWithStatus() and Error() internally. This method stops the chain, writes the status code and
+// AbortWithError calls `AbortWithStatus()` and `Error()` internally. This method stops the chain, writes the status code and
 // pushes the specified error to `c.Errors`.
 // See Context.Error() for more details.
 func (c *Context) AbortWithError(code int, err error) *Error {
@@ -121,7 +130,8 @@ func (c *Context) AbortWithError(code int, err error) *Error {
 
 // 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.
+// 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) *Error {
 	var parsedError *Error
 	switch err.(type) {
@@ -141,8 +151,8 @@ func (c *Context) Error(err error) *Error {
 /******** METADATA MANAGEMENT********/
 /************************************/
 
-// Sets a new pair key/value just for this context.
-// It also lazy initializes the hashmap if it was not used previously.
+// Set is used to store a new key/value pair exclusivelly for this context.
+// It also lazy initializes  c.Keys if it was not used previously.
 func (c *Context) Set(key string, value interface{}) {
 	if c.Keys == nil {
 		c.Keys = make(map[string]interface{})
@@ -150,7 +160,7 @@ func (c *Context) Set(key string, value interface{}) {
 	c.Keys[key] = value
 }
 
-// Returns the value for the given key, ie: (value, true).
+// Get returns the value for the given key, ie: (value, true).
 // If the value does not exists it returns (nil, false)
 func (c *Context) Get(key string) (value interface{}, exists bool) {
 	if c.Keys != nil {
@@ -171,19 +181,24 @@ func (c *Context) MustGet(key string) interface{} {
 /************ INPUT DATA ************/
 /************************************/
 
-// Shortcut for c.Request.URL.Query().Get(key)
+// Query is a shortcut for c.Request.URL.Query().Get(key)
+// It is used to return the url query values.
+// ?id=1234&name=Manu
+// c.Query("id") == "1234"
+// c.Query("name") == "Manu"
+// c.Query("wtf") == ""
 func (c *Context) Query(key string) (va string) {
 	va, _ = c.query(key)
 	return
 }
 
-// Shortcut for c.Request.PostFormValue(key)
+// PostForm is a shortcut for c.Request.PostFormValue(key)
 func (c *Context) PostForm(key string) (va string) {
 	va, _ = c.postForm(key)
 	return
 }
 
-// Shortcut for c.Params.ByName(key)
+// Param is a shortcut for c.Params.ByName(key)
 func (c *Context) Param(key string) string {
 	return c.Params.ByName(key)
 }
@@ -195,6 +210,13 @@ func (c *Context) DefaultPostForm(key, defaultValue string) string {
 	return defaultValue
 }
 
+// DefaultQuery returns the keyed url query value if it exists, othewise it returns the
+// specified defaultValue.
+// ```
+// /?name=Manu
+// c.DefaultQuery("name", "unknown") == "Manu"
+// c.DefaultQuery("id", "none") == "none"
+// ```
 func (c *Context) DefaultQuery(key, defaultValue string) string {
 	if va, ok := c.query(key); ok {
 		return va
@@ -224,22 +246,26 @@ func (c *Context) postForm(key string) (string, bool) {
 	return "", false
 }
 
-// This function checks the Content-Type to select a binding engine automatically,
+// Bind 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.
+// otherwise --> 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{}) error {
 	b := binding.Default(c.Request.Method, c.ContentType())
 	return c.BindWith(obj, b)
 }
 
-// Shortcut for c.BindWith(obj, binding.JSON)
+// BindJSON is a shortcut for c.BindWith(obj, binding.JSON)
 func (c *Context) BindJSON(obj interface{}) error {
 	return c.BindWith(obj, binding.JSON)
 }
 
+// BindWith binds the passed struct pointer using the specified binding engine.
+// See the binding package.
 func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
 	if err := b.Bind(c.Request, obj); err != nil {
 		c.AbortWithError(400, err).SetType(ErrorTypeBind)
@@ -248,7 +274,7 @@ func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
 	return nil
 }
 
-// Best effort algoritm to return the real client IP, it parses
+// ClientIP implements a best effort algorithm to return the real client IP, it parses
 // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
 func (c *Context) ClientIP() string {
 	if c.engine.ForwardedByClientIP {
@@ -268,6 +294,7 @@ func (c *Context) ClientIP() string {
 	return strings.TrimSpace(c.Request.RemoteAddr)
 }
 
+// ContentType returns the Content-Type header of the request.
 func (c *Context) ContentType() string {
 	return filterFlags(c.requestHeader("Content-Type"))
 }
@@ -283,8 +310,8 @@ func (c *Context) requestHeader(key string) string {
 /******** RESPONSE RENDERING ********/
 /************************************/
 
-// Intelligent shortcut for c.Writer.Header().Set(key, value)
-// it writes a header in the response.
+// Header is a intelligent shortcut for c.Writer.Header().Set(key, value)
+// It writes a header in the response.
 // If value == "", this method removes the header `c.Writer.Header().Del(key)`
 func (c *Context) Header(key, value string) {
 	if len(value) == 0 {
@@ -306,7 +333,7 @@ func (c *Context) renderError(err error) {
 	c.AbortWithError(500, err).SetType(ErrorTypeRender)
 }
 
-// Renders the HTTP template specified by its file name.
+// HTML 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{}) {
@@ -314,7 +341,7 @@ func (c *Context) HTML(code int, name string, obj interface{}) {
 	c.Render(code, instance)
 }
 
-// Serializes the given struct as pretty JSON (indented + endlines) into the response body.
+// IndentedJSON serializes the given struct as pretty JSON (indented + endlines) into the response body.
 // It also sets the Content-Type as "application/json".
 // WARNING: we recommend to use this only for development propuses since printing pretty JSON is
 // more CPU and bandwidth consuming. Use Context.JSON() instead.
@@ -322,7 +349,7 @@ 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.
+// JSON serializes the given struct as JSON into the response body.
 // It also sets the Content-Type as "application/json".
 func (c *Context) JSON(code int, obj interface{}) {
 	c.writermem.WriteHeader(code)
@@ -331,19 +358,19 @@ func (c *Context) JSON(code int, obj interface{}) {
 	}
 }
 
-// Serializes the given struct as XML into the response body.
+// XML serializes the given struct as XML into the response body.
 // It also sets the Content-Type as "application/xml".
 func (c *Context) XML(code int, obj interface{}) {
 	c.Render(code, render.XML{Data: obj})
 }
 
-// Writes the given string into the response body.
+// String writes the given string into the response body.
 func (c *Context) String(code int, format string, values ...interface{}) {
 	c.writermem.WriteHeader(code)
 	render.WriteString(c.Writer, format, values)
 }
 
-// Returns a HTTP redirect to the specific location.
+// Redirect returns a HTTP redirect to the specific location.
 func (c *Context) Redirect(code int, location string) {
 	c.Render(-1, render.Redirect{
 		Code:     code,
@@ -352,7 +379,7 @@ func (c *Context) Redirect(code int, location string) {
 	})
 }
 
-// Writes some data into the body stream and updates the HTTP code.
+// Data writes some data into the body stream and updates the HTTP code.
 func (c *Context) Data(code int, contentType string, data []byte) {
 	c.Render(code, render.Data{
 		ContentType: contentType,
@@ -360,11 +387,12 @@ func (c *Context) Data(code int, contentType string, data []byte) {
 	})
 }
 
-// Writes the specified file into the body stream in a efficient way.
+// File writes the specified file into the body stream in a efficient way.
 func (c *Context) File(filepath string) {
 	http.ServeFile(c.Writer, c.Request, filepath)
 }
 
+// SSEvent writes a Server-Sent Event into the body stream.
 func (c *Context) SSEvent(name string, message interface{}) {
 	c.Render(-1, sse.Event{
 		Event: name,

+ 65 - 13
context_test.go

@@ -42,6 +42,9 @@ func createMultipartRequest() *http.Request {
 	must(mw.SetBoundary(boundary))
 	must(mw.WriteField("foo", "bar"))
 	must(mw.WriteField("bar", "foo"))
+	must(mw.WriteField("bar", "foo2"))
+	must(mw.WriteField("array", "first"))
+	must(mw.WriteField("array", "second"))
 	req, err := http.NewRequest("POST", "/", body)
 	must(err)
 	req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
@@ -77,6 +80,25 @@ func TestContextReset(t *testing.T) {
 	assert.Equal(t, c.Writer.(*responseWriter), &c.writermem)
 }
 
+func TestContextHandlers(t *testing.T) {
+	c, _, _ := createTestContext()
+	assert.Nil(t, c.handlers)
+	assert.Nil(t, c.handlers.Last())
+
+	c.handlers = HandlersChain{}
+	assert.NotNil(t, c.handlers)
+	assert.Nil(t, c.handlers.Last())
+
+	f := func(c *Context) {}
+	g := func(c *Context) {}
+
+	c.handlers = HandlersChain{f}
+	compareFunc(t, f, c.handlers.Last())
+
+	c.handlers = HandlersChain{f, g}
+	compareFunc(t, g, c.handlers.Last())
+}
+
 // TestContextSetGet tests that a parameter is set correctly on the
 // current context and can be retrieved using Get.
 func TestContextSetGet(t *testing.T) {
@@ -129,12 +151,23 @@ func TestContextCopy(t *testing.T) {
 	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.index, abortIndex)
 	assert.Equal(t, cp.Keys, c.Keys)
 	assert.Equal(t, cp.engine, c.engine)
 	assert.Equal(t, cp.Params, c.Params)
 }
 
+func TestContextHandlerName(t *testing.T) {
+	c, _, _ := createTestContext()
+	c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest}
+
+	assert.Equal(t, c.HandlerName(), "github.com/gin-gonic/gin.handlerNameTest")
+}
+
+func handlerNameTest(c *Context) {
+
+}
+
 func TestContextQuery(t *testing.T) {
 	c, _, _ := createTestContext()
 	c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10", nil)
@@ -154,8 +187,8 @@ func TestContextQuery(t *testing.T) {
 
 func TestContextQueryAndPostForm(t *testing.T) {
 	c, _, _ := createTestContext()
-	body := bytes.NewBufferString("foo=bar&page=11&both=POST")
-	c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main", body)
+	body := bytes.NewBufferString("foo=bar&page=11&both=POST&foo=second")
+	c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body)
 	c.Request.Header.Add("Content-Type", MIMEPOSTForm)
 
 	assert.Equal(t, c.DefaultPostForm("foo", "none"), "bar")
@@ -178,16 +211,18 @@ func TestContextQueryAndPostForm(t *testing.T) {
 	assert.Empty(t, c.Query("NoKey"))
 
 	var obj struct {
-		Foo  string `form:"foo"`
-		Id   string `form:"id"`
-		Page string `form:"page"`
-		Both string `form:"both"`
+		Foo   string   `form:"foo"`
+		ID    string   `form:"id"`
+		Page  string   `form:"page"`
+		Both  string   `form:"both"`
+		Array []string `form:"array[]"`
 	}
 	assert.NoError(t, c.Bind(&obj))
 	assert.Equal(t, obj.Foo, "bar")
-	assert.Equal(t, obj.Id, "main")
+	assert.Equal(t, obj.ID, "main")
 	assert.Equal(t, obj.Page, "11")
 	assert.Equal(t, obj.Both, "POST")
+	assert.Equal(t, obj.Array, []string{"first", "second"})
 }
 
 func TestContextPostFormMultipart(t *testing.T) {
@@ -195,16 +230,19 @@ func TestContextPostFormMultipart(t *testing.T) {
 	c.Request = createMultipartRequest()
 
 	var obj struct {
-		Foo string `form:"foo"`
-		Bar string `form:"bar"`
+		Foo   string   `form:"foo"`
+		Bar   string   `form:"bar"`
+		Array []string `form:"array"`
 	}
 	assert.NoError(t, c.Bind(&obj))
 	assert.Equal(t, obj.Bar, "foo")
 	assert.Equal(t, obj.Foo, "bar")
+	assert.Equal(t, obj.Array, []string{"first", "second"})
 
 	assert.Empty(t, c.Query("foo"))
 	assert.Empty(t, c.Query("bar"))
 	assert.Equal(t, c.PostForm("foo"), "bar")
+	assert.Equal(t, c.PostForm("array"), "first")
 	assert.Equal(t, c.PostForm("bar"), "foo")
 }
 
@@ -313,7 +351,7 @@ func TestContextRenderSSE(t *testing.T) {
 		"bar": "foo",
 	})
 
-	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")
+	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")
 }
 
 func TestContextRenderFile(t *testing.T) {
@@ -397,6 +435,20 @@ func TestContextNegotiationFormatCustum(t *testing.T) {
 	assert.Equal(t, c.NegotiateFormat(MIMEJSON), MIMEJSON)
 }
 
+func TestContextIsAborted(t *testing.T) {
+	c, _, _ := createTestContext()
+	assert.False(t, c.IsAborted())
+
+	c.Abort()
+	assert.True(t, c.IsAborted())
+
+	c.Next()
+	assert.True(t, c.IsAborted())
+
+	c.index++
+	assert.True(t, c.IsAborted())
+}
+
 // TestContextData tests that the response can be written from `bytesting`
 // with specified MIME type
 func TestContextAbortWithStatus(t *testing.T) {
@@ -405,7 +457,7 @@ func TestContextAbortWithStatus(t *testing.T) {
 	c.AbortWithStatus(401)
 	c.Writer.WriteHeaderNow()
 
-	assert.Equal(t, c.index, AbortIndex)
+	assert.Equal(t, c.index, abortIndex)
 	assert.Equal(t, c.Writer.Status(), 401)
 	assert.Equal(t, w.Code, 401)
 	assert.True(t, c.IsAborted())
@@ -457,7 +509,7 @@ func TestContextAbortWithError(t *testing.T) {
 	c.Writer.WriteHeaderNow()
 
 	assert.Equal(t, w.Code, 401)
-	assert.Equal(t, c.index, AbortIndex)
+	assert.Equal(t, c.index, abortIndex)
 	assert.True(t, c.IsAborted())
 }
 

+ 32 - 3
debug.go

@@ -4,11 +4,18 @@
 
 package gin
 
-import "log"
+import (
+	"bytes"
+	"html/template"
+	"log"
+)
 
 func init() {
 	log.SetFlags(0)
 }
+
+// IsDebugging returns true if the framework is running in debug mode.
+// Use SetMode(gin.Release) to switch to disable the debug mode.
 func IsDebugging() bool {
 	return ginMode == debugCode
 }
@@ -16,18 +23,30 @@ func IsDebugging() bool {
 func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
 	if IsDebugging() {
 		nuHandlers := len(handlers)
-		handlerName := nameOfFunction(handlers[nuHandlers-1])
+		handlerName := nameOfFunction(handlers.Last())
 		debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
 	}
 }
 
+func debugPrintLoadTemplate(tmpl *template.Template) {
+	if IsDebugging() {
+		var buf bytes.Buffer
+		for _, tmpl := range tmpl.Templates() {
+			buf.WriteString("\t- ")
+			buf.WriteString(tmpl.Name())
+			buf.WriteString("\n")
+		}
+		debugPrint("Loaded HTML Templates (%d): \n%s\n", len(tmpl.Templates()), buf.String())
+	}
+}
+
 func debugPrint(format string, values ...interface{}) {
 	if IsDebugging() {
 		log.Printf("[GIN-debug] "+format, values...)
 	}
 }
 
-func debugPrintWARNING() {
+func debugPrintWARNINGNew() {
 	debugPrint(`[WARNING] Running in "debug" mode. Switch to "release" mode in production.
  - using env:	export GIN_MODE=release
  - using code:	gin.SetMode(gin.ReleaseMode)
@@ -35,6 +54,16 @@ func debugPrintWARNING() {
 `)
 }
 
+func debugPrintWARNINGSetHTMLTemplate() {
+	debugPrint(`[WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called
+at initialization. ie. before any route is registered or the router is listening in a socket:
+
+	router := gin.Default()
+	router.SetHTMLTemplate(template) // << good place
+
+`)
+}
+
 func debugPrintError(err error) {
 	if err != nil {
 		debugPrint("[ERROR] %v\n", err)

+ 9 - 0
debug_test.go

@@ -57,6 +57,15 @@ func TestDebugPrintError(t *testing.T) {
 	assert.Equal(t, w.String(), "[GIN-debug] [ERROR] this is an error\n")
 }
 
+func TestDebugPrintRoutes(t *testing.T) {
+	var w bytes.Buffer
+	setup(&w)
+	defer teardown()
+
+	debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
+	assert.Equal(t, w.String(), "[GIN-debug] GET   /path/to/route/:param     --> github.com/gin-gonic/gin.handlerNameTest (2 handlers)\n")
+}
+
 func setup(w io.Writer) {
 	SetMode(DebugMode)
 	log.SetOutput(w)

+ 3 - 3
errors.go

@@ -102,10 +102,10 @@ func (a errorMsgs) ByType(typ ErrorType) errorMsgs {
 // Shortcut for errors[len(errors)-1]
 func (a errorMsgs) Last() *Error {
 	length := len(a)
-	if length == 0 {
-		return nil
+	if length > 0 {
+		return a[length-1]
 	}
-	return a[length-1]
+	return nil
 }
 
 // Returns an array will all the error messages.

+ 1 - 0
errors_test.go

@@ -63,6 +63,7 @@ func TestErrorSlice(t *testing.T) {
 		{Err: errors.New("third"), Type: ErrorTypePublic, Meta: H{"status": "400"}},
 	}
 
+	assert.Equal(t, errs, errs.ByType(ErrorTypeAny))
 	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"})

+ 2 - 3
fs.go

@@ -14,7 +14,7 @@ type (
 	}
 )
 
-// It returns a http.Filesystem that can be used by http.FileServer(). It is used interally
+// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used interally
 // in router.Static().
 // if listDirectory == true, then it works the same as http.Dir() otherwise it returns
 // a filesystem that prevents http.FileServer() to list the directory files.
@@ -22,9 +22,8 @@ func Dir(root string, listDirectory bool) http.FileSystem {
 	fs := http.Dir(root)
 	if listDirectory {
 		return fs
-	} else {
-		return &onlyfilesFS{fs}
 	}
+	return &onlyfilesFS{fs}
 }
 
 // Conforms to http.Filesystem

+ 68 - 23
gin.go

@@ -14,16 +14,34 @@ import (
 	"github.com/gin-gonic/gin/render"
 )
 
+// Framework's version
 const Version = "v1.0rc2"
 
 var default404Body = []byte("404 page not found")
 var default405Body = []byte("405 method not allowed")
 
+type HandlerFunc func(*Context)
+type HandlersChain []HandlerFunc
+
+// Last returns the last handler in the chain. ie. the last handler is the main own.
+func (c HandlersChain) Last() HandlerFunc {
+	length := len(c)
+	if length > 0 {
+		return c[length-1]
+	}
+	return nil
+}
+
 type (
-	HandlerFunc   func(*Context)
-	HandlersChain []HandlerFunc
+	RoutesInfo []RouteInfo
+	RouteInfo  struct {
+		Method  string
+		Path    string
+		Handler string
+	}
 
-	// Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middleware.
+	// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
+	// Create an instance of Engine, by using New() or Default()
 	Engine struct {
 		RouterGroup
 		HTMLRender  render.HTMLRender
@@ -63,14 +81,20 @@ type (
 	}
 )
 
-// Returns a new blank Engine instance without any middleware attached.
-// The most basic configuration
+var _ IRouter = &Engine{}
+
+// New returns a new blank Engine instance without any middleware attached.
+// By default the configuration is:
+// - RedirectTrailingSlash:  true
+// - RedirectFixedPath:      false
+// - HandleMethodNotAllowed: false
+// - ForwardedByClientIP:    true
 func New() *Engine {
-	debugPrintWARNING()
+	debugPrintWARNINGNew()
 	engine := &Engine{
 		RouterGroup: RouterGroup{
 			Handlers: nil,
-			BasePath: "/",
+			basePath: "/",
 			root:     true,
 		},
 		RedirectTrailingSlash:  true,
@@ -86,7 +110,7 @@ func New() *Engine {
 	return engine
 }
 
-// Returns a Engine instance with the Logger and Recovery already attached.
+// Default returns an Engine instance with the Logger and Recovery middleware already attached.
 func Default() *Engine {
 	engine := New()
 	engine.Use(Recovery(), Logger())
@@ -99,6 +123,7 @@ func (engine *Engine) allocateContext() *Context {
 
 func (engine *Engine) LoadHTMLGlob(pattern string) {
 	if IsDebugging() {
+		debugPrintLoadTemplate(template.Must(template.ParseGlob(pattern)))
 		engine.HTMLRender = render.HTMLDebug{Glob: pattern}
 	} else {
 		templ := template.Must(template.ParseGlob(pattern))
@@ -117,12 +142,7 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
 
 func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
 	if len(engine.trees) > 0 {
-		debugPrint(`[WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called
-at initialization. ie. before any route is registered or the router is listening in a socket:
-
-	router := gin.Default()
-	router.SetHTMLTemplate(template) // << good place
-`)
+		debugPrintWARNINGSetHTMLTemplate()
 	}
 	engine.HTMLRender = render.HTMLProduction{Template: templ}
 }
@@ -142,7 +162,7 @@ func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
 // Attachs a global middleware to the router. ie. the middleware attached though Use() will be
 // included in the handlers chain for every single request. Even 404, 405, static files...
 // For example, this is the right place for a logger or error management middleware.
-func (engine *Engine) Use(middleware ...HandlerFunc) routesInterface {
+func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
 	engine.RouterGroup.Use(middleware...)
 	engine.rebuild404Handlers()
 	engine.rebuild405Handlers()
@@ -181,18 +201,43 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
 	root.addRoute(path, handlers)
 }
 
-// The router is attached to a http.Server and starts listening and serving HTTP requests.
+// Routes returns a slice of registered routes, including some useful information, such as:
+// the http method, path and the handler name.
+func (engine *Engine) Routes() (routes RoutesInfo) {
+	for _, tree := range engine.trees {
+		routes = iterate("", tree.method, routes, tree.root)
+	}
+	return routes
+}
+
+func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
+	path += root.path
+	if len(root.handlers) > 0 {
+		routes = append(routes, RouteInfo{
+			Method:  method,
+			Path:    path,
+			Handler: nameOfFunction(root.handlers.Last()),
+		})
+	}
+	for _, child := range root.children {
+		routes = iterate(path, method, routes, child)
+	}
+	return routes
+}
+
+// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
 // It is a shortcut for http.ListenAndServe(addr, router)
 // Note: this method will block the calling goroutine undefinitelly unless an error happens.
-func (engine *Engine) Run(addr string) (err error) {
-	debugPrint("Listening and serving HTTP on %s\n", addr)
+func (engine *Engine) Run(addr ...string) (err error) {
 	defer func() { debugPrintError(err) }()
 
-	err = http.ListenAndServe(addr, engine)
+	address := resolveAddress(addr)
+	debugPrint("Listening and serving HTTP on %s\n", address)
+	err = http.ListenAndServe(address, engine)
 	return
 }
 
-// The router is attached to a http.Server and starts listening and serving HTTPS requests.
+// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
 // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
 // Note: this method will block the calling goroutine undefinitelly unless an error happens.
 func (engine *Engine) RunTLS(addr string, certFile string, keyFile string) (err error) {
@@ -203,8 +248,8 @@ func (engine *Engine) RunTLS(addr string, certFile string, keyFile string) (err
 	return
 }
 
-// The router is attached to a http.Server and starts listening and serving HTTP requests
-// through the specified unix socket (ie. a file)
+// RunUnix attaches the router to a http.Server and starts listening and serving HTTP requests
+// through the specified unix socket (ie. a file).
 // Note: this method will block the calling goroutine undefinitelly unless an error happens.
 func (engine *Engine) RunUnix(file string) (err error) {
 	debugPrint("Listening and serving HTTP on unix:/%s", file)
@@ -251,7 +296,7 @@ func (engine *Engine) handleHTTPRequest(context *Context) {
 				return
 
 			} else if httpMethod != "CONNECT" && path != "/" {
-				if tsr && engine.RedirectFixedPath {
+				if tsr && engine.RedirectTrailingSlash {
 					redirectTrailingSlash(context)
 					return
 				}

+ 53 - 16
gin_integration_test.go

@@ -2,49 +2,86 @@ package gin
 
 import (
 	"bufio"
-	"bytes"
 	"fmt"
 	"io/ioutil"
 	"net"
 	"net/http"
+	"os"
 	"testing"
 	"time"
 
 	"github.com/stretchr/testify/assert"
 )
 
-func TestRun(t *testing.T) {
-	buffer := new(bytes.Buffer)
+func testRequest(t *testing.T, url string) {
+	resp, err := http.Get(url)
+	defer resp.Body.Close()
+	assert.NoError(t, err)
+
+	body, ioerr := ioutil.ReadAll(resp.Body)
+	assert.NoError(t, ioerr)
+	assert.Equal(t, "it worked", string(body), "resp body should match")
+	assert.Equal(t, "200 OK", resp.Status, "should get a 200")
+}
+
+func TestRunEmpty(t *testing.T) {
+	SetMode(DebugMode)
+	os.Setenv("PORT", "")
 	router := New()
 	go func() {
-		router.Use(LoggerWithWriter(buffer))
 		router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
-		router.Run(":5150")
+		assert.NoError(t, router.Run())
 	}()
 	// have to wait for the goroutine to start and run the server
 	// otherwise the main thread will complete
 	time.Sleep(5 * time.Millisecond)
 
-	assert.Error(t, router.Run(":5150"))
+	assert.Error(t, router.Run(":8080"))
+	testRequest(t, "http://localhost:8080/example")
+}
 
-	resp, err := http.Get("http://localhost:5150/example")
-	defer resp.Body.Close()
-	assert.NoError(t, err)
+func TestRunEmptyWithEnv(t *testing.T) {
+	os.Setenv("PORT", "3123")
+	router := New()
+	go func() {
+		router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
+		assert.NoError(t, router.Run())
+	}()
+	// have to wait for the goroutine to start and run the server
+	// otherwise the main thread will complete
+	time.Sleep(5 * time.Millisecond)
 
-	body, ioerr := ioutil.ReadAll(resp.Body)
-	assert.NoError(t, ioerr)
-	assert.Equal(t, "it worked", string(body[:]), "resp body should match")
-	assert.Equal(t, "200 OK", resp.Status, "should get a 200")
+	assert.Error(t, router.Run(":3123"))
+	testRequest(t, "http://localhost:3123/example")
+}
+
+func TestRunTooMuchParams(t *testing.T) {
+	router := New()
+	assert.Panics(t, func() {
+		router.Run("2", "2")
+	})
+}
+
+func TestRunWithPort(t *testing.T) {
+	router := New()
+	go func() {
+		router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
+		assert.NoError(t, router.Run(":5150"))
+	}()
+	// have to wait for the goroutine to start and run the server
+	// otherwise the main thread will complete
+	time.Sleep(5 * time.Millisecond)
+
+	assert.Error(t, router.Run(":5150"))
+	testRequest(t, "http://localhost:5150/example")
 }
 
 func TestUnixSocket(t *testing.T) {
-	buffer := new(bytes.Buffer)
 	router := New()
 
 	go func() {
-		router.Use(LoggerWithWriter(buffer))
 		router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
-		router.RunUnix("/tmp/unix_unit_test")
+		assert.NoError(t, router.RunUnix("/tmp/unix_unit_test"))
 	}()
 	// have to wait for the goroutine to start and run the server
 	// otherwise the main thread will complete

+ 65 - 2
gin_test.go

@@ -14,7 +14,6 @@ import (
 //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() {
@@ -23,11 +22,30 @@ func init() {
 
 func TestCreateEngine(t *testing.T) {
 	router := New()
-	assert.Equal(t, "/", router.BasePath)
+	assert.Equal(t, "/", router.basePath)
 	assert.Equal(t, router.engine, router)
 	assert.Empty(t, router.Handlers)
 }
 
+// func TestLoadHTMLDebugMode(t *testing.T) {
+// 	router := New()
+// 	SetMode(DebugMode)
+// 	router.LoadHTMLGlob("*.testtmpl")
+// 	r := router.HTMLRender.(render.HTMLDebug)
+// 	assert.Empty(t, r.Files)
+// 	assert.Equal(t, r.Glob, "*.testtmpl")
+//
+// 	router.LoadHTMLFiles("index.html.testtmpl", "login.html.testtmpl")
+// 	r = router.HTMLRender.(render.HTMLDebug)
+// 	assert.Empty(t, r.Glob)
+// 	assert.Equal(t, r.Files, []string{"index.html", "login.html"})
+// 	SetMode(TestMode)
+// }
+
+func TestLoadHTMLReleaseMode(t *testing.T) {
+
+}
+
 func TestAddRoute(t *testing.T) {
 	router := New()
 	router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})
@@ -180,3 +198,48 @@ func compareFunc(t *testing.T, a, b interface{}) {
 		t.Error("different functions")
 	}
 }
+
+func TestListOfRoutes(t *testing.T) {
+	router := New()
+	router.GET("/favicon.ico", handler_test1)
+	router.GET("/", handler_test1)
+	group := router.Group("/users")
+	{
+		group.GET("/", handler_test2)
+		group.GET("/:id", handler_test1)
+		group.POST("/:id", handler_test2)
+	}
+	router.Static("/static", ".")
+
+	list := router.Routes()
+
+	assert.Len(t, list, 7)
+	assert.Contains(t, list, RouteInfo{
+		Method:  "GET",
+		Path:    "/favicon.ico",
+		Handler: "github.com/gin-gonic/gin.handler_test1",
+	})
+	assert.Contains(t, list, RouteInfo{
+		Method:  "GET",
+		Path:    "/",
+		Handler: "github.com/gin-gonic/gin.handler_test1",
+	})
+	assert.Contains(t, list, RouteInfo{
+		Method:  "GET",
+		Path:    "/users/",
+		Handler: "github.com/gin-gonic/gin.handler_test2",
+	})
+	assert.Contains(t, list, RouteInfo{
+		Method:  "GET",
+		Path:    "/users/:id",
+		Handler: "github.com/gin-gonic/gin.handler_test1",
+	})
+	assert.Contains(t, list, RouteInfo{
+		Method:  "POST",
+		Path:    "/users/:id",
+		Handler: "github.com/gin-gonic/gin.handler_test2",
+	})
+}
+
+func handler_test1(c *Context) {}
+func handler_test2(c *Context) {}

+ 28 - 0
logger_test.go

@@ -6,6 +6,7 @@ package gin
 
 import (
 	"bytes"
+	"errors"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -96,3 +97,30 @@ func TestColorForStatus(t *testing.T) {
 	assert.Equal(t, colorForStatus(404), string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), "4xx should be yellow")
 	assert.Equal(t, colorForStatus(2), string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), "other things should be red")
 }
+
+func TestErrorLogger(t *testing.T) {
+	router := New()
+	router.Use(ErrorLogger())
+	router.GET("/error", func(c *Context) {
+		c.Error(errors.New("this is an error"))
+	})
+	router.GET("/abort", func(c *Context) {
+		c.AbortWithError(401, errors.New("no authorized"))
+	})
+	router.GET("/print", func(c *Context) {
+		c.Error(errors.New("this is an error"))
+		c.String(500, "hola!")
+	})
+
+	w := performRequest(router, "GET", "/error")
+	assert.Equal(t, w.Code, 200)
+	assert.Equal(t, w.Body.String(), "{\"error\":\"this is an error\"}\n")
+
+	w = performRequest(router, "GET", "/abort")
+	assert.Equal(t, w.Code, 401)
+	assert.Equal(t, w.Body.String(), "{\"error\":\"no authorized\"}\n")
+
+	w = performRequest(router, "GET", "/print")
+	assert.Equal(t, w.Code, 500)
+	assert.Equal(t, w.Body.String(), "hola!")
+}

+ 2 - 2
middleware_test.go

@@ -248,8 +248,8 @@ func TestMiddlewareWrite(t *testing.T) {
 	assert.Equal(t, w.Body.String(), `hola
 <map><foo>bar</foo></map>{"foo":"bar"}
 {"foo":"bar"}
-event: test
-data: message
+event:test
+data:message
 
 `)
 }

+ 10 - 0
response_writer.go

@@ -23,10 +23,20 @@ type (
 		http.Flusher
 		http.CloseNotifier
 
+		// Returns the HTTP response status code of the current request.
 		Status() int
+
+		// Returns the number of bytes already written into the response http body.
+		// See Written()
 		Size() int
+
+		// Writes the string into the response body.
 		WriteString(string) (int, error)
+
+		// Returns true if the response body was already written.
 		Written() bool
+
+		// Forces to write the http header (status code + headers).
 		WriteHeaderNow()
 	}
 

+ 73 - 56
routergroup.go

@@ -11,49 +11,69 @@ import (
 	"strings"
 )
 
-type routesInterface interface {
-	Use(...HandlerFunc) routesInterface
-
-	Handle(string, string, ...HandlerFunc) routesInterface
-	Any(string, ...HandlerFunc) routesInterface
-	GET(string, ...HandlerFunc) routesInterface
-	POST(string, ...HandlerFunc) routesInterface
-	DELETE(string, ...HandlerFunc) routesInterface
-	PATCH(string, ...HandlerFunc) routesInterface
-	PUT(string, ...HandlerFunc) routesInterface
-	OPTIONS(string, ...HandlerFunc) routesInterface
-	HEAD(string, ...HandlerFunc) routesInterface
-
-	StaticFile(string, string) routesInterface
-	Static(string, string) routesInterface
-	StaticFS(string, http.FileSystem) routesInterface
-}
-
-// Used internally to configure router, a RouterGroup is associated with a prefix
-// and an array of handlers (middleware)
-type RouterGroup struct {
-	Handlers HandlersChain
-	BasePath string
-	engine   *Engine
-	root     bool
-}
-
-// Adds middleware to the group, see example code in github.
-func (group *RouterGroup) Use(middleware ...HandlerFunc) routesInterface {
+type (
+	IRouter interface {
+		IRoutes
+		Group(string, ...HandlerFunc) *RouterGroup
+	}
+
+	IRoutes interface {
+		Use(...HandlerFunc) IRoutes
+
+		Handle(string, string, ...HandlerFunc) IRoutes
+		Any(string, ...HandlerFunc) IRoutes
+		GET(string, ...HandlerFunc) IRoutes
+		POST(string, ...HandlerFunc) IRoutes
+		DELETE(string, ...HandlerFunc) IRoutes
+		PATCH(string, ...HandlerFunc) IRoutes
+		PUT(string, ...HandlerFunc) IRoutes
+		OPTIONS(string, ...HandlerFunc) IRoutes
+		HEAD(string, ...HandlerFunc) IRoutes
+
+		StaticFile(string, string) IRoutes
+		Static(string, string) IRoutes
+		StaticFS(string, http.FileSystem) IRoutes
+	}
+
+	// RouterGroup is used internally to configure router, a RouterGroup is associated with a prefix
+	// and an array of handlers (middleware)
+	RouterGroup struct {
+		Handlers HandlersChain
+		basePath string
+		engine   *Engine
+		root     bool
+	}
+)
+
+var _ IRouter = &RouterGroup{}
+
+// Use adds middleware to the group, see example code in github.
+func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
 	group.Handlers = append(group.Handlers, middleware...)
 	return group.returnObj()
 }
 
-// Creates a new router group. You should add all the routes that have common middlwares or the same path prefix.
+// Group creates a new router group. You should add all the routes that have common middlwares or the same path prefix.
 // 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),
-		BasePath: group.calculateAbsolutePath(relativePath),
+		basePath: group.calculateAbsolutePath(relativePath),
 		engine:   group.engine,
 	}
 }
 
+func (group *RouterGroup) BasePath() string {
+	return group.basePath
+}
+
+func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
+	absolutePath := group.calculateAbsolutePath(relativePath)
+	handlers = group.combineHandlers(handlers)
+	group.engine.addRoute(httpMethod, absolutePath, handlers)
+	return group.returnObj()
+}
+
 // Handle registers a new request handle and middleware with the given path and method.
 // The last handler should be the real handler, the other ones should be middleware that can and should be shared among different routes.
 // See the example code in github.
@@ -64,14 +84,7 @@ 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 HandlersChain) routesInterface {
-	absolutePath := group.calculateAbsolutePath(relativePath)
-	handlers = group.combineHandlers(handlers)
-	group.engine.addRoute(httpMethod, absolutePath, handlers)
-	return group.returnObj()
-}
-
-func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) routesInterface {
+func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
 	if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil {
 		panic("http method " + httpMethod + " is not valid")
 	}
@@ -79,42 +92,43 @@ func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...Ha
 }
 
 // POST is a shortcut for router.Handle("POST", path, handle)
-func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) routesInterface {
+func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
 	return group.handle("POST", relativePath, handlers)
 }
 
 // GET is a shortcut for router.Handle("GET", path, handle)
-func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) routesInterface {
+func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
 	return group.handle("GET", relativePath, handlers)
 }
 
 // DELETE is a shortcut for router.Handle("DELETE", path, handle)
-func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) routesInterface {
+func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
 	return group.handle("DELETE", relativePath, handlers)
 }
 
 // PATCH is a shortcut for router.Handle("PATCH", path, handle)
-func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) routesInterface {
+func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
 	return group.handle("PATCH", relativePath, handlers)
 }
 
 // PUT is a shortcut for router.Handle("PUT", path, handle)
-func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) routesInterface {
+func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes {
 	return group.handle("PUT", relativePath, handlers)
 }
 
 // OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle)
-func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) routesInterface {
+func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {
 	return group.handle("OPTIONS", relativePath, handlers)
 }
 
 // HEAD is a shortcut for router.Handle("HEAD", path, handle)
-func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) routesInterface {
+func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
 	return group.handle("HEAD", relativePath, handlers)
 }
 
-func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) routesInterface {
-	// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE
+// Any registers a route that matches all the HTTP methods.
+// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE
+func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
 	group.handle("GET", relativePath, handlers)
 	group.handle("POST", relativePath, handlers)
 	group.handle("PUT", relativePath, handlers)
@@ -127,7 +141,9 @@ func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) rout
 	return group.returnObj()
 }
 
-func (group *RouterGroup) StaticFile(relativePath, filepath string) routesInterface {
+// StaticFile registers a single route in order to server a single file of the local filesystem.
+// router.StaticFile("favicon.ico", "./resources/favicon.ico")
+func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
 	if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
 		panic("URL parameters can not be used when serving a static file")
 	}
@@ -145,11 +161,13 @@ func (group *RouterGroup) StaticFile(relativePath, filepath string) routesInterf
 // To use the operating system's file system implementation,
 // use :
 //     router.Static("/static", "/var/www")
-func (group *RouterGroup) Static(relativePath, root string) routesInterface {
+func (group *RouterGroup) Static(relativePath, root string) IRoutes {
 	return group.StaticFS(relativePath, Dir(root, false))
 }
 
-func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) routesInterface {
+// StaticFS works just like `Static()` but a custom `http.FileSystem` can be used instead.
+// Gin by default user: gin.Dir()
+func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes {
 	if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
 		panic("URL parameters can not be used when serving a static folder")
 	}
@@ -176,7 +194,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
 
 func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
 	finalSize := len(group.Handlers) + len(handlers)
-	if finalSize >= int(AbortIndex) {
+	if finalSize >= int(abortIndex) {
 		panic("too many handlers")
 	}
 	mergedHandlers := make(HandlersChain, finalSize)
@@ -186,13 +204,12 @@ func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain
 }
 
 func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
-	return joinPaths(group.BasePath, relativePath)
+	return joinPaths(group.basePath, relativePath)
 }
 
-func (group *RouterGroup) returnObj() routesInterface {
+func (group *RouterGroup) returnObj() IRoutes {
 	if group.root {
 		return group.engine
-	} else {
-		return group
 	}
+	return group
 }

+ 5 - 5
routergroup_test.go

@@ -20,14 +20,14 @@ func TestRouterGroupBasic(t *testing.T) {
 	group.Use(func(c *Context) {})
 
 	assert.Len(t, group.Handlers, 2)
-	assert.Equal(t, group.BasePath, "/hola")
+	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.BasePath(), "/hola/manu")
 	assert.Equal(t, group2.engine, router)
 }
 
@@ -44,10 +44,10 @@ func TestRouterGroupBasicHandle(t *testing.T) {
 func performRequestInGroup(t *testing.T, method string) {
 	router := New()
 	v1 := router.Group("v1", func(c *Context) {})
-	assert.Equal(t, v1.BasePath, "/v1")
+	assert.Equal(t, v1.BasePath(), "/v1")
 
 	login := v1.Group("/login/", func(c *Context) {}, func(c *Context) {})
-	assert.Equal(t, login.BasePath, "/v1/login/")
+	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)
@@ -157,7 +157,7 @@ func TestRouterGroupPipeline(t *testing.T) {
 	testRoutesInterface(t, v1)
 }
 
-func testRoutesInterface(t *testing.T, r routesInterface) {
+func testRoutesInterface(t *testing.T, r IRoutes) {
 	handler := func(c *Context) {}
 	assert.Equal(t, r, r.Use(handler))
 

+ 76 - 3
routes_test.go

@@ -40,7 +40,6 @@ func testRouteOK(method string, t *testing.T) {
 
 	performRequest(r, method, "/test2")
 	assert.True(t, passedAny)
-
 }
 
 // TestSingleRouteOK tests that POST route is correctly invoked.
@@ -110,7 +109,6 @@ func TestRouterGroupRouteOK(t *testing.T) {
 	testRouteOK("TRACE", t)
 }
 
-// TestSingleRouteOK tests that POST route is correctly invoked.
 func TestRouteNotOK(t *testing.T) {
 	testRouteNotOK("GET", t)
 	testRouteNotOK("POST", t)
@@ -123,7 +121,6 @@ func TestRouteNotOK(t *testing.T) {
 	testRouteNotOK("TRACE", t)
 }
 
-// TestSingleRouteOK tests that POST route is correctly invoked.
 func TestRouteNotOK2(t *testing.T) {
 	testRouteNotOK2("GET", t)
 	testRouteNotOK2("POST", t)
@@ -136,6 +133,82 @@ func TestRouteNotOK2(t *testing.T) {
 	testRouteNotOK2("TRACE", t)
 }
 
+func TestRouteRedirectTrailingSlash(t *testing.T) {
+	router := New()
+	router.RedirectFixedPath = false
+	router.RedirectTrailingSlash = true
+	router.GET("/path", func(c *Context) {})
+	router.GET("/path2/", func(c *Context) {})
+	router.POST("/path3", func(c *Context) {})
+	router.PUT("/path4/", func(c *Context) {})
+
+	w := performRequest(router, "GET", "/path/")
+	assert.Equal(t, w.Header().Get("Location"), "/path")
+	assert.Equal(t, w.Code, 301)
+
+	w = performRequest(router, "GET", "/path2")
+	assert.Equal(t, w.Header().Get("Location"), "/path2/")
+	assert.Equal(t, w.Code, 301)
+
+	w = performRequest(router, "POST", "/path3/")
+	assert.Equal(t, w.Header().Get("Location"), "/path3")
+	assert.Equal(t, w.Code, 307)
+
+	w = performRequest(router, "PUT", "/path4")
+	assert.Equal(t, w.Header().Get("Location"), "/path4/")
+	assert.Equal(t, w.Code, 307)
+
+	w = performRequest(router, "GET", "/path")
+	assert.Equal(t, w.Code, 200)
+
+	w = performRequest(router, "GET", "/path2/")
+	assert.Equal(t, w.Code, 200)
+
+	w = performRequest(router, "POST", "/path3")
+	assert.Equal(t, w.Code, 200)
+
+	w = performRequest(router, "PUT", "/path4/")
+	assert.Equal(t, w.Code, 200)
+
+	router.RedirectTrailingSlash = false
+
+	w = performRequest(router, "GET", "/path/")
+	assert.Equal(t, w.Code, 404)
+	w = performRequest(router, "GET", "/path2")
+	assert.Equal(t, w.Code, 404)
+	w = performRequest(router, "POST", "/path3/")
+	assert.Equal(t, w.Code, 404)
+	w = performRequest(router, "PUT", "/path4")
+	assert.Equal(t, w.Code, 404)
+}
+
+func TestRouteRedirectFixedPath(t *testing.T) {
+	router := New()
+	router.RedirectFixedPath = true
+	router.RedirectTrailingSlash = false
+
+	router.GET("/path", func(c *Context) {})
+	router.GET("/Path2", func(c *Context) {})
+	router.POST("/PATH3", func(c *Context) {})
+	router.POST("/Path4/", func(c *Context) {})
+
+	w := performRequest(router, "GET", "/PATH")
+	assert.Equal(t, w.Header().Get("Location"), "/path")
+	assert.Equal(t, w.Code, 301)
+
+	w = performRequest(router, "GET", "/path2")
+	assert.Equal(t, w.Header().Get("Location"), "/Path2")
+	assert.Equal(t, w.Code, 301)
+
+	w = performRequest(router, "POST", "/path3")
+	assert.Equal(t, w.Header().Get("Location"), "/PATH3")
+	assert.Equal(t, w.Code, 307)
+
+	w = performRequest(router, "POST", "/path4")
+	assert.Equal(t, w.Header().Get("Location"), "/Path4/")
+	assert.Equal(t, w.Code, 307)
+}
+
 // TestContextParamsGet tests that a parameter can be parsed from the URL.
 func TestRouteParamsByName(t *testing.T) {
 	name := ""

+ 18 - 0
utils.go

@@ -7,6 +7,7 @@ package gin
 import (
 	"encoding/xml"
 	"net/http"
+	"os"
 	"path"
 	"reflect"
 	"runtime"
@@ -129,3 +130,20 @@ func joinPaths(absolutePath, relativePath string) string {
 	}
 	return finalPath
 }
+
+func resolveAddress(addr []string) string {
+	switch len(addr) {
+	case 0:
+		if port := os.Getenv("PORT"); len(port) > 0 {
+			debugPrint("Environment variable PORT=\"%s\"", port)
+			return ":" + port
+		} else {
+			debugPrint("Environment variable PORT is undefined. Using port :8080 by default")
+			return ":8080"
+		}
+	case 1:
+		return addr[0]
+	default:
+		panic("too much parameters")
+	}
+}

+ 27 - 0
utils_test.go

@@ -97,3 +97,30 @@ func TestJoinPaths(t *testing.T) {
 	assert.Equal(t, joinPaths("/a/", "/hola/"), "/a/hola/")
 	assert.Equal(t, joinPaths("/a/", "/hola//"), "/a/hola/")
 }
+
+type bindTestStruct struct {
+	Foo string `form:"foo" binding:"required"`
+	Bar int    `form:"bar" binding:"min=4"`
+}
+
+func TestBindMiddleware(t *testing.T) {
+	var value *bindTestStruct
+	var called bool
+	router := New()
+	router.GET("/", Bind(bindTestStruct{}), func(c *Context) {
+		called = true
+		value = c.MustGet(BindKey).(*bindTestStruct)
+	})
+	performRequest(router, "GET", "/?foo=hola&bar=10")
+	assert.True(t, called)
+	assert.Equal(t, value.Foo, "hola")
+	assert.Equal(t, value.Bar, 10)
+
+	called = false
+	performRequest(router, "GET", "/?foo=hola&bar=1")
+	assert.False(t, called)
+
+	assert.Panics(t, func() {
+		Bind(&bindTestStruct{})
+	})
+}