Merge branch 'master' into develop

Conflicts:
	README.md
	gin.go
	routergroup.go
This commit is contained in:
Manu Mtz-Almeida 2015-08-16 18:38:13 +02:00
commit e8bc8f48e9
27 changed files with 791 additions and 246 deletions

View File

@ -12,11 +12,20 @@
- [NEW] Benchmarks suite - [NEW] Benchmarks suite
- [NEW] Bind validation can be disabled and replaced with custom validators. - [NEW] Bind validation can be disabled and replaced with custom validators.
- [NEW] More flexible HTML render - [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] Binding multipart form
- [FIX] Integration tests - [FIX] Integration tests
- [FIX] Crash when binding non struct object in Context. - [FIX] Crash when binding non struct object in Context.
- [FIX] RunTLS() implementation - [FIX] RunTLS() implementation
- [FIX] Logger() unit tests - [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] Better approach to avoid directory listing in StaticFS()
- [FIX] Context.ClientIP() always returns the IP with trimmed spaces. - [FIX] Context.ClientIP() always returns the IP with trimmed spaces.
- [FIX] Better warning when running in debug mode. - [FIX] Better warning when running in debug mode.
@ -62,7 +71,7 @@
- [FIX] Better debugging messages - [FIX] Better debugging messages
- [FIX] ErrorLogger - [FIX] ErrorLogger
- [FIX] Debug HTTP render - [FIX] Debug HTTP render
- [FIX] Refactored binding and render modules - [FIX] Refactored binding and render modules
- [FIX] Refactored Context initialization - [FIX] Refactored Context initialization
- [FIX] Refactored BasicAuth() - [FIX] Refactored BasicAuth()
- [FIX] NoMethod/NoRoute handlers - [FIX] NoMethod/NoRoute handlers

170
README.md
View File

@ -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 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) ![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) [See all benchmarks](/BENCHMARKS.md)
``` Benchmark name | (1) | (2) | (3) | (4)
BenchmarkAce_GithubAll 10000 109482 ns/op 13792 B/op 167 allocs/op --------------------------------|----------:|----------:|----------:|------:
BenchmarkBear_GithubAll 10000 287490 ns/op 79952 B/op 943 allocs/op BenchmarkAce_GithubAll | 10000 | 109482 | 13792 | 167
BenchmarkBeego_GithubAll 3000 562184 ns/op 146272 B/op 2092 allocs/op BenchmarkBear_GithubAll | 10000 | 287490 | 79952 | 943
BenchmarkBone_GithubAll 500 2578716 ns/op 648016 B/op 8119 allocs/op BenchmarkBeego_GithubAll | 3000 | 562184 | 146272 | 2092
BenchmarkDenco_GithubAll 20000 94955 ns/op 20224 B/op 167 allocs/op BenchmarkBone_GithubAll | 500 | 2578716 | 648016 | 8119
BenchmarkEcho_GithubAll 30000 58705 ns/op 0 B/op 0 allocs/op BenchmarkDenco_GithubAll | 20000 | 94955 | 20224 | 167
BenchmarkGin_GithubAll 30000 50991 ns/op 0 B/op 0 allocs/op BenchmarkEcho_GithubAll | 30000 | 58705 | 0 | 0
BenchmarkGocraftWeb_GithubAll 5000 449648 ns/op 133280 B/op 1889 allocs/op **BenchmarkGin_GithubAll** | **30000** | **50991** | **0** | **0**
BenchmarkGoji_GithubAll 2000 689748 ns/op 56113 B/op 334 allocs/op BenchmarkGocraftWeb_GithubAll | 5000 | 449648 | 133280 | 1889
BenchmarkGoJsonRest_GithubAll 5000 537769 ns/op 135995 B/op 2940 allocs/op BenchmarkGoji_GithubAll | 2000 | 689748 | 56113 | 334
BenchmarkGoRestful_GithubAll 100 18410628 ns/op 797236 B/op 7725 allocs/op BenchmarkGoJsonRest_GithubAll | 5000 | 537769 | 135995 | 2940
BenchmarkGorillaMux_GithubAll 200 8036360 ns/op 153137 B/op 1791 allocs/op BenchmarkGoRestful_GithubAll | 100 | 18410628 | 797236 | 7725
BenchmarkHttpRouter_GithubAll 20000 63506 ns/op 13792 B/op 167 allocs/op BenchmarkGorillaMux_GithubAll | 200 | 8036360 | 153137 | 1791
BenchmarkHttpTreeMux_GithubAll 10000 165927 ns/op 56112 B/op 334 allocs/op BenchmarkHttpRouter_GithubAll | 20000 | 63506 | 13792 | 167
BenchmarkKocha_GithubAll 10000 171362 ns/op 23304 B/op 843 allocs/op BenchmarkHttpTreeMux_GithubAll | 10000 | 165927 | 56112 | 334
BenchmarkMacaron_GithubAll 2000 817008 ns/op 224960 B/op 2315 allocs/op BenchmarkKocha_GithubAll | 10000 | 171362 | 23304 | 843
BenchmarkMartini_GithubAll 100 12609209 ns/op 237952 B/op 2686 allocs/op BenchmarkMacaron_GithubAll | 2000 | 817008 | 224960 | 2315
BenchmarkPat_GithubAll 300 4830398 ns/op 1504101 B/op 32222 allocs/op BenchmarkMartini_GithubAll | 100 | 12609209 | 237952 | 2686
BenchmarkPossum_GithubAll 10000 301716 ns/op 97440 B/op 812 allocs/op BenchmarkPat_GithubAll | 300 | 4830398 | 1504101 | 32222
BenchmarkR2router_GithubAll 10000 270691 ns/op 77328 B/op 1182 allocs/op BenchmarkPossum_GithubAll | 10000 | 301716 | 97440 | 812
BenchmarkRevel_GithubAll 1000 1491919 ns/op 345553 B/op 5918 allocs/op BenchmarkR2router_GithubAll | 10000 | 270691 | 77328 | 1182
BenchmarkRivet_GithubAll 10000 283860 ns/op 84272 B/op 1079 allocs/op BenchmarkRevel_GithubAll | 1000 | 1491919 | 345553 | 5918
BenchmarkTango_GithubAll 5000 473821 ns/op 87078 B/op 2470 allocs/op BenchmarkRivet_GithubAll | 10000 | 283860 | 84272 | 1079
BenchmarkTigerTonic_GithubAll 2000 1120131 ns/op 241088 B/op 6052 allocs/op BenchmarkTango_GithubAll | 5000 | 473821 | 87078 | 2470
BenchmarkTraffic_GithubAll 200 8708979 ns/op 2664762 B/op 22390 allocs/op BenchmarkTigerTonic_GithubAll | 2000 | 1120131 | 241088 | 6052
BenchmarkVulcan_GithubAll 5000 353392 ns/op 19894 B/op 609 allocs/op BenchmarkTraffic_GithubAll | 200 | 8708979 | 2664762 | 22390
BenchmarkZeus_GithubAll 2000 944234 ns/op 300688 B/op 2648 allocs/op 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 ##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 #### Grouping routes
```go ```go
func main() { func main() {
@ -253,46 +293,41 @@ You can also specify that specific fields are required. If a field is decorated
```go ```go
// Binding from JSON // Binding from JSON
type LoginJSON struct { type Login struct {
User string `json:"user" binding:"required"` User string `form:"user" json:"user" binding:"required"`
Password string `json:"password" binding:"required"` Password string `form:"password" json:"password" binding:"required"`
}
// Binding from form values
type LoginForm struct {
User string `form:"user" binding:"required"`
Password string `form:"password" binding:"required"`
} }
func main() { func main() {
r := gin.Default() router := gin.Default()
// Example for binding JSON ({"user": "manu", "password": "123"}) // Example for binding JSON ({"user": "manu", "password": "123"})
r.POST("/loginJSON", func(c *gin.Context) { router.POST("/loginJSON", func(c *gin.Context) {
var json LoginJSON var json Login
if c.BindJSON(&json) == nil {
c.Bind(&json) // This will infer what binder to use depending on the content-type header. if json.User == "manu" && json.Password == "123" {
if json.User == "manu" && json.Password == "123" { c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) } else {
} else { c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) }
} }
}) })
// Example for binding a HTML form (user=manu&password=123) // Example for binding a HTML form (user=manu&password=123)
r.POST("/loginHTML", func(c *gin.Context) { router.POST("/loginForm", func(c *gin.Context) {
var form LoginForm var form Login
// This will infer what binder to use depending on the content-type header.
c.BindWith(&form, binding.Form) // You can also specify which binder to use. We support binding.Form, binding.JSON and binding.XML. if c.Bind(&form) == nil {
if form.User == "manu" && form.Password == "123" { if form.User == "manu" && form.Password == "123" {
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
} else { } else {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
}
} }
}) })
// Listen and server on 0.0.0.0:8080 // Listen and server on 0.0.0.0:8080
r.Run(":8080") router.Run(":8080")
} }
``` ```
@ -312,25 +347,22 @@ type LoginForm struct {
} }
func main() { func main() {
router := gin.Default() router := gin.Default()
router.POST("/login", func(c *gin.Context) { router.POST("/login", func(c *gin.Context) {
// you can bind multipart form with explicit binding declaration: // you can bind multipart form with explicit binding declaration:
// c.BindWith(&form, binding.Form) // c.BindWith(&form, binding.Form)
// or you can simply use autobinding with Bind method: // or you can simply use autobinding with Bind method:
var form LoginForm var form LoginForm
c.Bind(&form) // in this case proper binding will be automatically selected // in this case proper binding will be automatically selected
if c.Bind(&form) == nil {
if form.User == "user" && form.Password == "password" { if form.User == "user" && form.Password == "password" {
c.JSON(200, gin.H{"status": "you are logged in"}) c.JSON(200, gin.H{"status": "you are logged in"})
} else { } else {
c.JSON(401, gin.H{"status": "unauthorized"}) c.JSON(401, gin.H{"status": "unauthorized"})
} }
}
}) })
router.Run(":8080") router.Run(":8080")
} }
``` ```

16
auth.go
View File

@ -10,9 +10,7 @@ import (
"strconv" "strconv"
) )
const ( const AuthUserKey = "user"
AuthUserKey = "user"
)
type ( type (
Accounts map[string]string Accounts map[string]string
@ -35,8 +33,9 @@ func (a authPairs) searchCredential(authValue string) (string, bool) {
return "", false return "", false
} }
// Implements a basic Basic HTTP Authorization. It takes as arguments a map[string]string where // 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 // 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) // (see http://tools.ietf.org/html/rfc2617#section-1.2)
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
if realm == "" { 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. // the key is the user name and the value is the password.
func BasicAuth(accounts Accounts) HandlerFunc { func BasicAuth(accounts Accounts) HandlerFunc {
return BasicAuthForRealm(accounts, "") return BasicAuthForRealm(accounts, "")
@ -91,8 +90,7 @@ func authorizationHeader(user, password string) string {
func secureCompare(given, actual string) bool { func secureCompare(given, actual string) bool {
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 { if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
return subtle.ConstantTimeCompare([]byte(given), []byte(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
} }

View File

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

View File

@ -6,6 +6,7 @@ package binding
import ( import (
"bytes" "bytes"
"mime/multipart"
"net/http" "net/http"
"testing" "testing"
@ -64,6 +65,44 @@ func TestBindingXML(t *testing.T) {
"<map><foo>bar</foo></map>", "<map><bar>foo</bar></map>") "<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) { func TestValidationFails(t *testing.T) {
var obj FooStruct var obj FooStruct
req := requestWithBody("POST", "/", `{"bar": "foo"}`) req := requestWithBody("POST", "/", `{"bar": "foo"}`)

View File

@ -7,12 +7,14 @@ package binding
import "net/http" import "net/http"
type formBinding struct{} type formBinding struct{}
type formPostBinding struct{}
type formMultipartBinding struct{}
func (_ formBinding) Name() string { func (formBinding) Name() string {
return "form" 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 { if err := req.ParseForm(); err != nil {
return err return err
} }
@ -22,3 +24,31 @@ func (_ formBinding) Bind(req *http.Request, obj interface{}) error {
} }
return validate(obj) 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)
}

View File

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

View File

@ -12,11 +12,11 @@ import (
type jsonBinding struct{} type jsonBinding struct{}
func (_ jsonBinding) Name() string { func (jsonBinding) Name() string {
return "json" 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) decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(obj); err != nil { if err := decoder.Decode(obj); err != nil {
return err return err

View File

@ -11,11 +11,11 @@ import (
type xmlBinding struct{} type xmlBinding struct{}
func (_ xmlBinding) Name() string { func (xmlBinding) Name() string {
return "xml" 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) decoder := xml.NewDecoder(req.Body)
if err := decoder.Decode(obj); err != nil { if err := decoder.Decode(obj); err != nil {
return err return err

View File

@ -18,6 +18,7 @@ import (
"golang.org/x/net/context" "golang.org/x/net/context"
) )
// Content-Type MIME of the most common data formats
const ( const (
MIMEJSON = binding.MIMEJSON MIMEJSON = binding.MIMEJSON
MIMEHTML = binding.MIMEHTML MIMEHTML = binding.MIMEHTML
@ -28,7 +29,7 @@ const (
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm 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, // 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. // 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 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 { func (c *Context) Copy() *Context {
var cp Context = *c var cp Context = *c
cp.writermem.ResponseWriter = nil cp.writermem.ResponseWriter = nil
cp.Writer = &cp.writermem cp.Writer = &cp.writermem
cp.index = AbortIndex cp.index = abortIndex
cp.handlers = nil cp.handlers = nil
return &cp 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 ***********/ /*********** 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 { 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 // 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 // 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. // in order to stop the execution of the actual handler.
func (c *Context) Abort() { 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). // For example, a failed attempt to authentificate a request could use: context.AbortWithStatus(401).
func (c *Context) AbortWithStatus(code int) { func (c *Context) AbortWithStatus(code int) {
c.Writer.WriteHeader(code) c.Writer.WriteHeader(code)
c.Abort() 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`. // pushes the specified error to `c.Errors`.
// See Context.Error() for more details. // See Context.Error() for more details.
func (c *Context) AbortWithError(code int, err error) *Error { 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. // 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. // 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 { func (c *Context) Error(err error) *Error {
var parsedError *Error var parsedError *Error
switch err.(type) { switch err.(type) {
@ -141,8 +151,8 @@ func (c *Context) Error(err error) *Error {
/******** METADATA MANAGEMENT********/ /******** METADATA MANAGEMENT********/
/************************************/ /************************************/
// Sets a new pair key/value just for this context. // Set is used to store a new key/value pair exclusivelly for this context.
// It also lazy initializes the hashmap if it was not used previously. // It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value interface{}) { func (c *Context) Set(key string, value interface{}) {
if c.Keys == nil { if c.Keys == nil {
c.Keys = make(map[string]interface{}) c.Keys = make(map[string]interface{})
@ -150,7 +160,7 @@ func (c *Context) Set(key string, value interface{}) {
c.Keys[key] = value 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) // If the value does not exists it returns (nil, false)
func (c *Context) Get(key string) (value interface{}, exists bool) { func (c *Context) Get(key string) (value interface{}, exists bool) {
if c.Keys != nil { if c.Keys != nil {
@ -171,19 +181,24 @@ func (c *Context) MustGet(key string) interface{} {
/************ INPUT DATA ************/ /************ 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) { func (c *Context) Query(key string) (va string) {
va, _ = c.query(key) va, _ = c.query(key)
return return
} }
// Shortcut for c.Request.PostFormValue(key) // PostForm is a shortcut for c.Request.PostFormValue(key)
func (c *Context) PostForm(key string) (va string) { func (c *Context) PostForm(key string) (va string) {
va, _ = c.postForm(key) va, _ = c.postForm(key)
return return
} }
// Shortcut for c.Params.ByName(key) // Param is a shortcut for c.Params.ByName(key)
func (c *Context) Param(key string) string { func (c *Context) Param(key string) string {
return c.Params.ByName(key) return c.Params.ByName(key)
} }
@ -195,6 +210,13 @@ func (c *Context) DefaultPostForm(key, defaultValue string) string {
return defaultValue 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 { func (c *Context) DefaultQuery(key, defaultValue string) string {
if va, ok := c.query(key); ok { if va, ok := c.query(key); ok {
return va return va
@ -224,22 +246,26 @@ func (c *Context) postForm(key string) (string, bool) {
return "", false 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: // Depending the "Content-Type" header different bindings are used:
// "application/json" --> JSON binding // "application/json" --> JSON binding
// "application/xml" --> XML binding // "application/xml" --> XML binding
// else --> returns an error // 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. // 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 { func (c *Context) Bind(obj interface{}) error {
b := binding.Default(c.Request.Method, c.ContentType()) b := binding.Default(c.Request.Method, c.ContentType())
return c.BindWith(obj, b) 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 { func (c *Context) BindJSON(obj interface{}) error {
return c.BindWith(obj, binding.JSON) 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 { func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
if err := b.Bind(c.Request, obj); err != nil { if err := b.Bind(c.Request, obj); err != nil {
c.AbortWithError(400, err).SetType(ErrorTypeBind) c.AbortWithError(400, err).SetType(ErrorTypeBind)
@ -248,7 +274,7 @@ func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
return nil 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. // 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 { func (c *Context) ClientIP() string {
if c.engine.ForwardedByClientIP { if c.engine.ForwardedByClientIP {
@ -268,6 +294,7 @@ func (c *Context) ClientIP() string {
return strings.TrimSpace(c.Request.RemoteAddr) return strings.TrimSpace(c.Request.RemoteAddr)
} }
// ContentType returns the Content-Type header of the request.
func (c *Context) ContentType() string { func (c *Context) ContentType() string {
return filterFlags(c.requestHeader("Content-Type")) return filterFlags(c.requestHeader("Content-Type"))
} }
@ -283,8 +310,8 @@ func (c *Context) requestHeader(key string) string {
/******** RESPONSE RENDERING ********/ /******** RESPONSE RENDERING ********/
/************************************/ /************************************/
// Intelligent shortcut for c.Writer.Header().Set(key, value) // Header is a intelligent shortcut for c.Writer.Header().Set(key, value)
// it writes a header in the response. // It writes a header in the response.
// If value == "", this method removes the header `c.Writer.Header().Del(key)` // If value == "", this method removes the header `c.Writer.Header().Del(key)`
func (c *Context) Header(key, value string) { func (c *Context) Header(key, value string) {
if len(value) == 0 { if len(value) == 0 {
@ -306,7 +333,7 @@ func (c *Context) renderError(err error) {
c.AbortWithError(500, err).SetType(ErrorTypeRender) 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". // It also updates the HTTP code and sets the Content-Type as "text/html".
// See http://golang.org/doc/articles/wiki/ // See http://golang.org/doc/articles/wiki/
func (c *Context) HTML(code int, name string, obj interface{}) { 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) 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". // It also sets the Content-Type as "application/json".
// WARNING: we recommend to use this only for development propuses since printing pretty JSON is // WARNING: we recommend to use this only for development propuses since printing pretty JSON is
// more CPU and bandwidth consuming. Use Context.JSON() instead. // 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}) 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". // It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) { func (c *Context) JSON(code int, obj interface{}) {
c.writermem.WriteHeader(code) 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". // It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) { func (c *Context) XML(code int, obj interface{}) {
c.Render(code, render.XML{Data: obj}) 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{}) { func (c *Context) String(code int, format string, values ...interface{}) {
c.writermem.WriteHeader(code) c.writermem.WriteHeader(code)
render.WriteString(c.Writer, format, values) 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) { func (c *Context) Redirect(code int, location string) {
c.Render(-1, render.Redirect{ c.Render(-1, render.Redirect{
Code: code, 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) { func (c *Context) Data(code int, contentType string, data []byte) {
c.Render(code, render.Data{ c.Render(code, render.Data{
ContentType: contentType, 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) { func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath) 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{}) { func (c *Context) SSEvent(name string, message interface{}) {
c.Render(-1, sse.Event{ c.Render(-1, sse.Event{
Event: name, Event: name,

View File

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

View File

@ -4,11 +4,18 @@
package gin package gin
import "log" import (
"bytes"
"html/template"
"log"
)
func init() { func init() {
log.SetFlags(0) 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 { func IsDebugging() bool {
return ginMode == debugCode return ginMode == debugCode
} }
@ -16,18 +23,30 @@ func IsDebugging() bool {
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
if IsDebugging() { if IsDebugging() {
nuHandlers := len(handlers) nuHandlers := len(handlers)
handlerName := nameOfFunction(handlers[nuHandlers-1]) handlerName := nameOfFunction(handlers.Last())
debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) 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{}) { func debugPrint(format string, values ...interface{}) {
if IsDebugging() { if IsDebugging() {
log.Printf("[GIN-debug] "+format, values...) log.Printf("[GIN-debug] "+format, values...)
} }
} }
func debugPrintWARNING() { func debugPrintWARNINGNew() {
debugPrint(`[WARNING] Running in "debug" mode. Switch to "release" mode in production. debugPrint(`[WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release - using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode) - 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) { func debugPrintError(err error) {
if err != nil { if err != nil {
debugPrint("[ERROR] %v\n", err) debugPrint("[ERROR] %v\n", err)

View File

@ -57,6 +57,15 @@ func TestDebugPrintError(t *testing.T) {
assert.Equal(t, w.String(), "[GIN-debug] [ERROR] this is an error\n") 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) { func setup(w io.Writer) {
SetMode(DebugMode) SetMode(DebugMode)
log.SetOutput(w) log.SetOutput(w)

View File

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

View File

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

5
fs.go
View File

@ -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(). // in router.Static().
// if listDirectory == true, then it works the same as http.Dir() otherwise it returns // 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. // 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) fs := http.Dir(root)
if listDirectory { if listDirectory {
return fs return fs
} else {
return &onlyfilesFS{fs}
} }
return &onlyfilesFS{fs}
} }
// Conforms to http.Filesystem // Conforms to http.Filesystem

93
gin.go
View File

@ -14,16 +14,34 @@ import (
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
) )
// Framework's version
const Version = "v1.0rc2" const Version = "v1.0rc2"
var default404Body = []byte("404 page not found") var default404Body = []byte("404 page not found")
var default405Body = []byte("405 method not allowed") var default405Body = []byte("405 method not allowed")
type ( type HandlerFunc func(*Context)
HandlerFunc func(*Context) type HandlersChain []HandlerFunc
HandlersChain []HandlerFunc
// Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middleware. // 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 (
RoutesInfo []RouteInfo
RouteInfo struct {
Method string
Path string
Handler string
}
// 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 { Engine struct {
RouterGroup RouterGroup
HTMLRender render.HTMLRender HTMLRender render.HTMLRender
@ -63,14 +81,20 @@ type (
} }
) )
// Returns a new blank Engine instance without any middleware attached. var _ IRouter = &Engine{}
// The most basic configuration
// 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 { func New() *Engine {
debugPrintWARNING() debugPrintWARNINGNew()
engine := &Engine{ engine := &Engine{
RouterGroup: RouterGroup{ RouterGroup: RouterGroup{
Handlers: nil, Handlers: nil,
BasePath: "/", basePath: "/",
root: true, root: true,
}, },
RedirectTrailingSlash: true, RedirectTrailingSlash: true,
@ -86,7 +110,7 @@ func New() *Engine {
return 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 { func Default() *Engine {
engine := New() engine := New()
engine.Use(Recovery(), Logger()) engine.Use(Recovery(), Logger())
@ -99,6 +123,7 @@ func (engine *Engine) allocateContext() *Context {
func (engine *Engine) LoadHTMLGlob(pattern string) { func (engine *Engine) LoadHTMLGlob(pattern string) {
if IsDebugging() { if IsDebugging() {
debugPrintLoadTemplate(template.Must(template.ParseGlob(pattern)))
engine.HTMLRender = render.HTMLDebug{Glob: pattern} engine.HTMLRender = render.HTMLDebug{Glob: pattern}
} else { } else {
templ := template.Must(template.ParseGlob(pattern)) templ := template.Must(template.ParseGlob(pattern))
@ -117,12 +142,7 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
func (engine *Engine) SetHTMLTemplate(templ *template.Template) { func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
if len(engine.trees) > 0 { if len(engine.trees) > 0 {
debugPrint(`[WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called debugPrintWARNINGSetHTMLTemplate()
at initialization. ie. before any route is registered or the router is listening in a socket:
router := gin.Default()
router.SetHTMLTemplate(template) // << good place
`)
} }
engine.HTMLRender = render.HTMLProduction{Template: templ} 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 // 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... // 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. // 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.RouterGroup.Use(middleware...)
engine.rebuild404Handlers() engine.rebuild404Handlers()
engine.rebuild405Handlers() engine.rebuild405Handlers()
@ -181,18 +201,43 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
root.addRoute(path, handlers) 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) // It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine undefinitelly unless an error happens. // Note: this method will block the calling goroutine undefinitelly unless an error happens.
func (engine *Engine) Run(addr string) (err error) { func (engine *Engine) Run(addr ...string) (err error) {
debugPrint("Listening and serving HTTP on %s\n", addr)
defer func() { debugPrintError(err) }() 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 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) // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine undefinitelly unless an error happens. // 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) { 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 return
} }
// The router is attached to a http.Server and starts listening and serving HTTP requests // RunUnix attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified unix socket (ie. a file) // through the specified unix socket (ie. a file).
// Note: this method will block the calling goroutine undefinitelly unless an error happens. // Note: this method will block the calling goroutine undefinitelly unless an error happens.
func (engine *Engine) RunUnix(file string) (err error) { func (engine *Engine) RunUnix(file string) (err error) {
debugPrint("Listening and serving HTTP on unix:/%s", file) debugPrint("Listening and serving HTTP on unix:/%s", file)
@ -251,7 +296,7 @@ func (engine *Engine) handleHTTPRequest(context *Context) {
return return
} else if httpMethod != "CONNECT" && path != "/" { } else if httpMethod != "CONNECT" && path != "/" {
if tsr && engine.RedirectFixedPath { if tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(context) redirectTrailingSlash(context)
return return
} }

View File

@ -2,49 +2,86 @@ package gin
import ( import (
"bufio" "bufio"
"bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
"os"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestRun(t *testing.T) { func testRequest(t *testing.T, url string) {
buffer := new(bytes.Buffer) 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() router := New()
go func() { go func() {
router.Use(LoggerWithWriter(buffer))
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) 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(":8080"))
testRequest(t, "http://localhost:8080/example")
}
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)
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 // have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
assert.Error(t, router.Run(":5150")) assert.Error(t, router.Run(":5150"))
testRequest(t, "http://localhost:5150/example")
resp, err := http.Get("http://localhost:5150/example")
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 TestUnixSocket(t *testing.T) { func TestUnixSocket(t *testing.T) {
buffer := new(bytes.Buffer)
router := New() router := New()
go func() { go func() {
router.Use(LoggerWithWriter(buffer))
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) 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 // have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // otherwise the main thread will complete

View File

@ -14,7 +14,6 @@ import (
//TODO //TODO
// func (engine *Engine) LoadHTMLGlob(pattern string) { // func (engine *Engine) LoadHTMLGlob(pattern string) {
// func (engine *Engine) LoadHTMLFiles(files ...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 (engine *Engine) RunTLS(addr string, cert string, key string) error {
func init() { func init() {
@ -23,11 +22,30 @@ func init() {
func TestCreateEngine(t *testing.T) { func TestCreateEngine(t *testing.T) {
router := New() router := New()
assert.Equal(t, "/", router.BasePath) assert.Equal(t, "/", router.basePath)
assert.Equal(t, router.engine, router) assert.Equal(t, router.engine, router)
assert.Empty(t, router.Handlers) 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) { func TestAddRoute(t *testing.T) {
router := New() router := New()
router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}}) router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})
@ -180,3 +198,48 @@ func compareFunc(t *testing.T, a, b interface{}) {
t.Error("different functions") 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) {}

View File

@ -6,6 +6,7 @@ package gin
import ( import (
"bytes" "bytes"
"errors"
"testing" "testing"
"github.com/stretchr/testify/assert" "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(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") 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!")
}

View File

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

View File

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

View File

@ -11,49 +11,69 @@ import (
"strings" "strings"
) )
type routesInterface interface { type (
Use(...HandlerFunc) routesInterface IRouter interface {
IRoutes
Group(string, ...HandlerFunc) *RouterGroup
}
Handle(string, string, ...HandlerFunc) routesInterface IRoutes interface {
Any(string, ...HandlerFunc) routesInterface Use(...HandlerFunc) IRoutes
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 Handle(string, string, ...HandlerFunc) IRoutes
Static(string, string) routesInterface Any(string, ...HandlerFunc) IRoutes
StaticFS(string, http.FileSystem) routesInterface 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
// Used internally to configure router, a RouterGroup is associated with a prefix StaticFile(string, string) IRoutes
// and an array of handlers (middleware) Static(string, string) IRoutes
type RouterGroup struct { StaticFS(string, http.FileSystem) IRoutes
Handlers HandlersChain }
BasePath string
engine *Engine
root bool
}
// Adds middleware to the group, see example code in github. // RouterGroup is used internally to configure router, a RouterGroup is associated with a prefix
func (group *RouterGroup) Use(middleware ...HandlerFunc) routesInterface { // 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...) group.Handlers = append(group.Handlers, middleware...)
return group.returnObj() 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. // For example, all the routes that use a common middlware for authorization could be grouped.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{ return &RouterGroup{
Handlers: group.combineHandlers(handlers), Handlers: group.combineHandlers(handlers),
BasePath: group.calculateAbsolutePath(relativePath), basePath: group.calculateAbsolutePath(relativePath),
engine: group.engine, 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. // 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. // 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. // 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 // 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 // frequently used, non-standardized or custom methods (e.g. for internal
// communication with a proxy). // communication with a proxy).
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) routesInterface { func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
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 {
if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil { if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil {
panic("http method " + httpMethod + " is not valid") 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) // 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) return group.handle("POST", relativePath, handlers)
} }
// GET is a shortcut for router.Handle("GET", path, handle) // 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) return group.handle("GET", relativePath, handlers)
} }
// DELETE is a shortcut for router.Handle("DELETE", path, handle) // 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) return group.handle("DELETE", relativePath, handlers)
} }
// PATCH is a shortcut for router.Handle("PATCH", path, handle) // 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) return group.handle("PATCH", relativePath, handlers)
} }
// PUT is a shortcut for router.Handle("PUT", path, handle) // 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) return group.handle("PUT", relativePath, handlers)
} }
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) // 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) return group.handle("OPTIONS", relativePath, handlers)
} }
// HEAD is a shortcut for router.Handle("HEAD", path, handle) // 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) return group.handle("HEAD", relativePath, handlers)
} }
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) routesInterface { // Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE // 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("GET", relativePath, handlers)
group.handle("POST", relativePath, handlers) group.handle("POST", relativePath, handlers)
group.handle("PUT", relativePath, handlers) group.handle("PUT", relativePath, handlers)
@ -127,7 +141,9 @@ func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) rout
return group.returnObj() 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, "*") { if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
panic("URL parameters can not be used when serving a static file") 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, // To use the operating system's file system implementation,
// use : // use :
// router.Static("/static", "/var/www") // 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)) 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, "*") { if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
panic("URL parameters can not be used when serving a static folder") 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 { func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers) finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(AbortIndex) { if finalSize >= int(abortIndex) {
panic("too many handlers") panic("too many handlers")
} }
mergedHandlers := make(HandlersChain, finalSize) mergedHandlers := make(HandlersChain, finalSize)
@ -186,13 +204,12 @@ func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain
} }
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { 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 { if group.root {
return group.engine return group.engine
} else {
return group
} }
return group
} }

View File

@ -20,14 +20,14 @@ func TestRouterGroupBasic(t *testing.T) {
group.Use(func(c *Context) {}) group.Use(func(c *Context) {})
assert.Len(t, group.Handlers, 2) assert.Len(t, group.Handlers, 2)
assert.Equal(t, group.BasePath, "/hola") assert.Equal(t, group.BasePath(), "/hola")
assert.Equal(t, group.engine, router) assert.Equal(t, group.engine, router)
group2 := group.Group("manu") group2 := group.Group("manu")
group2.Use(func(c *Context) {}, func(c *Context) {}) group2.Use(func(c *Context) {}, func(c *Context) {})
assert.Len(t, group2.Handlers, 4) 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) assert.Equal(t, group2.engine, router)
} }
@ -44,10 +44,10 @@ func TestRouterGroupBasicHandle(t *testing.T) {
func performRequestInGroup(t *testing.T, method string) { func performRequestInGroup(t *testing.T, method string) {
router := New() router := New()
v1 := router.Group("v1", func(c *Context) {}) 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) {}) 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) { handler := func(c *Context) {
c.String(400, "the method was %s and index %d", c.Request.Method, c.index) 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) testRoutesInterface(t, v1)
} }
func testRoutesInterface(t *testing.T, r routesInterface) { func testRoutesInterface(t *testing.T, r IRoutes) {
handler := func(c *Context) {} handler := func(c *Context) {}
assert.Equal(t, r, r.Use(handler)) assert.Equal(t, r, r.Use(handler))

View File

@ -40,7 +40,6 @@ func testRouteOK(method string, t *testing.T) {
performRequest(r, method, "/test2") performRequest(r, method, "/test2")
assert.True(t, passedAny) assert.True(t, passedAny)
} }
// TestSingleRouteOK tests that POST route is correctly invoked. // TestSingleRouteOK tests that POST route is correctly invoked.
@ -110,7 +109,6 @@ func TestRouterGroupRouteOK(t *testing.T) {
testRouteOK("TRACE", t) testRouteOK("TRACE", t)
} }
// TestSingleRouteOK tests that POST route is correctly invoked.
func TestRouteNotOK(t *testing.T) { func TestRouteNotOK(t *testing.T) {
testRouteNotOK("GET", t) testRouteNotOK("GET", t)
testRouteNotOK("POST", t) testRouteNotOK("POST", t)
@ -123,7 +121,6 @@ func TestRouteNotOK(t *testing.T) {
testRouteNotOK("TRACE", t) testRouteNotOK("TRACE", t)
} }
// TestSingleRouteOK tests that POST route is correctly invoked.
func TestRouteNotOK2(t *testing.T) { func TestRouteNotOK2(t *testing.T) {
testRouteNotOK2("GET", t) testRouteNotOK2("GET", t)
testRouteNotOK2("POST", t) testRouteNotOK2("POST", t)
@ -136,6 +133,82 @@ func TestRouteNotOK2(t *testing.T) {
testRouteNotOK2("TRACE", 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. // TestContextParamsGet tests that a parameter can be parsed from the URL.
func TestRouteParamsByName(t *testing.T) { func TestRouteParamsByName(t *testing.T) {
name := "" name := ""

View File

@ -7,6 +7,7 @@ package gin
import ( import (
"encoding/xml" "encoding/xml"
"net/http" "net/http"
"os"
"path" "path"
"reflect" "reflect"
"runtime" "runtime"
@ -129,3 +130,20 @@ func joinPaths(absolutePath, relativePath string) string {
} }
return finalPath 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")
}
}

View File

@ -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/")
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{})
})
}