Merge branch 'master' into master

This commit is contained in:
田欧 2019-05-22 13:14:49 +08:00 committed by GitHub
commit 7dfeb6d03e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1326 additions and 800 deletions

View File

@ -1,22 +1,17 @@
language: go language: go
sudo: false
go:
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- 1.11.x
- 1.12.x
- master
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- go: 1.8.x
- go: 1.9.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: master
env: GO111MODULE=on
git: git:
depth: 10 depth: 10

View File

@ -1,4 +1,60 @@
# CHANGELOG
### Gin 1.4.0
- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569)
- [NEW] Refactor of form mapping multipart requesta [#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-dispositon 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

View File

@ -13,7 +13,6 @@
Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
![Gin console logger](https://gin-gonic.github.io/gin/other/console.png)
## Contents ## Contents
@ -70,7 +69,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.8+ 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
@ -111,7 +110,7 @@ $ govendor fetch github.com/gin-gonic/gin@v1.3
4. Copy a starting template inside your project 4. Copy a starting template inside your project
```sh ```sh
$ curl https://raw.githubusercontent.com/gin-gonic/gin/master/examples/basic/main.go > main.go $ curl https://raw.githubusercontent.com/gin-gonic/examples/master/basic/main.go > main.go
``` ```
5. Run your project 5. Run your project
@ -120,10 +119,6 @@ $ curl https://raw.githubusercontent.com/gin-gonic/gin/master/examples/basic/mai
$ 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
@ -1697,9 +1692,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 ...")
@ -1836,24 +1831,6 @@ $ curl "http://localhost:8080/getd?field_x=hello&field_d=world"
{"d":"world","x":{"FieldX":"hello"}} {"d":"world","x":{"FieldX":"hello"}}
``` ```
**NOTE**: NOT support the follow style struct:
```go
type StructX struct {
X struct {} `form:"name_x"` // HERE have form
}
type StructY struct {
Y StructX `form:"name_y"` // HERE have form
}
type StructZ struct {
Z *StructZ `form:"name_z"` // HERE have form
}
```
In a word, only support nested custom struct which have no `form` now.
### Try to bind body into different structs ### Try to bind body into different structs
The normal methods for binding request body consumes `c.Request.Body` and they The normal methods for binding request body consumes `c.Request.Body` and they

View File

@ -98,7 +98,9 @@ func Default(method, contentType string) Binding {
return MsgPack return MsgPack
case MIMEYAML: case MIMEYAML:
return YAML return YAML
default: //case MIMEPOSTForm, MIMEMultipartPOSTForm: case MIMEMultipartPOSTForm:
return FormMultipart
default: // case MIMEPOSTForm:
return Form return Form
} }
} }

View File

@ -8,10 +8,13 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"io"
"io/ioutil" "io/ioutil"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings"
"testing" "testing"
"time" "time"
@ -21,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"`
} }
@ -30,6 +43,18 @@ type FooBarStruct struct {
Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" binding:"required"` Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" binding:"required"`
} }
type FooBarFileStruct struct {
FooBarStruct
File *multipart.FileHeader `form:"file" binding:"required"`
}
type FooBarFileFailStruct struct {
FooBarStruct
File *multipart.FileHeader `invalid_name:"file" binding:"required"`
// for unexport test
data *multipart.FileHeader `form:"data" binding:"required"`
}
type FooDefaultBarStruct struct { type FooDefaultBarStruct struct {
FooStruct FooStruct
Bar string `msgpack:"bar" json:"bar" form:"bar,default=hello" xml:"bar" binding:"required"` Bar string `msgpack:"bar" json:"bar" form:"bar,default=hello" xml:"bar" binding:"required"`
@ -57,7 +82,6 @@ type FooStructForTimeTypeFailLocation struct {
} }
type FooStructForMapType struct { type FooStructForMapType struct {
// Unknown type: not support map
MapFoo map[string]interface{} `form:"map_foo"` MapFoo map[string]interface{} `form:"map_foo"`
} }
@ -100,71 +124,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"`
@ -187,8 +146,8 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, Form, Default("POST", MIMEPOSTForm)) assert.Equal(t, Form, Default("POST", MIMEPOSTForm))
assert.Equal(t, Form, Default("PUT", MIMEPOSTForm)) assert.Equal(t, Form, Default("PUT", MIMEPOSTForm))
assert.Equal(t, Form, Default("POST", MIMEMultipartPOSTForm)) assert.Equal(t, FormMultipart, Default("POST", MIMEMultipartPOSTForm))
assert.Equal(t, Form, Default("PUT", MIMEMultipartPOSTForm)) assert.Equal(t, FormMultipart, Default("PUT", MIMEMultipartPOSTForm))
assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF)) assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF))
assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF)) assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF))
@ -240,6 +199,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",
"/", "/", "/", "/",
@ -303,7 +274,7 @@ func TestBindingFormInvalidName2(t *testing.T) {
func TestBindingFormForType(t *testing.T) { func TestBindingFormForType(t *testing.T) {
testFormBindingForType(t, "POST", testFormBindingForType(t, "POST",
"/", "/", "/", "/",
"map_foo=", "bar2=1", "Map") "map_foo={\"bar\":123}", "map_foo=1", "Map")
testFormBindingForType(t, "POST", testFormBindingForType(t, "POST",
"/", "/", "/", "/",
@ -321,110 +292,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")
@ -508,24 +375,82 @@ func TestBindingYAMLFail(t *testing.T) {
`foo:\nbar`, `bar: foo`) `foo:\nbar`, `bar: foo`)
} }
func createFormPostRequest() *http.Request { func createFormPostRequest(t *testing.T) *http.Request {
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo")) req, err := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo"))
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEPOSTForm) req.Header.Set("Content-Type", MIMEPOSTForm)
return req return req
} }
func createDefaultFormPostRequest() *http.Request { func createDefaultFormPostRequest(t *testing.T) *http.Request {
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar")) req, err := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar"))
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEPOSTForm) req.Header.Set("Content-Type", MIMEPOSTForm)
return req return req
} }
func createFormPostRequestFail() *http.Request { func createFormPostRequestForMap(t *testing.T) *http.Request {
req, _ := http.NewRequest("POST", "/?map_foo=getfoo", bytes.NewBufferString("map_foo=bar")) req, err := http.NewRequest("POST", "/?map_foo=getfoo", bytes.NewBufferString("map_foo={\"bar\":123}"))
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEPOSTForm) req.Header.Set("Content-Type", MIMEPOSTForm)
return req return req
} }
func createFormPostRequestForMapFail(t *testing.T) *http.Request {
req, err := http.NewRequest("POST", "/?map_foo=getfoo", bytes.NewBufferString("map_foo=hello"))
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEPOSTForm)
return req
}
func createFormFilesMultipartRequest(t *testing.T) *http.Request {
boundary := "--testboundary"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
defer mw.Close()
assert.NoError(t, mw.SetBoundary(boundary))
assert.NoError(t, mw.WriteField("foo", "bar"))
assert.NoError(t, mw.WriteField("bar", "foo"))
f, err := os.Open("form.go")
assert.NoError(t, err)
defer f.Close()
fw, err1 := mw.CreateFormFile("file", "form.go")
assert.NoError(t, err1)
io.Copy(fw, f)
req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
assert.NoError(t, err2)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
}
func createFormFilesMultipartRequestFail(t *testing.T) *http.Request {
boundary := "--testboundary"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
defer mw.Close()
assert.NoError(t, mw.SetBoundary(boundary))
assert.NoError(t, mw.WriteField("foo", "bar"))
assert.NoError(t, mw.WriteField("bar", "foo"))
f, err := os.Open("form.go")
assert.NoError(t, err)
defer f.Close()
fw, err1 := mw.CreateFormFile("file_foo", "form_foo.go")
assert.NoError(t, err1)
io.Copy(fw, f)
req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
assert.NoError(t, err2)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
}
func createFormMultipartRequest(t *testing.T) *http.Request { func createFormMultipartRequest(t *testing.T) *http.Request {
boundary := "--testboundary" boundary := "--testboundary"
body := new(bytes.Buffer) body := new(bytes.Buffer)
@ -535,26 +460,42 @@ func createFormMultipartRequest(t *testing.T) *http.Request {
assert.NoError(t, mw.SetBoundary(boundary)) assert.NoError(t, mw.SetBoundary(boundary))
assert.NoError(t, mw.WriteField("foo", "bar")) assert.NoError(t, mw.WriteField("foo", "bar"))
assert.NoError(t, mw.WriteField("bar", "foo")) assert.NoError(t, mw.WriteField("bar", "foo"))
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) req, err := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req return req
} }
func createFormMultipartRequestFail(t *testing.T) *http.Request { func createFormMultipartRequestForMap(t *testing.T) *http.Request {
boundary := "--testboundary" boundary := "--testboundary"
body := new(bytes.Buffer) body := new(bytes.Buffer)
mw := multipart.NewWriter(body) mw := multipart.NewWriter(body)
defer mw.Close() defer mw.Close()
assert.NoError(t, mw.SetBoundary(boundary)) assert.NoError(t, mw.SetBoundary(boundary))
assert.NoError(t, mw.WriteField("map_foo", "bar")) assert.NoError(t, mw.WriteField("map_foo", "{\"bar\":123, \"name\":\"thinkerou\", \"pai\": 3.14}"))
req, _ := http.NewRequest("POST", "/?map_foo=getfoo", body) req, err := http.NewRequest("POST", "/?map_foo=getfoo", body)
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
}
func createFormMultipartRequestForMapFail(t *testing.T) *http.Request {
boundary := "--testboundary"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
defer mw.Close()
assert.NoError(t, mw.SetBoundary(boundary))
assert.NoError(t, mw.WriteField("map_foo", "3.14"))
req, err := http.NewRequest("POST", "/?map_foo=getfoo", body)
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req return req
} }
func TestBindingFormPost(t *testing.T) { func TestBindingFormPost(t *testing.T) {
req := createFormPostRequest() req := createFormPostRequest(t)
var obj FooBarStruct var obj FooBarStruct
assert.NoError(t, FormPost.Bind(req, &obj)) assert.NoError(t, FormPost.Bind(req, &obj))
@ -564,7 +505,7 @@ func TestBindingFormPost(t *testing.T) {
} }
func TestBindingDefaultValueFormPost(t *testing.T) { func TestBindingDefaultValueFormPost(t *testing.T) {
req := createDefaultFormPostRequest() req := createDefaultFormPostRequest(t)
var obj FooDefaultBarStruct var obj FooDefaultBarStruct
assert.NoError(t, FormPost.Bind(req, &obj)) assert.NoError(t, FormPost.Bind(req, &obj))
@ -572,10 +513,46 @@ func TestBindingDefaultValueFormPost(t *testing.T) {
assert.Equal(t, "hello", obj.Bar) assert.Equal(t, "hello", obj.Bar)
} }
func TestBindingFormPostFail(t *testing.T) { func TestBindingFormPostForMap(t *testing.T) {
req := createFormPostRequestFail() req := createFormPostRequestForMap(t)
var obj FooStructForMapType var obj FooStructForMapType
err := FormPost.Bind(req, &obj) err := FormPost.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, float64(123), obj.MapFoo["bar"].(float64))
}
func TestBindingFormPostForMapFail(t *testing.T) {
req := createFormPostRequestForMapFail(t)
var obj FooStructForMapType
err := FormPost.Bind(req, &obj)
assert.Error(t, err)
}
func TestBindingFormFilesMultipart(t *testing.T) {
req := createFormFilesMultipartRequest(t)
var obj FooBarFileStruct
FormMultipart.Bind(req, &obj)
// file from os
f, _ := os.Open("form.go")
defer f.Close()
fileActual, _ := ioutil.ReadAll(f)
// file from multipart
mf, _ := obj.File.Open()
defer mf.Close()
fileExpect, _ := ioutil.ReadAll(mf)
assert.Equal(t, FormMultipart.Name(), "multipart/form-data")
assert.Equal(t, obj.Foo, "bar")
assert.Equal(t, obj.Bar, "foo")
assert.Equal(t, fileExpect, fileActual)
}
func TestBindingFormFilesMultipartFail(t *testing.T) {
req := createFormFilesMultipartRequestFail(t)
var obj FooBarFileFailStruct
err := FormMultipart.Bind(req, &obj)
assert.Error(t, err) assert.Error(t, err)
} }
@ -589,8 +566,18 @@ func TestBindingFormMultipart(t *testing.T) {
assert.Equal(t, "foo", obj.Bar) assert.Equal(t, "foo", obj.Bar)
} }
func TestBindingFormMultipartFail(t *testing.T) { func TestBindingFormMultipartForMap(t *testing.T) {
req := createFormMultipartRequestFail(t) req := createFormMultipartRequestForMap(t)
var obj FooStructForMapType
err := FormMultipart.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, float64(123), obj.MapFoo["bar"].(float64))
assert.Equal(t, "thinkerou", obj.MapFoo["name"].(string))
assert.Equal(t, float64(3.14), obj.MapFoo["pai"].(float64))
}
func TestBindingFormMultipartForMapFail(t *testing.T) {
req := createFormMultipartRequestForMapFail(t)
var obj FooStructForMapType var obj FooStructForMapType
err := FormMultipart.Bind(req, &obj) err := FormMultipart.Bind(req, &obj)
assert.Error(t, err) assert.Error(t, err)
@ -723,6 +710,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())
@ -773,6 +777,17 @@ func TestFormBindingFail(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestFormBindingMultipartFail(t *testing.T) {
obj := FooBarStruct{}
req, err := http.NewRequest("POST", "/", strings.NewReader("foo=bar"))
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+";boundary=testboundary")
_, err = req.MultipartReader()
assert.NoError(t, err)
err = Form.Bind(req, &obj)
assert.Error(t, err)
}
func TestFormPostBindingFail(t *testing.T) { func TestFormPostBindingFail(t *testing.T) {
b := FormPost b := FormPost
assert.Equal(t, "form-urlencoded", b.Name()) assert.Equal(t, "form-urlencoded", b.Name())
@ -931,149 +946,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)
@ -1109,7 +981,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
case "Map": case "Map":
obj := FooStructForMapType{} obj := FooStructForMapType{}
err := b.Bind(req, &obj) err := b.Bind(req, &obj)
assert.Error(t, err) assert.NoError(t, err)
assert.Equal(t, float64(123), obj.MapFoo["bar"].(float64))
case "SliceMap": case "SliceMap":
obj := FooStructForSliceMapType{} obj := FooStructForSliceMapType{}
err := b.Bind(req, &obj) err := b.Bind(req, &obj)
@ -1308,12 +1181,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))
}

View File

@ -4,7 +4,11 @@
package binding package binding
import "net/http" import (
"mime/multipart"
"net/http"
"reflect"
)
const defaultMemory = 32 * 1024 * 1024 const defaultMemory = 32 * 1024 * 1024
@ -53,8 +57,33 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseMultipartForm(defaultMemory); err != nil { if err := req.ParseMultipartForm(defaultMemory); err != nil {
return err return err
} }
if err := mapForm(obj, req.MultipartForm.Value); err != nil { if err := mappingByPtr(obj, (*multipartRequest)(req), "form"); err != nil {
return err return err
} }
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)
}

View File

@ -6,12 +6,17 @@ package binding
import ( import (
"errors" "errors"
"fmt"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin/internal/json"
) )
var errUnknownType = errors.New("Unknown type")
func mapUri(ptr interface{}, m map[string][]string) error { func mapUri(ptr interface{}, m map[string][]string) error {
return mapFormByTag(ptr, m, "uri") return mapFormByTag(ptr, m, "uri")
} }
@ -20,124 +25,193 @@ func mapForm(ptr interface{}, form map[string][]string) error {
return mapFormByTag(ptr, form, "form") return mapFormByTag(ptr, form, "form")
} }
var emptyField = reflect.StructField{}
func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error { func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
typ := reflect.TypeOf(ptr).Elem() return mappingByPtr(ptr, formSource(form), tag)
val := reflect.ValueOf(ptr).Elem()
for i := 0; i < typ.NumField(); i++ {
typeField := typ.Field(i)
structField := val.Field(i)
if !structField.CanSet() {
continue
}
structFieldKind := structField.Kind()
inputFieldName := typeField.Tag.Get(tag)
inputFieldNameList := strings.Split(inputFieldName, ",")
inputFieldName = inputFieldNameList[0]
var defaultValue string
if len(inputFieldNameList) > 1 {
defaultList := strings.SplitN(inputFieldNameList[1], "=", 2)
if defaultList[0] == "default" {
defaultValue = defaultList[1]
}
}
if inputFieldName == "-" {
continue
}
if inputFieldName == "" {
inputFieldName = typeField.Name
// if "form" tag is nil, we inspect if the field is a struct or struct pointer.
// this would not make sense for JSON parsing but it does for a form
// since data is flatten
if structFieldKind == reflect.Ptr {
if !structField.Elem().IsValid() {
structField.Set(reflect.New(structField.Type().Elem()))
}
structField = structField.Elem()
structFieldKind = structField.Kind()
}
if structFieldKind == reflect.Struct {
err := mapFormByTag(structField.Addr().Interface(), form, tag)
if err != nil {
return err
}
continue
}
}
inputValue, exists := form[inputFieldName]
if !exists {
if defaultValue == "" {
continue
}
inputValue = make([]string, 1)
inputValue[0] = defaultValue
}
numElems := len(inputValue)
if structFieldKind == reflect.Slice && numElems > 0 {
sliceOf := structField.Type().Elem().Kind()
slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
for i := 0; i < numElems; i++ {
if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil {
return err
}
}
val.Field(i).Set(slice)
continue
}
if _, isTime := structField.Interface().(time.Time); isTime {
if err := setTimeField(inputValue[0], typeField, structField); err != nil {
return err
}
continue
}
if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
return err
}
}
return nil
} }
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { // setter tries to set value on a walking by fields of a struct
switch valueKind { type setter interface {
case reflect.Int: TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error)
return setIntField(val, 0, structField) }
case reflect.Int8:
return setIntField(val, 8, structField) type formSource map[string][]string
case reflect.Int16:
return setIntField(val, 16, structField) var _ setter = formSource(nil)
case reflect.Int32:
return setIntField(val, 32, structField) // TrySet tries to set a value by request's form source (like map[string][]string)
case reflect.Int64: func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
return setIntField(val, 64, structField) return setByForm(value, field, form, tagValue, opt)
case reflect.Uint: }
return setUintField(val, 0, structField)
case reflect.Uint8: func mappingByPtr(ptr interface{}, setter setter, tag string) error {
return setUintField(val, 8, structField) _, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)
case reflect.Uint16: return err
return setUintField(val, 16, structField) }
case reflect.Uint32:
return setUintField(val, 32, structField) func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
case reflect.Uint64: var vKind = value.Kind()
return setUintField(val, 64, structField)
case reflect.Bool: if vKind == reflect.Ptr {
return setBoolField(val, structField) var isNew bool
case reflect.Float32: vPtr := value
return setFloatField(val, 32, structField) if value.IsNil() {
case reflect.Float64: isNew = true
return setFloatField(val, 64, structField) vPtr = reflect.New(value.Type().Elem())
case reflect.String:
structField.SetString(val)
case reflect.Ptr:
if !structField.Elem().IsValid() {
structField.Set(reflect.New(structField.Type().Elem()))
} }
structFieldElem := structField.Elem() isSetted, err := mapping(vPtr.Elem(), field, setter, tag)
return setWithProperType(structFieldElem.Kind(), val, structFieldElem) if err != nil {
return false, err
}
if isNew && isSetted {
value.Set(vPtr)
}
return isSetted, nil
}
if vKind != reflect.Struct || !field.Anonymous {
ok, err := tryToSetValue(value, field, setter, tag)
if err != nil {
return false, err
}
if ok {
return true, nil
}
}
if vKind == reflect.Struct {
tValue := value.Type()
var isSetted bool
for i := 0; i < value.NumField(); i++ {
sf := tValue.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag)
if err != nil {
return false, err
}
isSetted = isSetted || ok
}
return isSetted, nil
}
return false, nil
}
type setOptions struct {
isDefaultExists bool
defaultValue string
}
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
var tagValue string
var setOpt setOptions
tagValue = field.Tag.Get(tag)
tagValue, opts := head(tagValue, ",")
if tagValue == "-" { // just ignoring this field
return false, nil
}
if tagValue == "" { // default value is FieldName
tagValue = field.Name
}
if tagValue == "" { // when field is "emptyField" variable
return false, nil
}
var opt string
for len(opts) > 0 {
opt, opts = head(opts, ",")
if k, v := head(opt, "="); k == "default" {
setOpt.isDefaultExists = true
setOpt.defaultValue = v
}
}
return setter.TrySet(value, field, tagValue, setOpt)
}
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSetted bool, err error) {
vs, ok := form[tagValue]
if !ok && !opt.isDefaultExists {
return false, nil
}
switch value.Kind() {
case reflect.Slice:
if !ok {
vs = []string{opt.defaultValue}
}
return true, setSlice(vs, value, field)
case reflect.Array:
if !ok {
vs = []string{opt.defaultValue}
}
if len(vs) != value.Len() {
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
}
return true, setArray(vs, value, field)
default: default:
return errors.New("Unknown type") var val string
if !ok {
val = opt.defaultValue
}
if len(vs) > 0 {
val = vs[0]
}
return true, setWithProperType(val, value, field)
}
}
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
switch value.Kind() {
case reflect.Int:
return setIntField(val, 0, value)
case reflect.Int8:
return setIntField(val, 8, value)
case reflect.Int16:
return setIntField(val, 16, value)
case reflect.Int32:
return setIntField(val, 32, value)
case reflect.Int64:
switch value.Interface().(type) {
case time.Duration:
return setTimeDuration(val, value, field)
}
return setIntField(val, 64, value)
case reflect.Uint:
return setUintField(val, 0, value)
case reflect.Uint8:
return setUintField(val, 8, value)
case reflect.Uint16:
return setUintField(val, 16, value)
case reflect.Uint32:
return setUintField(val, 32, value)
case reflect.Uint64:
return setUintField(val, 64, value)
case reflect.Bool:
return setBoolField(val, value)
case reflect.Float32:
return setFloatField(val, 32, value)
case reflect.Float64:
return setFloatField(val, 64, value)
case reflect.String:
value.SetString(val)
case reflect.Struct:
switch value.Interface().(type) {
case time.Time:
return setTimeField(val, field, value)
}
return json.Unmarshal([]byte(val), value.Addr().Interface())
case reflect.Map:
return json.Unmarshal([]byte(val), value.Addr().Interface())
default:
return errUnknownType
} }
return nil return nil
} }
@ -218,3 +292,40 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
value.Set(reflect.ValueOf(t)) value.Set(reflect.ValueOf(t))
return nil return nil
} }
func setArray(vals []string, value reflect.Value, field reflect.StructField) error {
for i, s := range vals {
err := setWithProperType(s, value.Index(i), field)
if err != nil {
return err
}
}
return nil
}
func setSlice(vals []string, value reflect.Value, field reflect.StructField) error {
slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
err := setArray(vals, slice, field)
if err != nil {
return err
}
value.Set(slice)
return nil
}
func setTimeDuration(val string, value reflect.Value, field reflect.StructField) error {
d, err := time.ParseDuration(val)
if err != nil {
return err
}
value.Set(reflect.ValueOf(d))
return nil
}
func head(str, sep string) (head string, tail string) {
idx := strings.Index(str, sep)
if idx < 0 {
return str, ""
}
return str[:idx], str[idx+len(sep):]
}

View File

@ -0,0 +1,61 @@
// 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"
"time"
"github.com/stretchr/testify/assert"
)
var form = map[string][]string{
"name": {"mike"},
"friends": {"anna", "nicole"},
"id_number": {"12345678"},
"id_date": {"2018-01-20"},
}
type structFull struct {
Name string `form:"name"`
Age int `form:"age,default=25"`
Friends []string `form:"friends"`
ID *struct {
Number string `form:"id_number"`
DateOfIssue time.Time `form:"id_date" time_format:"2006-01-02" time_utc:"true"`
}
Nationality *string `form:"nationality"`
}
func BenchmarkMapFormFull(b *testing.B) {
var s structFull
for i := 0; i < b.N; i++ {
mapForm(&s, form)
}
b.StopTimer()
t := b
assert.Equal(t, "mike", s.Name)
assert.Equal(t, 25, s.Age)
assert.Equal(t, []string{"anna", "nicole"}, s.Friends)
assert.Equal(t, "12345678", s.ID.Number)
assert.Equal(t, time.Date(2018, 1, 20, 0, 0, 0, 0, time.UTC), s.ID.DateOfIssue)
assert.Nil(t, s.Nationality)
}
type structName struct {
Name string `form:"name"`
}
func BenchmarkMapFormName(b *testing.B) {
var s structName
for i := 0; i < b.N; i++ {
mapForm(&s, form)
}
b.StopTimer()
t := b
assert.Equal(t, "mike", s.Name)
}

View File

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

View File

@ -442,11 +442,6 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
if values := req.PostForm[key]; len(values) > 0 { if values := req.PostForm[key]; len(values) > 0 {
return values, true return values, true
} }
if req.MultipartForm != nil && req.MultipartForm.File != nil {
if values := req.MultipartForm.Value[key]; len(values) > 0 {
return values, true
}
}
return []string{}, false return []string{}, false
} }
@ -465,13 +460,7 @@ func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
debugPrint("error on parse multipart form map: %v", err) debugPrint("error on parse multipart form map: %v", err)
} }
} }
dicts, exist := c.get(req.PostForm, key) return 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.
@ -685,7 +674,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
@ -831,6 +820,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{}) {

View File

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

View File

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

View File

@ -622,8 +622,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 +660,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"))
} }
@ -689,7 +688,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 +714,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 +793,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 +1103,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 +1117,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 +1281,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) {

View File

@ -8,13 +8,12 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"html/template" "html/template"
"os"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
) )
const ginSupportMinGoVer = 6 const ginSupportMinGoVer = 8
// 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.
@ -54,7 +53,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 +68,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.8 or later and Go 1.9 will be required soon.
`) `)
} }
@ -98,6 +97,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)
}
} }
} }

View File

@ -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.8 or later and Go 1.9 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)

2
doc.go
View File

@ -1,6 +1,6 @@
/* /*
Package gin implements a HTTP web framework called gin. Package gin implements a HTTP web framework called gin.
See https://gin-gonic.github.io/gin/ for more information about gin. See https://gin-gonic.com/ for more information about gin.
*/ */
package gin // import "github.com/gin-gonic/gin" package gin // import "github.com/gin-gonic/gin"

View File

@ -53,7 +53,7 @@ func (msg *Error) SetMeta(data interface{}) *Error {
return msg return msg
} }
// JSON creates a properly formated JSON // JSON creates a properly formatted JSON
func (msg *Error) JSON() interface{} { func (msg *Error) JSON() interface{} {
json := H{} json := H{}
if msg.Meta != nil { if msg.Meta != nil {

View File

@ -1,3 +1,3 @@
# Gin Examples # Gin Examples
⚠️ **NOTICE:** All gin examples has moved as alone repository to [here](https://github.com/gin-gonic/examples). ⚠️ **NOTICE:** All gin examples have been moved as standalone repository to [here](https://github.com/gin-gonic/examples).

19
gin.go
View File

@ -225,7 +225,7 @@ func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
engine.rebuild405Handlers() engine.rebuild405Handlers()
} }
// Use attachs a global middleware to the router. ie. the middleware attached though Use() will be // Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files... // included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware. // For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes { func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
@ -366,12 +366,13 @@ func (engine *Engine) HandleContext(c *Context) {
func (engine *Engine) handleHTTPRequest(c *Context) { func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method httpMethod := c.Request.Method
path := c.Request.URL.Path rPath := c.Request.URL.Path
unescape := false unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 { if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
path = 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,7 +382,7 @@ 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(path, c.Params, unescape) handlers, params, tsr := root.getValue(rPath, c.Params, unescape)
if handlers != nil { if handlers != nil {
c.handlers = handlers c.handlers = handlers
c.Params = params c.Params = params
@ -389,7 +390,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
c.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()
return return
} }
if httpMethod != "CONNECT" && path != "/" { if httpMethod != "CONNECT" && rPath != "/" {
if tsr && engine.RedirectTrailingSlash { if tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c) redirectTrailingSlash(c)
return return
@ -406,7 +407,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
if tree.method == httpMethod { if tree.method == httpMethod {
continue continue
} }
if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil { if handlers, _, _ := tree.root.getValue(rPath, nil, unescape); handlers != nil {
c.handlers = engine.allNoMethod c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body) serveError(c, http.StatusMethodNotAllowed, default405Body)
return return
@ -459,15 +460,15 @@ func redirectTrailingSlash(c *Context) {
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool { func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
req := c.Request req := c.Request
path := req.URL.Path rPath := req.URL.Path
if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(path), trailingSlash); ok { if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok {
code := http.StatusMovedPermanently // Permanent redirect, request with GET method code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != "GET" { if req.Method != "GET" {
code = http.StatusTemporaryRedirect code = http.StatusTemporaryRedirect
} }
req.URL.Path = string(fixedPath) req.URL.Path = string(fixedPath)
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) debugPrint("redirecting request %d: %s --> %s", code, rPath, req.URL.String())
http.Redirect(c.Writer, req, req.URL.String(), code) http.Redirect(c.Writer, req, req.URL.String(), code)
c.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()
return true return true

View File

@ -118,30 +118,42 @@ func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes {
return engine().StaticFS(relativePath, fs) return engine().StaticFS(relativePath, fs)
} }
// Use attachs a global middleware to the router. ie. the middlewares attached though Use() will be // Use attaches a global middleware to the router. ie. the middlewares attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files... // included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware. // For example, this is the right place for a logger or error management middleware.
func Use(middlewares ...gin.HandlerFunc) gin.IRoutes { func Use(middlewares ...gin.HandlerFunc) gin.IRoutes {
return engine().Use(middlewares...) return engine().Use(middlewares...)
} }
// Run : The router is attached to a http.Server and starts listening and serving HTTP requests. // Routes returns a slice of registered routes.
func Routes() gin.RoutesInfo {
return engine().Routes()
}
// Run attaches to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router) // It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens. // Note: this method will block the calling goroutine indefinitely unless an error happens.
func Run(addr ...string) (err error) { func Run(addr ...string) (err error) {
return engine().Run(addr...) return engine().Run(addr...)
} }
// RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests. // RunTLS attaches to a http.Server and starts listening and serving HTTPS requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens. // Note: this method will block the calling goroutine indefinitely unless an error happens.
func RunTLS(addr, certFile, keyFile string) (err error) { func RunTLS(addr, certFile, keyFile string) (err error) {
return engine().RunTLS(addr, certFile, keyFile) return engine().RunTLS(addr, certFile, keyFile)
} }
// RunUnix : The router is attached to a http.Server and starts listening and serving HTTP requests // RunUnix attaches to a http.Server and starts listening and serving HTTP requests
// through the specified unix socket (ie. a file) // through the specified unix socket (ie. a file)
// Note: this method will block the calling goroutine indefinitely unless an error happens. // Note: this method will block the calling goroutine indefinitely unless an error happens.
func RunUnix(file string) (err error) { func RunUnix(file string) (err error) {
return engine().RunUnix(file) return engine().RunUnix(file)
} }
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified file descriptor.
// Note: the method will block the calling goroutine indefinitely unless on error happens.
func RunFd(fd int) (err error) {
return engine().RunFd(fd)
}

View File

@ -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()

16
go.mod
View File

@ -1,17 +1,17 @@
module github.com/gin-gonic/gin module github.com/gin-gonic/gin
go 1.12
require ( require (
github.com/gin-contrib/sse v0.0.0-20190125020943-a7658810eb74 github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3
github.com/golang/protobuf v1.2.0 github.com/golang/protobuf v1.3.1
github.com/json-iterator/go v1.1.5 github.com/json-iterator/go v1.1.6
github.com/mattn/go-isatty v0.0.4 github.com/mattn/go-isatty v0.0.7
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/stretchr/testify v1.3.0 github.com/stretchr/testify v1.3.0
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 github.com/ugorji/go v1.1.4
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0 // indirect
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.v8 v8.18.2
gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v2 v2.2.2

38
go.sum
View File

@ -1,30 +1,34 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 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-20190125020943-a7658810eb74 h1:FaI7wNyesdMBSkIRVUuEEYEvmzufs7EqQvRAxfEXGbQ= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
github.com/gin-contrib/sse v0.0.0-20190125020943-a7658810eb74/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/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 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/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw=
github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 h1:BasDe+IErOQKrMVXab7UayvSlIpiyGwRvuX3EKYY7UA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0 h1:bzeyCHgoAyjZjAhvTpks+qM7sdlh4cCSitmXeCEO3B4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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/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.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=

View File

@ -11,6 +11,8 @@ import "encoding/json"
var ( var (
// Marshal is exported by gin/json package. // Marshal is exported by gin/json package.
Marshal = json.Marshal Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package. // MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package. // NewDecoder is exported by gin/json package.

View File

@ -12,6 +12,8 @@ var (
json = jsoniter.ConfigCompatibleWithStandardLibrary json = jsoniter.ConfigCompatibleWithStandardLibrary
// Marshal is exported by gin/json package. // Marshal is exported by gin/json package.
Marshal = json.Marshal Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package. // MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package. // NewDecoder is exported by gin/json package.

View File

@ -14,17 +14,24 @@ import (
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
) )
type consoleColorModeValue int
const (
autoColor consoleColorModeValue = iota
disableColor
forceColor
)
var ( var (
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109}) yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109})
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
reset = string([]byte{27, 91, 48, 109}) reset = string([]byte{27, 91, 48, 109})
disableColor = false consoleColorMode = autoColor
forceColor = false
) )
// LoggerConfig defines the config for Logger middleware. // LoggerConfig defines the config for Logger middleware.
@ -62,10 +69,12 @@ type LogFormatterParams struct {
Path string Path string
// ErrorMessage is set if error has occurred in processing the request. // ErrorMessage is set if error has occurred in processing the request.
ErrorMessage string ErrorMessage string
// IsTerm shows whether does gin's output descriptor refers to a terminal. // isTerm shows whether does gin's output descriptor refers to a terminal.
IsTerm bool isTerm bool
// BodySize is the size of the Response Body // BodySize is the size of the Response Body
BodySize int BodySize int
// Keys are the keys set on the request's context.
Keys map[string]interface{}
} }
// StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal. // StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal.
@ -113,15 +122,24 @@ func (p *LogFormatterParams) ResetColor() string {
return reset return reset
} }
// IsOutputColor indicates whether can colors be outputted to the log.
func (p *LogFormatterParams) IsOutputColor() bool {
return consoleColorMode == forceColor || (consoleColorMode == autoColor && p.isTerm)
}
// defaultLogFormatter is the default log format function Logger middleware uses. // defaultLogFormatter is the default log format function Logger middleware uses.
var defaultLogFormatter = func(param LogFormatterParams) string { var defaultLogFormatter = func(param LogFormatterParams) string {
var statusColor, methodColor, resetColor string var statusColor, methodColor, resetColor string
if param.IsTerm { if param.IsOutputColor() {
statusColor = param.StatusCodeColor() statusColor = param.StatusCodeColor()
methodColor = param.MethodColor() methodColor = param.MethodColor()
resetColor = param.ResetColor() resetColor = param.ResetColor()
} }
if param.Latency > time.Minute {
// Truncate in a golang < 1.8 safe way
param.Latency = param.Latency - param.Latency%time.Second
}
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"), param.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, param.StatusCode, resetColor, statusColor, param.StatusCode, resetColor,
@ -135,12 +153,12 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
// DisableConsoleColor disables color output in the console. // DisableConsoleColor disables color output in the console.
func DisableConsoleColor() { func DisableConsoleColor() {
disableColor = true consoleColorMode = disableColor
} }
// ForceConsoleColor force color output in the console. // ForceConsoleColor force color output in the console.
func ForceConsoleColor() { func ForceConsoleColor() {
forceColor = true consoleColorMode = forceColor
} }
// ErrorLogger returns a handlerfunc for any error type. // ErrorLogger returns a handlerfunc for any error type.
@ -197,9 +215,8 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
isTerm := true isTerm := true
if w, ok := out.(*os.File); (!ok || if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" ||
(os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd()))) || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) {
disableColor) && !forceColor {
isTerm = false isTerm = false
} }
@ -226,7 +243,8 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
if _, ok := skip[path]; !ok { if _, ok := skip[path]; !ok {
param := LogFormatterParams{ param := LogFormatterParams{
Request: c.Request, Request: c.Request,
IsTerm: isTerm, isTerm: isTerm,
Keys: c.Keys,
} }
// Stop timer // Stop timer

View File

@ -181,6 +181,7 @@ func TestLoggerWithFormatter(t *testing.T) {
func TestLoggerWithConfigFormatting(t *testing.T) { func TestLoggerWithConfigFormatting(t *testing.T) {
var gotParam LogFormatterParams var gotParam LogFormatterParams
var gotKeys map[string]interface{}
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
router := New() router := New()
@ -204,6 +205,7 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
router.GET("/example", func(c *Context) { router.GET("/example", func(c *Context) {
// set dummy ClientIP // set dummy ClientIP
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20") c.Request.Header.Set("X-Forwarded-For", "20.20.20.20")
gotKeys = c.Keys
}) })
performRequest(router, "GET", "/example?a=100") performRequest(router, "GET", "/example?a=100")
@ -223,6 +225,7 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
assert.Equal(t, "GET", gotParam.Method) assert.Equal(t, "GET", gotParam.Method)
assert.Equal(t, "/example?a=100", gotParam.Path) assert.Equal(t, "/example?a=100", gotParam.Path)
assert.Empty(t, gotParam.ErrorMessage) assert.Empty(t, gotParam.ErrorMessage)
assert.Equal(t, gotKeys, gotParam.Keys)
} }
@ -237,7 +240,7 @@ func TestDefaultLogFormatter(t *testing.T) {
Method: "GET", Method: "GET",
Path: "/", Path: "/",
ErrorMessage: "", ErrorMessage: "",
IsTerm: false, isTerm: false,
} }
termTrueParam := LogFormatterParams{ termTrueParam := LogFormatterParams{
@ -248,12 +251,36 @@ func TestDefaultLogFormatter(t *testing.T) {
Method: "GET", Method: "GET",
Path: "/", Path: "/",
ErrorMessage: "", ErrorMessage: "",
IsTerm: true, isTerm: true,
}
termTrueLongDurationParam := LogFormatterParams{
TimeStamp: timeStamp,
StatusCode: 200,
Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20",
Method: "GET",
Path: "/",
ErrorMessage: "",
isTerm: true,
}
termFalseLongDurationParam := LogFormatterParams{
TimeStamp: timeStamp,
StatusCode: 200,
Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20",
Method: "GET",
Path: "/",
ErrorMessage: "",
isTerm: false,
} }
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseLongDurationParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueLongDurationParam))
} }
func TestColorForMethod(t *testing.T) { func TestColorForMethod(t *testing.T) {
@ -293,6 +320,39 @@ func TestResetColor(t *testing.T) {
assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor()) assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor())
} }
func TestIsOutputColor(t *testing.T) {
// test with isTerm flag true.
p := LogFormatterParams{
isTerm: true,
}
consoleColorMode = autoColor
assert.Equal(t, true, p.IsOutputColor())
ForceConsoleColor()
assert.Equal(t, true, p.IsOutputColor())
DisableConsoleColor()
assert.Equal(t, false, p.IsOutputColor())
// test with isTerm flag false.
p = LogFormatterParams{
isTerm: false,
}
consoleColorMode = autoColor
assert.Equal(t, false, p.IsOutputColor())
ForceConsoleColor()
assert.Equal(t, true, p.IsOutputColor())
DisableConsoleColor()
assert.Equal(t, false, p.IsOutputColor())
// reset console color mode.
consoleColorMode = autoColor
}
func TestErrorLogger(t *testing.T) { func TestErrorLogger(t *testing.T) {
router := New() router := New()
router.Use(ErrorLogger()) router.Use(ErrorLogger())
@ -309,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) {
@ -355,14 +415,20 @@ func TestLoggerWithConfigSkippingPaths(t *testing.T) {
func TestDisableConsoleColor(t *testing.T) { func TestDisableConsoleColor(t *testing.T) {
New() New()
assert.False(t, disableColor) assert.Equal(t, autoColor, consoleColorMode)
DisableConsoleColor() DisableConsoleColor()
assert.True(t, disableColor) assert.Equal(t, disableColor, consoleColorMode)
// reset console color mode.
consoleColorMode = autoColor
} }
func TestForceConsoleColor(t *testing.T) { func TestForceConsoleColor(t *testing.T) {
New() New()
assert.False(t, forceColor) assert.Equal(t, autoColor, consoleColorMode)
ForceConsoleColor() ForceConsoleColor()
assert.True(t, forceColor) assert.Equal(t, forceColor, consoleColorMode)
// reset console color mode.
consoleColorMode = autoColor
} }

View File

@ -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))
} }

View File

@ -52,12 +52,19 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
} }
if logger != nil { if logger != nil {
stack := stack(3) stack := stack(3)
httprequest, _ := httputil.DumpRequest(c.Request, false) httpRequest, _ := httputil.DumpRequest(c.Request, false)
headers := strings.Split(string(httpRequest), "\r\n")
for idx, header := range headers {
current := strings.Split(header, ":")
if current[0] == "Authorization" {
headers[idx] = current[0] + ": *"
}
}
if brokenPipe { if brokenPipe {
logger.Printf("%s\n%s%s", err, string(httprequest), reset) logger.Printf("%s\n%s%s", err, string(httpRequest), reset)
} else if IsDebugging() { } else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), string(httprequest), err, stack, reset) timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset)
} else { } else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset) timeFormat(time.Now()), err, stack, reset)
@ -128,8 +135,8 @@ func function(pc uintptr) []byte {
// *T.ptrmethod // *T.ptrmethod
// Also the package path might contains dot (e.g. code.google.com/...), // Also the package path might contains dot (e.g. code.google.com/...),
// so first eliminate the path prefix // so first eliminate the path prefix
if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 { if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
name = name[lastslash+1:] name = name[lastSlash+1:]
} }
if period := bytes.Index(name, dot); period >= 0 { if period := bytes.Index(name, dot); period >= 0 {
name = name[period+1:] name = name[period+1:]

View File

@ -2,12 +2,11 @@
// 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 (
"bytes" "bytes"
"fmt"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -18,6 +17,37 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestPanicClean(t *testing.T) {
buffer := new(bytes.Buffer)
router := New()
password := "my-super-secret-password"
router.Use(RecoveryWithWriter(buffer))
router.GET("/recovery", func(c *Context) {
c.AbortWithStatus(http.StatusBadRequest)
panic("Oupps, Houston, we have a problem")
})
// RUN
w := performRequest(router, "GET", "/recovery",
header{
Key: "Host",
Value: "www.google.com",
},
header{
Key: "Authorization",
Value: fmt.Sprintf("Bearer %s", password),
},
header{
Key: "Content-Type",
Value: "application/json",
},
)
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
// Check the buffer does not have the secret key
assert.NotContains(t, buffer.String(), password)
}
// TestPanicInHandler assert that panic has been recovered. // TestPanicInHandler assert that panic has been recovered.
func TestPanicInHandler(t *testing.T) { func TestPanicInHandler(t *testing.T) {
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)

View File

@ -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
} }
@ -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)
}

View File

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

View File

@ -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)

View File

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

View File

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

View File

@ -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
}

View File

@ -1,12 +0,0 @@
// +build !go1.8
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
// ResponseWriter ...
type ResponseWriter interface {
responseWriterBase
}

View File

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

View File

@ -195,7 +195,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
// Check if file exists and/or if we have permission to access it // Check if file exists and/or if we have permission to access it
if _, err := fs.Open(file); err != nil { if _, err := fs.Open(file); err != nil {
c.Writer.WriteHeader(http.StatusNotFound) c.Writer.WriteHeader(http.StatusNotFound)
c.handlers = group.engine.allNoRoute c.handlers = group.engine.noRoute
// Reset index // Reset index
c.index = -1 c.index = -1
return return

View File

@ -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
@ -386,15 +419,14 @@ func TestRouterNotFound(t *testing.T) {
code int code int
location string location string
}{ }{
{"/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.StatusOK, ""}, // CleanPath
{"/../path", http.StatusMovedPermanently, "/path"}, // CleanPath {"/nope", http.StatusNotFound, ""}, // NotFound
{"/nope", http.StatusNotFound, ""}, // NotFound
} }
for _, tr := range testRoutes { for _, tr := range testRoutes {
w := performRequest(router, "GET", tr.route) w := performRequest(router, "GET", tr.route)
@ -429,7 +461,6 @@ func TestRouterNotFound(t *testing.T) {
func TestRouterStaticFSNotFound(t *testing.T) { func TestRouterStaticFSNotFound(t *testing.T) {
router := New() router := New()
router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/"))) router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/")))
router.NoRoute(func(c *Context) { router.NoRoute(func(c *Context) {
c.String(404, "non existent") c.String(404, "non existent")
@ -452,6 +483,27 @@ func TestRouterStaticFSFileNotFound(t *testing.T) {
}) })
} }
// Reproduction test for the bug of issue #1805
func TestMiddlewareCalledOnceByRouterStaticFSNotFound(t *testing.T) {
router := New()
// Middleware must be called just only once by per request.
middlewareCalledNum := 0
router.Use(func(c *Context) {
middlewareCalledNum += 1
})
router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/")))
// First access
performRequest(router, "GET", "/nonexistent")
assert.Equal(t, 1, middlewareCalledNum)
// Second access
performRequest(router, "HEAD", "/nonexistent")
assert.Equal(t, 2, middlewareCalledNum)
}
func TestRouteRawPath(t *testing.T) { func TestRouteRawPath(t *testing.T) {
route := New() route := New()
route.UseRawPath = true route.UseRawPath = true

View File

@ -514,7 +514,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 +618,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
} }

86
vendor/vendor.json vendored
View File

@ -1,5 +1,5 @@
{ {
"comment": "v1.3.0", "comment": "v1.4.0",
"ignore": "test", "ignore": "test",
"package": [ "package": [
{ {
@ -13,32 +13,44 @@
{ {
"checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=", "checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=",
"path": "github.com/gin-contrib/sse", "path": "github.com/gin-contrib/sse",
"revision": "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae", "revision": "5545eab6dad3bbbd6c5ae9186383c2a9d23c0dae",
"revisionTime": "2017-01-09T09:34:21Z" "revisionTime": "2019-03-01T06:25:29Z"
}, },
{ {
"checksumSHA1": "mE9XW26JSpe4meBObM6J/Oeq0eg=", "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": "TB2vxux9xQbvsTHOVt4aRTuvSn4=",
"path": "github.com/json-iterator/go", "path": "github.com/json-iterator/go",
"revision": "1624edc4454b8682399def8740d46db5e4362ba4", "revision": "0ff49de124c6f76f8494e194af75bde0f1a49a29",
"revisionTime": "2018-08-06T06:07:27Z", "revisionTime": "2019-03-06T14:29:09Z",
"version": "v1.1", "version": "v1.1",
"versionExact": "v1.1.5" "versionExact": "v1.1.6"
}, },
{ {
"checksumSHA1": "w5RcOnfv5YDr3j2bd1YydkPiZx4=", "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 +58,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,24 +81,24 @@
"versionExact": "v1.2.2" "versionExact": "v1.2.2"
}, },
{ {
"checksumSHA1": "5Bd8RPhhaKcEXkagzPqymP4Gx5E=", "checksumSHA1": "csplo594qomjp2IZj82y7mTueOw=",
"path": "github.com/ugorji/go/codec", "path": "github.com/ugorji/go/codec",
"revision": "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab", "revision": "2adff0894ba3bc2eeb9f9aea45fefd49802e1a13",
"revisionTime": "2018-04-07T10:07:33Z", "revisionTime": "2019-04-08T19:08:48Z",
"version": "v1.1", "version": "v1.1",
"versionExact": "v1.1.1" "versionExact": "v1.1.4"
}, },
{ {
"checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=", "checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=",
"path": "golang.org/x/net/context", "path": "golang.org/x/net/context",
"revision": "49bb7cea24b1df9410e1712aa6433dae904ff66a", "revision": "f4e77d36d62c17c2336347bb2670ddbd02d092b7",
"revisionTime": "2018-10-11T05:27:23Z" "revisionTime": "2019-05-02T22:26:14Z"
}, },
{ {
"checksumSHA1": "SiJNkx+YGtq3Gtr6Ldu6OW83O+U=", "checksumSHA1": "2gaep1KNRDNyDA3O+KgPTQsGWvs=",
"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": "P/k5ZGf0lEBgpKgkwy++F7K1PSg=",
@ -83,13 +109,13 @@
"versionExact": "v8.18.2" "versionExact": "v8.18.2"
}, },
{ {
"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"
} }