Merge pull request #1 from gin-gonic/master

pull request
This commit is contained in:
Sergey Cheung 2018-01-19 00:11:41 +08:00 committed by GitHub
commit 0fda834942
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1287 additions and 503 deletions

View File

@ -4,6 +4,7 @@ go:
- 1.6.x - 1.6.x
- 1.7.x - 1.7.x
- 1.8.x - 1.8.x
- 1.9.x
- master - master
git: git:

304
README.md
View File

@ -74,10 +74,10 @@ BenchmarkTigerTonic_GithubAll | 1000 | 1439483 | 239104
BenchmarkTraffic_GithubAll | 100 | 11383067 | 2659329 | 21848 BenchmarkTraffic_GithubAll | 100 | 11383067 | 2659329 | 21848
BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894 | 609 BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894 | 609
(1): Total Repetitions achieved in constant time, higher means more confident result - (1): Total Repetitions achieved in constant time, higher means more confident result
(2): Single Repetition Duration (ns/op), lower is better - (2): Single Repetition Duration (ns/op), lower is better
(3): Heap Memory (B/op), lower is better - (3): Heap Memory (B/op), lower is better
(4): Average Allocations per Repetition (allocs/op), lower is better - (4): Average Allocations per Repetition (allocs/op), lower is better
## Gin v1. stable ## Gin v1. stable
@ -117,7 +117,7 @@ $ go get github.com/kardianos/govendor
2. Create your project folder and `cd` inside 2. Create your project folder and `cd` inside
```sh ```sh
$ mkdir -p ~/go/src/github.com/myusername/project && cd "$_" $ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_"
``` ```
3. Vendor init your project and add gin 3. Vendor init your project and add gin
@ -277,6 +277,8 @@ References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail
```go ```go
func main() { func main() {
router := gin.Default() router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) { router.POST("/upload", func(c *gin.Context) {
// single file // single file
file, _ := c.FormFile("file") file, _ := c.FormFile("file")
@ -306,6 +308,8 @@ See the detail [example code](examples/upload-file/multiple).
```go ```go
func main() { func main() {
router := gin.Default() router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) { router.POST("/upload", func(c *gin.Context) {
// Multipart form // Multipart form
form, _ := c.MultipartForm() form, _ := c.MultipartForm()
@ -381,7 +385,8 @@ func main() {
r := gin.New() r := gin.New()
// Global middleware // Global middleware
// Logger middleware will write the logs to gin.DefaultWriter even you set with GIN_MODE=release. By default gin.DefaultWriter = os.Stdout // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
// By default gin.DefaultWriter = os.Stdout
r.Use(gin.Logger()) r.Use(gin.Logger())
// Recovery middleware recovers from any panics and writes a 500 if there was one. // Recovery middleware recovers from any panics and writes a 500 if there was one.
@ -412,6 +417,28 @@ func main() {
} }
``` ```
### How to write log file
```go
func main() {
// Disable Console Color, you don't need console color when writing the logs to file.
gin.DisableConsoleColor()
// Logging to a file.
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)
// Use the following code if you need to write the logs to file and console at the same time.
// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
r.Run(":8080")
}
```
### Model binding and validation ### Model binding and validation
To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz). To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz).
@ -420,9 +447,17 @@ Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/valid
Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`. Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`.
When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use BindWith. Also, Gin provides two sets of methods for binding:
- **Type** - Must bind
- **Methods** - `Bind`, `BindJSON`, `BindQuery`
- **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method.
- **Type** - Should bind
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindQuery`
- **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately.
You can also specify that specific fields are required. If a field is decorated with `binding:"required"` and has a empty value when binding, the current request will fail with an error. When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`.
You can also specify that specific fields are required. If a field is decorated with `binding:"required"` and has a empty value when binding, an error will be returned.
```go ```go
// Binding from JSON // Binding from JSON
@ -437,12 +472,14 @@ func main() {
// Example for binding JSON ({"user": "manu", "password": "123"}) // Example for binding JSON ({"user": "manu", "password": "123"})
router.POST("/loginJSON", func(c *gin.Context) { router.POST("/loginJSON", func(c *gin.Context) {
var json Login var json Login
if c.BindJSON(&json) == nil { if err := c.ShouldBindJSON(&json); err == nil {
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"})
} }
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
} }
}) })
@ -450,12 +487,14 @@ func main() {
router.POST("/loginForm", func(c *gin.Context) { router.POST("/loginForm", func(c *gin.Context) {
var form Login var form Login
// This will infer what binder to use depending on the content-type header. // This will infer what binder to use depending on the content-type header.
if c.Bind(&form) == nil { if err := c.ShouldBind(&form); err == 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"})
} }
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
} }
}) })
@ -464,9 +503,92 @@ func main() {
} }
``` ```
**Sample request**
```shell
$ curl -v -X POST \
http://localhost:8080/loginJSON \
-H 'content-type: application/json' \
-d '{ "user": "manu" }'
> POST /loginJSON HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.51.0
> Accept: */*
> content-type: application/json
> Content-Length: 18
>
* upload completely sent off: 18 out of 18 bytes
< HTTP/1.1 400 Bad Request
< Content-Type: application/json; charset=utf-8
< Date: Fri, 04 Aug 2017 03:51:31 GMT
< Content-Length: 100
<
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}
```
### Custom Validators
It is also possible to register custom validators. See the [example code](examples/custom-validation/server.go).
[embedmd]:# (examples/custom-validation/server.go go)
```go
package main
import (
"net/http"
"reflect"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8"
)
type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}
func bookableDate(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
if date, ok := field.Interface().(time.Time); ok {
today := time.Now()
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
return false
}
}
return true
}
func main() {
route := gin.Default()
binding.Validator.RegisterValidation("bookabledate", bookableDate)
route.GET("/bookable", getBookable)
route.Run(":8085")
}
func getBookable(c *gin.Context) {
var b Booking
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
```
```console
$ curl "localhost:8085/bookable?check_in=2017-08-16&check_out=2017-08-17"
{"message":"Booking dates are valid!"}
$ curl "localhost:8085/bookable?check_in=2017-08-15&check_out=2017-08-16"
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}
```
### Only Bind Query String ### Only Bind Query String
`BindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017). `ShouldBindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017).
```go ```go
package main package main
@ -490,7 +612,7 @@ func main() {
func startPage(c *gin.Context) { func startPage(c *gin.Context) {
var person Person var person Person
if c.BindQuery(&person) == nil { if c.ShouldBindQuery(&person) == nil {
log.Println("====== Only Bind By Query String ======") log.Println("====== Only Bind By Query String ======")
log.Println(person.Name) log.Println(person.Name)
log.Println(person.Address) log.Println(person.Address)
@ -509,10 +631,12 @@ package main
import "log" import "log"
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"
import "time"
type Person struct { type Person struct {
Name string `form:"name"` Name string `form:"name"`
Address string `form:"address"` Address string `form:"address"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
} }
func main() { func main() {
@ -526,15 +650,21 @@ func startPage(c *gin.Context) {
// If `GET`, only `Form` binding engine (`query`) used. // If `GET`, only `Form` binding engine (`query`) used.
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`). // If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48 // See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
if c.Bind(&person) == nil { if c.ShouldBind(&person) == nil {
log.Println(person.Name) log.Println(person.Name)
log.Println(person.Address) log.Println(person.Address)
log.Println(person.Birthday)
} }
c.String(200, "Success") c.String(200, "Success")
} }
``` ```
Test it with:
```sh
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15"
```
### Bind HTML checkboxes ### Bind HTML checkboxes
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092) See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
@ -552,7 +682,7 @@ type myForm struct {
func formHandler(c *gin.Context) { func formHandler(c *gin.Context) {
var fakeForm myForm var fakeForm myForm
c.Bind(&fakeForm) c.ShouldBind(&fakeForm)
c.JSON(200, gin.H{"color": fakeForm.Colors}) c.JSON(200, gin.H{"color": fakeForm.Colors})
} }
@ -599,11 +729,11 @@ 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.MustBindWith(&form, binding.Form) // c.ShouldBindWith(&form, binding.Form)
// or you can simply use autobinding with Bind method: // or you can simply use autobinding with ShouldBind method:
var form LoginForm var form LoginForm
// in this case proper binding will be automatically selected // in this case proper binding will be automatically selected
if c.Bind(&form) == nil { if c.ShouldBind(&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 {
@ -941,7 +1071,7 @@ func main() {
### Goroutines inside a middleware ### Goroutines inside a middleware
When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy. When starting new Goroutines inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy.
```go ```go
func main() { func main() {
@ -1003,7 +1133,7 @@ func main() {
example for 1-line LetsEncrypt HTTPS servers. example for 1-line LetsEncrypt HTTPS servers.
[embedmd]:# (examples/auto-tls/example1.go go) [embedmd]:# (examples/auto-tls/example1/main.go go)
```go ```go
package main package main
@ -1028,7 +1158,7 @@ func main() {
example for custom autocert manager. example for custom autocert manager.
[embedmd]:# (examples/auto-tls/example2.go go) [embedmd]:# (examples/auto-tls/example2/main.go go)
```go ```go
package main package main
@ -1058,6 +1188,88 @@ func main() {
} }
``` ```
### Run multiple service using Gin
See the [question](https://github.com/gin-gonic/gin/issues/346) and try the folling example:
[embedmd]:# (examples/multiple-service/main.go go)
```go
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)
var (
g errgroup.Group
)
func router01() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 01",
},
)
})
return e
}
func router02() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 02",
},
)
})
return e
}
func main() {
server01 := &http.Server{
Addr: ":8080",
Handler: router01(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
server02 := &http.Server{
Addr: ":8081",
Handler: router02(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
g.Go(func() error {
return server01.ListenAndServe()
})
g.Go(func() error {
return server02.ListenAndServe()
})
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}
```
### Graceful restart or stop ### Graceful restart or stop
Do you want to graceful restart or stop your web server? Do you want to graceful restart or stop your web server?
@ -1128,7 +1340,53 @@ func main() {
if err := srv.Shutdown(ctx); err != nil { if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err) log.Fatal("Server Shutdown:", err)
} }
log.Println("Server exist") log.Println("Server exiting")
}
```
## Testing
The `net/http/httptest` package is preferable way for HTTP testing.
```go
package main
func setupRouter() *gin.Engine {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
return r
}
func main() {
r := setupRouter()
r.Run(":8080")
}
```
Test for code example above:
```go
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPingRoute(t *testing.T) {
router := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/ping", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, "pong", w.Body.String())
} }
``` ```

18
auth.go
View File

@ -17,19 +17,19 @@ const AuthUserKey = "user"
type Accounts map[string]string type Accounts map[string]string
type authPair struct { type authPair struct {
Value string value string
User string user string
} }
type authPairs []authPair type authPairs []authPair
func (a authPairs) searchCredential(authValue string) (string, bool) { func (a authPairs) searchCredential(authValue string) (string, bool) {
if len(authValue) == 0 { if authValue == "" {
return "", false return "", false
} }
for _, pair := range a { for _, pair := range a {
if pair.Value == authValue { if pair.value == authValue {
return pair.User, true return pair.user, true
} }
} }
return "", false return "", false
@ -47,7 +47,7 @@ func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
pairs := processAccounts(accounts) pairs := processAccounts(accounts)
return func(c *Context) { return func(c *Context) {
// Search user in the slice of allowed credentials // Search user in the slice of allowed credentials
user, found := pairs.searchCredential(c.Request.Header.Get("Authorization")) user, found := pairs.searchCredential(c.requestHeader("Authorization"))
if !found { if !found {
// Credentials doesn't match, we return 401 and abort handlers chain. // Credentials doesn't match, we return 401 and abort handlers chain.
c.Header("WWW-Authenticate", realm) c.Header("WWW-Authenticate", realm)
@ -71,11 +71,11 @@ func processAccounts(accounts Accounts) authPairs {
assert1(len(accounts) > 0, "Empty list of authorized credentials") assert1(len(accounts) > 0, "Empty list of authorized credentials")
pairs := make(authPairs, 0, len(accounts)) pairs := make(authPairs, 0, len(accounts))
for user, password := range accounts { for user, password := range accounts {
assert1(len(user) > 0, "User can not be empty") assert1(user != "", "User can not be empty")
value := authorizationHeader(user, password) value := authorizationHeader(user, password)
pairs = append(pairs, authPair{ pairs = append(pairs, authPair{
Value: value, value: value,
User: user, user: user,
}) })
} }
return pairs return pairs

View File

@ -22,16 +22,16 @@ func TestBasicAuth(t *testing.T) {
assert.Len(t, pairs, 3) assert.Len(t, pairs, 3)
assert.Contains(t, pairs, authPair{ assert.Contains(t, pairs, authPair{
User: "bar", user: "bar",
Value: "Basic YmFyOmZvbw==", value: "Basic YmFyOmZvbw==",
}) })
assert.Contains(t, pairs, authPair{ assert.Contains(t, pairs, authPair{
User: "foo", user: "foo",
Value: "Basic Zm9vOmJhcg==", value: "Basic Zm9vOmJhcg==",
}) })
assert.Contains(t, pairs, authPair{ assert.Contains(t, pairs, authPair{
User: "admin", user: "admin",
Value: "Basic YWRtaW46cGFzc3dvcmQ=", value: "Basic YWRtaW46cGFzc3dvcmQ=",
}) })
} }

View File

@ -59,8 +59,6 @@ func BenchmarkOneRouteJSON(B *testing.B) {
runRequest(B, router, "GET", "/json") runRequest(B, router, "GET", "/json")
} }
var htmlContentType = []string{"text/html; charset=utf-8"}
func BenchmarkOneRouteHTML(B *testing.B) { func BenchmarkOneRouteHTML(B *testing.B) {
router := New() router := New()
t := template.Must(template.New("index").Parse(` t := template.Must(template.New("index").Parse(`

View File

@ -4,7 +4,11 @@
package binding package binding
import "net/http" import (
"net/http"
"gopkg.in/go-playground/validator.v8"
)
const ( const (
MIMEJSON = "application/json" MIMEJSON = "application/json"
@ -31,6 +35,11 @@ type StructValidator interface {
// If the struct is not valid or the validation itself fails, a descriptive error should be returned. // If the struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned. // Otherwise nil must be returned.
ValidateStruct(interface{}) error ValidateStruct(interface{}) error
// RegisterValidation adds a validation Func to a Validate's map of validators denoted by the key
// NOTE: if the key already exists, the previous validation function will be replaced.
// NOTE: this method is not thread-safe it is intended that these all be registered prior to any validation
RegisterValidation(string, validator.Func) error
} }
var Validator StructValidator = &defaultValidator{} var Validator StructValidator = &defaultValidator{}

View File

@ -28,6 +28,11 @@ func (v *defaultValidator) ValidateStruct(obj interface{}) error {
return nil return nil
} }
func (v *defaultValidator) RegisterValidation(key string, fn validator.Func) error {
v.lazyinit()
return v.validate.RegisterValidation(key, fn)
}
func (v *defaultValidator) lazyinit() { func (v *defaultValidator) lazyinit() {
v.once.Do(func() { v.once.Do(func() {
config := &validator.Config{TagName: "binding"} config := &validator.Config{TagName: "binding"}

View File

@ -6,6 +6,8 @@ package binding
import "net/http" import "net/http"
const defaultMemory = 32 * 1024 * 1024
type formBinding struct{} type formBinding struct{}
type formPostBinding struct{} type formPostBinding struct{}
type formMultipartBinding struct{} type formMultipartBinding struct{}
@ -18,7 +20,7 @@ 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
} }
req.ParseMultipartForm(32 << 10) // 32 MB req.ParseMultipartForm(defaultMemory)
if err := mapForm(obj, req.Form); err != nil { if err := mapForm(obj, req.Form); err != nil {
return err return err
} }
@ -44,7 +46,7 @@ func (formMultipartBinding) Name() string {
} }
func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error { func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseMultipartForm(32 << 10); err != nil { if err := req.ParseMultipartForm(defaultMemory); err != nil {
return err return err
} }
if err := mapForm(obj, req.MultipartForm.Value); err != nil { if err := mapForm(obj, req.MultipartForm.Value); err != nil {

View File

@ -163,6 +163,14 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
l = time.UTC l = time.UTC
} }
if locTag := structField.Tag.Get("time_location"); locTag != "" {
loc, err := time.LoadLocation(locTag)
if err != nil {
return err
}
l = loc
}
t, err := time.ParseInLocation(timeFormat, val, l) t, err := time.ParseInLocation(timeFormat, val, l)
if err != nil { if err != nil {
return err return err
@ -171,12 +179,3 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
value.Set(reflect.ValueOf(t)) value.Set(reflect.ValueOf(t))
return nil return nil
} }
// Don't pass in pointers to bind to. Can lead to bugs. See:
// https://github.com/codegangsta/martini-contrib/issues/40
// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659
func ensureNotPointer(obj interface{}) {
if reflect.TypeOf(obj).Kind() == reflect.Ptr {
panic("Pointers are not accepted as binding models")
}
}

View File

@ -10,9 +10,7 @@ import (
"github.com/gin-gonic/gin/json" "github.com/gin-gonic/gin/json"
) )
var ( var EnableDecoderUseNumber = false
EnableDecoderUseNumber = false
)
type jsonBinding struct{} type jsonBinding struct{}

View File

@ -4,9 +4,7 @@
package binding package binding
import ( import "net/http"
"net/http"
)
type queryBinding struct{} type queryBinding struct{}

View File

@ -6,10 +6,12 @@ package binding
import ( import (
"bytes" "bytes"
"reflect"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/go-playground/validator.v8"
) )
type testInterface interface { type testInterface interface {
@ -190,3 +192,42 @@ func TestValidatePrimitives(t *testing.T) {
assert.NoError(t, validate(&str)) assert.NoError(t, validate(&str))
assert.Equal(t, str, "value") assert.Equal(t, str, "value")
} }
// structCustomValidation is a helper struct we use to check that
// custom validation can be registered on it.
// The `notone` binding directive is for custom validation and registered later.
type structCustomValidation struct {
Integer int `binding:"notone"`
}
// notOne is a custom validator meant to be used with `validator.v8` library.
// The method signature for `v9` is significantly different and this function
// would need to be changed for tests to pass after upgrade.
// See https://github.com/gin-gonic/gin/pull/1015.
func notOne(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
if val, ok := field.Interface().(int); ok {
return val != 1
}
return false
}
func TestRegisterValidation(t *testing.T) {
// This validates that the function `notOne` matches
// the expected function signature by `defaultValidator`
// and by extension the validator library.
err := Validator.RegisterValidation("notone", notOne)
// Check that we can register custom validation without error
assert.Nil(t, err)
// Create an instance which will fail validation
withOne := structCustomValidation{Integer: 1}
errs := validate(withOne)
// Check that we got back non-nil errs
assert.NotNil(t, errs)
// Check that the error matches expactation
assert.Error(t, errs, "", "", "notone")
}

View File

@ -33,10 +33,7 @@ const (
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
) )
const ( const abortIndex int8 = math.MaxInt8 / 2
defaultMemory = 32 << 20 // 32 MB
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.
@ -106,8 +103,7 @@ func (c *Context) Handler() HandlerFunc {
// See example in GitHub. // See example in GitHub.
func (c *Context) Next() { func (c *Context) Next() {
c.index++ c.index++
s := int8(len(c.handlers)) for s := int8(len(c.handlers)); c.index < s; c.index++ {
for ; c.index < s; c.index++ {
c.handlers[c.index](c) c.handlers[c.index](c)
} }
} }
@ -407,7 +403,7 @@ func (c *Context) PostFormArray(key string) []string {
func (c *Context) GetPostFormArray(key string) ([]string, bool) { func (c *Context) GetPostFormArray(key string) ([]string, bool) {
req := c.Request req := c.Request
req.ParseForm() req.ParseForm()
req.ParseMultipartForm(defaultMemory) req.ParseMultipartForm(c.engine.MaxMultipartMemory)
if values := req.PostForm[key]; len(values) > 0 { if values := req.PostForm[key]; len(values) > 0 {
return values, true return values, true
} }
@ -427,7 +423,7 @@ func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
// MultipartForm is the parsed multipart form, including file uploads. // MultipartForm is the parsed multipart form, including file uploads.
func (c *Context) MultipartForm() (*multipart.Form, error) { func (c *Context) MultipartForm() (*multipart.Form, error) {
err := c.Request.ParseMultipartForm(defaultMemory) err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory)
return c.Request.MultipartForm, err return c.Request.MultipartForm, err
} }
@ -453,10 +449,10 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
// 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
// otherwise --> returns an error // otherwise --> returns an error.
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. // It 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. // It decodes the json payload into the struct specified as a pointer.
// It will writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid. // It writes a 400 error and sets Content-Type header "text/plain" in the response if input 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.MustBindWith(obj, b) return c.MustBindWith(obj, b)
@ -483,6 +479,29 @@ func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
return return
} }
// ShouldBind checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used:
// "application/json" --> JSON binding
// "application/xml" --> XML binding
// otherwise --> returns an error
// It 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 c.Bind() but this method does not set the response status code to 400 and abort if the json is not valid.
func (c *Context) ShouldBind(obj interface{}) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.ShouldBindWith(obj, b)
}
// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
func (c *Context) ShouldBindJSON(obj interface{}) error {
return c.ShouldBindWith(obj, binding.JSON)
}
// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
func (c *Context) ShouldBindQuery(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Query)
}
// ShouldBindWith binds the passed struct pointer using the specified binding engine. // ShouldBindWith binds the passed struct pointer using the specified binding engine.
// See the binding package. // See the binding package.
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error { func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
@ -499,17 +518,17 @@ func (c *Context) ClientIP() string {
clientIP = clientIP[0:index] clientIP = clientIP[0:index]
} }
clientIP = strings.TrimSpace(clientIP) clientIP = strings.TrimSpace(clientIP)
if len(clientIP) > 0 { if clientIP != "" {
return clientIP return clientIP
} }
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip")) clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
if len(clientIP) > 0 { if clientIP != "" {
return clientIP return clientIP
} }
} }
if c.engine.AppEngine { if c.engine.AppEngine {
if addr := c.Request.Header.Get("X-Appengine-Remote-Addr"); addr != "" { if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
return addr return addr
} }
} }
@ -537,10 +556,7 @@ func (c *Context) IsWebsocket() bool {
} }
func (c *Context) requestHeader(key string) string { func (c *Context) requestHeader(key string) string {
if values, _ := c.Request.Header[key]; len(values) > 0 { return c.Request.Header.Get(key)
return values[0]
}
return ""
} }
/************************************/ /************************************/
@ -569,7 +585,7 @@ func (c *Context) Status(code int) {
// 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 value == "" {
c.Writer.Header().Del(key) c.Writer.Header().Del(key)
} else { } else {
c.Writer.Header().Set(key, value) c.Writer.Header().Set(key, value)
@ -586,6 +602,9 @@ func (c *Context) GetRawData() ([]byte, error) {
return ioutil.ReadAll(c.Request.Body) return ioutil.ReadAll(c.Request.Body)
} }
// SetCookie adds a Set-Cookie header to the ResponseWriter's headers.
// The provided cookie must have a valid Name. Invalid cookies may be
// silently dropped.
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) { func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) {
if path == "" { if path == "" {
path = "/" path = "/"
@ -601,6 +620,10 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
}) })
} }
// Cookie returns the named cookie provided in the request or
// ErrNoCookie if not found. And return the named cookie is unescaped.
// If multiple cookies match the given name, only one cookie will
// be returned.
func (c *Context) Cookie(name string) (string, error) { func (c *Context) Cookie(name string) (string, error) {
cookie, err := c.Request.Cookie(name) cookie, err := c.Request.Cookie(name)
if err != nil { if err != nil {

View File

@ -45,6 +45,7 @@ func createMultipartRequest() *http.Request {
must(mw.WriteField("id", "")) must(mw.WriteField("id", ""))
must(mw.WriteField("time_local", "31/12/2016 14:55")) must(mw.WriteField("time_local", "31/12/2016 14:55"))
must(mw.WriteField("time_utc", "31/12/2016 14:55")) must(mw.WriteField("time_utc", "31/12/2016 14:55"))
must(mw.WriteField("time_location", "31/12/2016 14:55"))
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)
@ -179,14 +180,14 @@ func TestContextSetGet(t *testing.T) {
c.Set("foo", "bar") c.Set("foo", "bar")
value, err := c.Get("foo") value, err := c.Get("foo")
assert.Equal(t, value, "bar") assert.Equal(t, "bar", value)
assert.True(t, err) assert.True(t, err)
value, err = c.Get("foo2") value, err = c.Get("foo2")
assert.Nil(t, value) assert.Nil(t, value)
assert.False(t, err) assert.False(t, err)
assert.Equal(t, c.MustGet("foo"), "bar") assert.Equal(t, "bar", c.MustGet("foo"))
assert.Panics(t, func() { c.MustGet("no_exist") }) assert.Panics(t, func() { c.MustGet("no_exist") })
} }
@ -220,7 +221,7 @@ func TestContextGetString(t *testing.T) {
func TestContextSetGetBool(t *testing.T) { func TestContextSetGetBool(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("bool", true) c.Set("bool", true)
assert.Equal(t, true, c.GetBool("bool")) assert.True(t, c.GetBool("bool"))
} }
func TestContextGetInt(t *testing.T) { func TestContextGetInt(t *testing.T) {
@ -337,26 +338,26 @@ func TestContextQuery(t *testing.T) {
value, ok := c.GetQuery("foo") value, ok := c.GetQuery("foo")
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, value, "bar") assert.Equal(t, "bar", value)
assert.Equal(t, c.DefaultQuery("foo", "none"), "bar") assert.Equal(t, "bar", c.DefaultQuery("foo", "none"))
assert.Equal(t, c.Query("foo"), "bar") assert.Equal(t, "bar", c.Query("foo"))
value, ok = c.GetQuery("page") value, ok = c.GetQuery("page")
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, value, "10") assert.Equal(t, "10", value)
assert.Equal(t, c.DefaultQuery("page", "0"), "10") assert.Equal(t, "10", c.DefaultQuery("page", "0"))
assert.Equal(t, c.Query("page"), "10") assert.Equal(t, "10", c.Query("page"))
value, ok = c.GetQuery("id") value, ok = c.GetQuery("id")
assert.True(t, ok) assert.True(t, ok)
assert.Empty(t, value) assert.Empty(t, value)
assert.Equal(t, c.DefaultQuery("id", "nada"), "") assert.Empty(t, c.DefaultQuery("id", "nada"))
assert.Empty(t, c.Query("id")) assert.Empty(t, c.Query("id"))
value, ok = c.GetQuery("NoKey") value, ok = c.GetQuery("NoKey")
assert.False(t, ok) assert.False(t, ok)
assert.Empty(t, value) assert.Empty(t, value)
assert.Equal(t, c.DefaultQuery("NoKey", "nada"), "nada") assert.Equal(t, "nada", c.DefaultQuery("NoKey", "nada"))
assert.Empty(t, c.Query("NoKey")) assert.Empty(t, c.Query("NoKey"))
// postform should not mess // postform should not mess
@ -372,29 +373,29 @@ func TestContextQueryAndPostForm(t *testing.T) {
c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", 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, "bar", c.DefaultPostForm("foo", "none"))
assert.Equal(t, c.PostForm("foo"), "bar") assert.Equal(t, "bar", c.PostForm("foo"))
assert.Empty(t, c.Query("foo")) assert.Empty(t, c.Query("foo"))
value, ok := c.GetPostForm("page") value, ok := c.GetPostForm("page")
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, value, "11") assert.Equal(t, "11", value)
assert.Equal(t, c.DefaultPostForm("page", "0"), "11") assert.Equal(t, "11", c.DefaultPostForm("page", "0"))
assert.Equal(t, c.PostForm("page"), "11") assert.Equal(t, "11", c.PostForm("page"))
assert.Equal(t, c.Query("page"), "") assert.Empty(t, c.Query("page"))
value, ok = c.GetPostForm("both") value, ok = c.GetPostForm("both")
assert.True(t, ok) assert.True(t, ok)
assert.Empty(t, value) assert.Empty(t, value)
assert.Empty(t, c.PostForm("both")) assert.Empty(t, c.PostForm("both"))
assert.Equal(t, c.DefaultPostForm("both", "nothing"), "") assert.Empty(t, c.DefaultPostForm("both", "nothing"))
assert.Equal(t, c.Query("both"), "GET") assert.Equal(t, "GET", c.Query("both"), "GET")
value, ok = c.GetQuery("id") value, ok = c.GetQuery("id")
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, value, "main") assert.Equal(t, "main", value)
assert.Equal(t, c.DefaultPostForm("id", "000"), "000") assert.Equal(t, "000", c.DefaultPostForm("id", "000"))
assert.Equal(t, c.Query("id"), "main") assert.Equal(t, "main", c.Query("id"))
assert.Empty(t, c.PostForm("id")) assert.Empty(t, c.PostForm("id"))
value, ok = c.GetQuery("NoKey") value, ok = c.GetQuery("NoKey")
@ -403,8 +404,8 @@ func TestContextQueryAndPostForm(t *testing.T) {
value, ok = c.GetPostForm("NoKey") value, ok = c.GetPostForm("NoKey")
assert.False(t, ok) assert.False(t, ok)
assert.Empty(t, value) assert.Empty(t, value)
assert.Equal(t, c.DefaultPostForm("NoKey", "nada"), "nada") assert.Equal(t, "nada", c.DefaultPostForm("NoKey", "nada"))
assert.Equal(t, c.DefaultQuery("NoKey", "nothing"), "nothing") assert.Equal(t, "nothing", c.DefaultQuery("NoKey", "nothing"))
assert.Empty(t, c.PostForm("NoKey")) assert.Empty(t, c.PostForm("NoKey"))
assert.Empty(t, c.Query("NoKey")) assert.Empty(t, c.Query("NoKey"))
@ -416,11 +417,11 @@ func TestContextQueryAndPostForm(t *testing.T) {
Array []string `form:"array[]"` 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, "bar", obj.Foo, "bar")
assert.Equal(t, obj.ID, "main") assert.Equal(t, "main", obj.ID, "main")
assert.Equal(t, obj.Page, 11) assert.Equal(t, 11, obj.Page, 11)
assert.Equal(t, obj.Both, "") assert.Empty(t, obj.Both)
assert.Equal(t, obj.Array, []string{"first", "second"}) assert.Equal(t, []string{"first", "second"}, obj.Array)
values, ok := c.GetQueryArray("array[]") values, ok := c.GetQueryArray("array[]")
assert.True(t, ok) assert.True(t, ok)
@ -444,44 +445,48 @@ 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"`
BarAsInt int `form:"bar"` BarAsInt int `form:"bar"`
Array []string `form:"array"` Array []string `form:"array"`
ID string `form:"id"` ID string `form:"id"`
TimeLocal time.Time `form:"time_local" time_format:"02/01/2006 15:04"` TimeLocal time.Time `form:"time_local" time_format:"02/01/2006 15:04"`
TimeUTC time.Time `form:"time_utc" time_format:"02/01/2006 15:04" time_utc:"1"` TimeUTC time.Time `form:"time_utc" time_format:"02/01/2006 15:04" time_utc:"1"`
BlankTime time.Time `form:"blank_time" time_format:"02/01/2006 15:04"` TimeLocation time.Time `form:"time_location" time_format:"02/01/2006 15:04" time_location:"Asia/Tokyo"`
BlankTime time.Time `form:"blank_time" time_format:"02/01/2006 15:04"`
} }
assert.NoError(t, c.Bind(&obj)) assert.NoError(t, c.Bind(&obj))
assert.Equal(t, obj.Foo, "bar") assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, obj.Bar, "10") assert.Equal(t, "10", obj.Bar)
assert.Equal(t, obj.BarAsInt, 10) assert.Equal(t, 10, obj.BarAsInt)
assert.Equal(t, obj.Array, []string{"first", "second"}) assert.Equal(t, []string{"first", "second"}, obj.Array)
assert.Equal(t, obj.ID, "") assert.Empty(t, obj.ID)
assert.Equal(t, obj.TimeLocal.Format("02/01/2006 15:04"), "31/12/2016 14:55") assert.Equal(t, "31/12/2016 14:55", obj.TimeLocal.Format("02/01/2006 15:04"))
assert.Equal(t, obj.TimeLocal.Location(), time.Local) assert.Equal(t, time.Local, obj.TimeLocal.Location())
assert.Equal(t, obj.TimeUTC.Format("02/01/2006 15:04"), "31/12/2016 14:55") assert.Equal(t, "31/12/2016 14:55", obj.TimeUTC.Format("02/01/2006 15:04"))
assert.Equal(t, obj.TimeUTC.Location(), time.UTC) assert.Equal(t, time.UTC, obj.TimeUTC.Location())
loc, _ := time.LoadLocation("Asia/Tokyo")
assert.Equal(t, "31/12/2016 14:55", obj.TimeLocation.Format("02/01/2006 15:04"))
assert.Equal(t, loc, obj.TimeLocation.Location())
assert.True(t, obj.BlankTime.IsZero()) assert.True(t, obj.BlankTime.IsZero())
value, ok := c.GetQuery("foo") value, ok := c.GetQuery("foo")
assert.False(t, ok) assert.False(t, ok)
assert.Empty(t, value) assert.Empty(t, value)
assert.Empty(t, c.Query("bar")) assert.Empty(t, c.Query("bar"))
assert.Equal(t, c.DefaultQuery("id", "nothing"), "nothing") assert.Equal(t, "nothing", c.DefaultQuery("id", "nothing"))
value, ok = c.GetPostForm("foo") value, ok = c.GetPostForm("foo")
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, value, "bar") assert.Equal(t, "bar", value)
assert.Equal(t, c.PostForm("foo"), "bar") assert.Equal(t, "bar", c.PostForm("foo"))
value, ok = c.GetPostForm("array") value, ok = c.GetPostForm("array")
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, value, "first") assert.Equal(t, "first", value)
assert.Equal(t, c.PostForm("array"), "first") assert.Equal(t, "first", c.PostForm("array"))
assert.Equal(t, c.DefaultPostForm("bar", "nothing"), "10") assert.Equal(t, "10", c.DefaultPostForm("bar", "nothing"))
value, ok = c.GetPostForm("id") value, ok = c.GetPostForm("id")
assert.True(t, ok) assert.True(t, ok)
@ -492,7 +497,7 @@ func TestContextPostFormMultipart(t *testing.T) {
value, ok = c.GetPostForm("nokey") value, ok = c.GetPostForm("nokey")
assert.False(t, ok) assert.False(t, ok)
assert.Empty(t, value) assert.Empty(t, value)
assert.Equal(t, c.DefaultPostForm("nokey", "nothing"), "nothing") assert.Equal(t, "nothing", c.DefaultPostForm("nokey", "nothing"))
values, ok := c.GetPostFormArray("array") values, ok := c.GetPostFormArray("array")
assert.True(t, ok) assert.True(t, ok)
@ -514,13 +519,13 @@ func TestContextPostFormMultipart(t *testing.T) {
func TestContextSetCookie(t *testing.T) { func TestContextSetCookie(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.SetCookie("user", "gin", 1, "/", "localhost", true, true) c.SetCookie("user", "gin", 1, "/", "localhost", true, true)
assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure") assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie"))
} }
func TestContextSetCookiePathEmpty(t *testing.T) { func TestContextSetCookiePathEmpty(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.SetCookie("user", "gin", 1, "", "localhost", true, true) c.SetCookie("user", "gin", 1, "", "localhost", true, true)
assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure") assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie"))
} }
func TestContextGetCookie(t *testing.T) { func TestContextGetCookie(t *testing.T) {
@ -528,17 +533,17 @@ func TestContextGetCookie(t *testing.T) {
c.Request, _ = http.NewRequest("GET", "/get", nil) c.Request, _ = http.NewRequest("GET", "/get", nil)
c.Request.Header.Set("Cookie", "user=gin") c.Request.Header.Set("Cookie", "user=gin")
cookie, _ := c.Cookie("user") cookie, _ := c.Cookie("user")
assert.Equal(t, cookie, "gin") assert.Equal(t, "gin", cookie)
_, err := c.Cookie("nokey") _, err := c.Cookie("nokey")
assert.Error(t, err) assert.Error(t, err)
} }
func TestContextBodyAllowedForStatus(t *testing.T) { func TestContextBodyAllowedForStatus(t *testing.T) {
assert.Equal(t, false, bodyAllowedForStatus(102)) assert.False(t, false, bodyAllowedForStatus(102))
assert.Equal(t, false, bodyAllowedForStatus(204)) assert.False(t, false, bodyAllowedForStatus(204))
assert.Equal(t, false, bodyAllowedForStatus(304)) assert.False(t, false, bodyAllowedForStatus(304))
assert.Equal(t, true, bodyAllowedForStatus(500)) assert.True(t, true, bodyAllowedForStatus(500))
} }
type TestPanicRender struct { type TestPanicRender struct {
@ -584,7 +589,7 @@ func TestContextRenderNoContentJSON(t *testing.T) {
c.JSON(204, H{"foo": "bar"}) c.JSON(204, H{"foo": "bar"})
assert.Equal(t, 204, w.Code) assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
@ -611,7 +616,7 @@ func TestContextRenderNoContentAPIJSON(t *testing.T) {
c.JSON(204, H{"foo": "bar"}) c.JSON(204, H{"foo": "bar"})
assert.Equal(t, 204, w.Code) assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json")
} }
@ -623,7 +628,7 @@ func TestContextRenderIndentedJSON(t *testing.T) {
c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
assert.Equal(t, w.Code, 201) assert.Equal(t, 201, w.Code)
assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\",\n \"nested\": {\n \"foo\": \"bar\"\n }\n}", w.Body.String()) assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\",\n \"nested\": {\n \"foo\": \"bar\"\n }\n}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
@ -636,7 +641,7 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) {
c.IndentedJSON(204, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) c.IndentedJSON(204, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
assert.Equal(t, 204, w.Code) assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
@ -649,9 +654,9 @@ func TestContextRenderSecureJSON(t *testing.T) {
router.SecureJsonPrefix("&&&START&&&") router.SecureJsonPrefix("&&&START&&&")
c.SecureJSON(201, []string{"foo", "bar"}) c.SecureJSON(201, []string{"foo", "bar"})
assert.Equal(t, w.Code, 201) assert.Equal(t, 201, w.Code)
assert.Equal(t, w.Body.String(), "&&&START&&&[\"foo\",\"bar\"]") assert.Equal(t, "&&&START&&&[\"foo\",\"bar\"]", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// Tests that no Custom JSON is rendered if code is 204 // Tests that no Custom JSON is rendered if code is 204
@ -662,8 +667,8 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) {
c.SecureJSON(204, []string{"foo", "bar"}) c.SecureJSON(204, []string{"foo", "bar"})
assert.Equal(t, 204, w.Code) assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// Tests that the response executes the templates // Tests that the response executes the templates
@ -671,14 +676,39 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) {
func TestContextRenderHTML(t *testing.T) { func TestContextRenderHTML(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, router := CreateTestContext(w) c, router := CreateTestContext(w)
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ) router.SetHTMLTemplate(templ)
c.HTML(201, "t", H{"name": "alexandernyquist"}) c.HTML(201, "t", H{"name": "alexandernyquist"})
assert.Equal(t, w.Code, 201) assert.Equal(t, 201, w.Code)
assert.Equal(t, w.Body.String(), "Hello alexandernyquist") assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type"))
}
func TestContextRenderHTML2(t *testing.T) {
w := httptest.NewRecorder()
c, router := CreateTestContext(w)
// print debug warning log when Engine.trees > 0
router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 1)
var b bytes.Buffer
setup(&b)
defer teardown()
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ)
assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", b.String())
c.HTML(201, "t", H{"name": "alexandernyquist"})
assert.Equal(t, 201, w.Code)
assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// Tests that no HTML is rendered if code is 204 // Tests that no HTML is rendered if code is 204
@ -691,8 +721,8 @@ func TestContextRenderNoContentHTML(t *testing.T) {
c.HTML(204, "t", H{"name": "alexandernyquist"}) c.HTML(204, "t", H{"name": "alexandernyquist"})
assert.Equal(t, 204, w.Code) assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// TestContextXML tests that the response is serialized as XML // TestContextXML tests that the response is serialized as XML
@ -703,9 +733,9 @@ func TestContextRenderXML(t *testing.T) {
c.XML(201, H{"foo": "bar"}) c.XML(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201) assert.Equal(t, 201, w.Code)
assert.Equal(t, w.Body.String(), "<map><foo>bar</foo></map>") assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8") assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// Tests that no XML is rendered if code is 204 // Tests that no XML is rendered if code is 204
@ -716,8 +746,8 @@ func TestContextRenderNoContentXML(t *testing.T) {
c.XML(204, H{"foo": "bar"}) c.XML(204, H{"foo": "bar"})
assert.Equal(t, 204, w.Code) assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8") assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// TestContextString tests that the response is returned // TestContextString tests that the response is returned
@ -728,9 +758,9 @@ func TestContextRenderString(t *testing.T) {
c.String(201, "test %s %d", "string", 2) c.String(201, "test %s %d", "string", 2)
assert.Equal(t, w.Code, 201) assert.Equal(t, 201, w.Code)
assert.Equal(t, w.Body.String(), "test string 2") assert.Equal(t, "test string 2", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// Tests that no String is rendered if code is 204 // Tests that no String is rendered if code is 204
@ -741,8 +771,8 @@ func TestContextRenderNoContentString(t *testing.T) {
c.String(204, "test %s %d", "string", 2) c.String(204, "test %s %d", "string", 2)
assert.Equal(t, 204, w.Code) assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// TestContextString tests that the response is returned // TestContextString tests that the response is returned
@ -754,9 +784,9 @@ func TestContextRenderHTMLString(t *testing.T) {
c.Header("Content-Type", "text/html; charset=utf-8") c.Header("Content-Type", "text/html; charset=utf-8")
c.String(201, "<html>%s %d</html>", "string", 3) c.String(201, "<html>%s %d</html>", "string", 3)
assert.Equal(t, w.Code, 201) assert.Equal(t, 201, w.Code)
assert.Equal(t, w.Body.String(), "<html>string 3</html>") assert.Equal(t, "<html>string 3</html>", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// Tests that no HTML String is rendered if code is 204 // Tests that no HTML String is rendered if code is 204
@ -768,8 +798,8 @@ func TestContextRenderNoContentHTMLString(t *testing.T) {
c.String(204, "<html>%s %d</html>", "string", 3) c.String(204, "<html>%s %d</html>", "string", 3)
assert.Equal(t, 204, w.Code) assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// TestContextData tests that the response can be written from `bytesting` // TestContextData tests that the response can be written from `bytesting`
@ -780,9 +810,9 @@ func TestContextRenderData(t *testing.T) {
c.Data(201, "text/csv", []byte(`foo,bar`)) c.Data(201, "text/csv", []byte(`foo,bar`))
assert.Equal(t, w.Code, 201) assert.Equal(t, 201, w.Code)
assert.Equal(t, w.Body.String(), "foo,bar") assert.Equal(t, "foo,bar", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv") assert.Equal(t, "text/csv", w.HeaderMap.Get("Content-Type"))
} }
// Tests that no Custom Data is rendered if code is 204 // Tests that no Custom Data is rendered if code is 204
@ -793,8 +823,8 @@ func TestContextRenderNoContentData(t *testing.T) {
c.Data(204, "text/csv", []byte(`foo,bar`)) c.Data(204, "text/csv", []byte(`foo,bar`))
assert.Equal(t, 204, w.Code) assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv") assert.Equal(t, "text/csv", w.HeaderMap.Get("Content-Type"))
} }
func TestContextRenderSSE(t *testing.T) { func TestContextRenderSSE(t *testing.T) {
@ -821,9 +851,9 @@ func TestContextRenderFile(t *testing.T) {
c.Request, _ = http.NewRequest("GET", "/", nil) c.Request, _ = http.NewRequest("GET", "/", nil)
c.File("./gin.go") c.File("./gin.go")
assert.Equal(t, w.Code, 200) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {") assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// TestContextRenderYAML tests that the response is serialized as YAML // TestContextRenderYAML tests that the response is serialized as YAML
@ -834,9 +864,9 @@ func TestContextRenderYAML(t *testing.T) {
c.YAML(201, H{"foo": "bar"}) c.YAML(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201) assert.Equal(t, 201, w.Code)
assert.Equal(t, w.Body.String(), "foo: bar\n") assert.Equal(t, "foo: bar\n", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/x-yaml; charset=utf-8") assert.Equal(t, "application/x-yaml; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
func TestContextHeaders(t *testing.T) { func TestContextHeaders(t *testing.T) {
@ -844,13 +874,13 @@ func TestContextHeaders(t *testing.T) {
c.Header("Content-Type", "text/plain") c.Header("Content-Type", "text/plain")
c.Header("X-Custom", "value") c.Header("X-Custom", "value")
assert.Equal(t, c.Writer.Header().Get("Content-Type"), "text/plain") assert.Equal(t, "text/plain", c.Writer.Header().Get("Content-Type"))
assert.Equal(t, c.Writer.Header().Get("X-Custom"), "value") assert.Equal(t, "value", c.Writer.Header().Get("X-Custom"))
c.Header("Content-Type", "text/html") c.Header("Content-Type", "text/html")
c.Header("X-Custom", "") c.Header("X-Custom", "")
assert.Equal(t, c.Writer.Header().Get("Content-Type"), "text/html") assert.Equal(t, "text/html", c.Writer.Header().Get("Content-Type"))
_, exist := c.Writer.Header()["X-Custom"] _, exist := c.Writer.Header()["X-Custom"]
assert.False(t, exist) assert.False(t, exist)
} }
@ -866,8 +896,8 @@ func TestContextRenderRedirectWithRelativePath(t *testing.T) {
c.Redirect(301, "/path") c.Redirect(301, "/path")
c.Writer.WriteHeaderNow() c.Writer.WriteHeaderNow()
assert.Equal(t, w.Code, 301) assert.Equal(t, 301, w.Code)
assert.Equal(t, w.Header().Get("Location"), "/path") assert.Equal(t, "/path", w.Header().Get("Location"))
} }
func TestContextRenderRedirectWithAbsolutePath(t *testing.T) { func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
@ -878,8 +908,8 @@ func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
c.Redirect(302, "http://google.com") c.Redirect(302, "http://google.com")
c.Writer.WriteHeaderNow() c.Writer.WriteHeaderNow()
assert.Equal(t, w.Code, 302) assert.Equal(t, 302, w.Code)
assert.Equal(t, w.Header().Get("Location"), "http://google.com") assert.Equal(t, "http://google.com", w.Header().Get("Location"))
} }
func TestContextRenderRedirectWith201(t *testing.T) { func TestContextRenderRedirectWith201(t *testing.T) {
@ -890,8 +920,8 @@ func TestContextRenderRedirectWith201(t *testing.T) {
c.Redirect(201, "/resource") c.Redirect(201, "/resource")
c.Writer.WriteHeaderNow() c.Writer.WriteHeaderNow()
assert.Equal(t, w.Code, 201) assert.Equal(t, 201, w.Code)
assert.Equal(t, w.Header().Get("Location"), "/resource") assert.Equal(t, "/resource", w.Header().Get("Location"))
} }
func TestContextRenderRedirectAll(t *testing.T) { func TestContextRenderRedirectAll(t *testing.T) {
@ -972,8 +1002,8 @@ func TestContextNegotiationFormat(t *testing.T) {
c.Request, _ = http.NewRequest("POST", "", nil) c.Request, _ = http.NewRequest("POST", "", nil)
assert.Panics(t, func() { c.NegotiateFormat() }) assert.Panics(t, func() { c.NegotiateFormat() })
assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON) assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON, MIMEXML))
assert.Equal(t, c.NegotiateFormat(MIMEHTML, MIMEJSON), MIMEHTML) assert.Equal(t, MIMEHTML, c.NegotiateFormat(MIMEHTML, MIMEJSON))
} }
func TestContextNegotiationFormatWithAccept(t *testing.T) { func TestContextNegotiationFormatWithAccept(t *testing.T) {
@ -981,9 +1011,9 @@ func TestContextNegotiationFormatWithAccept(t *testing.T) {
c.Request, _ = http.NewRequest("POST", "/", nil) c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEXML) assert.Equal(t, MIMEXML, c.NegotiateFormat(MIMEJSON, MIMEXML))
assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEHTML) assert.Equal(t, MIMEHTML, c.NegotiateFormat(MIMEXML, MIMEHTML))
assert.Equal(t, c.NegotiateFormat(MIMEJSON), "") assert.Empty(t, c.NegotiateFormat(MIMEJSON))
} }
func TestContextNegotiationFormatCustum(t *testing.T) { func TestContextNegotiationFormatCustum(t *testing.T) {
@ -994,9 +1024,9 @@ func TestContextNegotiationFormatCustum(t *testing.T) {
c.Accepted = nil c.Accepted = nil
c.SetAccepted(MIMEJSON, MIMEXML) c.SetAccepted(MIMEJSON, MIMEXML)
assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON) assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON, MIMEXML))
assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEXML) assert.Equal(t, MIMEXML, c.NegotiateFormat(MIMEXML, MIMEHTML))
assert.Equal(t, c.NegotiateFormat(MIMEJSON), MIMEJSON) assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON))
} }
func TestContextIsAborted(t *testing.T) { func TestContextIsAborted(t *testing.T) {
@ -1022,9 +1052,9 @@ func TestContextAbortWithStatus(t *testing.T) {
c.index = 4 c.index = 4
c.AbortWithStatus(401) c.AbortWithStatus(401)
assert.Equal(t, c.index, abortIndex) assert.Equal(t, abortIndex, c.index)
assert.Equal(t, c.Writer.Status(), 401) assert.Equal(t, 401, c.Writer.Status())
assert.Equal(t, w.Code, 401) assert.Equal(t, 401, w.Code)
assert.True(t, c.IsAborted()) assert.True(t, c.IsAborted())
} }
@ -1044,13 +1074,13 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
c.AbortWithStatusJSON(415, in) c.AbortWithStatusJSON(415, in)
assert.Equal(t, c.index, abortIndex) assert.Equal(t, abortIndex, c.index)
assert.Equal(t, c.Writer.Status(), 415) assert.Equal(t, 415, c.Writer.Status())
assert.Equal(t, w.Code, 415) assert.Equal(t, 415, w.Code)
assert.True(t, c.IsAborted()) assert.True(t, c.IsAborted())
contentType := w.Header().Get("Content-Type") contentType := w.Header().Get("Content-Type")
assert.Equal(t, contentType, "application/json; charset=utf-8") assert.Equal(t, "application/json; charset=utf-8", contentType)
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
buf.ReadFrom(w.Body) buf.ReadFrom(w.Body)
@ -1064,7 +1094,7 @@ func TestContextError(t *testing.T) {
c.Error(errors.New("first error")) c.Error(errors.New("first error"))
assert.Len(t, c.Errors, 1) assert.Len(t, c.Errors, 1)
assert.Equal(t, c.Errors.String(), "Error #01: first error\n") assert.Equal(t, "Error #01: first error\n", c.Errors.String())
c.Error(&Error{ c.Error(&Error{
Err: errors.New("second error"), Err: errors.New("second error"),
@ -1073,13 +1103,13 @@ func TestContextError(t *testing.T) {
}) })
assert.Len(t, c.Errors, 2) assert.Len(t, c.Errors, 2)
assert.Equal(t, c.Errors[0].Err, errors.New("first error")) assert.Equal(t, errors.New("first error"), c.Errors[0].Err)
assert.Nil(t, c.Errors[0].Meta) assert.Nil(t, c.Errors[0].Meta)
assert.Equal(t, c.Errors[0].Type, ErrorTypePrivate) assert.Equal(t, ErrorTypePrivate, c.Errors[0].Type)
assert.Equal(t, c.Errors[1].Err, errors.New("second error")) assert.Equal(t, errors.New("second error"), c.Errors[1].Err)
assert.Equal(t, c.Errors[1].Meta, "some data 2") assert.Equal(t, "some data 2", c.Errors[1].Meta)
assert.Equal(t, c.Errors[1].Type, ErrorTypePublic) assert.Equal(t, ErrorTypePublic, c.Errors[1].Type)
assert.Equal(t, c.Errors.Last(), c.Errors[1]) assert.Equal(t, c.Errors.Last(), c.Errors[1])
@ -1097,12 +1127,12 @@ func TestContextTypedError(t *testing.T) {
c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate)
for _, err := range c.Errors.ByType(ErrorTypePublic) { for _, err := range c.Errors.ByType(ErrorTypePublic) {
assert.Equal(t, err.Type, ErrorTypePublic) assert.Equal(t, ErrorTypePublic, err.Type)
} }
for _, err := range c.Errors.ByType(ErrorTypePrivate) { for _, err := range c.Errors.ByType(ErrorTypePrivate) {
assert.Equal(t, err.Type, ErrorTypePrivate) assert.Equal(t, ErrorTypePrivate, err.Type)
} }
assert.Equal(t, c.Errors.Errors(), []string{"externo 0", "interno 0"}) assert.Equal(t, []string{"externo 0", "interno 0"}, c.Errors.Errors())
} }
func TestContextAbortWithError(t *testing.T) { func TestContextAbortWithError(t *testing.T) {
@ -1111,8 +1141,8 @@ func TestContextAbortWithError(t *testing.T) {
c.AbortWithError(401, errors.New("bad input")).SetMeta("some input") c.AbortWithError(401, errors.New("bad input")).SetMeta("some input")
assert.Equal(t, w.Code, 401) assert.Equal(t, 401, w.Code)
assert.Equal(t, c.index, abortIndex) assert.Equal(t, abortIndex, c.index)
assert.True(t, c.IsAborted()) assert.True(t, c.IsAborted())
} }
@ -1143,7 +1173,7 @@ func TestContextClientIP(t *testing.T) {
// no port // no port
c.Request.RemoteAddr = "50.50.50.50" c.Request.RemoteAddr = "50.50.50.50"
assert.Equal(t, "", c.ClientIP()) assert.Empty(t, c.ClientIP())
} }
func TestContextContentType(t *testing.T) { func TestContextContentType(t *testing.T) {
@ -1151,7 +1181,7 @@ func TestContextContentType(t *testing.T) {
c.Request, _ = http.NewRequest("POST", "/", nil) c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", "application/json; charset=utf-8") c.Request.Header.Set("Content-Type", "application/json; charset=utf-8")
assert.Equal(t, c.ContentType(), "application/json") assert.Equal(t, "application/json", c.ContentType())
} }
func TestContextAutoBindJSON(t *testing.T) { func TestContextAutoBindJSON(t *testing.T) {
@ -1164,8 +1194,8 @@ func TestContextAutoBindJSON(t *testing.T) {
Bar string `json:"bar"` Bar string `json:"bar"`
} }
assert.NoError(t, c.Bind(&obj)) assert.NoError(t, c.Bind(&obj))
assert.Equal(t, obj.Bar, "foo") assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, obj.Foo, "bar") assert.Equal(t, "bar", obj.Foo)
assert.Empty(t, c.Errors) assert.Empty(t, c.Errors)
} }
@ -1181,9 +1211,9 @@ func TestContextBindWithJSON(t *testing.T) {
Bar string `json:"bar"` Bar string `json:"bar"`
} }
assert.NoError(t, c.BindJSON(&obj)) assert.NoError(t, c.BindJSON(&obj))
assert.Equal(t, obj.Bar, "foo") assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, obj.Foo, "bar") assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, w.Body.Len(), 0) assert.Equal(t, 0, w.Body.Len())
} }
func TestContextBindWithQuery(t *testing.T) { func TestContextBindWithQuery(t *testing.T) {
@ -1219,10 +1249,77 @@ func TestContextBadAutoBind(t *testing.T) {
assert.Empty(t, obj.Bar) assert.Empty(t, obj.Bar)
assert.Empty(t, obj.Foo) assert.Empty(t, obj.Foo)
assert.Equal(t, w.Code, 400) assert.Equal(t, 400, w.Code)
assert.True(t, c.IsAborted()) assert.True(t, c.IsAborted())
} }
func TestContextAutoShouldBindJSON(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEJSON)
var obj struct {
Foo string `json:"foo"`
Bar string `json:"bar"`
}
assert.NoError(t, c.ShouldBind(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Empty(t, c.Errors)
}
func TestContextShouldBindWithJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct {
Foo string `json:"foo"`
Bar string `json:"bar"`
}
assert.NoError(t, c.ShouldBindJSON(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextShouldBindWithQuery(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
var obj struct {
Foo string `form:"foo"`
Bar string `form:"bar"`
}
assert.NoError(t, c.ShouldBindQuery(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBadAutoShouldBind(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEJSON)
var obj struct {
Foo string `json:"foo"`
Bar string `json:"bar"`
}
assert.False(t, c.IsAborted())
assert.Error(t, c.ShouldBind(&obj))
assert.Empty(t, obj.Bar)
assert.Empty(t, obj.Foo)
assert.False(t, c.IsAborted())
}
func TestContextGolangContext(t *testing.T) { func TestContextGolangContext(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
@ -1235,7 +1332,7 @@ func TestContextGolangContext(t *testing.T) {
assert.Nil(t, c.Value("foo")) assert.Nil(t, c.Value("foo"))
c.Set("foo", "bar") c.Set("foo", "bar")
assert.Equal(t, c.Value("foo"), "bar") assert.Equal(t, "bar", c.Value("foo"))
assert.Nil(t, c.Value(1)) assert.Nil(t, c.Value(1))
} }
@ -1267,7 +1364,7 @@ func TestGetRequestHeaderValue(t *testing.T) {
c.Request.Header.Set("Gin-Version", "1.0.0") c.Request.Header.Set("Gin-Version", "1.0.0")
assert.Equal(t, "1.0.0", c.GetHeader("Gin-Version")) assert.Equal(t, "1.0.0", c.GetHeader("Gin-Version"))
assert.Equal(t, "", c.GetHeader("Connection")) assert.Empty(t, c.GetHeader("Connection"))
} }
func TestContextGetRawData(t *testing.T) { func TestContextGetRawData(t *testing.T) {

View File

@ -15,7 +15,7 @@ func init() {
} }
// IsDebugging returns true if the framework is running in debug mode. // IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.Release) to disable debug mode. // Use SetMode(gin.ReleaseMode) to disable debug mode.
func IsDebugging() bool { func IsDebugging() bool {
return ginMode == debugCode return ginMode == debugCode
} }
@ -46,6 +46,12 @@ func debugPrint(format string, values ...interface{}) {
} }
} }
func debugPrintWARNINGDefault() {
debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
`)
}
func debugPrintWARNINGNew() { 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

View File

@ -86,6 +86,24 @@ func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) {
assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", w.String()) assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", w.String())
} }
func TestDebugPrintWARNINGDefault(t *testing.T) {
var w bytes.Buffer
setup(&w)
defer teardown()
debugPrintWARNINGDefault()
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", w.String())
}
func TestDebugPrintWARNINGNew(t *testing.T) {
var w bytes.Buffer
setup(&w)
defer teardown()
debugPrintWARNINGNew()
assert.Equal(t, "[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n", w.String())
}
func setup(w io.Writer) { func setup(w io.Writer) {
SetMode(DebugMode) SetMode(DebugMode)
log.SetOutput(w) log.SetOutput(w)

View File

@ -15,7 +15,7 @@ import (
func (c *Context) BindWith(obj interface{}, b binding.Binding) error { func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
log.Println(`BindWith(\"interface{}, binding.Binding\") error is going to log.Println(`BindWith(\"interface{}, binding.Binding\") error is going to
be deprecated, please check issue #662 and either use MustBindWith() if you be deprecated, please check issue #662 and either use MustBindWith() if you
want HTTP 400 to be automatically returned if any error occur, of use want HTTP 400 to be automatically returned if any error occur, or use
ShouldBindWith() if you need to manage the error.`) ShouldBindWith() if you need to manage the error.`)
return c.MustBindWith(obj, b) return c.MustBindWith(obj, b)
} }

31
deprecated_test.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin/binding"
"github.com/stretchr/testify/assert"
)
func TestBindWith(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
var obj struct {
Foo string `form:"foo"`
Bar string `form:"bar"`
}
assert.NoError(t, c.BindWith(&obj, binding.Form))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}

View File

@ -148,7 +148,7 @@ func (a errorMsgs) String() string {
} }
var buffer bytes.Buffer var buffer bytes.Buffer
for i, msg := range a { for i, msg := range a {
fmt.Fprintf(&buffer, "Error #%02d: %s\n", (i + 1), msg.Err) fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err)
if msg.Meta != nil { if msg.Meta != nil {
fmt.Fprintf(&buffer, " Meta: %v\n", msg.Meta) fmt.Fprintf(&buffer, " Meta: %v\n", msg.Meta)
} }

View File

@ -6,7 +6,7 @@ import (
var DB = make(map[string]string) var DB = make(map[string]string)
func main() { func setupRouter() *gin.Engine {
// Disable Console Color // Disable Console Color
// gin.DisableConsoleColor() // gin.DisableConsoleColor()
r := gin.Default() r := gin.Default()
@ -53,6 +53,11 @@ func main() {
} }
}) })
return r
}
func main() {
r := setupRouter()
// Listen and Server in 0.0.0.0:8080 // Listen and Server in 0.0.0.0:8080
r.Run(":8080") r.Run(":8080")
} }

View File

@ -0,0 +1,20 @@
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPingRoute(t *testing.T) {
router := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/ping", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, "pong", w.Body.String())
}

View File

@ -0,0 +1,45 @@
package main
import (
"net/http"
"reflect"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8"
)
type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}
func bookableDate(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
if date, ok := field.Interface().(time.Time); ok {
today := time.Now()
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
return false
}
}
return true
}
func main() {
route := gin.Default()
binding.Validator.RegisterValidation("bookabledate", bookableDate)
route.GET("/bookable", getBookable)
route.Run(":8085")
}
func getBookable(c *gin.Context) {
var b Booking
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}

View File

@ -41,5 +41,5 @@ func main() {
} }
} }
log.Println("Server exist") log.Println("Server exiting")
} }

View File

@ -44,5 +44,5 @@ func main() {
if err := srv.Shutdown(ctx); err != nil { if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err) log.Fatal("Server Shutdown:", err)
} }
log.Println("Server exist") log.Println("Server exiting")
} }

View File

@ -0,0 +1,74 @@
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)
var (
g errgroup.Group
)
func router01() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 01",
},
)
})
return e
}
func router02() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 02",
},
)
})
return e
}
func main() {
server01 := &http.Server{
Addr: ":8080",
Handler: router01(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
server02 := &http.Server{
Addr: ":8081",
Handler: router02(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
g.Go(func() error {
return server01.ListenAndServe()
})
g.Go(func() error {
return server02.ListenAndServe()
})
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}

View File

@ -29,8 +29,8 @@ func statsWorker() {
"timestamp": uint64(time.Now().Unix()), "timestamp": uint64(time.Now().Unix()),
"HeapInuse": stats.HeapInuse, "HeapInuse": stats.HeapInuse,
"StackInuse": stats.StackInuse, "StackInuse": stats.StackInuse,
"Mallocs": (stats.Mallocs - lastMallocs), "Mallocs": stats.Mallocs - lastMallocs,
"Frees": (stats.Frees - lastFrees), "Frees": stats.Frees - lastFrees,
"Inbound": uint64(messages.Get("inbound")), "Inbound": uint64(messages.Get("inbound")),
"Outbound": uint64(messages.Get("outbound")), "Outbound": uint64(messages.Get("outbound")),
"Connected": connectedUsers(), "Connected": connectedUsers(),

View File

@ -9,6 +9,8 @@ import (
func main() { func main() {
router := gin.Default() router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.Static("/", "./public") router.Static("/", "./public")
router.POST("/upload", func(c *gin.Context) { router.POST("/upload", func(c *gin.Context) {
name := c.PostForm("name") name := c.PostForm("name")

View File

@ -9,6 +9,8 @@ import (
func main() { func main() {
router := gin.Default() router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.Static("/", "./public") router.Static("/", "./public")
router.POST("/upload", func(c *gin.Context) { router.POST("/upload", func(c *gin.Context) {
name := c.PostForm("name") name := c.PostForm("name")

18
fixtures/testdata/cert.pem vendored Normal file
View File

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC9DCCAdygAwIBAgIQUNSK+OxWHYYFxHVJV0IlpDANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQKEwdBY21lIENvMB4XDTE3MTExNjEyMDA0N1oXDTE4MTExNjEyMDA0
N1owEjEQMA4GA1UEChMHQWNtZSBDbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAKmyj/YZpD59Bpy4w3qf6VzMw9uUBsWp+IP4kl7z5cmGHYUHn/YopTLH
vR23GAB12p6Km5QWzCBuJF4j61PJXHfg3/rjShZ77JcQ3kzxuy1iKDI+DNKN7Klz
rdjJ49QD0lczZHeBvvCL7JsJFKFjGy62rnStuW8LaIEdtjXT+GUZTxJh6G7yPYfD
MS1IsdMQGOdbGwNa+qogMuQPh0TzHw+C73myKrjY6pREijknMC/rnIMz9dLPt6Kl
xXy4br443dpY6dYGIhDuKhROT+vZ05HKasuuQUFhY7v/KoUpEZMB9rfUSzjQ5fak
eDUAMniXRcd+DmwvboG2TI6ixmuPK+ECAwEAAaNGMEQwDgYDVR0PAQH/BAQDAgWg
MBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDwYDVR0RBAgwBocE
fwAAATANBgkqhkiG9w0BAQsFAAOCAQEAMXOLvj7BFsxdbcfRPBd0OFrH/8lI7vPV
LRcJ6r5iv0cnNvZXXbIOQLbg4clJAWjoE08nRm1KvNXhKdns0ELEV86YN2S6jThq
rIGrBqKtaJLB3M9BtDSiQ6SGPLYrWvmhj3Avi8PbSGy51bpGbqopd16j6LYU7Cp2
TefMRlOAFtHojpCVon1CMpqcNxS0WNlQ3lUBSrw3HB0o12x++roja2ibF54tSHXB
KUuadoEzN+mMBwenEBychmAGzdiG4GQHRmhigh85+mtW6UMGiqyCZHs0EgE9FCLL
sRrsTI/VOzLz6lluygXkOsXrP+PP0SvmE3eylWjj9e2nj/u/Cy2YKg==
-----END CERTIFICATE-----

27
fixtures/testdata/key.pem vendored Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAqbKP9hmkPn0GnLjDep/pXMzD25QGxan4g/iSXvPlyYYdhQef
9iilMse9HbcYAHXanoqblBbMIG4kXiPrU8lcd+Df+uNKFnvslxDeTPG7LWIoMj4M
0o3sqXOt2Mnj1APSVzNkd4G+8IvsmwkUoWMbLraudK25bwtogR22NdP4ZRlPEmHo
bvI9h8MxLUix0xAY51sbA1r6qiAy5A+HRPMfD4LvebIquNjqlESKOScwL+ucgzP1
0s+3oqXFfLhuvjjd2ljp1gYiEO4qFE5P69nTkcpqy65BQWFju/8qhSkRkwH2t9RL
ONDl9qR4NQAyeJdFx34ObC9ugbZMjqLGa48r4QIDAQABAoIBAD5mhd+GMEo2KU9J
9b/Ku8I/HapJtW/L/7Fvn0tBPncrVQGM+zpGWfDhV95sbGwG6lwwNeNvuqIWPlNL
vAY0XkdKrrIQEDdSXH50WnpKzXxzwrou7QIj5Cmvevbjzl4xBZDBOilj0XWczmV4
IljyG5XC4UXQeAaoWEZaSZ1jk8yAt2Zq1Hgg7HqhHsK/arWXBgax+4K5nV/s9gZx
yjKU9mXTIs7k/aNnZqwQKqcZF+l3mvbZttOaFwsP14H0I8OFWhnM9hie54Dejqxi
f4/llNxDqUs6lqJfP3qNxtORLcFe75M+Yl8v7g2hkjtLdZBakPzSTEx3TAK/UHgi
aM8DdxECgYEA3fmg/PI4EgUEj0C3SCmQXR/CnQLMUQgb54s0asp4akvp+M7YCcr1
pQd3HFUpBwhBcJg5LeSe87vLupY7pHCKk56cl9WY6hse0b9sP/7DWJuGiO62m0E0
vNjQ2jpG99oR2ROIHHeWsGCpGLmrRT/kY+vR3M+AOLZniXlOCw8k0aUCgYEAw7WL
XFWLxgZYQYilywqrQmfv1MBfaUCvykO6oWB+f6mmnihSFjecI+nDw/b3yXVYGEgy
0ebkuw0jP8suC8wBqX9WuXj+9nZNomJRssJyOMiEhDEqUiTztFPSp9pdruoakLTh
Wk1p9NralOqGPUmxpXlFKVmYRTUbluikVxDypI0CgYBn6sqEQH0hann0+o4TWWn9
PrYkPUAbm1k8771tVTZERR/W3Dbldr/DL5iCihe39BR2urziEEqdvkglJNntJMar
TzDuIBADYQjvltb9qq4XGFBGYMLaMg+XbUVxNKEuvUdnwa4R7aZ9EfN34MwekkfA
w5Cu9/GGG1ajVEfGA6PwBQKBgA3o71jGs8KFXOx7e90sivOTU5Z5fc6LTHNB0Rf7
NcJ5GmCPWRY/KZfb25AoE4B8GKDRMNt+X69zxZeZJ1KrU0rqxA02rlhyHB54gnoE
G/4xMkn6/JkOC0w70PMhMBtohC7YzFOQwQEoNPT0nkno3Pl33xSLS6lPlwBo1JVj
nPtZAoGACXNLXYkR5vexE+w6FGl59r4RQhu1XU8Mr5DIHeB7kXPN3RKbS201M+Tb
SB5jbu0iDV477XkzSNmhaksFf2wM9MT6CaE+8n3UU5tMa+MmBGgwYTp/i9HkqVh5
jjpJifn1VWBINd4cpNzwCg9LXoo0tbtUPWwGzqVeyo/YE5GIHGo=
-----END RSA PRIVATE KEY-----

100
gin.go
View File

@ -14,12 +14,17 @@ import (
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
) )
// Version is Framework's version. const (
const Version = "v1.2" // Version is Framework's version.
Version = "v1.2"
defaultMultipartMemory = 32 << 20 // 32 MB
)
var default404Body = []byte("404 page not found") var (
var default405Body = []byte("405 method not allowed") default404Body = []byte("404 page not found")
var defaultAppEngine bool default405Body = []byte("405 method not allowed")
defaultAppEngine bool
)
type HandlerFunc func(*Context) type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc type HandlersChain []HandlerFunc
@ -44,16 +49,6 @@ type RoutesInfo []RouteInfo
// Create an instance of Engine, by using New() or Default() // Create an instance of Engine, by using New() or Default()
type Engine struct { type Engine struct {
RouterGroup RouterGroup
delims render.Delims
secureJsonPrefix string
HTMLRender render.HTMLRender
FuncMap template.FuncMap
allNoRoute HandlersChain
allNoMethod HandlersChain
noRoute HandlersChain
noMethod HandlersChain
pool sync.Pool
trees methodTrees
// Enables automatic redirection if the current route can't be matched but a // Enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists. // handler for the path with (without) the trailing slash exists.
@ -88,10 +83,26 @@ type Engine struct {
// If enabled, the url.RawPath will be used to find parameters. // If enabled, the url.RawPath will be used to find parameters.
UseRawPath bool UseRawPath bool
// If true, the path value will be unescaped. // If true, the path value will be unescaped.
// If UseRawPath is false (by default), the UnescapePathValues effectively is true, // If UseRawPath is false (by default), the UnescapePathValues effectively is true,
// as url.Path gonna be used, which is already unescaped. // as url.Path gonna be used, which is already unescaped.
UnescapePathValues bool UnescapePathValues bool
// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
// method call.
MaxMultipartMemory int64
delims render.Delims
secureJsonPrefix string
HTMLRender render.HTMLRender
FuncMap template.FuncMap
allNoRoute HandlersChain
allNoMethod HandlersChain
noRoute HandlersChain
noMethod HandlersChain
pool sync.Pool
trees methodTrees
} }
var _ IRouter = &Engine{} var _ IRouter = &Engine{}
@ -120,6 +131,7 @@ func New() *Engine {
AppEngine: defaultAppEngine, AppEngine: defaultAppEngine,
UseRawPath: false, UseRawPath: false,
UnescapePathValues: true, UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9), trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"}, delims: render.Delims{Left: "{{", Right: "}}"},
secureJsonPrefix: "while(1);", secureJsonPrefix: "while(1);",
@ -133,6 +145,7 @@ func New() *Engine {
// Default returns an Engine instance with the Logger and Recovery middleware already attached. // Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine { func Default() *Engine {
debugPrintWARNINGDefault()
engine := New() engine := New()
engine.Use(Logger(), Recovery()) engine.Use(Logger(), Recovery())
return engine return engine
@ -147,22 +160,30 @@ func (engine *Engine) Delims(left, right string) *Engine {
return engine return engine
} }
// SecureJsonPrefix sets the secureJsonPrefix used in Context.SecureJSON.
func (engine *Engine) SecureJsonPrefix(prefix string) *Engine { func (engine *Engine) SecureJsonPrefix(prefix string) *Engine {
engine.secureJsonPrefix = prefix engine.secureJsonPrefix = prefix
return engine return engine
} }
// LoadHTMLGlob loads HTML files identified by glob pattern
// and associates the result with HTML renderer.
func (engine *Engine) LoadHTMLGlob(pattern string) { func (engine *Engine) LoadHTMLGlob(pattern string) {
left := engine.delims.Left
right := engine.delims.Right
if IsDebugging() { if IsDebugging() {
debugPrintLoadTemplate(template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern))) debugPrintLoadTemplate(template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern)))
engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims} engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims}
return return
} }
templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern)) templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))
engine.SetHTMLTemplate(templ) engine.SetHTMLTemplate(templ)
} }
// LoadHTMLFiles loads a slice of HTML files
// and associates the result with HTML renderer.
func (engine *Engine) LoadHTMLFiles(files ...string) { func (engine *Engine) LoadHTMLFiles(files ...string) {
if IsDebugging() { if IsDebugging() {
engine.HTMLRender = render.HTMLDebug{Files: files, FuncMap: engine.FuncMap, Delims: engine.delims} engine.HTMLRender = render.HTMLDebug{Files: files, FuncMap: engine.FuncMap, Delims: engine.delims}
@ -173,6 +194,7 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
engine.SetHTMLTemplate(templ) engine.SetHTMLTemplate(templ)
} }
// SetHTMLTemplate associate a template with HTML renderer.
func (engine *Engine) SetHTMLTemplate(templ *template.Template) { func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
if len(engine.trees) > 0 { if len(engine.trees) > 0 {
debugPrintWARNINGSetHTMLTemplate() debugPrintWARNINGSetHTMLTemplate()
@ -181,6 +203,7 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
engine.HTMLRender = render.HTMLProduction{Template: templ.Funcs(engine.FuncMap)} engine.HTMLRender = render.HTMLProduction{Template: templ.Funcs(engine.FuncMap)}
} }
// SetFuncMap sets the FuncMap used for template.FuncMap.
func (engine *Engine) SetFuncMap(funcMap template.FuncMap) { func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
engine.FuncMap = funcMap engine.FuncMap = funcMap
} }
@ -217,7 +240,7 @@ func (engine *Engine) rebuild405Handlers() {
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
assert1(path[0] == '/', "path must begin with '/'") assert1(path[0] == '/', "path must begin with '/'")
assert1(len(method) > 0, "HTTP method can not be empty") assert1(method != "", "HTTP method can not be empty")
assert1(len(handlers) > 0, "there must be at least one handler") assert1(len(handlers) > 0, "there must be at least one handler")
debugPrintRoute(method, path, handlers) debugPrintRoute(method, path, handlers)
@ -314,16 +337,13 @@ func (engine *Engine) HandleContext(c *Context) {
engine.pool.Put(c) engine.pool.Put(c)
} }
func (engine *Engine) handleHTTPRequest(context *Context) { func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := context.Request.Method httpMethod := c.Request.Method
var path string path := c.Request.URL.Path
var unescape bool unescape := false
if engine.UseRawPath && len(context.Request.URL.RawPath) > 0 { if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
path = context.Request.URL.RawPath path = c.Request.URL.RawPath
unescape = engine.UnescapePathValues unescape = engine.UnescapePathValues
} else {
path = context.Request.URL.Path
unescape = false
} }
// Find root of the tree for the given HTTP method // Find root of the tree for the given HTTP method
@ -332,20 +352,20 @@ func (engine *Engine) handleHTTPRequest(context *Context) {
if t[i].method == httpMethod { if t[i].method == httpMethod {
root := t[i].root root := t[i].root
// Find route in tree // Find route in tree
handlers, params, tsr := root.getValue(path, context.Params, unescape) handlers, params, tsr := root.getValue(path, c.Params, unescape)
if handlers != nil { if handlers != nil {
context.handlers = handlers c.handlers = handlers
context.Params = params c.Params = params
context.Next() c.Next()
context.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()
return return
} }
if httpMethod != "CONNECT" && path != "/" { if httpMethod != "CONNECT" && path != "/" {
if tsr && engine.RedirectTrailingSlash { if tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(context) redirectTrailingSlash(c)
return return
} }
if engine.RedirectFixedPath && redirectFixedPath(context, root, engine.RedirectFixedPath) { if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return return
} }
} }
@ -357,15 +377,15 @@ func (engine *Engine) handleHTTPRequest(context *Context) {
for _, tree := range engine.trees { for _, tree := range engine.trees {
if tree.method != httpMethod { if tree.method != httpMethod {
if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil { if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
context.handlers = engine.allNoMethod c.handlers = engine.allNoMethod
serveError(context, 405, default405Body) serveError(c, 405, default405Body)
return return
} }
} }
} }
} }
context.handlers = engine.allNoRoute c.handlers = engine.allNoRoute
serveError(context, 404, default404Body) serveError(c, 404, default404Body)
} }
var mimePlain = []string{MIMEPlain} var mimePlain = []string{MIMEPlain}
@ -391,8 +411,8 @@ func redirectTrailingSlash(c *Context) {
code = 307 code = 307
} }
if len(path) > 1 && path[len(path)-1] == '/' { if length := len(path); length > 1 && path[length-1] == '/' {
req.URL.Path = path[:len(path)-1] req.URL.Path = path[:length-1]
} else { } else {
req.URL.Path = path + "/" req.URL.Path = path + "/"
} }

View File

@ -9,15 +9,15 @@ import (
"net/http" "net/http"
"sync" "sync"
. "github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var once sync.Once var once sync.Once
var internalEngine *Engine var internalEngine *gin.Engine
func engine() *Engine { func engine() *gin.Engine {
once.Do(func() { once.Do(func() {
internalEngine = Default() internalEngine = gin.Default()
}) })
return internalEngine return internalEngine
} }
@ -35,65 +35,65 @@ func SetHTMLTemplate(templ *template.Template) {
} }
// NoRoute adds handlers for NoRoute. It return a 404 code by default. // NoRoute adds handlers for NoRoute. It return a 404 code by default.
func NoRoute(handlers ...HandlerFunc) { func NoRoute(handlers ...gin.HandlerFunc) {
engine().NoRoute(handlers...) engine().NoRoute(handlers...)
} }
// NoMethod sets the handlers called when... TODO // NoMethod sets the handlers called when... TODO
func NoMethod(handlers ...HandlerFunc) { func NoMethod(handlers ...gin.HandlerFunc) {
engine().NoMethod(handlers...) engine().NoMethod(handlers...)
} }
// Group 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(relativePath string, handlers ...HandlerFunc) *RouterGroup { func Group(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup {
return engine().Group(relativePath, handlers...) return engine().Group(relativePath, handlers...)
} }
func Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { func Handle(httpMethod, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().Handle(httpMethod, relativePath, handlers...) return engine().Handle(httpMethod, relativePath, handlers...)
} }
// POST is a shortcut for router.Handle("POST", path, handle) // POST is a shortcut for router.Handle("POST", path, handle)
func POST(relativePath string, handlers ...HandlerFunc) IRoutes { func POST(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().POST(relativePath, handlers...) return engine().POST(relativePath, handlers...)
} }
// GET is a shortcut for router.Handle("GET", path, handle) // GET is a shortcut for router.Handle("GET", path, handle)
func GET(relativePath string, handlers ...HandlerFunc) IRoutes { func GET(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().GET(relativePath, handlers...) return engine().GET(relativePath, handlers...)
} }
// DELETE is a shortcut for router.Handle("DELETE", path, handle) // DELETE is a shortcut for router.Handle("DELETE", path, handle)
func DELETE(relativePath string, handlers ...HandlerFunc) IRoutes { func DELETE(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().DELETE(relativePath, handlers...) return engine().DELETE(relativePath, handlers...)
} }
// PATCH is a shortcut for router.Handle("PATCH", path, handle) // PATCH is a shortcut for router.Handle("PATCH", path, handle)
func PATCH(relativePath string, handlers ...HandlerFunc) IRoutes { func PATCH(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().PATCH(relativePath, handlers...) return engine().PATCH(relativePath, handlers...)
} }
// PUT is a shortcut for router.Handle("PUT", path, handle) // PUT is a shortcut for router.Handle("PUT", path, handle)
func PUT(relativePath string, handlers ...HandlerFunc) IRoutes { func PUT(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().PUT(relativePath, handlers...) return engine().PUT(relativePath, handlers...)
} }
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) // OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle)
func OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes { func OPTIONS(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().OPTIONS(relativePath, handlers...) return engine().OPTIONS(relativePath, handlers...)
} }
// HEAD is a shortcut for router.Handle("HEAD", path, handle) // HEAD is a shortcut for router.Handle("HEAD", path, handle)
func HEAD(relativePath string, handlers ...HandlerFunc) IRoutes { func HEAD(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().HEAD(relativePath, handlers...) return engine().HEAD(relativePath, handlers...)
} }
func Any(relativePath string, handlers ...HandlerFunc) IRoutes { func Any(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().Any(relativePath, handlers...) return engine().Any(relativePath, handlers...)
} }
func StaticFile(relativePath, filepath string) IRoutes { func StaticFile(relativePath, filepath string) gin.IRoutes {
return engine().StaticFile(relativePath, filepath) return engine().StaticFile(relativePath, filepath)
} }
@ -103,18 +103,18 @@ func StaticFile(relativePath, filepath string) IRoutes {
// 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 Static(relativePath, root string) IRoutes { func Static(relativePath, root string) gin.IRoutes {
return engine().Static(relativePath, root) return engine().Static(relativePath, root)
} }
func StaticFS(relativePath string, fs http.FileSystem) IRoutes { func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes {
return engine().StaticFS(relativePath, fs) return engine().StaticFS(relativePath, fs)
} }
// Use attachs a global middleware to the router. ie. the middlewares attached though Use() will be // Use attachs a global middleware to the router. ie. the middlewares 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 Use(middlewares ...HandlerFunc) IRoutes { func Use(middlewares ...gin.HandlerFunc) gin.IRoutes {
return engine().Use(middlewares...) return engine().Use(middlewares...)
} }

View File

@ -94,7 +94,7 @@ func TestUnixSocket(t *testing.T) {
c, err := net.Dial("unix", "/tmp/unix_unit_test") c, err := net.Dial("unix", "/tmp/unix_unit_test")
assert.NoError(t, err) assert.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n") fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c) scanner := bufio.NewScanner(c)
var response string var response string
for scanner.Scan() { for scanner.Scan() {

View File

@ -5,6 +5,7 @@
package gin package gin
import ( import (
"crypto/tls"
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil" "io/ioutil"
@ -21,9 +22,9 @@ func formatAsDate(t time.Time) string {
return fmt.Sprintf("%d/%02d/%02d", year, month, day) return fmt.Sprintf("%d/%02d/%02d", year, month, day)
} }
func setupHTMLFiles(t *testing.T) func() { func setupHTMLFiles(t *testing.T, mode string, tls bool) func() {
go func() { go func() {
SetMode(TestMode) SetMode(mode)
router := New() router := New()
router.Delims("{[{", "}]}") router.Delims("{[{", "}]}")
router.SetFuncMap(template.FuncMap{ router.SetFuncMap(template.FuncMap{
@ -38,16 +39,21 @@ func setupHTMLFiles(t *testing.T) func() {
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
}) })
}) })
router.Run(":8888") if tls {
// these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1`
router.RunTLS(":9999", "./fixtures/testdata/cert.pem", "./fixtures/testdata/key.pem")
} else {
router.Run(":8888")
}
}() }()
t.Log("waiting 1 second for server startup") t.Log("waiting 1 second for server startup")
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
return func() {} return func() {}
} }
func setupHTMLGlob(t *testing.T) func() { func setupHTMLGlob(t *testing.T, mode string, tls bool) func() {
go func() { go func() {
SetMode(DebugMode) SetMode(mode)
router := New() router := New()
router.Delims("{[{", "}]}") router.Delims("{[{", "}]}")
router.SetFuncMap(template.FuncMap{ router.SetFuncMap(template.FuncMap{
@ -62,16 +68,20 @@ func setupHTMLGlob(t *testing.T) func() {
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
}) })
}) })
router.Run(":8888") if tls {
// these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1`
router.RunTLS(":9999", "./fixtures/testdata/cert.pem", "./fixtures/testdata/key.pem")
} else {
router.Run(":8888")
}
}() }()
t.Log("waiting 1 second for server startup") t.Log("waiting 1 second for server startup")
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
return func() {} return func() {}
} }
//TODO
func TestLoadHTMLGlob(t *testing.T) { func TestLoadHTMLGlob(t *testing.T) {
td := setupHTMLGlob(t) td := setupHTMLGlob(t, DebugMode, false)
res, err := http.Get("http://127.0.0.1:8888/test") res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -83,9 +93,55 @@ func TestLoadHTMLGlob(t *testing.T) {
td() td()
} }
func TestLoadHTMLGlob2(t *testing.T) {
td := setupHTMLGlob(t, TestMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
td()
}
func TestLoadHTMLGlob3(t *testing.T) {
td := setupHTMLGlob(t, ReleaseMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
td()
}
func TestLoadHTMLGlobUsingTLS(t *testing.T) {
td := setupHTMLGlob(t, DebugMode, true)
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: tr}
res, err := client.Get("https://127.0.0.1:9999/test")
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
td()
}
func TestLoadHTMLGlobFromFuncMap(t *testing.T) { func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
time.Now() time.Now()
td := setupHTMLGlob(t) td := setupHTMLGlob(t, DebugMode, false)
res, err := http.Get("http://127.0.0.1:8888/raw") res, err := http.Get("http://127.0.0.1:8888/raw")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -97,9 +153,6 @@ func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
td() td()
} }
// func (engine *Engine) LoadHTMLFiles(files ...string) {
// func (engine *Engine) RunTLS(addr string, cert string, key string) error {
func init() { func init() {
SetMode(TestMode) SetMode(TestMode)
} }
@ -117,17 +170,17 @@ func TestCreateEngine(t *testing.T) {
// router.LoadHTMLGlob("*.testtmpl") // router.LoadHTMLGlob("*.testtmpl")
// r := router.HTMLRender.(render.HTMLDebug) // r := router.HTMLRender.(render.HTMLDebug)
// assert.Empty(t, r.Files) // assert.Empty(t, r.Files)
// assert.Equal(t, r.Glob, "*.testtmpl") // assert.Equal(t, "*.testtmpl", r.Glob)
// //
// router.LoadHTMLFiles("index.html.testtmpl", "login.html.testtmpl") // router.LoadHTMLFiles("index.html.testtmpl", "login.html.testtmpl")
// r = router.HTMLRender.(render.HTMLDebug) // r = router.HTMLRender.(render.HTMLDebug)
// assert.Empty(t, r.Glob) // assert.Empty(t, r.Glob)
// assert.Equal(t, r.Files, []string{"index.html", "login.html"}) // assert.Equal(t, []string{"index.html", "login.html"}, r.Files)
// SetMode(TestMode) // SetMode(TestMode)
// } // }
func TestLoadHTMLFiles(t *testing.T) { func TestLoadHTMLFiles(t *testing.T) {
td := setupHTMLFiles(t) td := setupHTMLFiles(t, TestMode, false)
res, err := http.Get("http://127.0.0.1:8888/test") res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -138,9 +191,52 @@ func TestLoadHTMLFiles(t *testing.T) {
td() td()
} }
func TestLoadHTMLFiles2(t *testing.T) {
td := setupHTMLFiles(t, DebugMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
td()
}
func TestLoadHTMLFiles3(t *testing.T) {
td := setupHTMLFiles(t, ReleaseMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
td()
}
func TestLoadHTMLFilesUsingTLS(t *testing.T) {
td := setupHTMLFiles(t, TestMode, true)
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: tr}
res, err := client.Get("https://127.0.0.1:9999/test")
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
td()
}
func TestLoadHTMLFilesFuncMap(t *testing.T) { func TestLoadHTMLFilesFuncMap(t *testing.T) {
time.Now() time.Now()
td := setupHTMLFiles(t) td := setupHTMLFiles(t, TestMode, false)
res, err := http.Get("http://127.0.0.1:8888/raw") res, err := http.Get("http://127.0.0.1:8888/raw")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -152,10 +248,6 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
td() td()
} }
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) {}})

View File

@ -6,9 +6,7 @@
package json package json
import ( import "encoding/json"
"encoding/json"
)
var ( var (
Marshal = json.Marshal Marshal = json.Marshal

View File

@ -6,9 +6,7 @@
package json package json
import ( import "github.com/json-iterator/go"
"github.com/json-iterator/go"
)
var ( var (
json = jsoniter.ConfigCompatibleWithStandardLibrary json = jsoniter.ConfigCompatibleWithStandardLibrary

View File

@ -91,10 +91,11 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
clientIP := c.ClientIP() clientIP := c.ClientIP()
method := c.Request.Method method := c.Request.Method
statusCode := c.Writer.Status() statusCode := c.Writer.Status()
var statusColor, methodColor string var statusColor, methodColor, resetColor string
if isTerm { if isTerm {
statusColor = colorForStatus(statusCode) statusColor = colorForStatus(statusCode)
methodColor = colorForMethod(method) methodColor = colorForMethod(method)
resetColor = reset
} }
comment := c.Errors.ByType(ErrorTypePrivate).String() comment := c.Errors.ByType(ErrorTypePrivate).String()
@ -104,10 +105,10 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
end.Format("2006/01/02 - 15:04:05"), end.Format("2006/01/02 - 15:04:05"),
statusColor, statusCode, reset, statusColor, statusCode, resetColor,
latency, latency,
clientIP, clientIP,
methodColor, method, reset, methodColor, method, resetColor,
path, path,
comment, comment,
) )

View File

@ -82,21 +82,21 @@ func TestLogger(t *testing.T) {
} }
func TestColorForMethod(t *testing.T) { func TestColorForMethod(t *testing.T) {
assert.Equal(t, colorForMethod("GET"), string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), "get should be blue") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue")
assert.Equal(t, colorForMethod("POST"), string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), "post should be cyan") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan")
assert.Equal(t, colorForMethod("PUT"), string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), "put should be yellow") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, colorForMethod("DELETE"), string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), "delete should be red") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, colorForMethod("PATCH"), string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), "patch should be green") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, colorForMethod("HEAD"), string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), "head should be magenta") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), colorForMethod("HEAD"), "head should be magenta")
assert.Equal(t, colorForMethod("OPTIONS"), string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), "options should be white") assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForMethod("OPTIONS"), "options should be white")
assert.Equal(t, colorForMethod("TRACE"), string([]byte{27, 91, 48, 109}), "trace is not defined and should be the reset color") assert.Equal(t, string([]byte{27, 91, 48, 109}), colorForMethod("TRACE"), "trace is not defined and should be the reset color")
} }
func TestColorForStatus(t *testing.T) { func TestColorForStatus(t *testing.T) {
assert.Equal(t, colorForStatus(200), string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), "2xx should be green") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(200), "2xx should be green")
assert.Equal(t, colorForStatus(301), string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), "3xx should be white") assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(301), "3xx should be white")
assert.Equal(t, colorForStatus(404), string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), "4xx should be yellow") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), colorForStatus(404), "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, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red")
} }
func TestErrorLogger(t *testing.T) { func TestErrorLogger(t *testing.T) {

14
mode.go
View File

@ -14,9 +14,9 @@ import (
const ENV_GIN_MODE = "GIN_MODE" const ENV_GIN_MODE = "GIN_MODE"
const ( const (
DebugMode string = "debug" DebugMode = "debug"
ReleaseMode string = "release" ReleaseMode = "release"
TestMode string = "test" TestMode = "test"
) )
const ( const (
debugCode = iota debugCode = iota
@ -39,16 +39,12 @@ var modeName = DebugMode
func init() { func init() {
mode := os.Getenv(ENV_GIN_MODE) mode := os.Getenv(ENV_GIN_MODE)
if len(mode) == 0 { SetMode(mode)
SetMode(DebugMode)
} else {
SetMode(mode)
}
} }
func SetMode(value string) { func SetMode(value string) {
switch value { switch value {
case DebugMode: case DebugMode, "":
ginMode = debugCode ginMode = debugCode
case ReleaseMode: case ReleaseMode:
ginMode = releaseCode ginMode = releaseCode

View File

@ -17,21 +17,21 @@ func init() {
} }
func TestSetMode(t *testing.T) { func TestSetMode(t *testing.T) {
assert.Equal(t, ginMode, testCode) assert.Equal(t, testCode, ginMode)
assert.Equal(t, Mode(), TestMode) assert.Equal(t, TestMode, Mode())
os.Unsetenv(ENV_GIN_MODE) os.Unsetenv(ENV_GIN_MODE)
SetMode(DebugMode) SetMode(DebugMode)
assert.Equal(t, ginMode, debugCode) assert.Equal(t, debugCode, ginMode)
assert.Equal(t, Mode(), DebugMode) assert.Equal(t, DebugMode, Mode())
SetMode(ReleaseMode) SetMode(ReleaseMode)
assert.Equal(t, ginMode, releaseCode) assert.Equal(t, releaseCode, ginMode)
assert.Equal(t, Mode(), ReleaseMode) assert.Equal(t, ReleaseMode, Mode())
SetMode(TestMode) SetMode(TestMode)
assert.Equal(t, ginMode, testCode) assert.Equal(t, testCode, ginMode)
assert.Equal(t, Mode(), TestMode) assert.Equal(t, TestMode, Mode())
assert.Panics(t, func() { SetMode("unknown") }) assert.Panics(t, func() { SetMode("unknown") })
} }

View File

@ -67,8 +67,8 @@ var cleanTests = []struct {
func TestPathClean(t *testing.T) { func TestPathClean(t *testing.T) {
for _, test := range cleanTests { for _, test := range cleanTests {
assert.Equal(t, cleanPath(test.path), test.result) assert.Equal(t, test.result, cleanPath(test.path))
assert.Equal(t, cleanPath(test.result), test.result) assert.Equal(t, test.result, cleanPath(test.result))
} }
} }

View File

@ -26,6 +26,7 @@ func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter) return RecoveryWithWriter(DefaultErrorWriter)
} }
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
func RecoveryWithWriter(out io.Writer) HandlerFunc { func RecoveryWithWriter(out io.Writer) HandlerFunc {
var logger *log.Logger var logger *log.Logger
if out != nil { if out != nil {
@ -46,7 +47,7 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
} }
} }
// stack returns a nicely formated stack frame, skipping skip frames. // stack returns a nicely formatted stack frame, skipping skip frames.
func stack(skip int) []byte { func stack(skip int) []byte {
buf := new(bytes.Buffer) // the returned data buf := new(bytes.Buffer) // the returned data
// As we loop, we open files and read them. These variables record the currently // As we loop, we open files and read them. These variables record the currently

View File

@ -22,7 +22,7 @@ func TestPanicInHandler(t *testing.T) {
// RUN // RUN
w := performRequest(router, "GET", "/recovery") w := performRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, w.Code, 500) assert.Equal(t, 500, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "TestPanicInHandler") assert.Contains(t, buffer.String(), "TestPanicInHandler")
@ -39,5 +39,5 @@ func TestPanicWithAbort(t *testing.T) {
// RUN // RUN
w := performRequest(router, "GET", "/recovery") w := performRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, w.Code, 400) assert.Equal(t, 400, w.Code)
} }

View File

@ -60,7 +60,7 @@ func (r HTMLDebug) loadTemplate() *template.Template {
if len(r.Files) > 0 { if len(r.Files) > 0 {
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFiles(r.Files...)) return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFiles(r.Files...))
} }
if len(r.Glob) > 0 { if r.Glob != "" {
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob)) return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
} }
panic("the HTML debug render was created without files or glob pattern") panic("the HTML debug render was created without files or glob pattern")
@ -69,7 +69,7 @@ func (r HTMLDebug) loadTemplate() *template.Template {
func (r HTML) Render(w http.ResponseWriter) error { func (r HTML) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
if len(r.Name) == 0 { if r.Name == "" {
return r.Template.Execute(w, r.Data) return r.Template.Execute(w, r.Data)
} }
return r.Template.ExecuteTemplate(w, r.Name, r.Data) return r.Template.ExecuteTemplate(w, r.Name, r.Data)

View File

@ -111,10 +111,8 @@ func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return err return err
} }
} }
if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
return err return e.EncodeToken(xml.EndElement{Name: start.Name})
}
return nil
} }
func TestRenderXML(t *testing.T) { func TestRenderXML(t *testing.T) {

View File

@ -20,15 +20,15 @@ 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, "/hola", group.BasePath())
assert.Equal(t, group.engine, router) assert.Equal(t, router, group.engine)
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, "/hola/manu", group2.BasePath())
assert.Equal(t, group2.engine, router) assert.Equal(t, router, group2.engine)
} }
func TestRouterGroupBasicHandle(t *testing.T) { func TestRouterGroupBasicHandle(t *testing.T) {
@ -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", v1.BasePath())
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, "/v1/login/", login.BasePath())
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)
@ -80,12 +80,12 @@ func performRequestInGroup(t *testing.T, method string) {
} }
w := performRequest(router, method, "/v1/login/test") w := performRequest(router, method, "/v1/login/test")
assert.Equal(t, w.Code, 400) assert.Equal(t, 400, w.Code)
assert.Equal(t, w.Body.String(), "the method was "+method+" and index 3") assert.Equal(t, "the method was "+method+" and index 3", w.Body.String())
w = performRequest(router, method, "/v1/test") w = performRequest(router, method, "/v1/test")
assert.Equal(t, w.Code, 400) assert.Equal(t, 400, w.Code)
assert.Equal(t, w.Body.String(), "the method was "+method+" and index 1") assert.Equal(t, "the method was "+method+" and index 1", w.Body.String())
} }
func TestRouterGroupInvalidStatic(t *testing.T) { func TestRouterGroupInvalidStatic(t *testing.T) {

View File

@ -36,7 +36,7 @@ func testRouteOK(method string, t *testing.T) {
w := performRequest(r, method, "/test") w := performRequest(r, method, "/test")
assert.True(t, passed) assert.True(t, passed)
assert.Equal(t, w.Code, http.StatusOK) assert.Equal(t, http.StatusOK, w.Code)
performRequest(r, method, "/test2") performRequest(r, method, "/test2")
assert.True(t, passedAny) assert.True(t, passedAny)
@ -53,7 +53,7 @@ func testRouteNotOK(method string, t *testing.T) {
w := performRequest(router, method, "/test") w := performRequest(router, method, "/test")
assert.False(t, passed) assert.False(t, passed)
assert.Equal(t, w.Code, http.StatusNotFound) assert.Equal(t, http.StatusNotFound, w.Code)
} }
// TestSingleRouteOK tests that POST route is correctly invoked. // TestSingleRouteOK tests that POST route is correctly invoked.
@ -74,7 +74,7 @@ func testRouteNotOK2(method string, t *testing.T) {
w := performRequest(router, method, "/test") w := performRequest(router, method, "/test")
assert.False(t, passed) assert.False(t, passed)
assert.Equal(t, w.Code, http.StatusMethodNotAllowed) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
} }
func TestRouterMethod(t *testing.T) { func TestRouterMethod(t *testing.T) {
@ -93,8 +93,8 @@ func TestRouterMethod(t *testing.T) {
w := performRequest(router, "PUT", "/hey") w := performRequest(router, "PUT", "/hey")
assert.Equal(t, w.Code, 200) assert.Equal(t, 200, w.Code)
assert.Equal(t, w.Body.String(), "called") assert.Equal(t, "called", w.Body.String())
} }
func TestRouterGroupRouteOK(t *testing.T) { func TestRouterGroupRouteOK(t *testing.T) {
@ -143,43 +143,43 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
router.PUT("/path4/", func(c *Context) {}) router.PUT("/path4/", func(c *Context) {})
w := performRequest(router, "GET", "/path/") w := performRequest(router, "GET", "/path/")
assert.Equal(t, w.Header().Get("Location"), "/path") assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, w.Code, 301) assert.Equal(t, 301, w.Code)
w = performRequest(router, "GET", "/path2") w = performRequest(router, "GET", "/path2")
assert.Equal(t, w.Header().Get("Location"), "/path2/") assert.Equal(t, "/path2/", w.Header().Get("Location"))
assert.Equal(t, w.Code, 301) assert.Equal(t, 301, w.Code)
w = performRequest(router, "POST", "/path3/") w = performRequest(router, "POST", "/path3/")
assert.Equal(t, w.Header().Get("Location"), "/path3") assert.Equal(t, "/path3", w.Header().Get("Location"))
assert.Equal(t, w.Code, 307) assert.Equal(t, 307, w.Code)
w = performRequest(router, "PUT", "/path4") w = performRequest(router, "PUT", "/path4")
assert.Equal(t, w.Header().Get("Location"), "/path4/") assert.Equal(t, "/path4/", w.Header().Get("Location"))
assert.Equal(t, w.Code, 307) assert.Equal(t, 307, w.Code)
w = performRequest(router, "GET", "/path") w = performRequest(router, "GET", "/path")
assert.Equal(t, w.Code, 200) assert.Equal(t, 200, w.Code)
w = performRequest(router, "GET", "/path2/") w = performRequest(router, "GET", "/path2/")
assert.Equal(t, w.Code, 200) assert.Equal(t, 200, w.Code)
w = performRequest(router, "POST", "/path3") w = performRequest(router, "POST", "/path3")
assert.Equal(t, w.Code, 200) assert.Equal(t, 200, w.Code)
w = performRequest(router, "PUT", "/path4/") w = performRequest(router, "PUT", "/path4/")
assert.Equal(t, w.Code, 200) assert.Equal(t, 200, w.Code)
router.RedirectTrailingSlash = false router.RedirectTrailingSlash = false
w = performRequest(router, "GET", "/path/") w = performRequest(router, "GET", "/path/")
assert.Equal(t, w.Code, 404) assert.Equal(t, 404, w.Code)
w = performRequest(router, "GET", "/path2") w = performRequest(router, "GET", "/path2")
assert.Equal(t, w.Code, 404) assert.Equal(t, 404, w.Code)
w = performRequest(router, "POST", "/path3/") w = performRequest(router, "POST", "/path3/")
assert.Equal(t, w.Code, 404) assert.Equal(t, 404, w.Code)
w = performRequest(router, "PUT", "/path4") w = performRequest(router, "PUT", "/path4")
assert.Equal(t, w.Code, 404) assert.Equal(t, 404, w.Code)
} }
func TestRouteRedirectFixedPath(t *testing.T) { func TestRouteRedirectFixedPath(t *testing.T) {
@ -193,20 +193,20 @@ func TestRouteRedirectFixedPath(t *testing.T) {
router.POST("/Path4/", func(c *Context) {}) router.POST("/Path4/", func(c *Context) {})
w := performRequest(router, "GET", "/PATH") w := performRequest(router, "GET", "/PATH")
assert.Equal(t, w.Header().Get("Location"), "/path") assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, w.Code, 301) assert.Equal(t, 301, w.Code)
w = performRequest(router, "GET", "/path2") w = performRequest(router, "GET", "/path2")
assert.Equal(t, w.Header().Get("Location"), "/Path2") assert.Equal(t, "/Path2", w.Header().Get("Location"))
assert.Equal(t, w.Code, 301) assert.Equal(t, 301, w.Code)
w = performRequest(router, "POST", "/path3") w = performRequest(router, "POST", "/path3")
assert.Equal(t, w.Header().Get("Location"), "/PATH3") assert.Equal(t, "/PATH3", w.Header().Get("Location"))
assert.Equal(t, w.Code, 307) assert.Equal(t, 307, w.Code)
w = performRequest(router, "POST", "/path4") w = performRequest(router, "POST", "/path4")
assert.Equal(t, w.Header().Get("Location"), "/Path4/") assert.Equal(t, "/Path4/", w.Header().Get("Location"))
assert.Equal(t, w.Code, 307) assert.Equal(t, 307, w.Code)
} }
// TestContextParamsGet tests that a parameter can be parsed from the URL. // TestContextParamsGet tests that a parameter can be parsed from the URL.
@ -236,10 +236,10 @@ func TestRouteParamsByName(t *testing.T) {
w := performRequest(router, "GET", "/test/john/smith/is/super/great") w := performRequest(router, "GET", "/test/john/smith/is/super/great")
assert.Equal(t, w.Code, 200) assert.Equal(t, 200, w.Code)
assert.Equal(t, name, "john") assert.Equal(t, "john", name)
assert.Equal(t, lastName, "smith") assert.Equal(t, "smith", lastName)
assert.Equal(t, wild, "/is/super/great") assert.Equal(t, "/is/super/great", wild)
} }
// TestHandleStaticFile - ensure the static file handles properly // TestHandleStaticFile - ensure the static file handles properly
@ -265,15 +265,15 @@ func TestRouteStaticFile(t *testing.T) {
w2 := performRequest(router, "GET", "/result") w2 := performRequest(router, "GET", "/result")
assert.Equal(t, w, w2) assert.Equal(t, w, w2)
assert.Equal(t, w.Code, 200) assert.Equal(t, 200, w.Code)
assert.Equal(t, w.Body.String(), "Gin Web Framework") assert.Equal(t, "Gin Web Framework", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type"))
w3 := performRequest(router, "HEAD", "/using_static/"+filename) w3 := performRequest(router, "HEAD", "/using_static/"+filename)
w4 := performRequest(router, "HEAD", "/result") w4 := performRequest(router, "HEAD", "/result")
assert.Equal(t, w3, w4) assert.Equal(t, w3, w4)
assert.Equal(t, w3.Code, 200) assert.Equal(t, 200, w3.Code)
} }
// TestHandleStaticDir - ensure the root/sub dir handles properly // TestHandleStaticDir - ensure the root/sub dir handles properly
@ -283,9 +283,9 @@ func TestRouteStaticListingDir(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
assert.Equal(t, w.Code, 200) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "gin.go") assert.Contains(t, w.Body.String(), "gin.go")
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// TestHandleHeadToDir - ensure the root/sub dir handles properly // TestHandleHeadToDir - ensure the root/sub dir handles properly
@ -295,7 +295,7 @@ func TestRouteStaticNoListing(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
assert.Equal(t, w.Code, 404) assert.Equal(t, 404, w.Code)
assert.NotContains(t, w.Body.String(), "gin.go") assert.NotContains(t, w.Body.String(), "gin.go")
} }
@ -310,12 +310,12 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
w := performRequest(router, "GET", "/gin.go") w := performRequest(router, "GET", "/gin.go")
assert.Equal(t, w.Code, 200) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "package gin") assert.Contains(t, w.Body.String(), "package gin")
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type"))
assert.NotEqual(t, w.HeaderMap.Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST") assert.NotEqual(t, w.HeaderMap.Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
assert.Equal(t, w.HeaderMap.Get("Expires"), "Mon, 02 Jan 2006 15:04:05 MST") assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.HeaderMap.Get("Expires"))
assert.Equal(t, w.HeaderMap.Get("x-GIN"), "Gin Framework") assert.Equal(t, "Gin Framework", w.HeaderMap.Get("x-GIN"))
} }
func TestRouteNotAllowedEnabled(t *testing.T) { func TestRouteNotAllowedEnabled(t *testing.T) {
@ -323,14 +323,14 @@ func TestRouteNotAllowedEnabled(t *testing.T) {
router.HandleMethodNotAllowed = true router.HandleMethodNotAllowed = true
router.POST("/path", func(c *Context) {}) router.POST("/path", func(c *Context) {})
w := performRequest(router, "GET", "/path") w := performRequest(router, "GET", "/path")
assert.Equal(t, w.Code, http.StatusMethodNotAllowed) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
router.NoMethod(func(c *Context) { router.NoMethod(func(c *Context) {
c.String(http.StatusTeapot, "responseText") c.String(http.StatusTeapot, "responseText")
}) })
w = performRequest(router, "GET", "/path") w = performRequest(router, "GET", "/path")
assert.Equal(t, w.Body.String(), "responseText") assert.Equal(t, "responseText", w.Body.String())
assert.Equal(t, w.Code, http.StatusTeapot) assert.Equal(t, http.StatusTeapot, w.Code)
} }
func TestRouteNotAllowedDisabled(t *testing.T) { func TestRouteNotAllowedDisabled(t *testing.T) {
@ -338,14 +338,14 @@ func TestRouteNotAllowedDisabled(t *testing.T) {
router.HandleMethodNotAllowed = false router.HandleMethodNotAllowed = false
router.POST("/path", func(c *Context) {}) router.POST("/path", func(c *Context) {})
w := performRequest(router, "GET", "/path") w := performRequest(router, "GET", "/path")
assert.Equal(t, w.Code, 404) assert.Equal(t, 404, w.Code)
router.NoMethod(func(c *Context) { router.NoMethod(func(c *Context) {
c.String(http.StatusTeapot, "responseText") c.String(http.StatusTeapot, "responseText")
}) })
w = performRequest(router, "GET", "/path") w = performRequest(router, "GET", "/path")
assert.Equal(t, w.Body.String(), "404 page not found") assert.Equal(t, "404 page not found", w.Body.String())
assert.Equal(t, w.Code, 404) assert.Equal(t, 404, w.Code)
} }
func TestRouterNotFound(t *testing.T) { func TestRouterNotFound(t *testing.T) {
@ -372,9 +372,9 @@ func TestRouterNotFound(t *testing.T) {
} }
for _, tr := range testRoutes { for _, tr := range testRoutes {
w := performRequest(router, "GET", tr.route) w := performRequest(router, "GET", tr.route)
assert.Equal(t, w.Code, tr.code) assert.Equal(t, tr.code, w.Code)
if w.Code != 404 { if w.Code != 404 {
assert.Equal(t, fmt.Sprint(w.Header().Get("Location")), tr.location) assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
} }
} }
@ -385,20 +385,20 @@ func TestRouterNotFound(t *testing.T) {
notFound = true notFound = true
}) })
w := performRequest(router, "GET", "/nope") w := performRequest(router, "GET", "/nope")
assert.Equal(t, w.Code, 404) assert.Equal(t, 404, w.Code)
assert.True(t, notFound) assert.True(t, notFound)
// Test other method than GET (want 307 instead of 301) // Test other method than GET (want 307 instead of 301)
router.PATCH("/path", func(c *Context) {}) router.PATCH("/path", func(c *Context) {})
w = performRequest(router, "PATCH", "/path/") w = performRequest(router, "PATCH", "/path/")
assert.Equal(t, w.Code, 307) assert.Equal(t, 307, w.Code)
assert.Equal(t, fmt.Sprint(w.Header()), "map[Location:[/path]]") assert.Equal(t, "map[Location:[/path]]", fmt.Sprint(w.Header()))
// Test special case where no node for the prefix "/" exists // Test special case where no node for the prefix "/" exists
router = New() router = New()
router.GET("/a", func(c *Context) {}) router.GET("/a", func(c *Context) {})
w = performRequest(router, "GET", "/") w = performRequest(router, "GET", "/")
assert.Equal(t, w.Code, 404) assert.Equal(t, 404, w.Code)
} }
func TestRouteRawPath(t *testing.T) { func TestRouteRawPath(t *testing.T) {
@ -409,15 +409,15 @@ func TestRouteRawPath(t *testing.T) {
name := c.Params.ByName("name") name := c.Params.ByName("name")
num := c.Params.ByName("num") num := c.Params.ByName("num")
assert.Equal(t, c.Param("name"), name) assert.Equal(t, name, c.Param("name"))
assert.Equal(t, c.Param("num"), num) assert.Equal(t, num, c.Param("num"))
assert.Equal(t, "Some/Other/Project", name) assert.Equal(t, "Some/Other/Project", name)
assert.Equal(t, "222", num) assert.Equal(t, "222", num)
}) })
w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/222") w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/222")
assert.Equal(t, w.Code, 200) assert.Equal(t, 200, w.Code)
} }
func TestRouteRawPathNoUnescape(t *testing.T) { func TestRouteRawPathNoUnescape(t *testing.T) {
@ -429,15 +429,15 @@ func TestRouteRawPathNoUnescape(t *testing.T) {
name := c.Params.ByName("name") name := c.Params.ByName("name")
num := c.Params.ByName("num") num := c.Params.ByName("num")
assert.Equal(t, c.Param("name"), name) assert.Equal(t, name, c.Param("name"))
assert.Equal(t, c.Param("num"), num) assert.Equal(t, num, c.Param("num"))
assert.Equal(t, "Some%2FOther%2FProject", name) assert.Equal(t, "Some%2FOther%2FProject", name)
assert.Equal(t, "333", num) assert.Equal(t, "333", num)
}) })
w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/333") w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/333")
assert.Equal(t, w.Code, 200) assert.Equal(t, 200, w.Code)
} }
func TestRouteServeErrorWithWriteHeader(t *testing.T) { func TestRouteServeErrorWithWriteHeader(t *testing.T) {

14
tree.go
View File

@ -87,13 +87,13 @@ const (
type node struct { type node struct {
path string path string
wildChild bool
nType nodeType
maxParams uint8
indices string indices string
children []*node children []*node
handlers HandlersChain handlers HandlersChain
priority uint32 priority uint32
nType nodeType
maxParams uint8
wildChild bool
} }
// increments priority of the given child and reorders if necessary. // increments priority of the given child and reorders if necessary.
@ -384,7 +384,7 @@ walk: // Outer loop for walking the tree
// Nothing found. // Nothing found.
// We can recommend to redirect to the same URL without a // We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path. // trailing slash if a leaf exists for that path.
tsr = (path == "/" && n.handlers != nil) tsr = path == "/" && n.handlers != nil
return return
} }
@ -424,7 +424,7 @@ walk: // Outer loop for walking the tree
} }
// ... but we can't // ... but we can't
tsr = (len(path) == end+1) tsr = len(path) == end+1
return return
} }
@ -435,7 +435,7 @@ walk: // Outer loop for walking the tree
// No handle found. Check if a handle for this path + a // No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation // trailing slash exists for TSR recommendation
n = n.children[0] n = n.children[0]
tsr = (n.path == "/" && n.handlers != nil) tsr = n.path == "/" && n.handlers != nil
} }
return return
@ -530,7 +530,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
// Nothing found. We can recommend to redirect to the same URL // Nothing found. We can recommend to redirect to the same URL
// without a trailing slash if a leaf exists for that path // without a trailing slash if a leaf exists for that path
found = (fixTrailingSlash && path == "/" && n.handlers != nil) found = fixTrailingSlash && path == "/" && n.handlers != nil
return return
} }

View File

@ -5,22 +5,11 @@
package gin package gin
import ( import (
"fmt"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
) )
func printChildren(n *node, prefix string) {
fmt.Printf(" %02d:%02d %s%s[%d] %v %t %d \r\n", n.priority, n.maxParams, prefix, n.path, len(n.children), n.handlers, n.wildChild, n.nType)
for l := len(n.path); l > 0; l-- {
prefix += " "
}
for _, child := range n.children {
printChildren(child, prefix)
}
}
// Used as a workaround since we can't compare functions or their addressses // Used as a workaround since we can't compare functions or their addressses
var fakeHandlerValue string var fakeHandlerValue string

View File

@ -33,18 +33,23 @@ func Bind(val interface{}) HandlerFunc {
} }
} }
// WrapF is a helper function for wrapping http.HandlerFunc
// Returns a Gin middleware
func WrapF(f http.HandlerFunc) HandlerFunc { func WrapF(f http.HandlerFunc) HandlerFunc {
return func(c *Context) { return func(c *Context) {
f(c.Writer, c.Request) f(c.Writer, c.Request)
} }
} }
// WrapH is a helper function for wrapping http.Handler
// Returns a Gin middleware
func WrapH(h http.Handler) HandlerFunc { func WrapH(h http.Handler) HandlerFunc {
return func(c *Context) { return func(c *Context) {
h.ServeHTTP(c.Writer, c.Request) h.ServeHTTP(c.Writer, c.Request)
} }
} }
// H is a shortcup for map[string]interface{}
type H map[string]interface{} type H map[string]interface{}
// MarshalXML allows type H to be used with xml.Marshal. // MarshalXML allows type H to be used with xml.Marshal.
@ -65,10 +70,8 @@ func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return err return err
} }
} }
if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
return err return e.EncodeToken(xml.EndElement{Name: start.Name})
}
return nil
} }
func assert1(guard bool, text string) { func assert1(guard bool, text string) {
@ -103,7 +106,7 @@ func parseAccept(acceptHeader string) []string {
if index := strings.IndexByte(part, ';'); index >= 0 { if index := strings.IndexByte(part, ';'); index >= 0 {
part = part[0:index] part = part[0:index]
} }
if part = strings.TrimSpace(part); len(part) > 0 { if part = strings.TrimSpace(part); part != "" {
out = append(out, part) out = append(out, part)
} }
} }
@ -111,11 +114,10 @@ func parseAccept(acceptHeader string) []string {
} }
func lastChar(str string) uint8 { func lastChar(str string) uint8 {
size := len(str) if str == "" {
if size == 0 {
panic("The length of the string can't be 0") panic("The length of the string can't be 0")
} }
return str[size-1] return str[len(str)-1]
} }
func nameOfFunction(f interface{}) string { func nameOfFunction(f interface{}) string {
@ -123,7 +125,7 @@ func nameOfFunction(f interface{}) string {
} }
func joinPaths(absolutePath, relativePath string) string { func joinPaths(absolutePath, relativePath string) string {
if len(relativePath) == 0 { if relativePath == "" {
return absolutePath return absolutePath
} }
@ -138,7 +140,7 @@ func joinPaths(absolutePath, relativePath string) string {
func resolveAddress(addr []string) string { func resolveAddress(addr []string) string {
switch len(addr) { switch len(addr) {
case 0: case 0:
if port := os.Getenv("PORT"); len(port) > 0 { if port := os.Getenv("PORT"); port != "" {
debugPrint("Environment variable PORT=\"%s\"", port) debugPrint("Environment variable PORT=\"%s\"", port)
return ":" + port return ":" + port
} }

View File

@ -21,8 +21,8 @@ type testStruct struct {
} }
func (t *testStruct) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (t *testStruct) ServeHTTP(w http.ResponseWriter, req *http.Request) {
assert.Equal(t.T, req.Method, "POST") assert.Equal(t.T, "POST", req.Method)
assert.Equal(t.T, req.URL.Path, "/path") assert.Equal(t.T, "/path", req.URL.Path)
w.WriteHeader(500) w.WriteHeader(500)
fmt.Fprint(w, "hello") fmt.Fprint(w, "hello")
} }
@ -31,50 +31,50 @@ func TestWrap(t *testing.T) {
router := New() router := New()
router.POST("/path", WrapH(&testStruct{t})) router.POST("/path", WrapH(&testStruct{t}))
router.GET("/path2", WrapF(func(w http.ResponseWriter, req *http.Request) { router.GET("/path2", WrapF(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(t, req.Method, "GET") assert.Equal(t, "GET", req.Method)
assert.Equal(t, req.URL.Path, "/path2") assert.Equal(t, "/path2", req.URL.Path)
w.WriteHeader(400) w.WriteHeader(400)
fmt.Fprint(w, "hola!") fmt.Fprint(w, "hola!")
})) }))
w := performRequest(router, "POST", "/path") w := performRequest(router, "POST", "/path")
assert.Equal(t, w.Code, 500) assert.Equal(t, 500, w.Code)
assert.Equal(t, w.Body.String(), "hello") assert.Equal(t, "hello", w.Body.String())
w = performRequest(router, "GET", "/path2") w = performRequest(router, "GET", "/path2")
assert.Equal(t, w.Code, 400) assert.Equal(t, 400, w.Code)
assert.Equal(t, w.Body.String(), "hola!") assert.Equal(t, "hola!", w.Body.String())
} }
func TestLastChar(t *testing.T) { func TestLastChar(t *testing.T) {
assert.Equal(t, lastChar("hola"), uint8('a')) assert.Equal(t, uint8('a'), lastChar("hola"))
assert.Equal(t, lastChar("adios"), uint8('s')) assert.Equal(t, uint8('s'), lastChar("adios"))
assert.Panics(t, func() { lastChar("") }) assert.Panics(t, func() { lastChar("") })
} }
func TestParseAccept(t *testing.T) { func TestParseAccept(t *testing.T) {
parts := parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8") parts := parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8")
assert.Len(t, parts, 4) assert.Len(t, parts, 4)
assert.Equal(t, parts[0], "text/html") assert.Equal(t, "text/html", parts[0])
assert.Equal(t, parts[1], "application/xhtml+xml") assert.Equal(t, "application/xhtml+xml", parts[1])
assert.Equal(t, parts[2], "application/xml") assert.Equal(t, "application/xml", parts[2])
assert.Equal(t, parts[3], "*/*") assert.Equal(t, "*/*", parts[3])
} }
func TestChooseData(t *testing.T) { func TestChooseData(t *testing.T) {
A := "a" A := "a"
B := "b" B := "b"
assert.Equal(t, chooseData(A, B), A) assert.Equal(t, A, chooseData(A, B))
assert.Equal(t, chooseData(nil, B), B) assert.Equal(t, B, chooseData(nil, B))
assert.Panics(t, func() { chooseData(nil, nil) }) assert.Panics(t, func() { chooseData(nil, nil) })
} }
func TestFilterFlags(t *testing.T) { func TestFilterFlags(t *testing.T) {
result := filterFlags("text/html ") result := filterFlags("text/html ")
assert.Equal(t, result, "text/html") assert.Equal(t, "text/html", result)
result = filterFlags("text/html;") result = filterFlags("text/html;")
assert.Equal(t, result, "text/html") assert.Equal(t, "text/html", result)
} }
func TestFunctionName(t *testing.T) { func TestFunctionName(t *testing.T) {
@ -86,16 +86,16 @@ func somefunction() {
} }
func TestJoinPaths(t *testing.T) { func TestJoinPaths(t *testing.T) {
assert.Equal(t, joinPaths("", ""), "") assert.Equal(t, "", joinPaths("", ""))
assert.Equal(t, joinPaths("", "/"), "/") assert.Equal(t, "/", joinPaths("", "/"))
assert.Equal(t, joinPaths("/a", ""), "/a") assert.Equal(t, "/a", joinPaths("/a", ""))
assert.Equal(t, joinPaths("/a/", ""), "/a/") assert.Equal(t, "/a/", joinPaths("/a/", ""))
assert.Equal(t, joinPaths("/a/", "/"), "/a/") assert.Equal(t, "/a/", joinPaths("/a/", "/"))
assert.Equal(t, joinPaths("/a", "/"), "/a/") assert.Equal(t, "/a/", joinPaths("/a", "/"))
assert.Equal(t, joinPaths("/a", "/hola"), "/a/hola") assert.Equal(t, "/a/hola", joinPaths("/a", "/hola"))
assert.Equal(t, joinPaths("/a/", "/hola"), "/a/hola") assert.Equal(t, "/a/hola", joinPaths("/a/", "/hola"))
assert.Equal(t, joinPaths("/a/", "/hola/"), "/a/hola/") assert.Equal(t, "/a/hola/", joinPaths("/a/", "/hola/"))
assert.Equal(t, joinPaths("/a/", "/hola//"), "/a/hola/") assert.Equal(t, "/a/hola/", joinPaths("/a/", "/hola//"))
} }
type bindTestStruct struct { type bindTestStruct struct {
@ -113,8 +113,8 @@ func TestBindMiddleware(t *testing.T) {
}) })
performRequest(router, "GET", "/?foo=hola&bar=10") performRequest(router, "GET", "/?foo=hola&bar=10")
assert.True(t, called) assert.True(t, called)
assert.Equal(t, value.Foo, "hola") assert.Equal(t, "hola", value.Foo)
assert.Equal(t, value.Bar, 10) assert.Equal(t, 10, value.Bar)
called = false called = false
performRequest(router, "GET", "/?foo=hola&bar=1") performRequest(router, "GET", "/?foo=hola&bar=1")

24
vendor/vendor.json vendored
View File

@ -22,10 +22,10 @@
"revisionTime": "2017-01-09T09:34:21Z" "revisionTime": "2017-01-09T09:34:21Z"
}, },
{ {
"checksumSHA1": "FJKrZuFmeLJp8HDeJc6UkIDBPUw=", "checksumSHA1": "+vZNyF2MykVjenLg1TpjjgjthV0=",
"path": "github.com/gin-gonic/autotls", "path": "github.com/gin-gonic/autotls",
"revision": "5b3297bdcee778ff3bbdc99ab7c41e1c2677d22d", "revision": "8ca25fbde72bb72a00466215b94b489c71fcb815",
"revisionTime": "2017-04-16T09:39:34Z" "revisionTime": "2017-09-16T16:54:15Z"
}, },
{ {
"checksumSHA1": "qlPUeFabwF4RKAOF1H+yBFU1Veg=", "checksumSHA1": "qlPUeFabwF4RKAOF1H+yBFU1Veg=",
@ -34,10 +34,10 @@
"revisionTime": "2017-06-01T23:02:30Z" "revisionTime": "2017-06-01T23:02:30Z"
}, },
{ {
"checksumSHA1": "0e59uuETpidkmpaRwipQ8auqwhM=", "checksumSHA1": "Ajh8TemnItg4nn+jKmVcsMRALBc=",
"path": "github.com/json-iterator/go", "path": "github.com/json-iterator/go",
"revision": "6b6938829d6156d7b9825f83eec757f0f571c981", "revision": "36b14963da70d11297d313183d7e6388c8510e1e",
"revisionTime": "2017-07-18T14:19:52Z" "revisionTime": "2017-08-29T15:58:51Z"
}, },
{ {
"checksumSHA1": "9if9IBLsxkarJ804NPWAzgskIAk=", "checksumSHA1": "9if9IBLsxkarJ804NPWAzgskIAk=",
@ -65,6 +65,12 @@
"revision": "976c720a22c8eb4eb6a0b4348ad85ad12491a506", "revision": "976c720a22c8eb4eb6a0b4348ad85ad12491a506",
"revisionTime": "2016-09-25T22:06:09Z" "revisionTime": "2016-09-25T22:06:09Z"
}, },
{
"checksumSHA1": "IopMW+arBezL5bqOfrVU6UEfn28=",
"path": "github.com/thinkerou/favicon",
"revision": "94a442a49da6e2d44bdd5e0d2e2e185c43a19d93",
"revisionTime": "2017-07-10T14:05:20Z"
},
{ {
"checksumSHA1": "CoxdaTYdPZNJXr8mJfLxye428N0=", "checksumSHA1": "CoxdaTYdPZNJXr8mJfLxye428N0=",
"path": "github.com/ugorji/go/codec", "path": "github.com/ugorji/go/codec",
@ -90,6 +96,12 @@
"revision": "d4c55e66d8c3a2f3382d264b08e3e3454a66355a", "revision": "d4c55e66d8c3a2f3382d264b08e3e3454a66355a",
"revisionTime": "2016-10-18T08:54:36Z" "revisionTime": "2016-10-18T08:54:36Z"
}, },
{
"checksumSHA1": "S0DP7Pn7sZUmXc55IzZnNvERu6s=",
"path": "golang.org/x/sync/errgroup",
"revision": "8e0aa688b654ef28caa72506fa5ec8dba9fc7690",
"revisionTime": "2017-07-19T03:38:01Z"
},
{ {
"checksumSHA1": "TVEkpH3gq84iQ39I4R+mlDwjuVI=", "checksumSHA1": "TVEkpH3gq84iQ39I4R+mlDwjuVI=",
"path": "golang.org/x/sys/unix", "path": "golang.org/x/sys/unix",