Merge branch 'master' into develop
Conflicts: README.md gin.go routergroup.go
This commit is contained in:
commit
e8bc8f48e9
@ -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.
|
||||||
|
170
README.md
170
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 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
16
auth.go
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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"}`)
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -56,7 +56,6 @@ func mapForm(ptr interface{}, form map[string][]string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
88
context.go
88
context.go
@ -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,
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
35
debug.go
35
debug.go
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
@ -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
5
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().
|
// 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
93
gin.go
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
67
gin_test.go
67
gin_test.go
@ -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) {}
|
||||||
|
@ -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!")
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
121
routergroup.go
121
routergroup.go
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -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 := ""
|
||||||
|
18
utils.go
18
utils.go
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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{})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user