Merge branch 'master' into context-status

This commit is contained in:
Bo-Yi Wu 2019-10-17 10:06:46 +08:00 committed by GitHub
commit 02d54ccad7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1702 additions and 1005 deletions

View File

@ -3,11 +3,47 @@
- Please provide source code and commit sha if you found a bug.
- Review existing issues and provide feedback or react to them.
## Description
<!-- Description of a problem -->
## How to reproduce
<!-- The smallest possible code example to show the problem that can be compiled, like -->
```
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
g := gin.Default()
g.GET("/hello/:name", func(c *gin.Context) {
c.String(200, "Hello %s", c.Param("name"))
})
g.Run(":9000")
}
```
## Expectations
<!-- Your expectation result of 'curl' command, like -->
```
$ curl http://localhost:8201/hello/world
Hello world
```
## Actual result
<!-- Actual result showing the problem -->
```
$ curl -i http://localhost:8201/hello/world
<YOUR RESULT>
```
## Environment
- go version:
- gin version (or commit ref):
- operating system:
## Description
## Screenshots

View File

@ -3,17 +3,13 @@ language: go
matrix:
fast_finish: true
include:
- go: 1.6.x
- go: 1.7.x
- go: 1.8.x
- go: 1.9.x
- go: 1.10.x
- go: 1.11.x
env: GO111MODULE=on
- go: 1.12.x
env: GO111MODULE=on
- go: 1.13.x
- go: master
env: GO111MODULE=on
git:
depth: 10

View File

@ -1,4 +1,60 @@
# CHANGELOG
### Gin 1.4.0
- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569)
- [NEW] Refactor of form mapping multipart request [#1829](https://github.com/gin-gonic/gin/pull/1829)
- [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830)
- [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802)
- [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264)
- [NEW] Add support for mapping arrays [#1797](https://github.com/gin-gonic/gin/pull/1797)
- [FIX] Readme updates [#1793](https://github.com/gin-gonic/gin/pull/1793) [#1788](https://github.com/gin-gonic/gin/pull/1788) [1789](https://github.com/gin-gonic/gin/pull/1789)
- [FIX] StaticFS: Fixed Logging two log lines on 404. [#1805](https://github.com/gin-gonic/gin/pull/1805), [#1804](https://github.com/gin-gonic/gin/pull/1804)
- [NEW] Make context.Keys available as LogFormatterParams [#1779](https://github.com/gin-gonic/gin/pull/1779)
- [NEW] Use internal/json for Marshal/Unmarshal [#1791](https://github.com/gin-gonic/gin/pull/1791)
- [NEW] Support mapping time.Duration [#1794](https://github.com/gin-gonic/gin/pull/1794)
- [NEW] Refactor form mappings [#1749](https://github.com/gin-gonic/gin/pull/1749)
- [NEW] Added flag to context.Stream indicates if client disconnected in middle of stream [#1252](https://github.com/gin-gonic/gin/pull/1252)
- [FIX] Moved [examples](https://github.com/gin-gonic/examples) to stand alone Repo [#1775](https://github.com/gin-gonic/gin/pull/1775)
- [NEW] Extend context.File to allow for the content-disposition attachments via a new method context.Attachment [#1260](https://github.com/gin-gonic/gin/pull/1260)
- [FIX] Support HTTP content negotiation wildcards [#1112](https://github.com/gin-gonic/gin/pull/1112)
- [NEW] Add prefix from X-Forwarded-Prefix in redirectTrailingSlash [#1238](https://github.com/gin-gonic/gin/pull/1238)
- [FIX] context.Copy() race condition [#1020](https://github.com/gin-gonic/gin/pull/1020)
- [NEW] Add context.HandlerNames() [#1729](https://github.com/gin-gonic/gin/pull/1729)
- [FIX] Change color methods to public in the defaultLogger. [#1771](https://github.com/gin-gonic/gin/pull/1771)
- [FIX] Update writeHeaders method to use http.Header.Set [#1722](https://github.com/gin-gonic/gin/pull/1722)
- [NEW] Add response size to LogFormatterParams [#1752](https://github.com/gin-gonic/gin/pull/1752)
- [NEW] Allow ignoring field on form mapping [#1733](https://github.com/gin-gonic/gin/pull/1733)
- [NEW] Add a function to force color in console output. [#1724](https://github.com/gin-gonic/gin/pull/1724)
- [FIX] Context.Next() - recheck len of handlers on every iteration. [#1745](https://github.com/gin-gonic/gin/pull/1745)
- [FIX] Fix all errcheck warnings [#1739](https://github.com/gin-gonic/gin/pull/1739) [#1653](https://github.com/gin-gonic/gin/pull/1653)
- [NEW] context: inherits context cancellation and deadline from http.Request context for Go>=1.7 [#1690](https://github.com/gin-gonic/gin/pull/1690)
- [NEW] Binding for URL Params [#1694](https://github.com/gin-gonic/gin/pull/1694)
- [NEW] Add LoggerWithFormatter method [#1677](https://github.com/gin-gonic/gin/pull/1677)
- [FIX] CI testing updates [#1671](https://github.com/gin-gonic/gin/pull/1671) [#1670](https://github.com/gin-gonic/gin/pull/1670) [#1682](https://github.com/gin-gonic/gin/pull/1682) [#1669](https://github.com/gin-gonic/gin/pull/1669)
- [FIX] StaticFS(): Send 404 when path does not exist [#1663](https://github.com/gin-gonic/gin/pull/1663)
- [FIX] Handle nil body for JSON binding [#1638](https://github.com/gin-gonic/gin/pull/1638)
- [FIX] Support bind uri param [#1612](https://github.com/gin-gonic/gin/pull/1612)
- [FIX] recovery: fix issue with syscall import on google app engine [#1640](https://github.com/gin-gonic/gin/pull/1640)
- [FIX] Make sure the debug log contains line breaks [#1650](https://github.com/gin-gonic/gin/pull/1650)
- [FIX] Panic stack trace being printed during recovery of broken pipe [#1089](https://github.com/gin-gonic/gin/pull/1089) [#1259](https://github.com/gin-gonic/gin/pull/1259)
- [NEW] RunFd method to run http.Server through a file descriptor [#1609](https://github.com/gin-gonic/gin/pull/1609)
- [NEW] Yaml binding support [#1618](https://github.com/gin-gonic/gin/pull/1618)
- [FIX] Pass MaxMultipartMemory when FormFile is called [#1600](https://github.com/gin-gonic/gin/pull/1600)
- [FIX] LoadHTML* tests [#1559](https://github.com/gin-gonic/gin/pull/1559)
- [FIX] Removed use of sync.pool from HandleContext [#1565](https://github.com/gin-gonic/gin/pull/1565)
- [FIX] Format output log to os.Stderr [#1571](https://github.com/gin-gonic/gin/pull/1571)
- [FIX] Make logger use a yellow background and a darkgray text for legibility [#1570](https://github.com/gin-gonic/gin/pull/1570)
- [FIX] Remove sensitive request information from panic log. [#1370](https://github.com/gin-gonic/gin/pull/1370)
- [FIX] log.Println() does not print timestamp [#829](https://github.com/gin-gonic/gin/pull/829) [#1560](https://github.com/gin-gonic/gin/pull/1560)
- [NEW] Add PureJSON renderer [#694](https://github.com/gin-gonic/gin/pull/694)
- [FIX] Add missing copyright and update if/else [#1497](https://github.com/gin-gonic/gin/pull/1497)
- [FIX] Update msgpack usage [#1498](https://github.com/gin-gonic/gin/pull/1498)
- [FIX] Use protobuf on render [#1496](https://github.com/gin-gonic/gin/pull/1496)
- [FIX] Add support for Protobuf format response [#1479](https://github.com/gin-gonic/gin/pull/1479)
- [NEW] Set default time format in form binding [#1487](https://github.com/gin-gonic/gin/pull/1487)
- [FIX] Add BindXML and ShouldBindXML [#1485](https://github.com/gin-gonic/gin/pull/1485)
- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491)
### Gin 1.3.0
@ -175,7 +231,7 @@
- [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults.
- [NEW] Flexible rendering API
- [NEW] Add Context.File()
- [NEW] Add shorcut RunTLS() for http.ListenAndServeTLS
- [NEW] Add shortcut RunTLS() for http.ListenAndServeTLS
- [FIX] Rename NotFound404() to NoRoute()
- [FIX] Errors in context are purged
- [FIX] Adds HEAD method in Static file serving
@ -198,7 +254,7 @@
- [NEW] New Bind() and BindWith() methods for parsing request body.
- [NEW] Add Content.Copy()
- [NEW] Add context.LastError()
- [NEW] Add shorcut for OPTIONS HTTP method
- [NEW] Add shortcut for OPTIONS HTTP method
- [FIX] Tons of README fixes
- [FIX] Header is written before body
- [FIX] BasicAuth() and changes API a little bit

134
README.md
View File

@ -40,6 +40,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Only Bind Query String](#only-bind-query-string)
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
- [Bind Uri](#bind-uri)
- [Bind Header](#bind-header)
- [Bind HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
@ -69,7 +70,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
To install Gin package, you need to install Go and set your Go workspace first.
1. Download and install it:
1. The first need [Go](https://golang.org/) installed (**version 1.10+ is required**), then you can use the below Go command to install Gin.
```sh
$ go get -u github.com/gin-gonic/gin
@ -100,6 +101,12 @@ $ go get github.com/kardianos/govendor
$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_"
```
If you are on a Mac and you're installing Go 1.8 (released: Feb 2017) or later, GOPATH is automatically determined by the Go toolchain for you. It defaults to $HOME/go on macOS so you can create your project like this
```sh
$ mkdir -p $HOME/go/src/github.com/myusername/project && cd "$_"
```
3. Vendor init your project and add gin
```sh
@ -119,10 +126,6 @@ $ curl https://raw.githubusercontent.com/gin-gonic/examples/master/basic/main.go
$ go run main.go
```
## Prerequisite
Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
## Quick start
```sh
@ -256,6 +259,11 @@ func main() {
c.String(http.StatusOK, message)
})
// For each matched request Context will hold the route definition
router.POST("/user/:name/*action", func(c *gin.Context) {
c.FullPath() == "/user/:name/*action" // true
})
router.Run(":8080")
}
```
@ -620,10 +628,10 @@ Note that you need to set the corresponding binding tag on all fields you want t
Also, Gin provides two sets of methods for binding:
- **Type** - Must bind
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`, `BindHeader`
- **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`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`, `ShouldBindHeader`
- **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.
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`.
@ -754,7 +762,7 @@ func bookableDate(
) bool {
if date, ok := field.Interface().(time.Time); ok {
today := time.Now()
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
if today.After(date) {
return false
}
}
@ -847,6 +855,8 @@ type Person struct {
Name string `form:"name"`
Address string `form:"address"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
}
func main() {
@ -864,6 +874,8 @@ func startPage(c *gin.Context) {
log.Println(person.Name)
log.Println(person.Address)
log.Println(person.Birthday)
log.Println(person.CreateTime)
log.Println(person.UnixTime)
}
c.String(200, "Success")
@ -872,7 +884,7 @@ func startPage(c *gin.Context) {
Test it with:
```sh
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15"
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
```
### Bind Uri
@ -909,6 +921,43 @@ $ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
$ curl -v localhost:8088/thinkerou/not-uuid
```
### Bind Header
```go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
}
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
h := testHeader{}
if err := c.ShouldBindHeader(&h); err != nil {
c.JSON(200, err)
}
fmt.Printf("%#v\n", h)
c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain})
})
r.Run()
// client
// curl -H "rate:300" -H "domain:music" 127.0.0.1:8080/
// output
// {"Domain":"music","Rate":300}
}
```
### Bind HTML checkboxes
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
@ -958,32 +1007,36 @@ result:
### Multipart/Urlencoded binding
```go
package main
type ProfileForm struct {
Name string `form:"name" binding:"required"`
Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
import (
"github.com/gin-gonic/gin"
)
type LoginForm struct {
User string `form:"user" binding:"required"`
Password string `form:"password" binding:"required"`
// or for multiple files
// Avatars []*multipart.FileHeader `form:"avatar" binding:"required"`
}
func main() {
router := gin.Default()
router.POST("/login", func(c *gin.Context) {
router.POST("/profile", func(c *gin.Context) {
// you can bind multipart form with explicit binding declaration:
// c.ShouldBindWith(&form, binding.Form)
// or you can simply use autobinding with ShouldBind method:
var form LoginForm
var form ProfileForm
// in this case proper binding will be automatically selected
if c.ShouldBind(&form) == nil {
if form.User == "user" && form.Password == "password" {
c.JSON(200, gin.H{"status": "you are logged in"})
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
if err := c.ShouldBind(&form); err != nil {
c.String(http.StatusBadRequest, "bad request")
return
}
err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename)
if err != nil {
c.String(http.StatusInternalServerError, "unknown error")
return
}
// db.Save(&form)
c.String(http.StatusOK, "ok")
})
router.Run(":8080")
}
@ -991,7 +1044,7 @@ func main() {
Test it with:
```sh
$ curl -v --form user=user --form password=password http://localhost:8080/login
$ curl -X POST -v --form name=user --form "avatar=@./avatar.png" http://localhost:8080/profile
```
### XML, JSON, YAML and ProtoBuf rendering
@ -1076,8 +1129,8 @@ Using JSONP to request data from a server in a different domain. Add callback t
func main() {
r := gin.Default()
r.GET("/JSONP?callback=x", func(c *gin.Context) {
data := map[string]interface{}{
r.GET("/JSONP", func(c *gin.Context) {
data := gin.H{
"foo": "bar",
}
@ -1088,19 +1141,22 @@ func main() {
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
// client
// curl http://127.0.0.1:8080/JSONP?callback=x
}
```
#### AsciiJSON
Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII chracters.
Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII characters.
```go
func main() {
r := gin.Default()
r.GET("/someJSON", func(c *gin.Context) {
data := map[string]interface{}{
data := gin.H{
"lang": "GO语言",
"tag": "<br>",
}
@ -1309,7 +1365,7 @@ func main() {
router.LoadHTMLFiles("./testdata/template/raw.tmpl")
router.GET("/raw", func(c *gin.Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
c.HTML(http.StatusOK, "raw.tmpl", gin.H{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
})
})
@ -1622,11 +1678,19 @@ func main() {
}
g.Go(func() error {
return server01.ListenAndServe()
err := server01.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
return err
})
g.Go(func() error {
return server02.ListenAndServe()
err := server02.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
return err
})
if err := g.Wait(); err != nil {
@ -1696,9 +1760,9 @@ func main() {
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
// kill (no param) default send syscanll.SIGTERM
// kill (no param) default send syscall.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
// kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutdown Server ...")
@ -1743,6 +1807,7 @@ func main() {
func loadTemplate() (*template.Template, error) {
t := template.New("")
for name, file := range Assets.Files {
defer file.Close()
if file.IsDir() || !strings.HasSuffix(name, ".tmpl") {
continue
}
@ -2068,3 +2133,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor
* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow.
* [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares.
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
* [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes.

View File

@ -5,7 +5,6 @@
package gin
import (
"crypto/subtle"
"encoding/base64"
"net/http"
"strconv"
@ -86,11 +85,3 @@ func authorizationHeader(user, password string) string {
base := user + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(base))
}
func secureCompare(given, actual string) bool {
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1
}
// Securely compare actual to itself to keep constant time, but always return false.
return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false
}

View File

@ -81,13 +81,6 @@ func TestBasicAuthAuthorizationHeader(t *testing.T) {
assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password"))
}
func TestBasicAuthSecureCompare(t *testing.T) {
assert.True(t, secureCompare("1234567890", "1234567890"))
assert.False(t, secureCompare("123456789", "1234567890"))
assert.False(t, secureCompare("12345678900", "1234567890"))
assert.False(t, secureCompare("1234567891", "1234567890"))
}
func TestBasicAuthSucceed(t *testing.T) {
accounts := Accounts{"admin": "password"}
router := New()

View File

@ -78,6 +78,7 @@ var (
MsgPack = msgpackBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
)
// Default returns the appropriate Binding instance based on the HTTP method

View File

@ -1,72 +0,0 @@
package binding
import (
"bytes"
"io/ioutil"
"testing"
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
func TestBindingBody(t *testing.T) {
for _, tt := range []struct {
name string
binding BindingBody
body string
want string
}{
{
name: "JSON binding",
binding: JSON,
body: `{"foo":"FOO"}`,
},
{
name: "XML binding",
binding: XML,
body: `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`,
},
{
name: "MsgPack binding",
binding: MsgPack,
body: msgPackBody(t),
},
{
name: "YAML binding",
binding: YAML,
body: `foo: FOO`,
},
} {
t.Logf("testing: %s", tt.name)
req := requestWithBody("POST", "/", tt.body)
form := FooStruct{}
body, _ := ioutil.ReadAll(req.Body)
assert.NoError(t, tt.binding.BindBody(body, &form))
assert.Equal(t, FooStruct{"FOO"}, form)
}
}
func msgPackBody(t *testing.T) string {
test := FooStruct{"FOO"}
h := new(codec.MsgpackHandle)
buf := bytes.NewBuffer(nil)
assert.NoError(t, codec.NewEncoder(buf, h).Encode(test))
return buf.String()
}
func TestBindingBodyProto(t *testing.T) {
test := protoexample.Test{
Label: proto.String("FOO"),
}
data, _ := proto.Marshal(&test)
req := requestWithBody("POST", "/", string(data))
form := protoexample.Test{}
body, _ := ioutil.ReadAll(req.Body)
assert.NoError(t, ProtoBuf.BindBody(body, &form))
assert.Equal(t, test, form)
}

View File

@ -24,6 +24,16 @@ import (
"github.com/ugorji/go/codec"
)
type appkey struct {
Appkey string `json:"appkey" form:"appkey"`
}
type QueryTest struct {
Page int `json:"page" form:"page"`
Size int `json:"size" form:"size"`
appkey
}
type FooStruct struct {
Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"`
}
@ -54,9 +64,20 @@ type FooStructUseNumber struct {
Foo interface{} `json:"foo" binding:"required"`
}
type FooStructDisallowUnknownFields struct {
Foo interface{} `json:"foo" binding:"required"`
}
type FooBarStructForTimeType struct {
TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
}
type FooStructForTimeTypeNotUnixFormat struct {
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
}
type FooStructForTimeTypeNotFormat struct {
@ -114,71 +135,6 @@ type FooStructForBoolType struct {
BoolFoo bool `form:"bool_foo"`
}
type FooBarStructForIntType struct {
IntFoo int `form:"int_foo"`
IntBar int `form:"int_bar" binding:"required"`
}
type FooBarStructForInt8Type struct {
Int8Foo int8 `form:"int8_foo"`
Int8Bar int8 `form:"int8_bar" binding:"required"`
}
type FooBarStructForInt16Type struct {
Int16Foo int16 `form:"int16_foo"`
Int16Bar int16 `form:"int16_bar" binding:"required"`
}
type FooBarStructForInt32Type struct {
Int32Foo int32 `form:"int32_foo"`
Int32Bar int32 `form:"int32_bar" binding:"required"`
}
type FooBarStructForInt64Type struct {
Int64Foo int64 `form:"int64_foo"`
Int64Bar int64 `form:"int64_bar" binding:"required"`
}
type FooBarStructForUintType struct {
UintFoo uint `form:"uint_foo"`
UintBar uint `form:"uint_bar" binding:"required"`
}
type FooBarStructForUint8Type struct {
Uint8Foo uint8 `form:"uint8_foo"`
Uint8Bar uint8 `form:"uint8_bar" binding:"required"`
}
type FooBarStructForUint16Type struct {
Uint16Foo uint16 `form:"uint16_foo"`
Uint16Bar uint16 `form:"uint16_bar" binding:"required"`
}
type FooBarStructForUint32Type struct {
Uint32Foo uint32 `form:"uint32_foo"`
Uint32Bar uint32 `form:"uint32_bar" binding:"required"`
}
type FooBarStructForUint64Type struct {
Uint64Foo uint64 `form:"uint64_foo"`
Uint64Bar uint64 `form:"uint64_bar" binding:"required"`
}
type FooBarStructForBoolType struct {
BoolFoo bool `form:"bool_foo"`
BoolBar bool `form:"bool_bar" binding:"required"`
}
type FooBarStructForFloat32Type struct {
Float32Foo float32 `form:"float32_foo"`
Float32Bar float32 `form:"float32_bar" binding:"required"`
}
type FooBarStructForFloat64Type struct {
Float64Foo float64 `form:"float64_foo"`
Float64Bar float64 `form:"float64_bar" binding:"required"`
}
type FooStructForStringPtrType struct {
PtrFoo *string `form:"ptr_foo"`
PtrBar *string `form:"ptr_bar" binding:"required"`
@ -242,6 +198,12 @@ func TestBindingJSONUseNumber2(t *testing.T) {
`{"foo": 123}`, `{"bar": "foo"}`)
}
func TestBindingJSONDisallowUnknownFields(t *testing.T) {
testBodyBindingDisallowUnknownFields(t, JSON,
"/", "/",
`{"foo": "bar"}`, `{"foo": "bar", "what": "this"}`)
}
func TestBindingForm(t *testing.T) {
testFormBinding(t, "POST",
"/", "/",
@ -254,6 +216,18 @@ func TestBindingForm2(t *testing.T) {
"", "")
}
func TestBindingFormEmbeddedStruct(t *testing.T) {
testFormBindingEmbeddedStruct(t, "POST",
"/", "/",
"page=1&size=2&appkey=test-appkey", "bar2=foo")
}
func TestBindingFormEmbeddedStruct2(t *testing.T) {
testFormBindingEmbeddedStruct(t, "GET",
"/?page=1&size=2&appkey=test-appkey", "/?bar2=foo",
"", "")
}
func TestBindingFormDefaultValue(t *testing.T) {
testFormBindingDefaultValue(t, "POST",
"/", "/",
@ -269,7 +243,10 @@ func TestBindingFormDefaultValue2(t *testing.T) {
func TestBindingFormForTime(t *testing.T) {
testFormBindingForTime(t, "POST",
"/", "/",
"time_foo=2017-11-15&time_bar=", "bar2=foo")
"time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "bar2=foo")
testFormBindingForTimeNotUnixFormat(t, "POST",
"/", "/",
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, "POST",
"/", "/",
"time_foo=2017-11-15", "bar2=foo")
@ -283,8 +260,11 @@ func TestBindingFormForTime(t *testing.T) {
func TestBindingFormForTime2(t *testing.T) {
testFormBindingForTime(t, "GET",
"/?time_foo=2017-11-15&time_bar=", "/?bar2=foo",
"/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "/?bar2=foo",
"", "")
testFormBindingForTimeNotUnixFormat(t, "POST",
"/", "/",
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, "GET",
"/?time_foo=2017-11-15", "/?bar2=foo",
"", "")
@ -335,110 +315,6 @@ func TestBindingFormForType(t *testing.T) {
"/?slice_map_foo=1&slice_map_foo=2", "/?bar2=1&bar2=2",
"", "", "SliceMap")
testFormBindingForType(t, "POST",
"/", "/",
"int_foo=&int_bar=-12", "bar2=-123", "Int")
testFormBindingForType(t, "GET",
"/?int_foo=&int_bar=-12", "/?bar2=-123",
"", "", "Int")
testFormBindingForType(t, "POST",
"/", "/",
"int8_foo=&int8_bar=-12", "bar2=-123", "Int8")
testFormBindingForType(t, "GET",
"/?int8_foo=&int8_bar=-12", "/?bar2=-123",
"", "", "Int8")
testFormBindingForType(t, "POST",
"/", "/",
"int16_foo=&int16_bar=-12", "bar2=-123", "Int16")
testFormBindingForType(t, "GET",
"/?int16_foo=&int16_bar=-12", "/?bar2=-123",
"", "", "Int16")
testFormBindingForType(t, "POST",
"/", "/",
"int32_foo=&int32_bar=-12", "bar2=-123", "Int32")
testFormBindingForType(t, "GET",
"/?int32_foo=&int32_bar=-12", "/?bar2=-123",
"", "", "Int32")
testFormBindingForType(t, "POST",
"/", "/",
"int64_foo=&int64_bar=-12", "bar2=-123", "Int64")
testFormBindingForType(t, "GET",
"/?int64_foo=&int64_bar=-12", "/?bar2=-123",
"", "", "Int64")
testFormBindingForType(t, "POST",
"/", "/",
"uint_foo=&uint_bar=12", "bar2=123", "Uint")
testFormBindingForType(t, "GET",
"/?uint_foo=&uint_bar=12", "/?bar2=123",
"", "", "Uint")
testFormBindingForType(t, "POST",
"/", "/",
"uint8_foo=&uint8_bar=12", "bar2=123", "Uint8")
testFormBindingForType(t, "GET",
"/?uint8_foo=&uint8_bar=12", "/?bar2=123",
"", "", "Uint8")
testFormBindingForType(t, "POST",
"/", "/",
"uint16_foo=&uint16_bar=12", "bar2=123", "Uint16")
testFormBindingForType(t, "GET",
"/?uint16_foo=&uint16_bar=12", "/?bar2=123",
"", "", "Uint16")
testFormBindingForType(t, "POST",
"/", "/",
"uint32_foo=&uint32_bar=12", "bar2=123", "Uint32")
testFormBindingForType(t, "GET",
"/?uint32_foo=&uint32_bar=12", "/?bar2=123",
"", "", "Uint32")
testFormBindingForType(t, "POST",
"/", "/",
"uint64_foo=&uint64_bar=12", "bar2=123", "Uint64")
testFormBindingForType(t, "GET",
"/?uint64_foo=&uint64_bar=12", "/?bar2=123",
"", "", "Uint64")
testFormBindingForType(t, "POST",
"/", "/",
"bool_foo=&bool_bar=true", "bar2=true", "Bool")
testFormBindingForType(t, "GET",
"/?bool_foo=&bool_bar=true", "/?bar2=true",
"", "", "Bool")
testFormBindingForType(t, "POST",
"/", "/",
"float32_foo=&float32_bar=-12.34", "bar2=12.3", "Float32")
testFormBindingForType(t, "GET",
"/?float32_foo=&float32_bar=-12.34", "/?bar2=12.3",
"", "", "Float32")
testFormBindingForType(t, "POST",
"/", "/",
"float64_foo=&float64_bar=-12.34", "bar2=12.3", "Float64")
testFormBindingForType(t, "GET",
"/?float64_foo=&float64_bar=-12.34", "/?bar2=12.3",
"", "", "Float64")
testFormBindingForType(t, "POST",
"/", "/",
"ptr_bar=test", "bar2=test", "Ptr")
@ -792,9 +668,9 @@ func TestValidationDisabled(t *testing.T) {
assert.NoError(t, err)
}
func TestExistsSucceeds(t *testing.T) {
func TestRequiredSucceeds(t *testing.T) {
type HogeStruct struct {
Hoge *int `json:"hoge" binding:"exists"`
Hoge *int `json:"hoge" binding:"required"`
}
var obj HogeStruct
@ -803,9 +679,9 @@ func TestExistsSucceeds(t *testing.T) {
assert.NoError(t, err)
}
func TestExistsFails(t *testing.T) {
func TestRequiredFails(t *testing.T) {
type HogeStruct struct {
Hoge *int `json:"foo" binding:"exists"`
Hoge *int `json:"foo" binding:"required"`
}
var obj HogeStruct
@ -814,6 +690,31 @@ func TestExistsFails(t *testing.T) {
assert.Error(t, err)
}
func TestHeaderBinding(t *testing.T) {
h := Header
assert.Equal(t, "header", h.Name())
type tHeader struct {
Limit int `header:"limit"`
}
var theader tHeader
req := requestWithBody("GET", "/", "")
req.Header.Add("limit", "1000")
assert.NoError(t, h.Bind(req, &theader))
assert.Equal(t, 1000, theader.Limit)
req = requestWithBody("GET", "/", "")
req.Header.Add("fail", `{fail:fail}`)
type failStruct struct {
Fail map[string]interface{} `header:"fail"`
}
err := h.Bind(req, &failStruct{})
assert.Error(t, err)
}
func TestUriBinding(t *testing.T) {
b := Uri
assert.Equal(t, "uri", b.Name())
@ -857,6 +758,23 @@ func TestUriInnerBinding(t *testing.T) {
assert.Equal(t, tag.S.Age, expectedAge)
}
func testFormBindingEmbeddedStruct(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
obj := QueryTest{}
req := requestWithBody(method, path, body)
if method == "POST" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, 1, obj.Page)
assert.Equal(t, 2, obj.Size)
assert.Equal(t, "test-appkey", obj.Appkey)
}
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
@ -954,6 +872,8 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String())
assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix())
assert.Equal(t, "UTC", obj.TimeBar.Location().String())
assert.Equal(t, int64(1562400033000000123), obj.CreateTime.UnixNano())
assert.Equal(t, int64(1562400033), obj.UnixTime.Unix())
obj = FooBarStructForTimeType{}
req = requestWithBody(method, badPath, badBody)
@ -961,6 +881,24 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
assert.Error(t, err)
}
func testFormBindingForTimeNotUnixFormat(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
obj := FooStructForTimeTypeNotUnixFormat{}
req := requestWithBody(method, path, body)
if method == "POST" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.Error(t, err)
obj = FooStructForTimeTypeNotUnixFormat{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
}
func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
@ -1076,149 +1014,6 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
req.Header.Add("Content-Type", MIMEPOSTForm)
}
switch typ {
case "Int":
obj := FooBarStructForIntType{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, int(0), obj.IntFoo)
assert.Equal(t, int(-12), obj.IntBar)
obj = FooBarStructForIntType{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Int8":
obj := FooBarStructForInt8Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, int8(0), obj.Int8Foo)
assert.Equal(t, int8(-12), obj.Int8Bar)
obj = FooBarStructForInt8Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Int16":
obj := FooBarStructForInt16Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, int16(0), obj.Int16Foo)
assert.Equal(t, int16(-12), obj.Int16Bar)
obj = FooBarStructForInt16Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Int32":
obj := FooBarStructForInt32Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, int32(0), obj.Int32Foo)
assert.Equal(t, int32(-12), obj.Int32Bar)
obj = FooBarStructForInt32Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Int64":
obj := FooBarStructForInt64Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, int64(0), obj.Int64Foo)
assert.Equal(t, int64(-12), obj.Int64Bar)
obj = FooBarStructForInt64Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Uint":
obj := FooBarStructForUintType{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, uint(0x0), obj.UintFoo)
assert.Equal(t, uint(0xc), obj.UintBar)
obj = FooBarStructForUintType{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Uint8":
obj := FooBarStructForUint8Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, uint8(0x0), obj.Uint8Foo)
assert.Equal(t, uint8(0xc), obj.Uint8Bar)
obj = FooBarStructForUint8Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Uint16":
obj := FooBarStructForUint16Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, uint16(0x0), obj.Uint16Foo)
assert.Equal(t, uint16(0xc), obj.Uint16Bar)
obj = FooBarStructForUint16Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Uint32":
obj := FooBarStructForUint32Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, uint32(0x0), obj.Uint32Foo)
assert.Equal(t, uint32(0xc), obj.Uint32Bar)
obj = FooBarStructForUint32Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Uint64":
obj := FooBarStructForUint64Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, uint64(0x0), obj.Uint64Foo)
assert.Equal(t, uint64(0xc), obj.Uint64Bar)
obj = FooBarStructForUint64Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Float32":
obj := FooBarStructForFloat32Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, float32(0.0), obj.Float32Foo)
assert.Equal(t, float32(-12.34), obj.Float32Bar)
obj = FooBarStructForFloat32Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Float64":
obj := FooBarStructForFloat64Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, float64(0.0), obj.Float64Foo)
assert.Equal(t, float64(-12.34), obj.Float64Bar)
obj = FooBarStructForFloat64Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Bool":
obj := FooBarStructForBoolType{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.False(t, obj.BoolFoo)
assert.True(t, obj.BoolBar)
obj = FooBarStructForBoolType{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Slice":
obj := FooStructForSliceType{}
err := b.Bind(req, &obj)
@ -1377,6 +1172,25 @@ func testBodyBindingUseNumber2(t *testing.T, b Binding, name, path, badPath, bod
assert.Error(t, err)
}
func testBodyBindingDisallowUnknownFields(t *testing.T, b Binding, path, badPath, body, badBody string) {
EnableDecoderDisallowUnknownFields = true
defer func() {
EnableDecoderDisallowUnknownFields = false
}()
obj := FooStructDisallowUnknownFields{}
req := requestWithBody("POST", path, body)
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStructDisallowUnknownFields{}
req = requestWithBody("POST", badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
assert.Contains(t, err.Error(), "what")
}
func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
@ -1454,97 +1268,3 @@ func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return
}
func TestCanSet(t *testing.T) {
type CanSetStruct struct {
lowerStart string `form:"lower"`
}
var c CanSetStruct
assert.Nil(t, mapForm(&c, nil))
}
func formPostRequest(path, body string) *http.Request {
req := requestWithBody("POST", path, body)
req.Header.Add("Content-Type", MIMEPOSTForm)
return req
}
func TestBindingSliceDefault(t *testing.T) {
var s struct {
Friends []string `form:"friends,default=mike"`
}
req := formPostRequest("", "")
err := Form.Bind(req, &s)
assert.NoError(t, err)
assert.Len(t, s.Friends, 1)
assert.Equal(t, "mike", s.Friends[0])
}
func TestBindingStructField(t *testing.T) {
var s struct {
Opts struct {
Port int
} `form:"opts"`
}
req := formPostRequest("", `opts={"Port": 8000}`)
err := Form.Bind(req, &s)
assert.NoError(t, err)
assert.Equal(t, 8000, s.Opts.Port)
}
func TestBindingUnknownTypeChan(t *testing.T) {
var s struct {
Stop chan bool `form:"stop"`
}
req := formPostRequest("", "stop=true")
err := Form.Bind(req, &s)
assert.Error(t, err)
assert.Equal(t, errUnknownType, err)
}
func TestBindingTimeDuration(t *testing.T) {
var s struct {
Timeout time.Duration `form:"timeout"`
}
// ok
req := formPostRequest("", "timeout=5s")
err := Form.Bind(req, &s)
assert.NoError(t, err)
assert.Equal(t, 5*time.Second, s.Timeout)
// error
req = formPostRequest("", "timeout=wrong")
err = Form.Bind(req, &s)
assert.Error(t, err)
}
func TestBindingArray(t *testing.T) {
var s struct {
Nums [2]int `form:"nums,default=4"`
}
// default
req := formPostRequest("", "")
err := Form.Bind(req, &s)
assert.Error(t, err)
assert.Equal(t, [2]int{0, 0}, s.Nums)
// ok
req = formPostRequest("", "nums=3&nums=8")
err = Form.Bind(req, &s)
assert.NoError(t, err)
assert.Equal(t, [2]int{3, 8}, s.Nums)
// not enough vals
req = formPostRequest("", "nums=3")
err = Form.Bind(req, &s)
assert.Error(t, err)
// error
req = formPostRequest("", "nums=3&nums=wrong")
err = Form.Bind(req, &s)
assert.Error(t, err)
}

View File

@ -8,7 +8,7 @@ import (
"reflect"
"sync"
"gopkg.in/go-playground/validator.v8"
"gopkg.in/go-playground/validator.v9"
)
type defaultValidator struct {
@ -45,7 +45,7 @@ func (v *defaultValidator) Engine() interface{} {
func (v *defaultValidator) lazyinit() {
v.once.Do(func() {
config := &validator.Config{TagName: "binding"}
v.validate = validator.New(config)
v.validate = validator.New()
v.validate.SetTagName("binding")
})
}

View File

@ -5,9 +5,7 @@
package binding
import (
"mime/multipart"
"net/http"
"reflect"
)
const defaultMemory = 32 * 1024 * 1024
@ -63,27 +61,3 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
return validate(obj)
}
type multipartRequest http.Request
var _ setter = (*multipartRequest)(nil)
var (
multipartFileHeaderStructType = reflect.TypeOf(multipart.FileHeader{})
)
// TrySet tries to set a value by the multipart request with the binding a form file
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
if value.Type() == multipartFileHeaderStructType {
_, file, err := (*http.Request)(r).FormFile(key)
if err != nil {
return false, err
}
if file != nil {
value.Set(reflect.ValueOf(*file))
return true, nil
}
}
return setByForm(value, field, r.MultipartForm.Value, key, opt)
}

View File

@ -70,6 +70,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
return isSetted, nil
}
if vKind != reflect.Struct || !field.Anonymous {
ok, err := tryToSetValue(value, field, setter, tag)
if err != nil {
return false, err
@ -77,13 +78,15 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
if ok {
return true, nil
}
}
if vKind == reflect.Struct {
tValue := value.Type()
var isSetted bool
for i := 0; i < value.NumField(); i++ {
if !value.Field(i).CanSet() {
sf := tValue.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag)
@ -123,9 +126,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
for len(opts) > 0 {
opt, opts = head(opts, ",")
k, v := head(opt, "=")
switch k {
case "default":
if k, v := head(opt, "="); k == "default" {
setOpt.isDefaultExists = true
setOpt.defaultValue = v
}
@ -265,6 +266,24 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
timeFormat = time.RFC3339
}
switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixnano":
tv, err := strconv.ParseInt(val, 10, 0)
if err != nil {
return err
}
d := time.Duration(1)
if tf == "unixnano" {
d = time.Second
}
t := time.Unix(tv/int64(d), tv%int64(d))
value.Set(reflect.ValueOf(t))
return nil
}
if val == "" {
value.Set(reflect.ValueOf(time.Time{}))
return nil

View File

@ -0,0 +1,271 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestMappingBaseTypes(t *testing.T) {
intPtr := func(i int) *int {
return &i
}
for _, tt := range []struct {
name string
value interface{}
form string
expect interface{}
}{
{"base type", struct{ F int }{}, "9", int(9)},
{"base type", struct{ F int8 }{}, "9", int8(9)},
{"base type", struct{ F int16 }{}, "9", int16(9)},
{"base type", struct{ F int32 }{}, "9", int32(9)},
{"base type", struct{ F int64 }{}, "9", int64(9)},
{"base type", struct{ F uint }{}, "9", uint(9)},
{"base type", struct{ F uint8 }{}, "9", uint8(9)},
{"base type", struct{ F uint16 }{}, "9", uint16(9)},
{"base type", struct{ F uint32 }{}, "9", uint32(9)},
{"base type", struct{ F uint64 }{}, "9", uint64(9)},
{"base type", struct{ F bool }{}, "True", true},
{"base type", struct{ F float32 }{}, "9.1", float32(9.1)},
{"base type", struct{ F float64 }{}, "9.1", float64(9.1)},
{"base type", struct{ F string }{}, "test", string("test")},
{"base type", struct{ F *int }{}, "9", intPtr(9)},
// zero values
{"zero value", struct{ F int }{}, "", int(0)},
{"zero value", struct{ F uint }{}, "", uint(0)},
{"zero value", struct{ F bool }{}, "", false},
{"zero value", struct{ F float32 }{}, "", float32(0)},
} {
tp := reflect.TypeOf(tt.value)
testName := tt.name + ":" + tp.Field(0).Type.String()
val := reflect.New(reflect.TypeOf(tt.value))
val.Elem().Set(reflect.ValueOf(tt.value))
field := val.Elem().Type().Field(0)
_, err := mapping(val, emptyField, formSource{field.Name: {tt.form}}, "form")
assert.NoError(t, err, testName)
actual := val.Elem().Field(0).Interface()
assert.Equal(t, tt.expect, actual, testName)
}
}
func TestMappingDefault(t *testing.T) {
var s struct {
Int int `form:",default=9"`
Slice []int `form:",default=9"`
Array [1]int `form:",default=9"`
}
err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err)
assert.Equal(t, 9, s.Int)
assert.Equal(t, []int{9}, s.Slice)
assert.Equal(t, [1]int{9}, s.Array)
}
func TestMappingSkipField(t *testing.T) {
var s struct {
A int
}
err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err)
assert.Equal(t, 0, s.A)
}
func TestMappingIgnoreField(t *testing.T) {
var s struct {
A int `form:"A"`
B int `form:"-"`
}
err := mappingByPtr(&s, formSource{"A": {"9"}, "B": {"9"}}, "form")
assert.NoError(t, err)
assert.Equal(t, 9, s.A)
assert.Equal(t, 0, s.B)
}
func TestMappingUnexportedField(t *testing.T) {
var s struct {
A int `form:"a"`
b int `form:"b"`
}
err := mappingByPtr(&s, formSource{"a": {"9"}, "b": {"9"}}, "form")
assert.NoError(t, err)
assert.Equal(t, 9, s.A)
assert.Equal(t, 0, s.b)
}
func TestMappingPrivateField(t *testing.T) {
var s struct {
f int `form:"field"`
}
err := mappingByPtr(&s, formSource{"field": {"6"}}, "form")
assert.NoError(t, err)
assert.Equal(t, int(0), s.f)
}
func TestMappingUnknownFieldType(t *testing.T) {
var s struct {
U uintptr
}
err := mappingByPtr(&s, formSource{"U": {"unknown"}}, "form")
assert.Error(t, err)
assert.Equal(t, errUnknownType, err)
}
func TestMappingURI(t *testing.T) {
var s struct {
F int `uri:"field"`
}
err := mapUri(&s, map[string][]string{"field": {"6"}})
assert.NoError(t, err)
assert.Equal(t, int(6), s.F)
}
func TestMappingForm(t *testing.T) {
var s struct {
F int `form:"field"`
}
err := mapForm(&s, map[string][]string{"field": {"6"}})
assert.NoError(t, err)
assert.Equal(t, int(6), s.F)
}
func TestMappingTime(t *testing.T) {
var s struct {
Time time.Time
LocalTime time.Time `time_format:"2006-01-02"`
ZeroValue time.Time
CSTTime time.Time `time_format:"2006-01-02" time_location:"Asia/Shanghai"`
UTCTime time.Time `time_format:"2006-01-02" time_utc:"1"`
}
var err error
time.Local, err = time.LoadLocation("Europe/Berlin")
assert.NoError(t, err)
err = mapForm(&s, map[string][]string{
"Time": {"2019-01-20T16:02:58Z"},
"LocalTime": {"2019-01-20"},
"ZeroValue": {},
"CSTTime": {"2019-01-20"},
"UTCTime": {"2019-01-20"},
})
assert.NoError(t, err)
assert.Equal(t, "2019-01-20 16:02:58 +0000 UTC", s.Time.String())
assert.Equal(t, "2019-01-20 00:00:00 +0100 CET", s.LocalTime.String())
assert.Equal(t, "2019-01-19 23:00:00 +0000 UTC", s.LocalTime.UTC().String())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", s.ZeroValue.String())
assert.Equal(t, "2019-01-20 00:00:00 +0800 CST", s.CSTTime.String())
assert.Equal(t, "2019-01-19 16:00:00 +0000 UTC", s.CSTTime.UTC().String())
assert.Equal(t, "2019-01-20 00:00:00 +0000 UTC", s.UTCTime.String())
// wrong location
var wrongLoc struct {
Time time.Time `time_location:"wrong"`
}
err = mapForm(&wrongLoc, map[string][]string{"Time": {"2019-01-20T16:02:58Z"}})
assert.Error(t, err)
// wrong time value
var wrongTime struct {
Time time.Time
}
err = mapForm(&wrongTime, map[string][]string{"Time": {"wrong"}})
assert.Error(t, err)
}
func TestMapiingTimeDuration(t *testing.T) {
var s struct {
D time.Duration
}
// ok
err := mappingByPtr(&s, formSource{"D": {"5s"}}, "form")
assert.NoError(t, err)
assert.Equal(t, 5*time.Second, s.D)
// error
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
assert.Error(t, err)
}
func TestMappingSlice(t *testing.T) {
var s struct {
Slice []int `form:"slice,default=9"`
}
// default value
err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err)
assert.Equal(t, []int{9}, s.Slice)
// ok
err = mappingByPtr(&s, formSource{"slice": {"3", "4"}}, "form")
assert.NoError(t, err)
assert.Equal(t, []int{3, 4}, s.Slice)
// error
err = mappingByPtr(&s, formSource{"slice": {"wrong"}}, "form")
assert.Error(t, err)
}
func TestMappingArray(t *testing.T) {
var s struct {
Array [2]int `form:"array,default=9"`
}
// wrong default
err := mappingByPtr(&s, formSource{}, "form")
assert.Error(t, err)
// ok
err = mappingByPtr(&s, formSource{"array": {"3", "4"}}, "form")
assert.NoError(t, err)
assert.Equal(t, [2]int{3, 4}, s.Array)
// error - not enough vals
err = mappingByPtr(&s, formSource{"array": {"3"}}, "form")
assert.Error(t, err)
// error - wrong value
err = mappingByPtr(&s, formSource{"array": {"wrong"}}, "form")
assert.Error(t, err)
}
func TestMappingStructField(t *testing.T) {
var s struct {
J struct {
I int
}
}
err := mappingByPtr(&s, formSource{"J": {`{"I": 9}`}}, "form")
assert.NoError(t, err)
assert.Equal(t, 9, s.J.I)
}
func TestMappingMapField(t *testing.T) {
var s struct {
M map[string]int
}
err := mappingByPtr(&s, formSource{"M": {`{"one": 1}`}}, "form")
assert.NoError(t, err)
assert.Equal(t, map[string]int{"one": 1}, s.M)
}

34
binding/header.go Normal file
View File

@ -0,0 +1,34 @@
package binding
import (
"net/http"
"net/textproto"
"reflect"
)
type headerBinding struct{}
func (headerBinding) Name() string {
return "header"
}
func (headerBinding) Bind(req *http.Request, obj interface{}) error {
if err := mapHeader(obj, req.Header); err != nil {
return err
}
return validate(obj)
}
func mapHeader(ptr interface{}, h map[string][]string) error {
return mappingByPtr(ptr, headerSource(h), "header")
}
type headerSource map[string][]string
var _ setter = headerSource(nil)
func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt)
}

View File

@ -18,6 +18,12 @@ import (
// interface{} as a Number instead of as a float64.
var EnableDecoderUseNumber = false
// EnableDecoderDisallowUnknownFields is used to call the DisallowUnknownFields method
// on the JSON Decoder instance. DisallowUnknownFields causes the Decoder to
// return an error when the destination is a struct and the input contains object
// keys which do not match any non-ignored, exported fields in the destination.
var EnableDecoderDisallowUnknownFields = false
type jsonBinding struct{}
func (jsonBinding) Name() string {
@ -40,6 +46,9 @@ func decodeJSON(r io.Reader, obj interface{}) error {
if EnableDecoderUseNumber {
decoder.UseNumber()
}
if EnableDecoderDisallowUnknownFields {
decoder.DisallowUnknownFields()
}
if err := decoder.Decode(obj); err != nil {
return err
}

21
binding/json_test.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJSONBindingBindBody(t *testing.T) {
var s struct {
Foo string `json:"foo"`
}
err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO"}`), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

32
binding/msgpack_test.go Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec"
)
func TestMsgpackBindingBindBody(t *testing.T) {
type teststruct struct {
Foo string `msgpack:"foo"`
}
var s teststruct
err := msgpackBinding{}.BindBody(msgpackBody(t, teststruct{"FOO"}), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}
func msgpackBody(t *testing.T, obj interface{}) []byte {
var bs bytes.Buffer
h := &codec.MsgpackHandle{}
err := codec.NewEncoder(&bs, h).Encode(obj)
require.NoError(t, err)
return bs.Bytes()
}

View File

@ -0,0 +1,66 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"errors"
"mime/multipart"
"net/http"
"reflect"
)
type multipartRequest http.Request
var _ setter = (*multipartRequest)(nil)
// TrySet tries to set a value by the multipart request with the binding a form file
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
if files := r.MultipartForm.File[key]; len(files) != 0 {
return setByMultipartFormFile(value, field, files)
}
return setByForm(value, field, r.MultipartForm.Value, key, opt)
}
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
switch value.Kind() {
case reflect.Ptr:
switch value.Interface().(type) {
case *multipart.FileHeader:
value.Set(reflect.ValueOf(files[0]))
return true, nil
}
case reflect.Struct:
switch value.Interface().(type) {
case multipart.FileHeader:
value.Set(reflect.ValueOf(*files[0]))
return true, nil
}
case reflect.Slice:
slice := reflect.MakeSlice(value.Type(), len(files), len(files))
isSetted, err = setArrayOfMultipartFormFiles(slice, field, files)
if err != nil || !isSetted {
return isSetted, err
}
value.Set(slice)
return true, nil
case reflect.Array:
return setArrayOfMultipartFormFiles(value, field, files)
}
return false, errors.New("unsupported field type for multipart.FileHeader")
}
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
if value.Len() != len(files) {
return false, errors.New("unsupported len of array for []*multipart.FileHeader")
}
for i := range files {
setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
if err != nil || !setted {
return setted, err
}
}
return true, nil
}

View File

@ -0,0 +1,138 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"bytes"
"io/ioutil"
"mime/multipart"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFormMultipartBindingBindOneFile(t *testing.T) {
var s struct {
FileValue multipart.FileHeader `form:"file"`
FilePtr *multipart.FileHeader `form:"file"`
SliceValues []multipart.FileHeader `form:"file"`
SlicePtrs []*multipart.FileHeader `form:"file"`
ArrayValues [1]multipart.FileHeader `form:"file"`
ArrayPtrs [1]*multipart.FileHeader `form:"file"`
}
file := testFile{"file", "file1", []byte("hello")}
req := createRequestMultipartFiles(t, file)
err := FormMultipart.Bind(req, &s)
assert.NoError(t, err)
assertMultipartFileHeader(t, &s.FileValue, file)
assertMultipartFileHeader(t, s.FilePtr, file)
assert.Len(t, s.SliceValues, 1)
assertMultipartFileHeader(t, &s.SliceValues[0], file)
assert.Len(t, s.SlicePtrs, 1)
assertMultipartFileHeader(t, s.SlicePtrs[0], file)
assertMultipartFileHeader(t, &s.ArrayValues[0], file)
assertMultipartFileHeader(t, s.ArrayPtrs[0], file)
}
func TestFormMultipartBindingBindTwoFiles(t *testing.T) {
var s struct {
SliceValues []multipart.FileHeader `form:"file"`
SlicePtrs []*multipart.FileHeader `form:"file"`
ArrayValues [2]multipart.FileHeader `form:"file"`
ArrayPtrs [2]*multipart.FileHeader `form:"file"`
}
files := []testFile{
{"file", "file1", []byte("hello")},
{"file", "file2", []byte("world")},
}
req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, &s)
assert.NoError(t, err)
assert.Len(t, s.SliceValues, len(files))
assert.Len(t, s.SlicePtrs, len(files))
assert.Len(t, s.ArrayValues, len(files))
assert.Len(t, s.ArrayPtrs, len(files))
for i, file := range files {
assertMultipartFileHeader(t, &s.SliceValues[i], file)
assertMultipartFileHeader(t, s.SlicePtrs[i], file)
assertMultipartFileHeader(t, &s.ArrayValues[i], file)
assertMultipartFileHeader(t, s.ArrayPtrs[i], file)
}
}
func TestFormMultipartBindingBindError(t *testing.T) {
files := []testFile{
{"file", "file1", []byte("hello")},
{"file", "file2", []byte("world")},
}
for _, tt := range []struct {
name string
s interface{}
}{
{"wrong type", &struct {
Files int `form:"file"`
}{}},
{"wrong array size", &struct {
Files [1]*multipart.FileHeader `form:"file"`
}{}},
{"wrong slice type", &struct {
Files []int `form:"file"`
}{}},
} {
req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, tt.s)
assert.Error(t, err)
}
}
type testFile struct {
Fieldname string
Filename string
Content []byte
}
func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request {
var body bytes.Buffer
mw := multipart.NewWriter(&body)
for _, file := range files {
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
assert.NoError(t, err)
n, err := fw.Write(file.Content)
assert.NoError(t, err)
assert.Equal(t, len(file.Content), n)
}
err := mw.Close()
assert.NoError(t, err)
req, err := http.NewRequest("POST", "/", &body)
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
return req
}
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) {
assert.Equal(t, file.Filename, fh.Filename)
// assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8
fl, err := fh.Open()
assert.NoError(t, err)
body, err := ioutil.ReadAll(fl)
assert.NoError(t, err)
assert.Equal(t, string(file.Content), string(body))
err = fl.Close()
assert.NoError(t, err)
}

View File

@ -6,12 +6,11 @@ package binding
import (
"bytes"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"gopkg.in/go-playground/validator.v8"
"gopkg.in/go-playground/validator.v9"
)
type testInterface interface {
@ -200,15 +199,8 @@ 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 {
func notOne(f1 validator.FieldLevel) bool {
if val, ok := f1.Field().Interface().(int); ok {
return val != 1
}
return false

25
binding/xml_test.go Normal file
View File

@ -0,0 +1,25 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestXMLBindingBindBody(t *testing.T) {
var s struct {
Foo string `xml:"foo"`
}
xmlBody := `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`
err := xmlBinding{}.BindBody([]byte(xmlBody), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

21
binding/yaml_test.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestYAMLBindingBindBody(t *testing.T) {
var s struct {
Foo string `yaml:"foo"`
}
err := yamlBinding{}.BindBody([]byte("foo: FOO"), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

View File

@ -48,6 +48,7 @@ type Context struct {
Params Params
handlers HandlersChain
index int8
fullPath string
engine *Engine
@ -59,6 +60,13 @@ type Context struct {
// Accepted defines a list of manually accepted formats for content negotiation.
Accepted []string
// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
queryCache url.Values
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
// or PUT body parameters.
formCache url.Values
}
/************************************/
@ -70,9 +78,12 @@ func (c *Context) reset() {
c.Params = c.Params[0:0]
c.handlers = nil
c.index = -1
c.fullPath = ""
c.Keys = nil
c.Errors = c.Errors[0:0]
c.Accepted = nil
c.queryCache = nil
c.formCache = nil
}
// Copy returns a copy of the current context that can be safely used outside the request's scope.
@ -87,6 +98,9 @@ func (c *Context) Copy() *Context {
for k, v := range c.Keys {
cp.Keys[k] = v
}
paramCopy := make([]Param, len(cp.Params))
copy(paramCopy, cp.Params)
cp.Params = paramCopy
return &cp
}
@ -111,6 +125,15 @@ func (c *Context) Handler() HandlerFunc {
return c.handlers.Last()
}
// FullPath returns a matched route full path. For not found routes
// returns an empty string.
// router.GET("/user/:id", func(c *gin.Context) {
// c.FullPath() == "/user/:id" // true
// })
func (c *Context) FullPath() string {
return c.fullPath
}
/************************************/
/*********** FLOW CONTROL ***********/
/************************************/
@ -368,10 +391,17 @@ func (c *Context) QueryArray(key string) []string {
return values
}
func (c *Context) getQueryCache() {
if c.queryCache == nil {
c.queryCache = c.Request.URL.Query()
}
}
// GetQueryArray returns a slice of strings for a given query key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetQueryArray(key string) ([]string, bool) {
if values, ok := c.Request.URL.Query()[key]; ok && len(values) > 0 {
c.getQueryCache()
if values, ok := c.queryCache[key]; ok && len(values) > 0 {
return values, true
}
return []string{}, false
@ -386,7 +416,8 @@ func (c *Context) QueryMap(key string) map[string]string {
// GetQueryMap returns a map for a given query key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
return c.get(c.Request.URL.Query(), key)
c.getQueryCache()
return c.get(c.queryCache, key)
}
// PostForm returns the specified key from a POST urlencoded form or multipart form
@ -427,23 +458,26 @@ func (c *Context) PostFormArray(key string) []string {
return values
}
// GetPostFormArray returns a slice of strings for a given form key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
func (c *Context) getFormCache() {
if c.formCache == nil {
c.formCache = make(url.Values)
req := c.Request
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
if err != http.ErrNotMultipart {
debugPrint("error on parse multipart form array: %v", err)
}
}
if values := req.PostForm[key]; len(values) > 0 {
return values, true
c.formCache = req.PostForm
}
if req.MultipartForm != nil && req.MultipartForm.File != nil {
if values := req.MultipartForm.Value[key]; len(values) > 0 {
}
// GetPostFormArray returns a slice of strings for a given form key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
c.getFormCache()
if values := c.formCache[key]; len(values) > 0 {
return values, true
}
}
return []string{}, false
}
@ -456,19 +490,8 @@ func (c *Context) PostFormMap(key string) map[string]string {
// GetPostFormMap returns a map for a given form key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
req := c.Request
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
if err != http.ErrNotMultipart {
debugPrint("error on parse multipart form map: %v", err)
}
}
dicts, exist := c.get(req.PostForm, key)
if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil {
dicts, exist = c.get(req.MultipartForm.Value, key)
}
return dicts, exist
c.getFormCache()
return c.get(c.formCache, key)
}
// get is an internal method and returns a map which satisfy conditions.
@ -554,6 +577,11 @@ func (c *Context) BindYAML(obj interface{}) error {
return c.MustBindWith(obj, binding.YAML)
}
// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).
func (c *Context) BindHeader(obj interface{}) error {
return c.MustBindWith(obj, binding.Header)
}
// BindUri binds the passed struct pointer using binding.Uri.
// It will abort the request with HTTP 400 if any error occurs.
func (c *Context) BindUri(obj interface{}) error {
@ -608,6 +636,11 @@ func (c *Context) ShouldBindYAML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.YAML)
}
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
func (c *Context) ShouldBindHeader(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Header)
}
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
func (c *Context) ShouldBindUri(obj interface{}) error {
m := make(map[string][]string)
@ -682,7 +715,7 @@ func (c *Context) ContentType() string {
// handshake is being initiated by the client.
func (c *Context) IsWebsocket() bool {
if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&
strings.ToLower(c.requestHeader("Upgrade")) == "websocket" {
strings.EqualFold(c.requestHeader("Upgrade"), "websocket") {
return true
}
return false
@ -828,6 +861,12 @@ func (c *Context) AsciiJSON(code int, obj interface{}) {
c.Render(code, render.AsciiJSON{Data: obj})
}
// PureJSON serializes the given struct as JSON into the response body.
// PureJSON, unlike JSON, does not replace special html characters with their unicode entities.
func (c *Context) PureJSON(code int, obj interface{}) {
c.Render(code, render.PureJSON{Data: obj})
}
// XML serializes the given struct as XML into the response body.
// It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) {

View File

@ -1,17 +0,0 @@
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build go1.7
package gin
import (
"github.com/gin-gonic/gin/render"
)
// PureJSON serializes the given struct as JSON into the response body.
// PureJSON, unlike JSON, does not replace special html characters with their unicode entities.
func (c *Context) PureJSON(code int, obj interface{}) {
c.Render(code, render.PureJSON{Data: obj})
}

View File

@ -1,27 +0,0 @@
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build go1.7
package gin
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
// Tests that the response is serialized as JSON
// and Content-Type is set to application/json
// and special HTML characters are preserved
func TestContextRenderPureJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.PureJSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}

View File

@ -6,6 +6,7 @@ package gin
import (
"bytes"
"context"
"errors"
"fmt"
"html/template"
@ -13,8 +14,10 @@ import (
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"sync"
"testing"
"time"
@ -22,7 +25,6 @@ import (
"github.com/gin-gonic/gin/binding"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
testdata "github.com/gin-gonic/gin/testdata/protoexample"
)
@ -622,8 +624,7 @@ func TestContextGetCookie(t *testing.T) {
}
func TestContextBodyAllowedForStatus(t *testing.T) {
// todo(thinkerou): go1.6 not support StatusProcessing
assert.False(t, false, bodyAllowedForStatus(102))
assert.False(t, false, bodyAllowedForStatus(http.StatusProcessing))
assert.False(t, false, bodyAllowedForStatus(http.StatusNoContent))
assert.False(t, false, bodyAllowedForStatus(http.StatusNotModified))
assert.True(t, true, bodyAllowedForStatus(http.StatusInternalServerError))
@ -661,7 +662,7 @@ func TestContextRenderJSON(t *testing.T) {
c.JSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -675,7 +676,7 @@ func TestContextRenderJSONP(t *testing.T) {
c.JSONP(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "x({\"foo\":\"bar\"})", w.Body.String())
assert.Equal(t, "x({\"foo\":\"bar\"});", w.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -689,7 +690,7 @@ func TestContextRenderJSONPWithoutCallback(t *testing.T) {
c.JSONP(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -715,7 +716,7 @@ func TestContextRenderAPIJSON(t *testing.T) {
c.JSON(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type"))
}
@ -794,6 +795,18 @@ func TestContextRenderNoContentAsciiJSON(t *testing.T) {
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
}
// Tests that the response is serialized as JSON
// and Content-Type is set to application/json
// and special HTML characters are preserved
func TestContextRenderPureJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.PureJSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
// Tests that the response executes the templates
// and responds with Content-Type set to text/html
func TestContextRenderHTML(t *testing.T) {
@ -1092,9 +1105,7 @@ func TestContextRenderRedirectAll(t *testing.T) {
assert.Panics(t, func() { c.Redirect(299, "/resource") })
assert.Panics(t, func() { c.Redirect(309, "/resource") })
assert.NotPanics(t, func() { c.Redirect(http.StatusMultipleChoices, "/resource") })
// todo(thinkerou): go1.6 not support StatusPermanentRedirect(308)
// when we upgrade go version we can use http.StatusPermanentRedirect
assert.NotPanics(t, func() { c.Redirect(308, "/resource") })
assert.NotPanics(t, func() { c.Redirect(http.StatusPermanentRedirect, "/resource") })
}
func TestContextNegotiationWithJSON(t *testing.T) {
@ -1108,7 +1119,7 @@ func TestContextNegotiationWithJSON(t *testing.T) {
})
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -1272,7 +1283,7 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
_, err := buf.ReadFrom(w.Body)
assert.NoError(t, err)
jsonStringBody := buf.String()
assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody)
assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}\n"), jsonStringBody)
}
func TestContextError(t *testing.T) {
@ -1425,6 +1436,28 @@ func TestContextBindWithXML(t *testing.T) {
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBindHeader(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("rate", "8000")
c.Request.Header.Add("domain", "music")
c.Request.Header.Add("limit", "1000")
var testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
Limit int `header:"limit"`
}
assert.NoError(t, c.BindHeader(&testHeader))
assert.Equal(t, 8000, testHeader.Rate)
assert.Equal(t, "music", testHeader.Domain)
assert.Equal(t, 1000, testHeader.Limit)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBindWithQuery(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -1532,6 +1565,28 @@ func TestContextShouldBindWithXML(t *testing.T) {
assert.Equal(t, 0, w.Body.Len())
}
func TestContextShouldBindHeader(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("rate", "8000")
c.Request.Header.Add("domain", "music")
c.Request.Header.Add("limit", "1000")
var testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
Limit int `header:"limit"`
}
assert.NoError(t, c.ShouldBindHeader(&testHeader))
assert.Equal(t, 8000, testHeader.Rate)
assert.Equal(t, "music", testHeader.Domain)
assert.Equal(t, 1000, testHeader.Limit)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextShouldBindWithQuery(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -1812,3 +1867,24 @@ func TestContextResetInHandler(t *testing.T) {
c.Next()
})
}
func TestRaceParamsContextCopy(t *testing.T) {
DefaultWriter = os.Stdout
router := Default()
nameGroup := router.Group("/:name")
var wg sync.WaitGroup
wg.Add(2)
{
nameGroup.GET("/api", func(c *Context) {
go func(c *Context, param string) {
defer wg.Done()
// First assert must be executed after the second request
time.Sleep(50 * time.Millisecond)
assert.Equal(t, c.Param("name"), param)
}(c.Copy(), c.Param("name"))
})
}
performRequest(router, "GET", "/name1/api")
performRequest(router, "GET", "/name2/api")
wg.Wait()
}

View File

@ -5,16 +5,14 @@
package gin
import (
"bytes"
"fmt"
"html/template"
"os"
"runtime"
"strconv"
"strings"
)
const ginSupportMinGoVer = 6
const ginSupportMinGoVer = 10
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -39,7 +37,7 @@ func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
func debugPrintLoadTemplate(tmpl *template.Template) {
if IsDebugging() {
var buf bytes.Buffer
var buf strings.Builder
for _, tmpl := range tmpl.Templates() {
buf.WriteString("\t- ")
buf.WriteString(tmpl.Name())
@ -54,7 +52,7 @@ func debugPrint(format string, values ...interface{}) {
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(os.Stderr, "[GIN-debug] "+format, values...)
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
}
}
@ -69,7 +67,7 @@ func getMinVer(v string) (uint64, error) {
func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
debugPrint(`[WARNING] Now Gin requires Go 1.10 or later and Go 1.11 will be required soon.
`)
}
@ -98,6 +96,8 @@ at initialization. ie. before any route is registered or the router is listening
func debugPrintError(err error) {
if err != nil {
debugPrint("[ERROR] %v\n", err)
if IsDebugging() {
fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
}
}
}

View File

@ -91,7 +91,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
})
m, e := getMinVer(runtime.Version())
if e == nil && m <= ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.10 or later and Go 1.11 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
@ -111,15 +111,15 @@ func captureOutput(t *testing.T, f func()) string {
if err != nil {
panic(err)
}
stdout := os.Stdout
stderr := os.Stderr
defaultWriter := DefaultWriter
defaultErrorWriter := DefaultErrorWriter
defer func() {
os.Stdout = stdout
os.Stderr = stderr
DefaultWriter = defaultWriter
DefaultErrorWriter = defaultErrorWriter
log.SetOutput(os.Stderr)
}()
os.Stdout = writer
os.Stderr = writer
DefaultWriter = writer
DefaultErrorWriter = writer
log.SetOutput(writer)
out := make(chan string)
wg := new(sync.WaitGroup)

View File

@ -5,9 +5,9 @@
package gin
import (
"bytes"
"fmt"
"reflect"
"strings"
"github.com/gin-gonic/gin/internal/json"
)
@ -158,7 +158,7 @@ func (a errorMsgs) String() string {
if len(a) == 0 {
return ""
}
var buffer bytes.Buffer
var buffer strings.Builder
for i, msg := range a {
fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err)
if msg.Meta != nil {

25
gin.go
View File

@ -252,6 +252,7 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
root := engine.trees.get(method)
if root == nil {
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
@ -337,6 +338,15 @@ func (engine *Engine) RunFd(fd int) (err error) {
return
}
defer listener.Close()
err = engine.RunListener(listener)
return
}
// RunListener attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified net.Listener
func (engine *Engine) RunListener(listener net.Listener) (err error) {
debugPrint("Listening and serving HTTP on listener what's bind with address@%s", listener.Addr())
defer func() { debugPrintError(err) }()
err = http.Serve(listener, engine)
return
}
@ -372,6 +382,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}
rPath = cleanPath(rPath)
// Find root of the tree for the given HTTP method
t := engine.trees
@ -381,16 +392,17 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
}
root := t[i].root
// Find route in tree
handlers, params, tsr := root.getValue(rPath, c.Params, unescape)
if handlers != nil {
c.handlers = handlers
c.Params = params
value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && rPath != "/" {
if tsr && engine.RedirectTrailingSlash {
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
@ -406,7 +418,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
if tree.method == httpMethod {
continue
}
if handlers, _, _ := tree.root.getValue(rPath, nil, unescape); handlers != nil {
if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
@ -434,7 +446,6 @@ func serveError(c *Context, code int, defaultMessage []byte) {
return
}
c.writermem.WriteHeaderNow()
return
}
func redirectTrailingSlash(c *Context) {

View File

@ -8,6 +8,7 @@ import (
"bufio"
"crypto/tls"
"fmt"
"html/template"
"io/ioutil"
"net"
"net/http"
@ -69,6 +70,42 @@ func TestRunTLS(t *testing.T) {
testRequest(t, "https://localhost:8443/example")
}
func TestPusher(t *testing.T) {
var html = template.Must(template.New("https").Parse(`
<html>
<head>
<title>Https Test</title>
<script src="/assets/app.js"></script>
</head>
<body>
<h1 style="color:red;">Welcome, Ginner!</h1>
</body>
</html>
`))
router := New()
router.Static("./assets", "./assets")
router.SetHTMLTemplate(html)
go func() {
router.GET("/pusher", func(c *Context) {
if pusher := c.Writer.Pusher(); pusher != nil {
pusher.Push("/assets/app.js", nil)
}
c.String(http.StatusOK, "it worked")
})
assert.NoError(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
assert.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8449/pusher")
}
func TestRunEmptyWithEnv(t *testing.T) {
os.Setenv("PORT", "3123")
router := New()
@ -170,6 +207,42 @@ func TestBadFileDescriptor(t *testing.T) {
assert.Error(t, router.RunFd(0))
}
func TestListener(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunListener(listener))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String())
assert.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
for scanner.Scan() {
response += scanner.Text()
}
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
func TestBadListener(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:10086")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
listener.Close()
assert.Error(t, router.RunListener(listener))
}
func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
router := New()
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })

20
go.mod
View File

@ -3,16 +3,16 @@ module github.com/gin-gonic/gin
go 1.12
require (
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3
github.com/golang/protobuf v1.3.0
github.com/json-iterator/go v1.1.5
github.com/mattn/go-isatty v0.0.6
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/stretchr/testify v1.3.0
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95
github.com/gin-contrib/sse v0.1.0
github.com/go-playground/locales v0.12.1 // indirect
github.com/go-playground/universal-translator v0.16.0 // indirect
github.com/golang/protobuf v1.3.2
github.com/json-iterator/go v1.1.7
github.com/leodido/go-urn v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.9
github.com/stretchr/testify v1.4.0
github.com/ugorji/go/codec v1.1.7
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2
gopkg.in/go-playground/validator.v9 v9.29.1
gopkg.in/yaml.v2 v2.2.2
)

59
go.sum
View File

@ -1,39 +1,42 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA=
github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs=
github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 h1:BasDe+IErOQKrMVXab7UayvSlIpiyGwRvuX3EKYY7UA=
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU=
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -22,18 +22,19 @@ const (
forceColor
)
var (
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109})
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
reset = string([]byte{27, 91, 48, 109})
consoleColorMode = autoColor
const (
green = "\033[97;42m"
white = "\033[90;47m"
yellow = "\033[90;43m"
red = "\033[97;41m"
blue = "\033[97;44m"
magenta = "\033[97;45m"
cyan = "\033[97;46m"
reset = "\033[0m"
)
var consoleColorMode = autoColor
// LoggerConfig defines the config for Logger middleware.
type LoggerConfig struct {
// Optional. Default value is gin.defaultLogFormatter

View File

@ -291,14 +291,14 @@ func TestColorForMethod(t *testing.T) {
return p.MethodColor()
}
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), colorForMethod("HEAD"), "head should be magenta")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForMethod("OPTIONS"), "options should be white")
assert.Equal(t, string([]byte{27, 91, 48, 109}), colorForMethod("TRACE"), "trace is not defined and should be the reset color")
assert.Equal(t, blue, colorForMethod("GET"), "get should be blue")
assert.Equal(t, cyan, colorForMethod("POST"), "post should be cyan")
assert.Equal(t, yellow, colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, red, colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta")
assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white")
assert.Equal(t, reset, colorForMethod("TRACE"), "trace is not defined and should be the reset color")
}
func TestColorForStatus(t *testing.T) {
@ -309,10 +309,10 @@ func TestColorForStatus(t *testing.T) {
return p.StatusCodeColor()
}
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red")
assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green")
assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white")
assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow")
assert.Equal(t, red, colorForStatus(2), "other things should be red")
}
func TestResetColor(t *testing.T) {
@ -369,15 +369,15 @@ func TestErrorLogger(t *testing.T) {
w := performRequest(router, "GET", "/error")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
assert.Equal(t, "{\"error\":\"this is an error\"}\n", w.Body.String())
w = performRequest(router, "GET", "/abort")
assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
assert.Equal(t, "{\"error\":\"no authorized\"}\n", w.Body.String())
w = performRequest(router, "GET", "/print")
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
assert.Equal(t, "hola!{\"error\":\"this is an error\"}\n", w.Body.String())
}
func TestLoggerWithWriterSkippingPaths(t *testing.T) {

View File

@ -246,5 +246,5 @@ func TestMiddlewareWrite(t *testing.T) {
w := performRequest(router, "GET", "/")
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
}

View File

@ -71,12 +71,18 @@ func DisableBindValidation() {
binding.Validator = nil
}
// EnableJsonDecoderUseNumber sets true for binding.EnableDecoderUseNumberto to
// EnableJsonDecoderUseNumber sets true for binding.EnableDecoderUseNumber to
// call the UseNumber method on the JSON Decoder instance.
func EnableJsonDecoderUseNumber() {
binding.EnableDecoderUseNumber = true
}
// EnableJsonDisallowUnknownFields sets true for binding.EnableDecoderDisallowUnknownFields to
// call the DisallowUnknownFields method on the JSON Decoder instance.
func EnableJsonDecoderDisallowUnknownFields() {
binding.EnableDecoderDisallowUnknownFields = true
}
// Mode returns currently gin mode.
func Mode() string {
return modeName

View File

@ -40,8 +40,22 @@ func TestSetMode(t *testing.T) {
assert.Panics(t, func() { SetMode("unknown") })
}
func TestDisableBindValidation(t *testing.T) {
v := binding.Validator
assert.NotNil(t, binding.Validator)
DisableBindValidation()
assert.Nil(t, binding.Validator)
binding.Validator = v
}
func TestEnableJsonDecoderUseNumber(t *testing.T) {
assert.False(t, binding.EnableDecoderUseNumber)
EnableJsonDecoderUseNumber()
assert.True(t, binding.EnableDecoderUseNumber)
}
func TestEnableJsonDecoderDisallowUnknownFields(t *testing.T) {
assert.False(t, binding.EnableDecoderDisallowUnknownFields)
EnableJsonDecoderDisallowUnknownFields()
assert.True(t, binding.EnableDecoderDisallowUnknownFields)
}

View File

@ -2,8 +2,6 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build go1.7
package gin
import (

View File

@ -43,6 +43,11 @@ type AsciiJSON struct {
// SecureJSONPrefix is a string which represents SecureJSON prefix.
type SecureJSONPrefix string
// PureJSON contains the given interface object.
type PureJSON struct {
Data interface{}
}
var jsonContentType = []string{"application/json; charset=utf-8"}
var jsonpContentType = []string{"application/javascript; charset=utf-8"}
var jsonAsciiContentType = []string{"application/json"}
@ -63,11 +68,8 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
// WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, jsonContentType)
jsonBytes, err := json.Marshal(obj)
if err != nil {
return err
}
_, err = w.Write(jsonBytes)
encoder := json.NewEncoder(w)
err := encoder.Encode(&obj)
return err
}
@ -136,7 +138,7 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
if err != nil {
return err
}
_, err = w.Write([]byte(")"))
_, err = w.Write([]byte(");"))
if err != nil {
return err
}
@ -174,3 +176,16 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonAsciiContentType)
}
// Render (PureJSON) writes custom ContentType and encodes the given interface object.
func (r PureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
encoder := json.NewEncoder(w)
encoder.SetEscapeHTML(false)
return encoder.Encode(r.Data)
}
// WriteContentType (PureJSON) writes custom ContentType.
func (r PureJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType)
}

View File

@ -1,31 +0,0 @@
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build go1.7
package render
import (
"net/http"
"github.com/gin-gonic/gin/internal/json"
)
// PureJSON contains the given interface object.
type PureJSON struct {
Data interface{}
}
// Render (PureJSON) writes custom ContentType and encodes the given interface object.
func (r PureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
encoder := json.NewEncoder(w)
encoder.SetEscapeHTML(false)
return encoder.Encode(r.Data)
}
// WriteContentType (PureJSON) writes custom ContentType.
func (r PureJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType)
}

View File

@ -21,7 +21,9 @@ type Reader struct {
// Render (Reader) writes data with custom ContentType and headers.
func (r Reader) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w)
if r.ContentLength >= 0 {
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
}
r.writeHeaders(w, r.Headers)
_, err = io.Copy(w, r.Reader)
return

View File

@ -18,9 +18,7 @@ type Redirect struct {
// Render (Redirect) redirects the http request to new location and writes redirect response.
func (r Redirect) Render(w http.ResponseWriter) error {
// todo(thinkerou): go1.6 not support StatusPermanentRedirect(308)
// when we upgrade go version we can use http.StatusPermanentRedirect
if (r.Code < 300 || r.Code > 308) && r.Code != 201 {
if (r.Code < http.StatusMultipleChoices || r.Code > http.StatusPermanentRedirect) && r.Code != http.StatusCreated {
panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code))
}
http.Redirect(w, r.Request, r.Location, r.Code)

View File

@ -1,26 +0,0 @@
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build go1.7
package render
import (
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRenderPureJSON(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
"foo": "bar",
"html": "<b>",
}
err := (PureJSON{data}).Render(w)
assert.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}

View File

@ -45,7 +45,7 @@ func TestRenderMsgPack(t *testing.T) {
err = codec.NewEncoder(buf, h).Encode(data)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), string(buf.Bytes()))
assert.Equal(t, w.Body.String(), buf.String())
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -62,7 +62,7 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w)
assert.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -146,7 +146,7 @@ func TestRenderJsonpJSON(t *testing.T) {
err1 := (JsonpJSON{"x", data}).Render(w1)
assert.NoError(t, err1)
assert.Equal(t, "x({\"foo\":\"bar\"})", w1.Body.String())
assert.Equal(t, "x({\"foo\":\"bar\"});", w1.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder()
@ -158,7 +158,7 @@ func TestRenderJsonpJSON(t *testing.T) {
err2 := (JsonpJSON{"x", datas}).Render(w2)
assert.NoError(t, err2)
assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}])", w2.Body.String())
assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}]);", w2.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type"))
}
@ -215,6 +215,18 @@ func TestRenderAsciiJSONFail(t *testing.T) {
assert.Error(t, (AsciiJSON{data}).Render(w))
}
func TestRenderPureJSON(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
"foo": "bar",
"html": "<b>",
}
err := (PureJSON{data}).Render(w)
assert.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
type xmlmap map[string]interface{}
// Allows type H to be used with xml.Marshal
@ -335,7 +347,17 @@ func TestRenderRedirect(t *testing.T) {
}
w = httptest.NewRecorder()
assert.Panics(t, func() { assert.NoError(t, data2.Render(w)) })
assert.PanicsWithValue(t, "Cannot redirect with status code 200", func() { data2.Render(w) })
data3 := Redirect{
Code: http.StatusCreated,
Request: req,
Location: "/new/location",
}
w = httptest.NewRecorder()
err = data3.Render(w)
assert.NoError(t, err)
// only improve coverage
data2.WriteContentType(w)
@ -486,3 +508,26 @@ func TestRenderReader(t *testing.T) {
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
}
func TestRenderReaderNoContentLength(t *testing.T) {
w := httptest.NewRecorder()
body := "#!PNG some raw data"
headers := make(map[string]string)
headers["Content-Disposition"] = `attachment; filename="filename.png"`
headers["x-request-id"] = "requestId"
err := (Reader{
ContentLength: -1,
ContentType: "image/png",
Reader: strings.NewReader(body),
Headers: headers,
}).Render(w)
assert.NoError(t, err)
assert.Equal(t, body, w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.NotContains(t, "Content-Length", w.Header())
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
}

View File

@ -16,7 +16,8 @@ const (
defaultStatus = http.StatusOK
)
type responseWriterBase interface {
// ResponseWriter ...
type ResponseWriter interface {
http.ResponseWriter
http.Hijacker
http.Flusher
@ -37,6 +38,9 @@ type responseWriterBase interface {
// Forces to write the http header (status code + headers).
WriteHeaderNow()
// get the http.Pusher for server push
Pusher() http.Pusher
}
type responseWriter struct {
@ -113,3 +117,10 @@ func (w *responseWriter) Flush() {
w.WriteHeaderNow()
w.ResponseWriter.(http.Flusher).Flush()
}
func (w *responseWriter) Pusher() (pusher http.Pusher) {
if pusher, ok := w.ResponseWriter.(http.Pusher); ok {
return pusher
}
return nil
}

View File

@ -1,12 +0,0 @@
// +build !go1.8
// Copyright 2018 Gin Core Team. 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
// ResponseWriter ...
type ResponseWriter interface {
responseWriterBase
}

View File

@ -1,25 +0,0 @@
// +build go1.8
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"net/http"
)
// ResponseWriter ...
type ResponseWriter interface {
responseWriterBase
// get the http.Pusher for server push
Pusher() http.Pusher
}
func (w *responseWriter) Pusher() (pusher http.Pusher) {
if pusher, ok := w.ResponseWriter.(http.Pusher); ok {
return pusher
}
return nil
}

View File

@ -29,38 +29,38 @@ func init() {
}
func TestResponseWriterReset(t *testing.T) {
testWritter := httptest.NewRecorder()
testWriter := httptest.NewRecorder()
writer := &responseWriter{}
var w ResponseWriter = writer
writer.reset(testWritter)
writer.reset(testWriter)
assert.Equal(t, -1, writer.size)
assert.Equal(t, http.StatusOK, writer.status)
assert.Equal(t, testWritter, writer.ResponseWriter)
assert.Equal(t, testWriter, writer.ResponseWriter)
assert.Equal(t, -1, w.Size())
assert.Equal(t, http.StatusOK, w.Status())
assert.False(t, w.Written())
}
func TestResponseWriterWriteHeader(t *testing.T) {
testWritter := httptest.NewRecorder()
testWriter := httptest.NewRecorder()
writer := &responseWriter{}
writer.reset(testWritter)
writer.reset(testWriter)
w := ResponseWriter(writer)
w.WriteHeader(http.StatusMultipleChoices)
assert.False(t, w.Written())
assert.Equal(t, http.StatusMultipleChoices, w.Status())
assert.NotEqual(t, http.StatusMultipleChoices, testWritter.Code)
assert.NotEqual(t, http.StatusMultipleChoices, testWriter.Code)
w.WriteHeader(-1)
assert.Equal(t, http.StatusMultipleChoices, w.Status())
}
func TestResponseWriterWriteHeadersNow(t *testing.T) {
testWritter := httptest.NewRecorder()
testWriter := httptest.NewRecorder()
writer := &responseWriter{}
writer.reset(testWritter)
writer.reset(testWriter)
w := ResponseWriter(writer)
w.WriteHeader(http.StatusMultipleChoices)
@ -68,7 +68,7 @@ func TestResponseWriterWriteHeadersNow(t *testing.T) {
assert.True(t, w.Written())
assert.Equal(t, 0, w.Size())
assert.Equal(t, http.StatusMultipleChoices, testWritter.Code)
assert.Equal(t, http.StatusMultipleChoices, testWriter.Code)
writer.size = 10
w.WriteHeaderNow()
@ -76,30 +76,30 @@ func TestResponseWriterWriteHeadersNow(t *testing.T) {
}
func TestResponseWriterWrite(t *testing.T) {
testWritter := httptest.NewRecorder()
testWriter := httptest.NewRecorder()
writer := &responseWriter{}
writer.reset(testWritter)
writer.reset(testWriter)
w := ResponseWriter(writer)
n, err := w.Write([]byte("hola"))
assert.Equal(t, 4, n)
assert.Equal(t, 4, w.Size())
assert.Equal(t, http.StatusOK, w.Status())
assert.Equal(t, http.StatusOK, testWritter.Code)
assert.Equal(t, "hola", testWritter.Body.String())
assert.Equal(t, http.StatusOK, testWriter.Code)
assert.Equal(t, "hola", testWriter.Body.String())
assert.NoError(t, err)
n, err = w.Write([]byte(" adios"))
assert.Equal(t, 6, n)
assert.Equal(t, 10, w.Size())
assert.Equal(t, "hola adios", testWritter.Body.String())
assert.Equal(t, "hola adios", testWriter.Body.String())
assert.NoError(t, err)
}
func TestResponseWriterHijack(t *testing.T) {
testWritter := httptest.NewRecorder()
testWriter := httptest.NewRecorder()
writer := &responseWriter{}
writer.reset(testWritter)
writer.reset(testWriter)
w := ResponseWriter(writer)
assert.Panics(t, func() {

View File

@ -22,7 +22,7 @@ type header struct {
}
func performRequest(r http.Handler, method, path string, headers ...header) *httptest.ResponseRecorder {
req, _ := http.NewRequest(method, path, nil)
req := httptest.NewRequest(method, path, nil)
for _, h := range headers {
req.Header.Add(h.Key, h.Value)
}
@ -257,6 +257,39 @@ func TestRouteParamsByName(t *testing.T) {
assert.Equal(t, "/is/super/great", wild)
}
// TestContextParamsGet tests that a parameter can be parsed from the URL even with extra slashes.
func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
name := ""
lastName := ""
wild := ""
router := New()
router.GET("/test/:name/:last_name/*wild", func(c *Context) {
name = c.Params.ByName("name")
lastName = c.Params.ByName("last_name")
var ok bool
wild, ok = c.Params.Get("wild")
assert.True(t, ok)
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf"))
assert.Empty(t, c.Params.ByName("wtf"))
wtf, ok := c.Params.Get("wtf")
assert.Empty(t, wtf)
assert.False(t, ok)
})
w := performRequest(router, "GET", "//test//john//smith//is//super//great")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "john", name)
assert.Equal(t, "smith", lastName)
assert.Equal(t, "/is/super/great", wild)
}
// TestHandleStaticFile - ensure the static file handles properly
func TestRouteStaticFile(t *testing.T) {
// SETUP file
@ -388,12 +421,11 @@ func TestRouterNotFound(t *testing.T) {
}{
{"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/
{"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/
{"", http.StatusMovedPermanently, "/"}, // TSR +/
{"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case
{"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case
{"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/
{"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/
{"/../path", http.StatusMovedPermanently, "/path"}, // CleanPath
{"/../path", http.StatusOK, ""}, // CleanPath
{"/nope", http.StatusNotFound, ""}, // NotFound
}
for _, tr := range testRoutes {
@ -522,3 +554,43 @@ func TestRouteServeErrorWithWriteHeader(t *testing.T) {
assert.Equal(t, 421, w.Code)
assert.Equal(t, 0, w.Body.Len())
}
func TestRouteContextHoldsFullPath(t *testing.T) {
router := New()
// Test routes
routes := []string{
"/simple",
"/project/:name",
"/",
"/news/home",
"/news",
"/simple-two/one",
"/simple-two/one-two",
"/project/:name/build/*params",
"/project/:name/bui",
}
for _, route := range routes {
actualRoute := route
router.GET(route, func(c *Context) {
// For each defined route context should contain its full path
assert.Equal(t, actualRoute, c.FullPath())
c.AbortWithStatus(http.StatusOK)
})
}
for _, route := range routes {
w := performRequest(router, "GET", route)
assert.Equal(t, http.StatusOK, w.Code)
}
// Test not found
router.Use(func(c *Context) {
// For not found routes full path is empty
assert.Equal(t, "", c.FullPath())
})
w := performRequest(router, "GET", "/not-found")
assert.Equal(t, http.StatusNotFound, w.Code)
}

88
tree.go
View File

@ -65,11 +65,10 @@ func min(a, b int) int {
func countParams(path string) uint8 {
var n uint
for i := 0; i < len(path); i++ {
if path[i] != ':' && path[i] != '*' {
continue
}
if path[i] == ':' || path[i] == '*' {
n++
}
}
if n >= 255 {
return 255
}
@ -94,6 +93,7 @@ type node struct {
nType nodeType
maxParams uint8
wildChild bool
fullPath string
}
// increments priority of the given child and reorders if necessary.
@ -127,6 +127,8 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
n.priority++
numParams := countParams(path)
parentFullPathIndex := 0
// non-empty tree
if len(n.path) > 0 || len(n.children) > 0 {
walk:
@ -154,6 +156,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
children: n.children,
handlers: n.handlers,
priority: n.priority - 1,
fullPath: n.fullPath,
}
// Update maxParams (max of all children)
@ -169,6 +172,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
n.path = path[:i]
n.handlers = nil
n.wildChild = false
n.fullPath = fullPath[:parentFullPathIndex+i]
}
// Make new node a child of this node
@ -176,6 +180,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
path = path[i:]
if n.wildChild {
parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++
@ -209,6 +214,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
// slash after param
if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++
continue walk
@ -217,6 +223,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
// Check if a child with the next path byte exists
for i := 0; i < len(n.indices); i++ {
if c == n.indices[i] {
parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
@ -229,6 +236,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
n.indices += string([]byte{c})
child := &node{
maxParams: numParams,
fullPath: fullPath,
}
n.children = append(n.children, child)
n.incrementChildPrio(len(n.indices) - 1)
@ -296,6 +304,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
child := &node{
nType: param,
maxParams: numParams,
fullPath: fullPath,
}
n.children = []*node{child}
n.wildChild = true
@ -312,6 +321,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
child := &node{
maxParams: numParams,
priority: 1,
fullPath: fullPath,
}
n.children = []*node{child}
n = child
@ -339,6 +349,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
wildChild: true,
nType: catchAll,
maxParams: 1,
fullPath: fullPath,
}
n.children = []*node{child}
n.indices = string(path[i])
@ -352,6 +363,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
maxParams: 1,
handlers: handlers,
priority: 1,
fullPath: fullPath,
}
n.children = []*node{child}
@ -362,6 +374,15 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
// insert remaining path part and handle to the leaf
n.path = path[offset:]
n.handlers = handlers
n.fullPath = fullPath
}
// nodeValue holds return values of (*Node).getValue method
type nodeValue struct {
handlers HandlersChain
params Params
tsr bool
fullPath string
}
// getValue returns the handle registered with the given path (key). The values of
@ -369,8 +390,8 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
// made if a handle exists with an extra (without the) trailing slash for the
// given path.
func (n *node) getValue(path string, po Params, unescape bool) (handlers HandlersChain, p Params, tsr bool) {
p = po
func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
value.params = po
walk: // Outer loop for walking the tree
for {
if len(path) > len(n.path) {
@ -391,7 +412,7 @@ walk: // Outer loop for walking the tree
// Nothing found.
// We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path.
tsr = path == "/" && n.handlers != nil
value.tsr = path == "/" && n.handlers != nil
return
}
@ -406,20 +427,20 @@ walk: // Outer loop for walking the tree
}
// save param value
if cap(p) < int(n.maxParams) {
p = make(Params, 0, n.maxParams)
if cap(value.params) < int(n.maxParams) {
value.params = make(Params, 0, n.maxParams)
}
i := len(p)
p = p[:i+1] // expand slice within preallocated capacity
p[i].Key = n.path[1:]
i := len(value.params)
value.params = value.params[:i+1] // expand slice within preallocated capacity
value.params[i].Key = n.path[1:]
val := path[:end]
if unescape {
var err error
if p[i].Value, err = url.QueryUnescape(val); err != nil {
p[i].Value = val // fallback, in case of error
if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
value.params[i].Value = val // fallback, in case of error
}
} else {
p[i].Value = val
value.params[i].Value = val
}
// we need to go deeper!
@ -431,40 +452,42 @@ walk: // Outer loop for walking the tree
}
// ... but we can't
tsr = len(path) == end+1
value.tsr = len(path) == end+1
return
}
if handlers = n.handlers; handlers != nil {
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return
}
if len(n.children) == 1 {
// No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation
n = n.children[0]
tsr = n.path == "/" && n.handlers != nil
value.tsr = n.path == "/" && n.handlers != nil
}
return
case catchAll:
// save param value
if cap(p) < int(n.maxParams) {
p = make(Params, 0, n.maxParams)
if cap(value.params) < int(n.maxParams) {
value.params = make(Params, 0, n.maxParams)
}
i := len(p)
p = p[:i+1] // expand slice within preallocated capacity
p[i].Key = n.path[2:]
i := len(value.params)
value.params = value.params[:i+1] // expand slice within preallocated capacity
value.params[i].Key = n.path[2:]
if unescape {
var err error
if p[i].Value, err = url.QueryUnescape(path); err != nil {
p[i].Value = path // fallback, in case of error
if value.params[i].Value, err = url.QueryUnescape(path); err != nil {
value.params[i].Value = path // fallback, in case of error
}
} else {
p[i].Value = path
value.params[i].Value = path
}
handlers = n.handlers
value.handlers = n.handlers
value.fullPath = n.fullPath
return
default:
@ -474,12 +497,13 @@ walk: // Outer loop for walking the tree
} else if path == n.path {
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if handlers = n.handlers; handlers != nil {
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return
}
if path == "/" && n.wildChild && n.nType != root {
tsr = true
value.tsr = true
return
}
@ -488,7 +512,7 @@ walk: // Outer loop for walking the tree
for i := 0; i < len(n.indices); i++ {
if n.indices[i] == '/' {
n = n.children[i]
tsr = (len(n.path) == 1 && n.handlers != nil) ||
value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil)
return
}
@ -499,7 +523,7 @@ walk: // Outer loop for walking the tree
// Nothing found. We can recommend to redirect to the same URL with an
// extra trailing slash if a leaf exists for that path
tsr = (path == "/") ||
value.tsr = (path == "/") ||
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
path == n.path[:len(n.path)-1] && n.handlers != nil)
return
@ -514,7 +538,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory
// Outer loop for walking the tree
for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) {
for len(path) >= len(n.path) && strings.EqualFold(path[:len(n.path)], n.path) {
path = path[len(n.path):]
ciPath = append(ciPath, n.path...)
@ -618,7 +642,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
return ciPath, true
}
if len(path)+1 == len(n.path) && n.path[len(path)] == '/' &&
strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) &&
strings.EqualFold(path, n.path[:len(path)]) &&
n.handlers != nil {
return append(ciPath, n.path...), true
}

View File

@ -35,22 +35,22 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
}
for _, request := range requests {
handler, ps, _ := tree.getValue(request.path, nil, unescape)
value := tree.getValue(request.path, nil, unescape)
if handler == nil {
if value.handlers == nil {
if !request.nilHandler {
t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
}
} else if request.nilHandler {
t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
} else {
handler[0](nil)
value.handlers[0](nil)
if fakeHandlerValue != request.route {
t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route)
}
}
if !reflect.DeepEqual(ps, request.ps) {
if !reflect.DeepEqual(value.params, request.ps) {
t.Errorf("Params mismatch for route '%s'", request.path)
}
}
@ -454,10 +454,10 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/doc/",
}
for _, route := range tsrRoutes {
handler, _, tsr := tree.getValue(route, nil, false)
if handler != nil {
value := tree.getValue(route, nil, false)
if value.handlers != nil {
t.Fatalf("non-nil handler for TSR route '%s", route)
} else if !tsr {
} else if !value.tsr {
t.Errorf("expected TSR recommendation for route '%s'", route)
}
}
@ -471,10 +471,10 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/api/world/abc",
}
for _, route := range noTsrRoutes {
handler, _, tsr := tree.getValue(route, nil, false)
if handler != nil {
value := tree.getValue(route, nil, false)
if value.handlers != nil {
t.Fatalf("non-nil handler for No-TSR route '%s", route)
} else if tsr {
} else if value.tsr {
t.Errorf("expected no TSR recommendation for route '%s'", route)
}
}
@ -490,10 +490,10 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
t.Fatalf("panic inserting test route: %v", recv)
}
handler, _, tsr := tree.getValue("/", nil, false)
if handler != nil {
value := tree.getValue("/", nil, false)
if value.handlers != nil {
t.Fatalf("non-nil handler")
} else if tsr {
} else if value.tsr {
t.Errorf("expected no TSR recommendation")
}
}

View File

@ -146,6 +146,6 @@ func resolveAddress(addr []string) string {
case 1:
return addr[0]
default:
panic("too much parameters")
panic("too many parameters")
}
}

140
vendor/vendor.json vendored
View File

@ -1,5 +1,5 @@
{
"comment": "v1.3.0",
"comment": "v1.4.0",
"ignore": "test",
"package": [
{
@ -11,34 +11,78 @@
"versionExact": "v1.1.1"
},
{
"checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=",
"checksumSHA1": "qlEzrgKgIkh7y0ePm9BNo1cNdXo=",
"path": "github.com/gin-contrib/sse",
"revision": "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae",
"revisionTime": "2017-01-09T09:34:21Z"
"revision": "54d8467d122d380a14768b6b4e5cd7ca4755938f",
"revisionTime": "2019-06-02T15:02:53Z",
"version": "v0.1",
"versionExact": "v0.1.0"
},
{
"checksumSHA1": "mE9XW26JSpe4meBObM6J/Oeq0eg=",
"checksumSHA1": "b4DmyMT9bicTRVJw1hJXHLhIH+0=",
"path": "github.com/go-playground/locales",
"revision": "f63010822830b6fe52288ee52d5a1151088ce039",
"revisionTime": "2018-03-23T16:04:04Z",
"version": "v0.12",
"versionExact": "v0.12.1"
},
{
"checksumSHA1": "JgF260rC9YpWyY5WEljjimWLUXs=",
"path": "github.com/go-playground/locales/currency",
"revision": "630ebbb602847eba93e75ae38bbc7bb7abcf1ff3",
"revisionTime": "2019-04-30T15:33:29Z"
},
{
"checksumSHA1": "9pKcUHBaVS+360X6h4IowhmOPjk=",
"path": "github.com/go-playground/universal-translator",
"revision": "b32fa301c9fe55953584134cb6853a13c87ec0a1",
"revisionTime": "2017-02-09T16:11:52Z",
"version": "v0.16",
"versionExact": "v0.16.0"
},
{
"checksumSHA1": "Y2MOwzNZfl4NRNDbLCZa6sgx7O0=",
"path": "github.com/golang/protobuf/proto",
"revision": "aa810b61a9c79d51363740d207bb46cf8e620ed5",
"revisionTime": "2018-08-14T21:14:27Z",
"version": "v1.2",
"versionExact": "v1.2.0"
"revision": "c823c79ea1570fb5ff454033735a8e68575d1d0f",
"revisionTime": "2019-02-05T22:20:52Z",
"version": "v1.3",
"versionExact": "v1.3.0"
},
{
"checksumSHA1": "WqeEgS7pqqkwK8mlrAZmDgtWJMY=",
"path": "github.com/json-iterator/go",
"revision": "1624edc4454b8682399def8740d46db5e4362ba4",
"revisionTime": "2018-08-06T06:07:27Z",
"checksumSHA1": "zNo6yGy/bCJuzkEcP70oEBtOB2M=",
"path": "github.com/leodido/go-urn",
"revision": "70078a794e8ea4b497ba7c19a78cd60f90ccf0f4",
"revisionTime": "2018-05-24T03:26:21Z",
"version": "v1.1",
"versionExact": "v1.1.5"
"versionExact": "v1.1.0"
},
{
"checksumSHA1": "w5RcOnfv5YDr3j2bd1YydkPiZx4=",
"checksumSHA1": "TB2vxux9xQbvsTHOVt4aRTuvSn4=",
"path": "github.com/json-iterator/go",
"revision": "0ff49de124c6f76f8494e194af75bde0f1a49a29",
"revisionTime": "2019-03-06T14:29:09Z",
"version": "v1.1",
"versionExact": "v1.1.6"
},
{
"checksumSHA1": "Ya+baVBU/RkXXUWD3LGFmGJiiIg=",
"path": "github.com/mattn/go-isatty",
"revision": "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c",
"revisionTime": "2017-11-07T05:05:31Z",
"revision": "c2a7a6ca930a4cd0bc33a3f298eb71960732a3a7",
"revisionTime": "2019-03-12T13:58:54Z",
"version": "v0.0",
"versionExact": "v0.0.4"
"versionExact": "v0.0.7"
},
{
"checksumSHA1": "ZTcgWKWHsrX0RXYVXn5Xeb8Q0go=",
"path": "github.com/modern-go/concurrent",
"revision": "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94",
"revisionTime": "2018-03-06T01:26:44Z"
},
{
"checksumSHA1": "qvH48wzTIV3QKSDqI0dLFtVjaDI=",
"path": "github.com/modern-go/reflect2",
"revision": "94122c33edd36123c84d5368cfb2b69df93a0ec8",
"revisionTime": "2018-07-18T01:23:57Z"
},
{
"checksumSHA1": "LuFv4/jlrmFNnDb/5SCSEPAM9vU=",
@ -46,6 +90,20 @@
"revision": "5d4384ee4fb2527b0a1256a821ebfc92f91efefc",
"revisionTime": "2018-12-26T10:54:42Z"
},
{
"checksumSHA1": "cpNsoLqBprpKh+VZTBOZNVXzBEk=",
"path": "github.com/stretchr/objx",
"revision": "c61a9dfcced1815e7d40e214d00d1a8669a9f58c",
"revisionTime": "2019-02-11T16:23:28Z"
},
{
"checksumSHA1": "DBdcVxnvaINHhWyyGgih/Mel6gE=",
"path": "github.com/stretchr/testify",
"revision": "ffdc059bfe9ce6a4e144ba849dbedead332c6053",
"revisionTime": "2018-12-05T02:12:43Z",
"version": "v1.3",
"versionExact": "v1.3.0"
},
{
"checksumSHA1": "c6pbpF7eowwO59phRTpF8cQ80Z0=",
"path": "github.com/stretchr/testify/assert",
@ -55,40 +113,40 @@
"versionExact": "v1.2.2"
},
{
"checksumSHA1": "5Bd8RPhhaKcEXkagzPqymP4Gx5E=",
"checksumSHA1": "wnEANt4k5X/KGwoFyfSSnpxULm4=",
"path": "github.com/stretchr/testify/require",
"revision": "f35b8ab0b5a2cef36673838d662e249dd9c94686",
"revisionTime": "2018-05-06T18:05:49Z"
},
{
"checksumSHA1": "S4ei9eSqVThDio0Jn2sav6yUbvg=",
"path": "github.com/ugorji/go/codec",
"revision": "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab",
"revisionTime": "2018-04-07T10:07:33Z",
"revision": "82dbfaf494e3b01d2d481376f11f6a5c8cf9599f",
"revisionTime": "2019-07-02T14:15:27Z",
"version": "v1.1",
"versionExact": "v1.1.1"
"versionExact": "v1.1.6"
},
{
"checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=",
"path": "golang.org/x/net/context",
"revision": "49bb7cea24b1df9410e1712aa6433dae904ff66a",
"revisionTime": "2018-10-11T05:27:23Z"
},
{
"checksumSHA1": "SiJNkx+YGtq3Gtr6Ldu6OW83O+U=",
"checksumSHA1": "2gaep1KNRDNyDA3O+KgPTQsGWvs=",
"path": "golang.org/x/sys/unix",
"revision": "fa43e7bc11baaae89f3f902b2b4d832b68234844",
"revisionTime": "2018-10-11T14:35:51Z"
"revision": "a43fa875dd822b81eb6d2ad538bc1f4caba169bd",
"revisionTime": "2019-05-02T15:41:39Z"
},
{
"checksumSHA1": "P/k5ZGf0lEBgpKgkwy++F7K1PSg=",
"path": "gopkg.in/go-playground/validator.v8",
"revision": "5f1438d3fca68893a817e4a66806cea46a9e4ebf",
"revisionTime": "2017-07-30T05:02:35Z",
"version": "v8.18.2",
"versionExact": "v8.18.2"
"checksumSHA1": "ACzc7AkwLtNgKhqtj8V7SGUJgnw=",
"path": "gopkg.in/go-playground/validator.v9",
"revision": "46b4b1e301c24cac870ffcb4ba5c8a703d1ef475",
"revisionTime": "2019-03-31T13:31:25Z",
"version": "v9.28",
"versionExact": "v9.28.0"
},
{
"checksumSHA1": "ZSWoOPUNRr5+3dhkLK3C4cZAQPk=",
"checksumSHA1": "QqDq2x8XOU7IoOR98Cx1eiV5QY8=",
"path": "gopkg.in/yaml.v2",
"revision": "5420a8b6744d3b0345ab293f6fcba19c978f1183",
"revisionTime": "2018-03-28T19:50:20Z",
"revision": "51d6538a90f86fe93ac480b35f37b2be17fef232",
"revisionTime": "2018-11-15T11:05:04Z",
"version": "v2.2",
"versionExact": "v2.2.1"
"versionExact": "v2.2.2"
}
],
"rootPath": "github.com/gin-gonic/gin"