mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-20 00:02:16 +08:00
Merge branch 'master' into context-status
This commit is contained in:
commit
02d54ccad7
46
.github/ISSUE_TEMPLATE.md
vendored
46
.github/ISSUE_TEMPLATE.md
vendored
@ -3,11 +3,47 @@
|
|||||||
- Please provide source code and commit sha if you found a bug.
|
- Please provide source code and commit sha if you found a bug.
|
||||||
- Review existing issues and provide feedback or react to them.
|
- 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:
|
- go version:
|
||||||
- gin version (or commit ref):
|
- gin version (or commit ref):
|
||||||
- operating system:
|
- operating system:
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||
|
@ -3,17 +3,13 @@ language: go
|
|||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
include:
|
include:
|
||||||
- go: 1.6.x
|
|
||||||
- go: 1.7.x
|
|
||||||
- go: 1.8.x
|
|
||||||
- go: 1.9.x
|
|
||||||
- go: 1.10.x
|
- go: 1.10.x
|
||||||
- go: 1.11.x
|
- go: 1.11.x
|
||||||
env: GO111MODULE=on
|
env: GO111MODULE=on
|
||||||
- go: 1.12.x
|
- go: 1.12.x
|
||||||
env: GO111MODULE=on
|
env: GO111MODULE=on
|
||||||
|
- go: 1.13.x
|
||||||
- go: master
|
- go: master
|
||||||
env: GO111MODULE=on
|
|
||||||
|
|
||||||
git:
|
git:
|
||||||
depth: 10
|
depth: 10
|
||||||
|
62
CHANGELOG.md
62
CHANGELOG.md
@ -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
|
### Gin 1.3.0
|
||||||
|
|
||||||
@ -175,7 +231,7 @@
|
|||||||
- [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults.
|
- [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults.
|
||||||
- [NEW] Flexible rendering API
|
- [NEW] Flexible rendering API
|
||||||
- [NEW] Add Context.File()
|
- [NEW] Add Context.File()
|
||||||
- [NEW] Add shorcut RunTLS() for http.ListenAndServeTLS
|
- [NEW] Add shortcut RunTLS() for http.ListenAndServeTLS
|
||||||
- [FIX] Rename NotFound404() to NoRoute()
|
- [FIX] Rename NotFound404() to NoRoute()
|
||||||
- [FIX] Errors in context are purged
|
- [FIX] Errors in context are purged
|
||||||
- [FIX] Adds HEAD method in Static file serving
|
- [FIX] Adds HEAD method in Static file serving
|
||||||
@ -198,7 +254,7 @@
|
|||||||
- [NEW] New Bind() and BindWith() methods for parsing request body.
|
- [NEW] New Bind() and BindWith() methods for parsing request body.
|
||||||
- [NEW] Add Content.Copy()
|
- [NEW] Add Content.Copy()
|
||||||
- [NEW] Add context.LastError()
|
- [NEW] Add context.LastError()
|
||||||
- [NEW] Add shorcut for OPTIONS HTTP method
|
- [NEW] Add shortcut for OPTIONS HTTP method
|
||||||
- [FIX] Tons of README fixes
|
- [FIX] Tons of README fixes
|
||||||
- [FIX] Header is written before body
|
- [FIX] Header is written before body
|
||||||
- [FIX] BasicAuth() and changes API a little bit
|
- [FIX] BasicAuth() and changes API a little bit
|
||||||
|
134
README.md
134
README.md
@ -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)
|
- [Only Bind Query String](#only-bind-query-string)
|
||||||
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
|
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
|
||||||
- [Bind Uri](#bind-uri)
|
- [Bind Uri](#bind-uri)
|
||||||
|
- [Bind Header](#bind-header)
|
||||||
- [Bind HTML checkboxes](#bind-html-checkboxes)
|
- [Bind HTML checkboxes](#bind-html-checkboxes)
|
||||||
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
|
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
|
||||||
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
|
- [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.
|
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
|
```sh
|
||||||
$ go get -u github.com/gin-gonic/gin
|
$ 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 "$_"
|
$ 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
|
3. Vendor init your project and add gin
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@ -119,10 +126,6 @@ $ curl https://raw.githubusercontent.com/gin-gonic/examples/master/basic/main.go
|
|||||||
$ go run 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
|
## Quick start
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@ -256,6 +259,11 @@ func main() {
|
|||||||
c.String(http.StatusOK, message)
|
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")
|
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:
|
Also, Gin provides two sets of methods for binding:
|
||||||
- **Type** - Must bind
|
- **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.
|
- **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
|
- **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.
|
- **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`.
|
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 {
|
) bool {
|
||||||
if date, ok := field.Interface().(time.Time); ok {
|
if date, ok := field.Interface().(time.Time); ok {
|
||||||
today := time.Now()
|
today := time.Now()
|
||||||
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
|
if today.After(date) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -847,6 +855,8 @@ type Person struct {
|
|||||||
Name string `form:"name"`
|
Name string `form:"name"`
|
||||||
Address string `form:"address"`
|
Address string `form:"address"`
|
||||||
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
|
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() {
|
func main() {
|
||||||
@ -864,6 +874,8 @@ func startPage(c *gin.Context) {
|
|||||||
log.Println(person.Name)
|
log.Println(person.Name)
|
||||||
log.Println(person.Address)
|
log.Println(person.Address)
|
||||||
log.Println(person.Birthday)
|
log.Println(person.Birthday)
|
||||||
|
log.Println(person.CreateTime)
|
||||||
|
log.Println(person.UnixTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.String(200, "Success")
|
c.String(200, "Success")
|
||||||
@ -872,7 +884,7 @@ func startPage(c *gin.Context) {
|
|||||||
|
|
||||||
Test it with:
|
Test it with:
|
||||||
```sh
|
```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
|
### Bind Uri
|
||||||
@ -909,6 +921,43 @@ $ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
|
|||||||
$ curl -v localhost:8088/thinkerou/not-uuid
|
$ 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
|
### Bind HTML checkboxes
|
||||||
|
|
||||||
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
|
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
|
||||||
@ -958,32 +1007,36 @@ result:
|
|||||||
### Multipart/Urlencoded binding
|
### Multipart/Urlencoded binding
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
type ProfileForm struct {
|
||||||
|
Name string `form:"name" binding:"required"`
|
||||||
|
Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
|
||||||
|
|
||||||
import (
|
// or for multiple files
|
||||||
"github.com/gin-gonic/gin"
|
// Avatars []*multipart.FileHeader `form:"avatar" binding:"required"`
|
||||||
)
|
|
||||||
|
|
||||||
type LoginForm struct {
|
|
||||||
User string `form:"user" binding:"required"`
|
|
||||||
Password string `form:"password" binding:"required"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
router := gin.Default()
|
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:
|
// you can bind multipart form with explicit binding declaration:
|
||||||
// c.ShouldBindWith(&form, binding.Form)
|
// c.ShouldBindWith(&form, binding.Form)
|
||||||
// or you can simply use autobinding with ShouldBind method:
|
// or you can simply use autobinding with ShouldBind method:
|
||||||
var form LoginForm
|
var form ProfileForm
|
||||||
// in this case proper binding will be automatically selected
|
// in this case proper binding will be automatically selected
|
||||||
if c.ShouldBind(&form) == nil {
|
if err := c.ShouldBind(&form); err != nil {
|
||||||
if form.User == "user" && form.Password == "password" {
|
c.String(http.StatusBadRequest, "bad request")
|
||||||
c.JSON(200, gin.H{"status": "you are logged in"})
|
return
|
||||||
} else {
|
|
||||||
c.JSON(401, gin.H{"status": "unauthorized"})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
router.Run(":8080")
|
||||||
}
|
}
|
||||||
@ -991,7 +1044,7 @@ func main() {
|
|||||||
|
|
||||||
Test it with:
|
Test it with:
|
||||||
```sh
|
```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
|
### 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() {
|
func main() {
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
r.GET("/JSONP?callback=x", func(c *gin.Context) {
|
r.GET("/JSONP", func(c *gin.Context) {
|
||||||
data := map[string]interface{}{
|
data := gin.H{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1088,19 +1141,22 @@ func main() {
|
|||||||
|
|
||||||
// Listen and serve on 0.0.0.0:8080
|
// Listen and serve on 0.0.0.0:8080
|
||||||
r.Run(":8080")
|
r.Run(":8080")
|
||||||
|
|
||||||
|
// client
|
||||||
|
// curl http://127.0.0.1:8080/JSONP?callback=x
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### AsciiJSON
|
#### 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
|
```go
|
||||||
func main() {
|
func main() {
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
r.GET("/someJSON", func(c *gin.Context) {
|
r.GET("/someJSON", func(c *gin.Context) {
|
||||||
data := map[string]interface{}{
|
data := gin.H{
|
||||||
"lang": "GO语言",
|
"lang": "GO语言",
|
||||||
"tag": "<br>",
|
"tag": "<br>",
|
||||||
}
|
}
|
||||||
@ -1309,7 +1365,7 @@ func main() {
|
|||||||
router.LoadHTMLFiles("./testdata/template/raw.tmpl")
|
router.LoadHTMLFiles("./testdata/template/raw.tmpl")
|
||||||
|
|
||||||
router.GET("/raw", func(c *gin.Context) {
|
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),
|
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -1622,11 +1678,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
g.Go(func() error {
|
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 {
|
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 {
|
if err := g.Wait(); err != nil {
|
||||||
@ -1696,9 +1760,9 @@ func main() {
|
|||||||
// Wait for interrupt signal to gracefully shutdown the server with
|
// Wait for interrupt signal to gracefully shutdown the server with
|
||||||
// a timeout of 5 seconds.
|
// a timeout of 5 seconds.
|
||||||
quit := make(chan os.Signal)
|
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 -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)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-quit
|
<-quit
|
||||||
log.Println("Shutdown Server ...")
|
log.Println("Shutdown Server ...")
|
||||||
@ -1743,6 +1807,7 @@ func main() {
|
|||||||
func loadTemplate() (*template.Template, error) {
|
func loadTemplate() (*template.Template, error) {
|
||||||
t := template.New("")
|
t := template.New("")
|
||||||
for name, file := range Assets.Files {
|
for name, file := range Assets.Files {
|
||||||
|
defer file.Close()
|
||||||
if file.IsDir() || !strings.HasSuffix(name, ".tmpl") {
|
if file.IsDir() || !strings.HasSuffix(name, ".tmpl") {
|
||||||
continue
|
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.
|
* [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.
|
* [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.
|
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
|
||||||
|
* [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes.
|
||||||
|
9
auth.go
9
auth.go
@ -5,7 +5,6 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/subtle"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -86,11 +85,3 @@ func authorizationHeader(user, password string) string {
|
|||||||
base := user + ":" + password
|
base := user + ":" + password
|
||||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(base))
|
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
|
|
||||||
}
|
|
||||||
|
@ -81,13 +81,6 @@ func TestBasicAuthAuthorizationHeader(t *testing.T) {
|
|||||||
assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password"))
|
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) {
|
func TestBasicAuthSucceed(t *testing.T) {
|
||||||
accounts := Accounts{"admin": "password"}
|
accounts := Accounts{"admin": "password"}
|
||||||
router := New()
|
router := New()
|
||||||
|
@ -78,6 +78,7 @@ var (
|
|||||||
MsgPack = msgpackBinding{}
|
MsgPack = msgpackBinding{}
|
||||||
YAML = yamlBinding{}
|
YAML = yamlBinding{}
|
||||||
Uri = uriBinding{}
|
Uri = uriBinding{}
|
||||||
|
Header = headerBinding{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default returns the appropriate Binding instance based on the HTTP method
|
// Default returns the appropriate Binding instance based on the HTTP method
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -24,6 +24,16 @@ import (
|
|||||||
"github.com/ugorji/go/codec"
|
"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 {
|
type FooStruct struct {
|
||||||
Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"`
|
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"`
|
Foo interface{} `json:"foo" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FooStructDisallowUnknownFields struct {
|
||||||
|
Foo interface{} `json:"foo" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type FooBarStructForTimeType struct {
|
type FooBarStructForTimeType struct {
|
||||||
TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
|
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"`
|
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 {
|
type FooStructForTimeTypeNotFormat struct {
|
||||||
@ -114,71 +135,6 @@ type FooStructForBoolType struct {
|
|||||||
BoolFoo bool `form:"bool_foo"`
|
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 {
|
type FooStructForStringPtrType struct {
|
||||||
PtrFoo *string `form:"ptr_foo"`
|
PtrFoo *string `form:"ptr_foo"`
|
||||||
PtrBar *string `form:"ptr_bar" binding:"required"`
|
PtrBar *string `form:"ptr_bar" binding:"required"`
|
||||||
@ -242,6 +198,12 @@ func TestBindingJSONUseNumber2(t *testing.T) {
|
|||||||
`{"foo": 123}`, `{"bar": "foo"}`)
|
`{"foo": 123}`, `{"bar": "foo"}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBindingJSONDisallowUnknownFields(t *testing.T) {
|
||||||
|
testBodyBindingDisallowUnknownFields(t, JSON,
|
||||||
|
"/", "/",
|
||||||
|
`{"foo": "bar"}`, `{"foo": "bar", "what": "this"}`)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindingForm(t *testing.T) {
|
func TestBindingForm(t *testing.T) {
|
||||||
testFormBinding(t, "POST",
|
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) {
|
func TestBindingFormDefaultValue(t *testing.T) {
|
||||||
testFormBindingDefaultValue(t, "POST",
|
testFormBindingDefaultValue(t, "POST",
|
||||||
"/", "/",
|
"/", "/",
|
||||||
@ -269,7 +243,10 @@ func TestBindingFormDefaultValue2(t *testing.T) {
|
|||||||
func TestBindingFormForTime(t *testing.T) {
|
func TestBindingFormForTime(t *testing.T) {
|
||||||
testFormBindingForTime(t, "POST",
|
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",
|
testFormBindingForTimeNotFormat(t, "POST",
|
||||||
"/", "/",
|
"/", "/",
|
||||||
"time_foo=2017-11-15", "bar2=foo")
|
"time_foo=2017-11-15", "bar2=foo")
|
||||||
@ -283,8 +260,11 @@ func TestBindingFormForTime(t *testing.T) {
|
|||||||
|
|
||||||
func TestBindingFormForTime2(t *testing.T) {
|
func TestBindingFormForTime2(t *testing.T) {
|
||||||
testFormBindingForTime(t, "GET",
|
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",
|
testFormBindingForTimeNotFormat(t, "GET",
|
||||||
"/?time_foo=2017-11-15", "/?bar2=foo",
|
"/?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",
|
"/?slice_map_foo=1&slice_map_foo=2", "/?bar2=1&bar2=2",
|
||||||
"", "", "SliceMap")
|
"", "", "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",
|
testFormBindingForType(t, "POST",
|
||||||
"/", "/",
|
"/", "/",
|
||||||
"ptr_bar=test", "bar2=test", "Ptr")
|
"ptr_bar=test", "bar2=test", "Ptr")
|
||||||
@ -792,9 +668,9 @@ func TestValidationDisabled(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExistsSucceeds(t *testing.T) {
|
func TestRequiredSucceeds(t *testing.T) {
|
||||||
type HogeStruct struct {
|
type HogeStruct struct {
|
||||||
Hoge *int `json:"hoge" binding:"exists"`
|
Hoge *int `json:"hoge" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var obj HogeStruct
|
var obj HogeStruct
|
||||||
@ -803,9 +679,9 @@ func TestExistsSucceeds(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExistsFails(t *testing.T) {
|
func TestRequiredFails(t *testing.T) {
|
||||||
type HogeStruct struct {
|
type HogeStruct struct {
|
||||||
Hoge *int `json:"foo" binding:"exists"`
|
Hoge *int `json:"foo" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var obj HogeStruct
|
var obj HogeStruct
|
||||||
@ -814,6 +690,31 @@ func TestExistsFails(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
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) {
|
func TestUriBinding(t *testing.T) {
|
||||||
b := Uri
|
b := Uri
|
||||||
assert.Equal(t, "uri", b.Name())
|
assert.Equal(t, "uri", b.Name())
|
||||||
@ -857,6 +758,23 @@ func TestUriInnerBinding(t *testing.T) {
|
|||||||
assert.Equal(t, tag.S.Age, expectedAge)
|
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) {
|
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
|
||||||
b := Form
|
b := Form
|
||||||
assert.Equal(t, "form", b.Name())
|
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, "Asia/Chongqing", obj.TimeFoo.Location().String())
|
||||||
assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix())
|
assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix())
|
||||||
assert.Equal(t, "UTC", obj.TimeBar.Location().String())
|
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{}
|
obj = FooBarStructForTimeType{}
|
||||||
req = requestWithBody(method, badPath, badBody)
|
req = requestWithBody(method, badPath, badBody)
|
||||||
@ -961,6 +881,24 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
|
|||||||
assert.Error(t, err)
|
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) {
|
func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) {
|
||||||
b := Form
|
b := Form
|
||||||
assert.Equal(t, "form", b.Name())
|
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)
|
req.Header.Add("Content-Type", MIMEPOSTForm)
|
||||||
}
|
}
|
||||||
switch typ {
|
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":
|
case "Slice":
|
||||||
obj := FooStructForSliceType{}
|
obj := FooStructForSliceType{}
|
||||||
err := b.Bind(req, &obj)
|
err := b.Bind(req, &obj)
|
||||||
@ -1377,6 +1172,25 @@ func testBodyBindingUseNumber2(t *testing.T, b Binding, name, path, badPath, bod
|
|||||||
assert.Error(t, err)
|
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) {
|
func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
|
||||||
assert.Equal(t, name, b.Name())
|
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))
|
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
|
||||||
return
|
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)
|
|
||||||
}
|
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gopkg.in/go-playground/validator.v8"
|
"gopkg.in/go-playground/validator.v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
type defaultValidator struct {
|
type defaultValidator struct {
|
||||||
@ -45,7 +45,7 @@ func (v *defaultValidator) Engine() interface{} {
|
|||||||
|
|
||||||
func (v *defaultValidator) lazyinit() {
|
func (v *defaultValidator) lazyinit() {
|
||||||
v.once.Do(func() {
|
v.once.Do(func() {
|
||||||
config := &validator.Config{TagName: "binding"}
|
v.validate = validator.New()
|
||||||
v.validate = validator.New(config)
|
v.validate.SetTagName("binding")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,7 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultMemory = 32 * 1024 * 1024
|
const defaultMemory = 32 * 1024 * 1024
|
||||||
@ -63,27 +61,3 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
|
|||||||
|
|
||||||
return validate(obj)
|
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)
|
|
||||||
}
|
|
||||||
|
@ -70,6 +70,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
|||||||
return isSetted, nil
|
return isSetted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if vKind != reflect.Struct || !field.Anonymous {
|
||||||
ok, err := tryToSetValue(value, field, setter, tag)
|
ok, err := tryToSetValue(value, field, setter, tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -77,13 +78,15 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
|||||||
if ok {
|
if ok {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if vKind == reflect.Struct {
|
if vKind == reflect.Struct {
|
||||||
tValue := value.Type()
|
tValue := value.Type()
|
||||||
|
|
||||||
var isSetted bool
|
var isSetted bool
|
||||||
for i := 0; i < value.NumField(); i++ {
|
for i := 0; i < value.NumField(); i++ {
|
||||||
if !value.Field(i).CanSet() {
|
sf := tValue.Field(i)
|
||||||
|
if sf.PkgPath != "" && !sf.Anonymous { // unexported
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag)
|
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 {
|
for len(opts) > 0 {
|
||||||
opt, opts = head(opts, ",")
|
opt, opts = head(opts, ",")
|
||||||
|
|
||||||
k, v := head(opt, "=")
|
if k, v := head(opt, "="); k == "default" {
|
||||||
switch k {
|
|
||||||
case "default":
|
|
||||||
setOpt.isDefaultExists = true
|
setOpt.isDefaultExists = true
|
||||||
setOpt.defaultValue = v
|
setOpt.defaultValue = v
|
||||||
}
|
}
|
||||||
@ -265,6 +266,24 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
|||||||
timeFormat = time.RFC3339
|
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 == "" {
|
if val == "" {
|
||||||
value.Set(reflect.ValueOf(time.Time{}))
|
value.Set(reflect.ValueOf(time.Time{}))
|
||||||
return nil
|
return nil
|
||||||
|
271
binding/form_mapping_test.go
Normal file
271
binding/form_mapping_test.go
Normal 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
34
binding/header.go
Normal 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)
|
||||||
|
}
|
@ -18,6 +18,12 @@ import (
|
|||||||
// interface{} as a Number instead of as a float64.
|
// interface{} as a Number instead of as a float64.
|
||||||
var EnableDecoderUseNumber = false
|
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{}
|
type jsonBinding struct{}
|
||||||
|
|
||||||
func (jsonBinding) Name() string {
|
func (jsonBinding) Name() string {
|
||||||
@ -40,6 +46,9 @@ func decodeJSON(r io.Reader, obj interface{}) error {
|
|||||||
if EnableDecoderUseNumber {
|
if EnableDecoderUseNumber {
|
||||||
decoder.UseNumber()
|
decoder.UseNumber()
|
||||||
}
|
}
|
||||||
|
if EnableDecoderDisallowUnknownFields {
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
}
|
||||||
if err := decoder.Decode(obj); err != nil {
|
if err := decoder.Decode(obj); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
21
binding/json_test.go
Normal file
21
binding/json_test.go
Normal 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
32
binding/msgpack_test.go
Normal 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()
|
||||||
|
}
|
66
binding/multipart_form_mapping.go
Normal file
66
binding/multipart_form_mapping.go
Normal 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
|
||||||
|
}
|
138
binding/multipart_form_mapping_test.go
Normal file
138
binding/multipart_form_mapping_test.go
Normal 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)
|
||||||
|
}
|
@ -6,12 +6,11 @@ package binding
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"gopkg.in/go-playground/validator.v8"
|
"gopkg.in/go-playground/validator.v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testInterface interface {
|
type testInterface interface {
|
||||||
@ -200,15 +199,8 @@ type structCustomValidation struct {
|
|||||||
Integer int `binding:"notone"`
|
Integer int `binding:"notone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// notOne is a custom validator meant to be used with `validator.v8` library.
|
func notOne(f1 validator.FieldLevel) bool {
|
||||||
// The method signature for `v9` is significantly different and this function
|
if val, ok := f1.Field().Interface().(int); ok {
|
||||||
// would need to be changed for tests to pass after upgrade.
|
|
||||||
// See https://github.com/gin-gonic/gin/pull/1015.
|
|
||||||
func notOne(
|
|
||||||
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
|
|
||||||
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
|
|
||||||
) bool {
|
|
||||||
if val, ok := field.Interface().(int); ok {
|
|
||||||
return val != 1
|
return val != 1
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
25
binding/xml_test.go
Normal file
25
binding/xml_test.go
Normal 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
21
binding/yaml_test.go
Normal 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)
|
||||||
|
}
|
87
context.go
87
context.go
@ -48,6 +48,7 @@ type Context struct {
|
|||||||
Params Params
|
Params Params
|
||||||
handlers HandlersChain
|
handlers HandlersChain
|
||||||
index int8
|
index int8
|
||||||
|
fullPath string
|
||||||
|
|
||||||
engine *Engine
|
engine *Engine
|
||||||
|
|
||||||
@ -59,6 +60,13 @@ type Context struct {
|
|||||||
|
|
||||||
// Accepted defines a list of manually accepted formats for content negotiation.
|
// Accepted defines a list of manually accepted formats for content negotiation.
|
||||||
Accepted []string
|
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.Params = c.Params[0:0]
|
||||||
c.handlers = nil
|
c.handlers = nil
|
||||||
c.index = -1
|
c.index = -1
|
||||||
|
c.fullPath = ""
|
||||||
c.Keys = nil
|
c.Keys = nil
|
||||||
c.Errors = c.Errors[0:0]
|
c.Errors = c.Errors[0:0]
|
||||||
c.Accepted = nil
|
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.
|
// 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 {
|
for k, v := range c.Keys {
|
||||||
cp.Keys[k] = v
|
cp.Keys[k] = v
|
||||||
}
|
}
|
||||||
|
paramCopy := make([]Param, len(cp.Params))
|
||||||
|
copy(paramCopy, cp.Params)
|
||||||
|
cp.Params = paramCopy
|
||||||
return &cp
|
return &cp
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +125,15 @@ func (c *Context) Handler() HandlerFunc {
|
|||||||
return c.handlers.Last()
|
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 ***********/
|
/*********** FLOW CONTROL ***********/
|
||||||
/************************************/
|
/************************************/
|
||||||
@ -368,10 +391,17 @@ func (c *Context) QueryArray(key string) []string {
|
|||||||
return values
|
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
|
// 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.
|
// a boolean value whether at least one value exists for the given key.
|
||||||
func (c *Context) GetQueryArray(key string) ([]string, bool) {
|
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 values, true
|
||||||
}
|
}
|
||||||
return []string{}, false
|
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
|
// GetQueryMap returns a map for a given query key, plus a boolean value
|
||||||
// whether at least one value exists for the given key.
|
// whether at least one value exists for the given key.
|
||||||
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
|
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
|
// 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
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPostFormArray returns a slice of strings for a given form key, plus
|
func (c *Context) getFormCache() {
|
||||||
// a boolean value whether at least one value exists for the given key.
|
if c.formCache == nil {
|
||||||
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
|
c.formCache = make(url.Values)
|
||||||
req := c.Request
|
req := c.Request
|
||||||
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
|
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
|
||||||
if err != http.ErrNotMultipart {
|
if err != http.ErrNotMultipart {
|
||||||
debugPrint("error on parse multipart form array: %v", err)
|
debugPrint("error on parse multipart form array: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if values := req.PostForm[key]; len(values) > 0 {
|
c.formCache = req.PostForm
|
||||||
return values, true
|
|
||||||
}
|
}
|
||||||
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 values, true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return []string{}, false
|
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
|
// GetPostFormMap returns a map for a given form key, plus a boolean value
|
||||||
// whether at least one value exists for the given key.
|
// whether at least one value exists for the given key.
|
||||||
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
|
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
|
||||||
req := c.Request
|
c.getFormCache()
|
||||||
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
|
return c.get(c.formCache, key)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get is an internal method and returns a map which satisfy conditions.
|
// 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)
|
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.
|
// BindUri binds the passed struct pointer using binding.Uri.
|
||||||
// It will abort the request with HTTP 400 if any error occurs.
|
// It will abort the request with HTTP 400 if any error occurs.
|
||||||
func (c *Context) BindUri(obj interface{}) error {
|
func (c *Context) BindUri(obj interface{}) error {
|
||||||
@ -608,6 +636,11 @@ func (c *Context) ShouldBindYAML(obj interface{}) error {
|
|||||||
return c.ShouldBindWith(obj, binding.YAML)
|
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.
|
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
|
||||||
func (c *Context) ShouldBindUri(obj interface{}) error {
|
func (c *Context) ShouldBindUri(obj interface{}) error {
|
||||||
m := make(map[string][]string)
|
m := make(map[string][]string)
|
||||||
@ -682,7 +715,7 @@ func (c *Context) ContentType() string {
|
|||||||
// handshake is being initiated by the client.
|
// handshake is being initiated by the client.
|
||||||
func (c *Context) IsWebsocket() bool {
|
func (c *Context) IsWebsocket() bool {
|
||||||
if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&
|
if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&
|
||||||
strings.ToLower(c.requestHeader("Upgrade")) == "websocket" {
|
strings.EqualFold(c.requestHeader("Upgrade"), "websocket") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -828,6 +861,12 @@ func (c *Context) AsciiJSON(code int, obj interface{}) {
|
|||||||
c.Render(code, render.AsciiJSON{Data: obj})
|
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.
|
// XML serializes the given struct as XML into the response body.
|
||||||
// It also sets the Content-Type as "application/xml".
|
// It also sets the Content-Type as "application/xml".
|
||||||
func (c *Context) XML(code int, obj interface{}) {
|
func (c *Context) XML(code int, obj interface{}) {
|
||||||
|
@ -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})
|
|
||||||
}
|
|
@ -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"))
|
|
||||||
}
|
|
100
context_test.go
100
context_test.go
@ -6,6 +6,7 @@ package gin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
@ -13,8 +14,10 @@ import (
|
|||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -22,7 +25,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"golang.org/x/net/context"
|
|
||||||
|
|
||||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||||
)
|
)
|
||||||
@ -622,8 +624,7 @@ func TestContextGetCookie(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestContextBodyAllowedForStatus(t *testing.T) {
|
func TestContextBodyAllowedForStatus(t *testing.T) {
|
||||||
// todo(thinkerou): go1.6 not support StatusProcessing
|
assert.False(t, false, bodyAllowedForStatus(http.StatusProcessing))
|
||||||
assert.False(t, false, bodyAllowedForStatus(102))
|
|
||||||
assert.False(t, false, bodyAllowedForStatus(http.StatusNoContent))
|
assert.False(t, false, bodyAllowedForStatus(http.StatusNoContent))
|
||||||
assert.False(t, false, bodyAllowedForStatus(http.StatusNotModified))
|
assert.False(t, false, bodyAllowedForStatus(http.StatusNotModified))
|
||||||
assert.True(t, true, bodyAllowedForStatus(http.StatusInternalServerError))
|
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>"})
|
c.JSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
|
||||||
|
|
||||||
assert.Equal(t, http.StatusCreated, w.Code)
|
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"))
|
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"})
|
c.JSONP(http.StatusCreated, H{"foo": "bar"})
|
||||||
|
|
||||||
assert.Equal(t, http.StatusCreated, w.Code)
|
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"))
|
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"})
|
c.JSONP(http.StatusCreated, H{"foo": "bar"})
|
||||||
|
|
||||||
assert.Equal(t, http.StatusCreated, w.Code)
|
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"))
|
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"})
|
c.JSON(http.StatusCreated, H{"foo": "bar"})
|
||||||
|
|
||||||
assert.Equal(t, http.StatusCreated, w.Code)
|
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"))
|
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"))
|
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
|
// Tests that the response executes the templates
|
||||||
// and responds with Content-Type set to text/html
|
// and responds with Content-Type set to text/html
|
||||||
func TestContextRenderHTML(t *testing.T) {
|
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(299, "/resource") })
|
||||||
assert.Panics(t, func() { c.Redirect(309, "/resource") })
|
assert.Panics(t, func() { c.Redirect(309, "/resource") })
|
||||||
assert.NotPanics(t, func() { c.Redirect(http.StatusMultipleChoices, "/resource") })
|
assert.NotPanics(t, func() { c.Redirect(http.StatusMultipleChoices, "/resource") })
|
||||||
// todo(thinkerou): go1.6 not support StatusPermanentRedirect(308)
|
assert.NotPanics(t, func() { c.Redirect(http.StatusPermanentRedirect, "/resource") })
|
||||||
// when we upgrade go version we can use http.StatusPermanentRedirect
|
|
||||||
assert.NotPanics(t, func() { c.Redirect(308, "/resource") })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContextNegotiationWithJSON(t *testing.T) {
|
func TestContextNegotiationWithJSON(t *testing.T) {
|
||||||
@ -1108,7 +1119,7 @@ func TestContextNegotiationWithJSON(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
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"))
|
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)
|
_, err := buf.ReadFrom(w.Body)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
jsonStringBody := buf.String()
|
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) {
|
func TestContextError(t *testing.T) {
|
||||||
@ -1425,6 +1436,28 @@ func TestContextBindWithXML(t *testing.T) {
|
|||||||
assert.Equal(t, 0, w.Body.Len())
|
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) {
|
func TestContextBindWithQuery(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := CreateTestContext(w)
|
c, _ := CreateTestContext(w)
|
||||||
@ -1532,6 +1565,28 @@ func TestContextShouldBindWithXML(t *testing.T) {
|
|||||||
assert.Equal(t, 0, w.Body.Len())
|
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) {
|
func TestContextShouldBindWithQuery(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := CreateTestContext(w)
|
c, _ := CreateTestContext(w)
|
||||||
@ -1812,3 +1867,24 @@ func TestContextResetInHandler(t *testing.T) {
|
|||||||
c.Next()
|
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()
|
||||||
|
}
|
||||||
|
14
debug.go
14
debug.go
@ -5,16 +5,14 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"os"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ginSupportMinGoVer = 6
|
const ginSupportMinGoVer = 10
|
||||||
|
|
||||||
// IsDebugging returns true if the framework is running in debug mode.
|
// IsDebugging returns true if the framework is running in debug mode.
|
||||||
// Use SetMode(gin.ReleaseMode) to disable 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) {
|
func debugPrintLoadTemplate(tmpl *template.Template) {
|
||||||
if IsDebugging() {
|
if IsDebugging() {
|
||||||
var buf bytes.Buffer
|
var buf strings.Builder
|
||||||
for _, tmpl := range tmpl.Templates() {
|
for _, tmpl := range tmpl.Templates() {
|
||||||
buf.WriteString("\t- ")
|
buf.WriteString("\t- ")
|
||||||
buf.WriteString(tmpl.Name())
|
buf.WriteString(tmpl.Name())
|
||||||
@ -54,7 +52,7 @@ func debugPrint(format string, values ...interface{}) {
|
|||||||
if !strings.HasSuffix(format, "\n") {
|
if !strings.HasSuffix(format, "\n") {
|
||||||
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() {
|
func debugPrintWARNINGDefault() {
|
||||||
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
|
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) {
|
func debugPrintError(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugPrint("[ERROR] %v\n", err)
|
if IsDebugging() {
|
||||||
|
fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
|
|||||||
})
|
})
|
||||||
m, e := getMinVer(runtime.Version())
|
m, e := getMinVer(runtime.Version())
|
||||||
if e == nil && m <= ginSupportMinGoVer {
|
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 {
|
} else {
|
||||||
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
stdout := os.Stdout
|
defaultWriter := DefaultWriter
|
||||||
stderr := os.Stderr
|
defaultErrorWriter := DefaultErrorWriter
|
||||||
defer func() {
|
defer func() {
|
||||||
os.Stdout = stdout
|
DefaultWriter = defaultWriter
|
||||||
os.Stderr = stderr
|
DefaultErrorWriter = defaultErrorWriter
|
||||||
log.SetOutput(os.Stderr)
|
log.SetOutput(os.Stderr)
|
||||||
}()
|
}()
|
||||||
os.Stdout = writer
|
DefaultWriter = writer
|
||||||
os.Stderr = writer
|
DefaultErrorWriter = writer
|
||||||
log.SetOutput(writer)
|
log.SetOutput(writer)
|
||||||
out := make(chan string)
|
out := make(chan string)
|
||||||
wg := new(sync.WaitGroup)
|
wg := new(sync.WaitGroup)
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/internal/json"
|
"github.com/gin-gonic/gin/internal/json"
|
||||||
)
|
)
|
||||||
@ -158,7 +158,7 @@ func (a errorMsgs) String() string {
|
|||||||
if len(a) == 0 {
|
if len(a) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
var buffer bytes.Buffer
|
var buffer strings.Builder
|
||||||
for i, msg := range a {
|
for i, msg := range a {
|
||||||
fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err)
|
fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err)
|
||||||
if msg.Meta != nil {
|
if msg.Meta != nil {
|
||||||
|
25
gin.go
25
gin.go
@ -252,6 +252,7 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
|
|||||||
root := engine.trees.get(method)
|
root := engine.trees.get(method)
|
||||||
if root == nil {
|
if root == nil {
|
||||||
root = new(node)
|
root = new(node)
|
||||||
|
root.fullPath = "/"
|
||||||
engine.trees = append(engine.trees, methodTree{method: method, root: root})
|
engine.trees = append(engine.trees, methodTree{method: method, root: root})
|
||||||
}
|
}
|
||||||
root.addRoute(path, handlers)
|
root.addRoute(path, handlers)
|
||||||
@ -337,6 +338,15 @@ func (engine *Engine) RunFd(fd int) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer listener.Close()
|
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)
|
err = http.Serve(listener, engine)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -372,6 +382,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
|||||||
rPath = c.Request.URL.RawPath
|
rPath = c.Request.URL.RawPath
|
||||||
unescape = engine.UnescapePathValues
|
unescape = engine.UnescapePathValues
|
||||||
}
|
}
|
||||||
|
rPath = cleanPath(rPath)
|
||||||
|
|
||||||
// Find root of the tree for the given HTTP method
|
// Find root of the tree for the given HTTP method
|
||||||
t := engine.trees
|
t := engine.trees
|
||||||
@ -381,16 +392,17 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
|||||||
}
|
}
|
||||||
root := t[i].root
|
root := t[i].root
|
||||||
// Find route in tree
|
// Find route in tree
|
||||||
handlers, params, tsr := root.getValue(rPath, c.Params, unescape)
|
value := root.getValue(rPath, c.Params, unescape)
|
||||||
if handlers != nil {
|
if value.handlers != nil {
|
||||||
c.handlers = handlers
|
c.handlers = value.handlers
|
||||||
c.Params = params
|
c.Params = value.params
|
||||||
|
c.fullPath = value.fullPath
|
||||||
c.Next()
|
c.Next()
|
||||||
c.writermem.WriteHeaderNow()
|
c.writermem.WriteHeaderNow()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if httpMethod != "CONNECT" && rPath != "/" {
|
if httpMethod != "CONNECT" && rPath != "/" {
|
||||||
if tsr && engine.RedirectTrailingSlash {
|
if value.tsr && engine.RedirectTrailingSlash {
|
||||||
redirectTrailingSlash(c)
|
redirectTrailingSlash(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -406,7 +418,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
|||||||
if tree.method == httpMethod {
|
if tree.method == httpMethod {
|
||||||
continue
|
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
|
c.handlers = engine.allNoMethod
|
||||||
serveError(c, http.StatusMethodNotAllowed, default405Body)
|
serveError(c, http.StatusMethodNotAllowed, default405Body)
|
||||||
return
|
return
|
||||||
@ -434,7 +446,6 @@ func serveError(c *Context, code int, defaultMessage []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.writermem.WriteHeaderNow()
|
c.writermem.WriteHeaderNow()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func redirectTrailingSlash(c *Context) {
|
func redirectTrailingSlash(c *Context) {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -69,6 +70,42 @@ func TestRunTLS(t *testing.T) {
|
|||||||
testRequest(t, "https://localhost:8443/example")
|
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) {
|
func TestRunEmptyWithEnv(t *testing.T) {
|
||||||
os.Setenv("PORT", "3123")
|
os.Setenv("PORT", "3123")
|
||||||
router := New()
|
router := New()
|
||||||
@ -170,6 +207,42 @@ func TestBadFileDescriptor(t *testing.T) {
|
|||||||
assert.Error(t, router.RunFd(0))
|
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) {
|
func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
|
||||||
router := New()
|
router := New()
|
||||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||||
|
20
go.mod
20
go.mod
@ -3,16 +3,16 @@ module github.com/gin-gonic/gin
|
|||||||
go 1.12
|
go 1.12
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3
|
github.com/gin-contrib/sse v0.1.0
|
||||||
github.com/golang/protobuf v1.3.0
|
github.com/go-playground/locales v0.12.1 // indirect
|
||||||
github.com/json-iterator/go v1.1.5
|
github.com/go-playground/universal-translator v0.16.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.6
|
github.com/golang/protobuf v1.3.2
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/json-iterator/go v1.1.7
|
||||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
github.com/leodido/go-urn v1.1.0 // indirect
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/mattn/go-isatty v0.0.9
|
||||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43
|
github.com/stretchr/testify v1.4.0
|
||||||
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95
|
github.com/ugorji/go/codec v1.1.7
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
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
|
gopkg.in/yaml.v2 v2.2.2
|
||||||
)
|
)
|
||||||
|
59
go.sum
59
go.sum
@ -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/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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
|
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
|
||||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||||
github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA=
|
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
|
||||||
github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/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/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 h1:BasDe+IErOQKrMVXab7UayvSlIpiyGwRvuX3EKYY7UA=
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
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=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
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/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 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
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.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
|
||||||
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/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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
21
logger.go
21
logger.go
@ -22,18 +22,19 @@ const (
|
|||||||
forceColor
|
forceColor
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const (
|
||||||
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
|
green = "\033[97;42m"
|
||||||
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
|
white = "\033[90;47m"
|
||||||
yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109})
|
yellow = "\033[90;43m"
|
||||||
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
|
red = "\033[97;41m"
|
||||||
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
|
blue = "\033[97;44m"
|
||||||
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
|
magenta = "\033[97;45m"
|
||||||
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
|
cyan = "\033[97;46m"
|
||||||
reset = string([]byte{27, 91, 48, 109})
|
reset = "\033[0m"
|
||||||
consoleColorMode = autoColor
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var consoleColorMode = autoColor
|
||||||
|
|
||||||
// LoggerConfig defines the config for Logger middleware.
|
// LoggerConfig defines the config for Logger middleware.
|
||||||
type LoggerConfig struct {
|
type LoggerConfig struct {
|
||||||
// Optional. Default value is gin.defaultLogFormatter
|
// Optional. Default value is gin.defaultLogFormatter
|
||||||
|
@ -291,14 +291,14 @@ func TestColorForMethod(t *testing.T) {
|
|||||||
return p.MethodColor()
|
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, blue, 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, cyan, 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, yellow, 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, red, 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, green, 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, magenta, 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, white, 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, reset, colorForMethod("TRACE"), "trace is not defined and should be the reset color")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestColorForStatus(t *testing.T) {
|
func TestColorForStatus(t *testing.T) {
|
||||||
@ -309,10 +309,10 @@ func TestColorForStatus(t *testing.T) {
|
|||||||
return p.StatusCodeColor()
|
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, green, 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, white, 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, yellow, 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, red, colorForStatus(2), "other things should be red")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResetColor(t *testing.T) {
|
func TestResetColor(t *testing.T) {
|
||||||
@ -369,15 +369,15 @@ func TestErrorLogger(t *testing.T) {
|
|||||||
|
|
||||||
w := performRequest(router, "GET", "/error")
|
w := performRequest(router, "GET", "/error")
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
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")
|
w = performRequest(router, "GET", "/abort")
|
||||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
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")
|
w = performRequest(router, "GET", "/print")
|
||||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
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) {
|
func TestLoggerWithWriterSkippingPaths(t *testing.T) {
|
||||||
|
@ -246,5 +246,5 @@ func TestMiddlewareWrite(t *testing.T) {
|
|||||||
w := performRequest(router, "GET", "/")
|
w := performRequest(router, "GET", "/")
|
||||||
|
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
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))
|
||||||
}
|
}
|
||||||
|
8
mode.go
8
mode.go
@ -71,12 +71,18 @@ func DisableBindValidation() {
|
|||||||
binding.Validator = nil
|
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.
|
// call the UseNumber method on the JSON Decoder instance.
|
||||||
func EnableJsonDecoderUseNumber() {
|
func EnableJsonDecoderUseNumber() {
|
||||||
binding.EnableDecoderUseNumber = true
|
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.
|
// Mode returns currently gin mode.
|
||||||
func Mode() string {
|
func Mode() string {
|
||||||
return modeName
|
return modeName
|
||||||
|
14
mode_test.go
14
mode_test.go
@ -40,8 +40,22 @@ func TestSetMode(t *testing.T) {
|
|||||||
assert.Panics(t, func() { SetMode("unknown") })
|
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) {
|
func TestEnableJsonDecoderUseNumber(t *testing.T) {
|
||||||
assert.False(t, binding.EnableDecoderUseNumber)
|
assert.False(t, binding.EnableDecoderUseNumber)
|
||||||
EnableJsonDecoderUseNumber()
|
EnableJsonDecoderUseNumber()
|
||||||
assert.True(t, binding.EnableDecoderUseNumber)
|
assert.True(t, binding.EnableDecoderUseNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnableJsonDecoderDisallowUnknownFields(t *testing.T) {
|
||||||
|
assert.False(t, binding.EnableDecoderDisallowUnknownFields)
|
||||||
|
EnableJsonDecoderDisallowUnknownFields()
|
||||||
|
assert.True(t, binding.EnableDecoderDisallowUnknownFields)
|
||||||
|
}
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
// Use of this source code is governed by a MIT style
|
// Use of this source code is governed by a MIT style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// +build go1.7
|
|
||||||
|
|
||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -43,6 +43,11 @@ type AsciiJSON struct {
|
|||||||
// SecureJSONPrefix is a string which represents SecureJSON prefix.
|
// SecureJSONPrefix is a string which represents SecureJSON prefix.
|
||||||
type SecureJSONPrefix string
|
type SecureJSONPrefix string
|
||||||
|
|
||||||
|
// PureJSON contains the given interface object.
|
||||||
|
type PureJSON struct {
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
var jsonContentType = []string{"application/json; charset=utf-8"}
|
var jsonContentType = []string{"application/json; charset=utf-8"}
|
||||||
var jsonpContentType = []string{"application/javascript; charset=utf-8"}
|
var jsonpContentType = []string{"application/javascript; charset=utf-8"}
|
||||||
var jsonAsciiContentType = []string{"application/json"}
|
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.
|
// WriteJSON marshals the given interface object and writes it with custom ContentType.
|
||||||
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
|
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
|
||||||
writeContentType(w, jsonContentType)
|
writeContentType(w, jsonContentType)
|
||||||
jsonBytes, err := json.Marshal(obj)
|
encoder := json.NewEncoder(w)
|
||||||
if err != nil {
|
err := encoder.Encode(&obj)
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = w.Write(jsonBytes)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +138,7 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = w.Write([]byte(")"))
|
_, err = w.Write([]byte(");"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -174,3 +176,16 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
|
|||||||
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
|
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
|
||||||
writeContentType(w, jsonAsciiContentType)
|
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)
|
||||||
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -21,7 +21,9 @@ type Reader struct {
|
|||||||
// Render (Reader) writes data with custom ContentType and headers.
|
// Render (Reader) writes data with custom ContentType and headers.
|
||||||
func (r Reader) Render(w http.ResponseWriter) (err error) {
|
func (r Reader) Render(w http.ResponseWriter) (err error) {
|
||||||
r.WriteContentType(w)
|
r.WriteContentType(w)
|
||||||
|
if r.ContentLength >= 0 {
|
||||||
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
|
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
|
||||||
|
}
|
||||||
r.writeHeaders(w, r.Headers)
|
r.writeHeaders(w, r.Headers)
|
||||||
_, err = io.Copy(w, r.Reader)
|
_, err = io.Copy(w, r.Reader)
|
||||||
return
|
return
|
||||||
|
@ -18,9 +18,7 @@ type Redirect struct {
|
|||||||
|
|
||||||
// Render (Redirect) redirects the http request to new location and writes redirect response.
|
// Render (Redirect) redirects the http request to new location and writes redirect response.
|
||||||
func (r Redirect) Render(w http.ResponseWriter) error {
|
func (r Redirect) Render(w http.ResponseWriter) error {
|
||||||
// todo(thinkerou): go1.6 not support StatusPermanentRedirect(308)
|
if (r.Code < http.StatusMultipleChoices || r.Code > http.StatusPermanentRedirect) && r.Code != http.StatusCreated {
|
||||||
// when we upgrade go version we can use http.StatusPermanentRedirect
|
|
||||||
if (r.Code < 300 || r.Code > 308) && r.Code != 201 {
|
|
||||||
panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code))
|
panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code))
|
||||||
}
|
}
|
||||||
http.Redirect(w, r.Request, r.Location, r.Code)
|
http.Redirect(w, r.Request, r.Location, r.Code)
|
||||||
|
@ -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"))
|
|
||||||
}
|
|
@ -45,7 +45,7 @@ func TestRenderMsgPack(t *testing.T) {
|
|||||||
err = codec.NewEncoder(buf, h).Encode(data)
|
err = codec.NewEncoder(buf, h).Encode(data)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
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"))
|
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)
|
err := (JSON{data}).Render(w)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
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"))
|
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)
|
err1 := (JsonpJSON{"x", data}).Render(w1)
|
||||||
|
|
||||||
assert.NoError(t, err1)
|
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"))
|
assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type"))
|
||||||
|
|
||||||
w2 := httptest.NewRecorder()
|
w2 := httptest.NewRecorder()
|
||||||
@ -158,7 +158,7 @@ func TestRenderJsonpJSON(t *testing.T) {
|
|||||||
|
|
||||||
err2 := (JsonpJSON{"x", datas}).Render(w2)
|
err2 := (JsonpJSON{"x", datas}).Render(w2)
|
||||||
assert.NoError(t, err2)
|
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"))
|
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))
|
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{}
|
type xmlmap map[string]interface{}
|
||||||
|
|
||||||
// Allows type H to be used with xml.Marshal
|
// Allows type H to be used with xml.Marshal
|
||||||
@ -335,7 +347,17 @@ func TestRenderRedirect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w = httptest.NewRecorder()
|
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
|
// only improve coverage
|
||||||
data2.WriteContentType(w)
|
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["Content-Disposition"], w.Header().Get("Content-Disposition"))
|
||||||
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
|
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"))
|
||||||
|
}
|
||||||
|
@ -16,7 +16,8 @@ const (
|
|||||||
defaultStatus = http.StatusOK
|
defaultStatus = http.StatusOK
|
||||||
)
|
)
|
||||||
|
|
||||||
type responseWriterBase interface {
|
// ResponseWriter ...
|
||||||
|
type ResponseWriter interface {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
http.Hijacker
|
http.Hijacker
|
||||||
http.Flusher
|
http.Flusher
|
||||||
@ -37,6 +38,9 @@ type responseWriterBase interface {
|
|||||||
|
|
||||||
// Forces to write the http header (status code + headers).
|
// Forces to write the http header (status code + headers).
|
||||||
WriteHeaderNow()
|
WriteHeaderNow()
|
||||||
|
|
||||||
|
// get the http.Pusher for server push
|
||||||
|
Pusher() http.Pusher
|
||||||
}
|
}
|
||||||
|
|
||||||
type responseWriter struct {
|
type responseWriter struct {
|
||||||
@ -113,3 +117,10 @@ func (w *responseWriter) Flush() {
|
|||||||
w.WriteHeaderNow()
|
w.WriteHeaderNow()
|
||||||
w.ResponseWriter.(http.Flusher).Flush()
|
w.ResponseWriter.(http.Flusher).Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) Pusher() (pusher http.Pusher) {
|
||||||
|
if pusher, ok := w.ResponseWriter.(http.Pusher); ok {
|
||||||
|
return pusher
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -29,38 +29,38 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestResponseWriterReset(t *testing.T) {
|
func TestResponseWriterReset(t *testing.T) {
|
||||||
testWritter := httptest.NewRecorder()
|
testWriter := httptest.NewRecorder()
|
||||||
writer := &responseWriter{}
|
writer := &responseWriter{}
|
||||||
var w ResponseWriter = writer
|
var w ResponseWriter = writer
|
||||||
|
|
||||||
writer.reset(testWritter)
|
writer.reset(testWriter)
|
||||||
assert.Equal(t, -1, writer.size)
|
assert.Equal(t, -1, writer.size)
|
||||||
assert.Equal(t, http.StatusOK, writer.status)
|
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, -1, w.Size())
|
||||||
assert.Equal(t, http.StatusOK, w.Status())
|
assert.Equal(t, http.StatusOK, w.Status())
|
||||||
assert.False(t, w.Written())
|
assert.False(t, w.Written())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResponseWriterWriteHeader(t *testing.T) {
|
func TestResponseWriterWriteHeader(t *testing.T) {
|
||||||
testWritter := httptest.NewRecorder()
|
testWriter := httptest.NewRecorder()
|
||||||
writer := &responseWriter{}
|
writer := &responseWriter{}
|
||||||
writer.reset(testWritter)
|
writer.reset(testWriter)
|
||||||
w := ResponseWriter(writer)
|
w := ResponseWriter(writer)
|
||||||
|
|
||||||
w.WriteHeader(http.StatusMultipleChoices)
|
w.WriteHeader(http.StatusMultipleChoices)
|
||||||
assert.False(t, w.Written())
|
assert.False(t, w.Written())
|
||||||
assert.Equal(t, http.StatusMultipleChoices, w.Status())
|
assert.Equal(t, http.StatusMultipleChoices, w.Status())
|
||||||
assert.NotEqual(t, http.StatusMultipleChoices, testWritter.Code)
|
assert.NotEqual(t, http.StatusMultipleChoices, testWriter.Code)
|
||||||
|
|
||||||
w.WriteHeader(-1)
|
w.WriteHeader(-1)
|
||||||
assert.Equal(t, http.StatusMultipleChoices, w.Status())
|
assert.Equal(t, http.StatusMultipleChoices, w.Status())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResponseWriterWriteHeadersNow(t *testing.T) {
|
func TestResponseWriterWriteHeadersNow(t *testing.T) {
|
||||||
testWritter := httptest.NewRecorder()
|
testWriter := httptest.NewRecorder()
|
||||||
writer := &responseWriter{}
|
writer := &responseWriter{}
|
||||||
writer.reset(testWritter)
|
writer.reset(testWriter)
|
||||||
w := ResponseWriter(writer)
|
w := ResponseWriter(writer)
|
||||||
|
|
||||||
w.WriteHeader(http.StatusMultipleChoices)
|
w.WriteHeader(http.StatusMultipleChoices)
|
||||||
@ -68,7 +68,7 @@ func TestResponseWriterWriteHeadersNow(t *testing.T) {
|
|||||||
|
|
||||||
assert.True(t, w.Written())
|
assert.True(t, w.Written())
|
||||||
assert.Equal(t, 0, w.Size())
|
assert.Equal(t, 0, w.Size())
|
||||||
assert.Equal(t, http.StatusMultipleChoices, testWritter.Code)
|
assert.Equal(t, http.StatusMultipleChoices, testWriter.Code)
|
||||||
|
|
||||||
writer.size = 10
|
writer.size = 10
|
||||||
w.WriteHeaderNow()
|
w.WriteHeaderNow()
|
||||||
@ -76,30 +76,30 @@ func TestResponseWriterWriteHeadersNow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestResponseWriterWrite(t *testing.T) {
|
func TestResponseWriterWrite(t *testing.T) {
|
||||||
testWritter := httptest.NewRecorder()
|
testWriter := httptest.NewRecorder()
|
||||||
writer := &responseWriter{}
|
writer := &responseWriter{}
|
||||||
writer.reset(testWritter)
|
writer.reset(testWriter)
|
||||||
w := ResponseWriter(writer)
|
w := ResponseWriter(writer)
|
||||||
|
|
||||||
n, err := w.Write([]byte("hola"))
|
n, err := w.Write([]byte("hola"))
|
||||||
assert.Equal(t, 4, n)
|
assert.Equal(t, 4, n)
|
||||||
assert.Equal(t, 4, w.Size())
|
assert.Equal(t, 4, w.Size())
|
||||||
assert.Equal(t, http.StatusOK, w.Status())
|
assert.Equal(t, http.StatusOK, w.Status())
|
||||||
assert.Equal(t, http.StatusOK, testWritter.Code)
|
assert.Equal(t, http.StatusOK, testWriter.Code)
|
||||||
assert.Equal(t, "hola", testWritter.Body.String())
|
assert.Equal(t, "hola", testWriter.Body.String())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
n, err = w.Write([]byte(" adios"))
|
n, err = w.Write([]byte(" adios"))
|
||||||
assert.Equal(t, 6, n)
|
assert.Equal(t, 6, n)
|
||||||
assert.Equal(t, 10, w.Size())
|
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)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResponseWriterHijack(t *testing.T) {
|
func TestResponseWriterHijack(t *testing.T) {
|
||||||
testWritter := httptest.NewRecorder()
|
testWriter := httptest.NewRecorder()
|
||||||
writer := &responseWriter{}
|
writer := &responseWriter{}
|
||||||
writer.reset(testWritter)
|
writer.reset(testWriter)
|
||||||
w := ResponseWriter(writer)
|
w := ResponseWriter(writer)
|
||||||
|
|
||||||
assert.Panics(t, func() {
|
assert.Panics(t, func() {
|
||||||
|
@ -22,7 +22,7 @@ type header struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func performRequest(r http.Handler, method, path string, headers ...header) *httptest.ResponseRecorder {
|
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 {
|
for _, h := range headers {
|
||||||
req.Header.Add(h.Key, h.Value)
|
req.Header.Add(h.Key, h.Value)
|
||||||
}
|
}
|
||||||
@ -257,6 +257,39 @@ func TestRouteParamsByName(t *testing.T) {
|
|||||||
assert.Equal(t, "/is/super/great", wild)
|
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
|
// TestHandleStaticFile - ensure the static file handles properly
|
||||||
func TestRouteStaticFile(t *testing.T) {
|
func TestRouteStaticFile(t *testing.T) {
|
||||||
// SETUP file
|
// SETUP file
|
||||||
@ -388,12 +421,11 @@ func TestRouterNotFound(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/
|
{"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/
|
||||||
{"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/
|
{"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/
|
||||||
{"", http.StatusMovedPermanently, "/"}, // TSR +/
|
|
||||||
{"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case
|
{"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case
|
||||||
{"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case
|
{"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case
|
||||||
{"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/
|
{"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/
|
||||||
{"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/
|
{"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/
|
||||||
{"/../path", http.StatusMovedPermanently, "/path"}, // CleanPath
|
{"/../path", http.StatusOK, ""}, // CleanPath
|
||||||
{"/nope", http.StatusNotFound, ""}, // NotFound
|
{"/nope", http.StatusNotFound, ""}, // NotFound
|
||||||
}
|
}
|
||||||
for _, tr := range testRoutes {
|
for _, tr := range testRoutes {
|
||||||
@ -522,3 +554,43 @@ func TestRouteServeErrorWithWriteHeader(t *testing.T) {
|
|||||||
assert.Equal(t, 421, w.Code)
|
assert.Equal(t, 421, w.Code)
|
||||||
assert.Equal(t, 0, w.Body.Len())
|
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
88
tree.go
@ -65,11 +65,10 @@ func min(a, b int) int {
|
|||||||
func countParams(path string) uint8 {
|
func countParams(path string) uint8 {
|
||||||
var n uint
|
var n uint
|
||||||
for i := 0; i < len(path); i++ {
|
for i := 0; i < len(path); i++ {
|
||||||
if path[i] != ':' && path[i] != '*' {
|
if path[i] == ':' || path[i] == '*' {
|
||||||
continue
|
|
||||||
}
|
|
||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if n >= 255 {
|
if n >= 255 {
|
||||||
return 255
|
return 255
|
||||||
}
|
}
|
||||||
@ -94,6 +93,7 @@ type node struct {
|
|||||||
nType nodeType
|
nType nodeType
|
||||||
maxParams uint8
|
maxParams uint8
|
||||||
wildChild bool
|
wildChild bool
|
||||||
|
fullPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// increments priority of the given child and reorders if necessary.
|
// increments priority of the given child and reorders if necessary.
|
||||||
@ -127,6 +127,8 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
|||||||
n.priority++
|
n.priority++
|
||||||
numParams := countParams(path)
|
numParams := countParams(path)
|
||||||
|
|
||||||
|
parentFullPathIndex := 0
|
||||||
|
|
||||||
// non-empty tree
|
// non-empty tree
|
||||||
if len(n.path) > 0 || len(n.children) > 0 {
|
if len(n.path) > 0 || len(n.children) > 0 {
|
||||||
walk:
|
walk:
|
||||||
@ -154,6 +156,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
|||||||
children: n.children,
|
children: n.children,
|
||||||
handlers: n.handlers,
|
handlers: n.handlers,
|
||||||
priority: n.priority - 1,
|
priority: n.priority - 1,
|
||||||
|
fullPath: n.fullPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update maxParams (max of all children)
|
// Update maxParams (max of all children)
|
||||||
@ -169,6 +172,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
|||||||
n.path = path[:i]
|
n.path = path[:i]
|
||||||
n.handlers = nil
|
n.handlers = nil
|
||||||
n.wildChild = false
|
n.wildChild = false
|
||||||
|
n.fullPath = fullPath[:parentFullPathIndex+i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make new node a child of this node
|
// Make new node a child of this node
|
||||||
@ -176,6 +180,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
|||||||
path = path[i:]
|
path = path[i:]
|
||||||
|
|
||||||
if n.wildChild {
|
if n.wildChild {
|
||||||
|
parentFullPathIndex += len(n.path)
|
||||||
n = n.children[0]
|
n = n.children[0]
|
||||||
n.priority++
|
n.priority++
|
||||||
|
|
||||||
@ -209,6 +214,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
|||||||
|
|
||||||
// slash after param
|
// slash after param
|
||||||
if n.nType == param && c == '/' && len(n.children) == 1 {
|
if n.nType == param && c == '/' && len(n.children) == 1 {
|
||||||
|
parentFullPathIndex += len(n.path)
|
||||||
n = n.children[0]
|
n = n.children[0]
|
||||||
n.priority++
|
n.priority++
|
||||||
continue walk
|
continue walk
|
||||||
@ -217,6 +223,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
|||||||
// Check if a child with the next path byte exists
|
// Check if a child with the next path byte exists
|
||||||
for i := 0; i < len(n.indices); i++ {
|
for i := 0; i < len(n.indices); i++ {
|
||||||
if c == n.indices[i] {
|
if c == n.indices[i] {
|
||||||
|
parentFullPathIndex += len(n.path)
|
||||||
i = n.incrementChildPrio(i)
|
i = n.incrementChildPrio(i)
|
||||||
n = n.children[i]
|
n = n.children[i]
|
||||||
continue walk
|
continue walk
|
||||||
@ -229,6 +236,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
|||||||
n.indices += string([]byte{c})
|
n.indices += string([]byte{c})
|
||||||
child := &node{
|
child := &node{
|
||||||
maxParams: numParams,
|
maxParams: numParams,
|
||||||
|
fullPath: fullPath,
|
||||||
}
|
}
|
||||||
n.children = append(n.children, child)
|
n.children = append(n.children, child)
|
||||||
n.incrementChildPrio(len(n.indices) - 1)
|
n.incrementChildPrio(len(n.indices) - 1)
|
||||||
@ -296,6 +304,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
|
|||||||
child := &node{
|
child := &node{
|
||||||
nType: param,
|
nType: param,
|
||||||
maxParams: numParams,
|
maxParams: numParams,
|
||||||
|
fullPath: fullPath,
|
||||||
}
|
}
|
||||||
n.children = []*node{child}
|
n.children = []*node{child}
|
||||||
n.wildChild = true
|
n.wildChild = true
|
||||||
@ -312,6 +321,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
|
|||||||
child := &node{
|
child := &node{
|
||||||
maxParams: numParams,
|
maxParams: numParams,
|
||||||
priority: 1,
|
priority: 1,
|
||||||
|
fullPath: fullPath,
|
||||||
}
|
}
|
||||||
n.children = []*node{child}
|
n.children = []*node{child}
|
||||||
n = child
|
n = child
|
||||||
@ -339,6 +349,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
|
|||||||
wildChild: true,
|
wildChild: true,
|
||||||
nType: catchAll,
|
nType: catchAll,
|
||||||
maxParams: 1,
|
maxParams: 1,
|
||||||
|
fullPath: fullPath,
|
||||||
}
|
}
|
||||||
n.children = []*node{child}
|
n.children = []*node{child}
|
||||||
n.indices = string(path[i])
|
n.indices = string(path[i])
|
||||||
@ -352,6 +363,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
|
|||||||
maxParams: 1,
|
maxParams: 1,
|
||||||
handlers: handlers,
|
handlers: handlers,
|
||||||
priority: 1,
|
priority: 1,
|
||||||
|
fullPath: fullPath,
|
||||||
}
|
}
|
||||||
n.children = []*node{child}
|
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
|
// insert remaining path part and handle to the leaf
|
||||||
n.path = path[offset:]
|
n.path = path[offset:]
|
||||||
n.handlers = handlers
|
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
|
// 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
|
// 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
|
// made if a handle exists with an extra (without the) trailing slash for the
|
||||||
// given path.
|
// given path.
|
||||||
func (n *node) getValue(path string, po Params, unescape bool) (handlers HandlersChain, p Params, tsr bool) {
|
func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
|
||||||
p = po
|
value.params = po
|
||||||
walk: // Outer loop for walking the tree
|
walk: // Outer loop for walking the tree
|
||||||
for {
|
for {
|
||||||
if len(path) > len(n.path) {
|
if len(path) > len(n.path) {
|
||||||
@ -391,7 +412,7 @@ walk: // Outer loop for walking the tree
|
|||||||
// Nothing found.
|
// Nothing found.
|
||||||
// We can recommend to redirect to the same URL without a
|
// We can recommend to redirect to the same URL without a
|
||||||
// trailing slash if a leaf exists for that path.
|
// trailing slash if a leaf exists for that path.
|
||||||
tsr = path == "/" && n.handlers != nil
|
value.tsr = path == "/" && n.handlers != nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,20 +427,20 @@ walk: // Outer loop for walking the tree
|
|||||||
}
|
}
|
||||||
|
|
||||||
// save param value
|
// save param value
|
||||||
if cap(p) < int(n.maxParams) {
|
if cap(value.params) < int(n.maxParams) {
|
||||||
p = make(Params, 0, n.maxParams)
|
value.params = make(Params, 0, n.maxParams)
|
||||||
}
|
}
|
||||||
i := len(p)
|
i := len(value.params)
|
||||||
p = p[:i+1] // expand slice within preallocated capacity
|
value.params = value.params[:i+1] // expand slice within preallocated capacity
|
||||||
p[i].Key = n.path[1:]
|
value.params[i].Key = n.path[1:]
|
||||||
val := path[:end]
|
val := path[:end]
|
||||||
if unescape {
|
if unescape {
|
||||||
var err error
|
var err error
|
||||||
if p[i].Value, err = url.QueryUnescape(val); err != nil {
|
if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
|
||||||
p[i].Value = val // fallback, in case of error
|
value.params[i].Value = val // fallback, in case of error
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
p[i].Value = val
|
value.params[i].Value = val
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need to go deeper!
|
// we need to go deeper!
|
||||||
@ -431,40 +452,42 @@ walk: // Outer loop for walking the tree
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ... but we can't
|
// ... but we can't
|
||||||
tsr = len(path) == end+1
|
value.tsr = len(path) == end+1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if handlers = n.handlers; handlers != nil {
|
if value.handlers = n.handlers; value.handlers != nil {
|
||||||
|
value.fullPath = n.fullPath
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(n.children) == 1 {
|
if len(n.children) == 1 {
|
||||||
// No handle found. Check if a handle for this path + a
|
// No handle found. Check if a handle for this path + a
|
||||||
// trailing slash exists for TSR recommendation
|
// trailing slash exists for TSR recommendation
|
||||||
n = n.children[0]
|
n = n.children[0]
|
||||||
tsr = n.path == "/" && n.handlers != nil
|
value.tsr = n.path == "/" && n.handlers != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
case catchAll:
|
case catchAll:
|
||||||
// save param value
|
// save param value
|
||||||
if cap(p) < int(n.maxParams) {
|
if cap(value.params) < int(n.maxParams) {
|
||||||
p = make(Params, 0, n.maxParams)
|
value.params = make(Params, 0, n.maxParams)
|
||||||
}
|
}
|
||||||
i := len(p)
|
i := len(value.params)
|
||||||
p = p[:i+1] // expand slice within preallocated capacity
|
value.params = value.params[:i+1] // expand slice within preallocated capacity
|
||||||
p[i].Key = n.path[2:]
|
value.params[i].Key = n.path[2:]
|
||||||
if unescape {
|
if unescape {
|
||||||
var err error
|
var err error
|
||||||
if p[i].Value, err = url.QueryUnescape(path); err != nil {
|
if value.params[i].Value, err = url.QueryUnescape(path); err != nil {
|
||||||
p[i].Value = path // fallback, in case of error
|
value.params[i].Value = path // fallback, in case of error
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
p[i].Value = path
|
value.params[i].Value = path
|
||||||
}
|
}
|
||||||
|
|
||||||
handlers = n.handlers
|
value.handlers = n.handlers
|
||||||
|
value.fullPath = n.fullPath
|
||||||
return
|
return
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -474,12 +497,13 @@ walk: // Outer loop for walking the tree
|
|||||||
} else if path == n.path {
|
} else if path == n.path {
|
||||||
// We should have reached the node containing the handle.
|
// We should have reached the node containing the handle.
|
||||||
// Check if this node has a handle registered.
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if path == "/" && n.wildChild && n.nType != root {
|
if path == "/" && n.wildChild && n.nType != root {
|
||||||
tsr = true
|
value.tsr = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -488,7 +512,7 @@ walk: // Outer loop for walking the tree
|
|||||||
for i := 0; i < len(n.indices); i++ {
|
for i := 0; i < len(n.indices); i++ {
|
||||||
if n.indices[i] == '/' {
|
if n.indices[i] == '/' {
|
||||||
n = n.children[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)
|
(n.nType == catchAll && n.children[0].handlers != nil)
|
||||||
return
|
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
|
// Nothing found. We can recommend to redirect to the same URL with an
|
||||||
// extra trailing slash if a leaf exists for that path
|
// 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)] == '/' &&
|
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
|
||||||
path == n.path[:len(n.path)-1] && n.handlers != nil)
|
path == n.path[:len(n.path)-1] && n.handlers != nil)
|
||||||
return
|
return
|
||||||
@ -514,7 +538,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
|
|||||||
ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory
|
ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory
|
||||||
|
|
||||||
// Outer loop for walking the tree
|
// 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):]
|
path = path[len(n.path):]
|
||||||
ciPath = append(ciPath, n.path...)
|
ciPath = append(ciPath, n.path...)
|
||||||
|
|
||||||
@ -618,7 +642,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
|
|||||||
return ciPath, true
|
return ciPath, true
|
||||||
}
|
}
|
||||||
if len(path)+1 == len(n.path) && n.path[len(path)] == '/' &&
|
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 {
|
n.handlers != nil {
|
||||||
return append(ciPath, n.path...), true
|
return append(ciPath, n.path...), true
|
||||||
}
|
}
|
||||||
|
26
tree_test.go
26
tree_test.go
@ -35,22 +35,22 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, request := range requests {
|
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 {
|
if !request.nilHandler {
|
||||||
t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
|
t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
|
||||||
}
|
}
|
||||||
} else if request.nilHandler {
|
} else if request.nilHandler {
|
||||||
t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
|
t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
|
||||||
} else {
|
} else {
|
||||||
handler[0](nil)
|
value.handlers[0](nil)
|
||||||
if fakeHandlerValue != request.route {
|
if fakeHandlerValue != request.route {
|
||||||
t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, 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)
|
t.Errorf("Params mismatch for route '%s'", request.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -454,10 +454,10 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
|
|||||||
"/doc/",
|
"/doc/",
|
||||||
}
|
}
|
||||||
for _, route := range tsrRoutes {
|
for _, route := range tsrRoutes {
|
||||||
handler, _, tsr := tree.getValue(route, nil, false)
|
value := tree.getValue(route, nil, false)
|
||||||
if handler != nil {
|
if value.handlers != nil {
|
||||||
t.Fatalf("non-nil handler for TSR route '%s", route)
|
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)
|
t.Errorf("expected TSR recommendation for route '%s'", route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -471,10 +471,10 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
|
|||||||
"/api/world/abc",
|
"/api/world/abc",
|
||||||
}
|
}
|
||||||
for _, route := range noTsrRoutes {
|
for _, route := range noTsrRoutes {
|
||||||
handler, _, tsr := tree.getValue(route, nil, false)
|
value := tree.getValue(route, nil, false)
|
||||||
if handler != nil {
|
if value.handlers != nil {
|
||||||
t.Fatalf("non-nil handler for No-TSR route '%s", route)
|
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)
|
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)
|
t.Fatalf("panic inserting test route: %v", recv)
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, _, tsr := tree.getValue("/", nil, false)
|
value := tree.getValue("/", nil, false)
|
||||||
if handler != nil {
|
if value.handlers != nil {
|
||||||
t.Fatalf("non-nil handler")
|
t.Fatalf("non-nil handler")
|
||||||
} else if tsr {
|
} else if value.tsr {
|
||||||
t.Errorf("expected no TSR recommendation")
|
t.Errorf("expected no TSR recommendation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
utils.go
2
utils.go
@ -146,6 +146,6 @@ func resolveAddress(addr []string) string {
|
|||||||
case 1:
|
case 1:
|
||||||
return addr[0]
|
return addr[0]
|
||||||
default:
|
default:
|
||||||
panic("too much parameters")
|
panic("too many parameters")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
140
vendor/vendor.json
vendored
140
vendor/vendor.json
vendored
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"comment": "v1.3.0",
|
"comment": "v1.4.0",
|
||||||
"ignore": "test",
|
"ignore": "test",
|
||||||
"package": [
|
"package": [
|
||||||
{
|
{
|
||||||
@ -11,34 +11,78 @@
|
|||||||
"versionExact": "v1.1.1"
|
"versionExact": "v1.1.1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=",
|
"checksumSHA1": "qlEzrgKgIkh7y0ePm9BNo1cNdXo=",
|
||||||
"path": "github.com/gin-contrib/sse",
|
"path": "github.com/gin-contrib/sse",
|
||||||
"revision": "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae",
|
"revision": "54d8467d122d380a14768b6b4e5cd7ca4755938f",
|
||||||
"revisionTime": "2017-01-09T09:34:21Z"
|
"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",
|
"path": "github.com/golang/protobuf/proto",
|
||||||
"revision": "aa810b61a9c79d51363740d207bb46cf8e620ed5",
|
"revision": "c823c79ea1570fb5ff454033735a8e68575d1d0f",
|
||||||
"revisionTime": "2018-08-14T21:14:27Z",
|
"revisionTime": "2019-02-05T22:20:52Z",
|
||||||
"version": "v1.2",
|
"version": "v1.3",
|
||||||
"versionExact": "v1.2.0"
|
"versionExact": "v1.3.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "WqeEgS7pqqkwK8mlrAZmDgtWJMY=",
|
"checksumSHA1": "zNo6yGy/bCJuzkEcP70oEBtOB2M=",
|
||||||
"path": "github.com/json-iterator/go",
|
"path": "github.com/leodido/go-urn",
|
||||||
"revision": "1624edc4454b8682399def8740d46db5e4362ba4",
|
"revision": "70078a794e8ea4b497ba7c19a78cd60f90ccf0f4",
|
||||||
"revisionTime": "2018-08-06T06:07:27Z",
|
"revisionTime": "2018-05-24T03:26:21Z",
|
||||||
"version": "v1.1",
|
"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",
|
"path": "github.com/mattn/go-isatty",
|
||||||
"revision": "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c",
|
"revision": "c2a7a6ca930a4cd0bc33a3f298eb71960732a3a7",
|
||||||
"revisionTime": "2017-11-07T05:05:31Z",
|
"revisionTime": "2019-03-12T13:58:54Z",
|
||||||
"version": "v0.0",
|
"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=",
|
"checksumSHA1": "LuFv4/jlrmFNnDb/5SCSEPAM9vU=",
|
||||||
@ -46,6 +90,20 @@
|
|||||||
"revision": "5d4384ee4fb2527b0a1256a821ebfc92f91efefc",
|
"revision": "5d4384ee4fb2527b0a1256a821ebfc92f91efefc",
|
||||||
"revisionTime": "2018-12-26T10:54:42Z"
|
"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=",
|
"checksumSHA1": "c6pbpF7eowwO59phRTpF8cQ80Z0=",
|
||||||
"path": "github.com/stretchr/testify/assert",
|
"path": "github.com/stretchr/testify/assert",
|
||||||
@ -55,40 +113,40 @@
|
|||||||
"versionExact": "v1.2.2"
|
"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",
|
"path": "github.com/ugorji/go/codec",
|
||||||
"revision": "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab",
|
"revision": "82dbfaf494e3b01d2d481376f11f6a5c8cf9599f",
|
||||||
"revisionTime": "2018-04-07T10:07:33Z",
|
"revisionTime": "2019-07-02T14:15:27Z",
|
||||||
"version": "v1.1",
|
"version": "v1.1",
|
||||||
"versionExact": "v1.1.1"
|
"versionExact": "v1.1.6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=",
|
"checksumSHA1": "2gaep1KNRDNyDA3O+KgPTQsGWvs=",
|
||||||
"path": "golang.org/x/net/context",
|
|
||||||
"revision": "49bb7cea24b1df9410e1712aa6433dae904ff66a",
|
|
||||||
"revisionTime": "2018-10-11T05:27:23Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"checksumSHA1": "SiJNkx+YGtq3Gtr6Ldu6OW83O+U=",
|
|
||||||
"path": "golang.org/x/sys/unix",
|
"path": "golang.org/x/sys/unix",
|
||||||
"revision": "fa43e7bc11baaae89f3f902b2b4d832b68234844",
|
"revision": "a43fa875dd822b81eb6d2ad538bc1f4caba169bd",
|
||||||
"revisionTime": "2018-10-11T14:35:51Z"
|
"revisionTime": "2019-05-02T15:41:39Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "P/k5ZGf0lEBgpKgkwy++F7K1PSg=",
|
"checksumSHA1": "ACzc7AkwLtNgKhqtj8V7SGUJgnw=",
|
||||||
"path": "gopkg.in/go-playground/validator.v8",
|
"path": "gopkg.in/go-playground/validator.v9",
|
||||||
"revision": "5f1438d3fca68893a817e4a66806cea46a9e4ebf",
|
"revision": "46b4b1e301c24cac870ffcb4ba5c8a703d1ef475",
|
||||||
"revisionTime": "2017-07-30T05:02:35Z",
|
"revisionTime": "2019-03-31T13:31:25Z",
|
||||||
"version": "v8.18.2",
|
"version": "v9.28",
|
||||||
"versionExact": "v8.18.2"
|
"versionExact": "v9.28.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "ZSWoOPUNRr5+3dhkLK3C4cZAQPk=",
|
"checksumSHA1": "QqDq2x8XOU7IoOR98Cx1eiV5QY8=",
|
||||||
"path": "gopkg.in/yaml.v2",
|
"path": "gopkg.in/yaml.v2",
|
||||||
"revision": "5420a8b6744d3b0345ab293f6fcba19c978f1183",
|
"revision": "51d6538a90f86fe93ac480b35f37b2be17fef232",
|
||||||
"revisionTime": "2018-03-28T19:50:20Z",
|
"revisionTime": "2018-11-15T11:05:04Z",
|
||||||
"version": "v2.2",
|
"version": "v2.2",
|
||||||
"versionExact": "v2.2.1"
|
"versionExact": "v2.2.2"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rootPath": "github.com/gin-gonic/gin"
|
"rootPath": "github.com/gin-gonic/gin"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user