Merge branch 'master' into patch-1

This commit is contained in:
田欧 2019-02-20 10:09:45 +08:00 committed by GitHub
commit 0179a5e0d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1728 additions and 476 deletions

2
.gitignore vendored
View File

@ -3,3 +3,5 @@ vendor/*
coverage.out coverage.out
count.out count.out
test test
profile.out
tmp.out

View File

@ -10,14 +10,21 @@ go:
- master - master
matrix: matrix:
allow_failures: fast_finish: true
- go: master include:
- go: 1.11.x
env: GO111MODULE=on
git: git:
depth: 10 depth: 10
before_install:
- if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi
install: install:
- make install - if [[ "${GO111MODULE}" = "on" ]]; then go mod download; else make install; fi
- if [[ "${GO111MODULE}" = "on" ]]; then export PATH="${GOPATH}/bin:${GOROOT}/bin:${PATH}"; fi
- if [[ "${GO111MODULE}" = "on" ]]; then make tools; fi
go_import_path: github.com/gin-gonic/gin go_import_path: github.com/gin-gonic/gin

View File

@ -1,7 +1,9 @@
GO ?= go
GOFMT ?= gofmt "-s" GOFMT ?= gofmt "-s"
PACKAGES ?= $(shell go list ./... | grep -v /vendor/) PACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/)
VETPACKAGES ?= $(shell go list ./... | grep -v /vendor/ | grep -v /examples/) VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/ | grep -v /examples/)
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*") GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*")
TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples)
all: install all: install
@ -10,7 +12,22 @@ install: deps
.PHONY: test .PHONY: test
test: test:
sh coverage.sh echo "mode: count" > coverage.out
for d in $(TESTFOLDER); do \
$(GO) test -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
cat tmp.out; \
if grep -q "^--- FAIL" tmp.out; then \
rm tmp.out; \
exit 1; \
elif grep -q "build failed" tmp.out; then \
rm tmp.out; \
exit; \
fi; \
if [ -f profile.out ]; then \
cat profile.out | grep -v "mode:" >> coverage.out; \
rm profile.out; \
fi; \
done
.PHONY: fmt .PHONY: fmt
fmt: fmt:
@ -18,7 +35,6 @@ fmt:
.PHONY: fmt-check .PHONY: fmt-check
fmt-check: fmt-check:
# get all go files and run go fmt on them
@diff=$$($(GOFMT) -d $(GOFILES)); \ @diff=$$($(GOFMT) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \ echo "Please run 'make fmt' and commit the result:"; \
@ -27,14 +43,14 @@ fmt-check:
fi; fi;
vet: vet:
go vet $(VETPACKAGES) $(GO) vet $(VETPACKAGES)
deps: deps:
@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/kardianos/govendor; \ $(GO) get -u github.com/kardianos/govendor; \
fi fi
@hash embedmd > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash embedmd > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/campoy/embedmd; \ $(GO) get -u github.com/campoy/embedmd; \
fi fi
embedmd: embedmd:
@ -43,20 +59,26 @@ embedmd:
.PHONY: lint .PHONY: lint
lint: lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/golang/lint/golint; \ $(GO) get -u golang.org/x/lint/golint; \
fi fi
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done; for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
.PHONY: misspell-check .PHONY: misspell-check
misspell-check: misspell-check:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/client9/misspell/cmd/misspell; \ $(GO) get -u github.com/client9/misspell/cmd/misspell; \
fi fi
misspell -error $(GOFILES) misspell -error $(GOFILES)
.PHONY: misspell .PHONY: misspell
misspell: misspell:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/client9/misspell/cmd/misspell; \ $(GO) get -u github.com/client9/misspell/cmd/misspell; \
fi fi
misspell -w $(GOFILES) misspell -w $(GOFILES)
.PHONY: tools
tools:
go install golang.org/x/lint/golint; \
go install github.com/client9/misspell/cmd/misspell; \
go install github.com/campoy/embedmd;

140
README.md
View File

@ -35,10 +35,12 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default) - [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
- [Using middleware](#using-middleware) - [Using middleware](#using-middleware)
- [How to write log file](#how-to-write-log-file) - [How to write log file](#how-to-write-log-file)
- [Custom Log Format](#custom-log-format)
- [Model binding and validation](#model-binding-and-validation) - [Model binding and validation](#model-binding-and-validation)
- [Custom Validators](#custom-validators) - [Custom Validators](#custom-validators)
- [Only Bind Query String](#only-bind-query-string) - [Only Bind Query String](#only-bind-query-string)
- [Bind Query String or Post Data](#bind-query-string-or-post-data) - [Bind Query String or Post Data](#bind-query-string-or-post-data)
- [Bind Uri](#bind-uri)
- [Bind HTML checkboxes](#bind-html-checkboxes) - [Bind HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding) - [Multipart/Urlencoded binding](#multiparturlencoded-binding)
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering) - [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
@ -60,6 +62,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Try to bind body into different structs](#try-to-bind-body-into-different-structs) - [Try to bind body into different structs](#try-to-bind-body-into-different-structs)
- [http2 server push](#http2-server-push) - [http2 server push](#http2-server-push)
- [Define format for the log of routes](#define-format-for-the-log-of-routes) - [Define format for the log of routes](#define-format-for-the-log-of-routes)
- [Set and get a cookie](#set-and-get-a-cookie)
- [Testing](#testing) - [Testing](#testing)
- [Users](#users) - [Users](#users)
@ -361,6 +364,10 @@ ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]
References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single). References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single).
`file.Filename` **SHOULD NOT** be trusted. See [`Content-Disposition` on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Directives) and [#1693](https://github.com/gin-gonic/gin/issues/1693)
> The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done.
```go ```go
func main() { func main() {
router := gin.Default() router := gin.Default()
@ -526,9 +533,46 @@ func main() {
} }
``` ```
### Custom Log Format
```go
func main() {
router := gin.New()
// LoggerWithFormatter middleware will write the logs to gin.DefaultWriter
// By default gin.DefaultWriter = os.Stdout
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
// your custom format
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
param.ClientIP,
param.TimeStamp.Format(time.RFC1123),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
}))
router.Use(gin.Recovery())
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}
```
**Sample Output**
```
::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" "
```
### Model binding and validation ### Model binding and validation
To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz). To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz).
Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags). Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags).
@ -536,10 +580,10 @@ Note that you need to set the corresponding binding tag on all fields you want t
Also, Gin provides two sets of methods for binding: Also, Gin provides two sets of methods for binding:
- **Type** - Must bind - **Type** - Must bind
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery` - **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`
- **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method. - **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method.
- **Type** - Should bind - **Type** - Should bind
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery` - **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`
- **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately. - **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately.
When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`. When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`.
@ -576,7 +620,7 @@ func main() {
// <?xml version="1.0" encoding="UTF-8"?> // <?xml version="1.0" encoding="UTF-8"?>
// <root> // <root>
// <user>user</user> // <user>user</user>
// <password>123</user> // <password>123</password>
// </root>) // </root>)
router.POST("/loginXML", func(c *gin.Context) { router.POST("/loginXML", func(c *gin.Context) {
var xml Login var xml Login
@ -792,6 +836,40 @@ Test it with:
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15" $ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15"
``` ```
### Bind Uri
See the [detail information](https://github.com/gin-gonic/gin/issues/846).
```go
package main
import "github.com/gin-gonic/gin"
type Person struct {
ID string `uri:"id" binding:"required,uuid"`
Name string `uri:"name" binding:"required"`
}
func main() {
route := gin.Default()
route.GET("/:name/:id", func(c *gin.Context) {
var person Person
if err := c.ShouldBindUri(&person); err != nil {
c.JSON(400, gin.H{"msg": err})
return
}
c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
})
route.Run(":8088")
}
```
Test it with:
```sh
$ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
$ curl -v localhost:8088/thinkerou/not-uuid
```
### Bind HTML checkboxes ### Bind HTML checkboxes
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092) See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
@ -823,12 +901,12 @@ form.html
<form action="/" method="POST"> <form action="/" method="POST">
<p>Check some colors</p> <p>Check some colors</p>
<label for="red">Red</label> <label for="red">Red</label>
<input type="checkbox" name="colors[]" value="red" id="red" /> <input type="checkbox" name="colors[]" value="red" id="red">
<label for="green">Green</label> <label for="green">Green</label>
<input type="checkbox" name="colors[]" value="green" id="green" /> <input type="checkbox" name="colors[]" value="green" id="green">
<label for="blue">Blue</label> <label for="blue">Blue</label>
<input type="checkbox" name="colors[]" value="blue" id="blue" /> <input type="checkbox" name="colors[]" value="blue" id="blue">
<input type="submit" /> <input type="submit">
</form> </form>
``` ```
@ -1021,7 +1099,7 @@ func main() {
}) })
// listen and serve on 0.0.0.0:8080 // listen and serve on 0.0.0.0:8080
r.Run(":8080) r.Run(":8080")
} }
``` ```
@ -1159,7 +1237,7 @@ You may use custom delims
```go ```go
r := gin.Default() r := gin.Default()
r.Delims("{[{", "}]}") r.Delims("{[{", "}]}")
r.LoadHTMLGlob("/path/to/templates")) r.LoadHTMLGlob("/path/to/templates")
``` ```
#### Custom Template Funcs #### Custom Template Funcs
@ -1555,6 +1633,7 @@ import (
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"syscall"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -1582,7 +1661,10 @@ 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)
signal.Notify(quit, os.Interrupt) // kill (no param) default send syscanll.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit <-quit
log.Println("Shutdown Server ...") log.Println("Shutdown Server ...")
@ -1721,11 +1803,11 @@ type StructX struct {
} }
type StructY struct { type StructY struct {
Y StructX `form:"name_y"` // HERE hava form Y StructX `form:"name_y"` // HERE have form
} }
type StructZ struct { type StructZ struct {
Z *StructZ `form:"name_z"` // HERE hava form Z *StructZ `form:"name_z"` // HERE have form
} }
``` ```
@ -1880,6 +1962,35 @@ func main() {
} }
``` ```
### Set and get a cookie
```go
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
}
fmt.Printf("Cookie value: %s \n", cookie)
})
router.Run()
}
```
## Testing ## Testing
@ -1934,3 +2045,6 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor
* [drone](https://github.com/drone/drone): Drone is a Continuous Delivery platform built on Docker, written in Go. * [drone](https://github.com/drone/drone): Drone is a Continuous Delivery platform built on Docker, written in Go.
* [gorush](https://github.com/appleboy/gorush): A push notification server written in Go. * [gorush](https://github.com/appleboy/gorush): A push notification server written in Go.
* [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform. * [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform.
* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow.
* [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares.
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.

View File

@ -122,7 +122,7 @@ func TestBasicAuth401(t *testing.T) {
assert.False(t, called) assert.False(t, called)
assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "Basic realm=\"Authorization Required\"", w.HeaderMap.Get("WWW-Authenticate")) assert.Equal(t, "Basic realm=\"Authorization Required\"", w.Header().Get("WWW-Authenticate"))
} }
func TestBasicAuth401WithCustomRealm(t *testing.T) { func TestBasicAuth401WithCustomRealm(t *testing.T) {
@ -142,5 +142,5 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
assert.False(t, called) assert.False(t, called)
assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.HeaderMap.Get("WWW-Authenticate")) assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.Header().Get("WWW-Authenticate"))
} }

View File

@ -18,6 +18,7 @@ const (
MIMEPROTOBUF = "application/x-protobuf" MIMEPROTOBUF = "application/x-protobuf"
MIMEMSGPACK = "application/x-msgpack" MIMEMSGPACK = "application/x-msgpack"
MIMEMSGPACK2 = "application/msgpack" MIMEMSGPACK2 = "application/msgpack"
MIMEYAML = "application/x-yaml"
) )
// Binding describes the interface which needs to be implemented for binding the // Binding describes the interface which needs to be implemented for binding the
@ -35,9 +36,16 @@ type BindingBody interface {
BindBody([]byte, interface{}) error BindBody([]byte, interface{}) error
} }
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it read the Params.
type BindingUri interface {
Name() string
BindUri(map[string][]string, interface{}) error
}
// StructValidator is the minimal interface which needs to be implemented in // StructValidator is the minimal interface which needs to be implemented in
// order for it to be used as the validator engine for ensuring the correctness // order for it to be used as the validator engine for ensuring the correctness
// of the reqest. Gin provides a default implementation for this using // of the request. Gin provides a default implementation for this using
// https://github.com/go-playground/validator/tree/v8.18.2. // https://github.com/go-playground/validator/tree/v8.18.2.
type StructValidator interface { type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
@ -68,6 +76,8 @@ var (
FormMultipart = formMultipartBinding{} FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{} ProtoBuf = protobufBinding{}
MsgPack = msgpackBinding{} MsgPack = msgpackBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}
) )
// Default returns the appropriate Binding instance based on the HTTP method // Default returns the appropriate Binding instance based on the HTTP method
@ -86,6 +96,8 @@ func Default(method, contentType string) Binding {
return ProtoBuf return ProtoBuf
case MIMEMSGPACK, MIMEMSGPACK2: case MIMEMSGPACK, MIMEMSGPACK2:
return MsgPack return MsgPack
case MIMEYAML:
return YAML
default: //case MIMEPOSTForm, MIMEMultipartPOSTForm: default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
return Form return Form
} }

View File

@ -19,12 +19,12 @@ func TestBindingBody(t *testing.T) {
want string want string
}{ }{
{ {
name: "JSON bidning", name: "JSON binding",
binding: JSON, binding: JSON,
body: `{"foo":"FOO"}`, body: `{"foo":"FOO"}`,
}, },
{ {
name: "XML bidning", name: "XML binding",
binding: XML, binding: XML,
body: `<?xml version="1.0" encoding="UTF-8"?> body: `<?xml version="1.0" encoding="UTF-8"?>
<root> <root>
@ -36,6 +36,11 @@ func TestBindingBody(t *testing.T) {
binding: MsgPack, binding: MsgPack,
body: msgPackBody(t), body: msgPackBody(t),
}, },
{
name: "YAML binding",
binding: YAML,
body: `foo: FOO`,
},
} { } {
t.Logf("testing: %s", tt.name) t.Logf("testing: %s", tt.name)
req := requestWithBody("POST", "/", tt.body) req := requestWithBody("POST", "/", tt.body)

View File

@ -11,6 +11,7 @@ import (
"io/ioutil" "io/ioutil"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"strconv"
"testing" "testing"
"time" "time"
@ -190,6 +191,16 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK)) assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2)) assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
assert.Equal(t, YAML, Default("POST", MIMEYAML))
assert.Equal(t, YAML, Default("PUT", MIMEYAML))
}
func TestBindingJSONNilBody(t *testing.T) {
var obj FooStruct
req, _ := http.NewRequest(http.MethodPost, "/", nil)
err := JSON.Bind(req, &obj)
assert.Error(t, err)
} }
func TestBindingJSON(t *testing.T) { func TestBindingJSON(t *testing.T) {
@ -473,6 +484,20 @@ func TestBindingXMLFail(t *testing.T) {
"<map><foo>bar<foo></map>", "<map><bar>foo</bar></map>") "<map><foo>bar<foo></map>", "<map><bar>foo</bar></map>")
} }
func TestBindingYAML(t *testing.T) {
testBodyBinding(t,
YAML, "yaml",
"/", "/",
`foo: bar`, `bar: foo`)
}
func TestBindingYAMLFail(t *testing.T) {
testBodyBindingFail(t,
YAML, "yaml",
"/", "/",
`foo:\nbar`, `bar: foo`)
}
func createFormPostRequest() *http.Request { func createFormPostRequest() *http.Request {
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo")) req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo"))
req.Header.Set("Content-Type", MIMEPOSTForm) req.Header.Set("Content-Type", MIMEPOSTForm)
@ -491,28 +516,28 @@ func createFormPostRequestFail() *http.Request {
return req return req
} }
func createFormMultipartRequest() *http.Request { func createFormMultipartRequest(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()
mw.SetBoundary(boundary) assert.NoError(t, mw.SetBoundary(boundary))
mw.WriteField("foo", "bar") assert.NoError(t, mw.WriteField("foo", "bar"))
mw.WriteField("bar", "foo") assert.NoError(t, mw.WriteField("bar", "foo"))
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req return req
} }
func createFormMultipartRequestFail() *http.Request { func createFormMultipartRequestFail(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()
mw.SetBoundary(boundary) assert.NoError(t, mw.SetBoundary(boundary))
mw.WriteField("map_foo", "bar") assert.NoError(t, mw.WriteField("map_foo", "bar"))
req, _ := http.NewRequest("POST", "/?map_foo=getfoo", body) req, _ := http.NewRequest("POST", "/?map_foo=getfoo", body)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req return req
@ -521,7 +546,7 @@ func createFormMultipartRequestFail() *http.Request {
func TestBindingFormPost(t *testing.T) { func TestBindingFormPost(t *testing.T) {
req := createFormPostRequest() req := createFormPostRequest()
var obj FooBarStruct var obj FooBarStruct
FormPost.Bind(req, &obj) assert.NoError(t, FormPost.Bind(req, &obj))
assert.Equal(t, "form-urlencoded", FormPost.Name()) assert.Equal(t, "form-urlencoded", FormPost.Name())
assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "bar", obj.Foo)
@ -531,7 +556,7 @@ func TestBindingFormPost(t *testing.T) {
func TestBindingDefaultValueFormPost(t *testing.T) { func TestBindingDefaultValueFormPost(t *testing.T) {
req := createDefaultFormPostRequest() req := createDefaultFormPostRequest()
var obj FooDefaultBarStruct var obj FooDefaultBarStruct
FormPost.Bind(req, &obj) assert.NoError(t, FormPost.Bind(req, &obj))
assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, "hello", obj.Bar) assert.Equal(t, "hello", obj.Bar)
@ -545,9 +570,9 @@ func TestBindingFormPostFail(t *testing.T) {
} }
func TestBindingFormMultipart(t *testing.T) { func TestBindingFormMultipart(t *testing.T) {
req := createFormMultipartRequest() req := createFormMultipartRequest(t)
var obj FooBarStruct var obj FooBarStruct
FormMultipart.Bind(req, &obj) assert.NoError(t, FormMultipart.Bind(req, &obj))
assert.Equal(t, "multipart/form-data", FormMultipart.Name()) assert.Equal(t, "multipart/form-data", FormMultipart.Name())
assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "bar", obj.Foo)
@ -555,7 +580,7 @@ func TestBindingFormMultipart(t *testing.T) {
} }
func TestBindingFormMultipartFail(t *testing.T) { func TestBindingFormMultipartFail(t *testing.T) {
req := createFormMultipartRequestFail() req := createFormMultipartRequestFail(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)
@ -645,6 +670,49 @@ func TestExistsFails(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestUriBinding(t *testing.T) {
b := Uri
assert.Equal(t, "uri", b.Name())
type Tag struct {
Name string `uri:"name"`
}
var tag Tag
m := make(map[string][]string)
m["name"] = []string{"thinkerou"}
assert.NoError(t, b.BindUri(m, &tag))
assert.Equal(t, "thinkerou", tag.Name)
type NotSupportStruct struct {
Name map[string]interface{} `uri:"name"`
}
var not NotSupportStruct
assert.Error(t, b.BindUri(m, &not))
assert.Equal(t, map[string]interface{}(nil), not.Name)
}
func TestUriInnerBinding(t *testing.T) {
type Tag struct {
Name string `uri:"name"`
S struct {
Age int `uri:"age"`
}
}
expectedName := "mike"
expectedAge := 25
m := map[string][]string{
"name": {expectedName},
"age": {strconv.Itoa(expectedAge)},
}
var tag Tag
assert.NoError(t, Uri.BindUri(m, &tag))
assert.Equal(t, tag.Name, expectedName)
assert.Equal(t, tag.S.Age, expectedAge)
}
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())
@ -1215,3 +1283,12 @@ 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

@ -20,7 +20,11 @@ func (formBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil { if err := req.ParseForm(); err != nil {
return err return err
} }
req.ParseMultipartForm(defaultMemory) if err := req.ParseMultipartForm(defaultMemory); err != nil {
if err != http.ErrNotMultipart {
return err
}
}
if err := mapForm(obj, req.Form); err != nil { if err := mapForm(obj, req.Form); err != nil {
return err return err
} }

View File

@ -12,7 +12,15 @@ import (
"time" "time"
) )
func mapUri(ptr interface{}, m map[string][]string) error {
return mapFormByTag(ptr, m, "uri")
}
func mapForm(ptr interface{}, form map[string][]string) error { func mapForm(ptr interface{}, form map[string][]string) error {
return mapFormByTag(ptr, form, "form")
}
func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
typ := reflect.TypeOf(ptr).Elem() typ := reflect.TypeOf(ptr).Elem()
val := reflect.ValueOf(ptr).Elem() val := reflect.ValueOf(ptr).Elem()
for i := 0; i < typ.NumField(); i++ { for i := 0; i < typ.NumField(); i++ {
@ -23,7 +31,7 @@ func mapForm(ptr interface{}, form map[string][]string) error {
} }
structFieldKind := structField.Kind() structFieldKind := structField.Kind()
inputFieldName := typeField.Tag.Get("form") inputFieldName := typeField.Tag.Get(tag)
inputFieldNameList := strings.Split(inputFieldName, ",") inputFieldNameList := strings.Split(inputFieldName, ",")
inputFieldName = inputFieldNameList[0] inputFieldName = inputFieldNameList[0]
var defaultValue string var defaultValue string
@ -47,7 +55,7 @@ func mapForm(ptr interface{}, form map[string][]string) error {
structFieldKind = structField.Kind() structFieldKind = structField.Kind()
} }
if structFieldKind == reflect.Struct { if structFieldKind == reflect.Struct {
err := mapForm(structField.Addr().Interface(), form) err := mapFormByTag(structField.Addr().Interface(), form, tag)
if err != nil { if err != nil {
return err return err
} }

View File

@ -6,6 +6,7 @@ package binding
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"net/http" "net/http"
@ -24,6 +25,9 @@ func (jsonBinding) Name() string {
} }
func (jsonBinding) Bind(req *http.Request, obj interface{}) error { func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
if req == nil || req.Body == nil {
return fmt.Errorf("invalid request")
}
return decodeJSON(req.Body, obj) return decodeJSON(req.Body, obj)
} }

View File

@ -29,7 +29,7 @@ func (protobufBinding) BindBody(body []byte, obj interface{}) error {
if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil { if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil {
return err return err
} }
// Here it's same to return validate(obj), but util now we cann't add // Here it's same to return validate(obj), but util now we can't add
// `binding:""` to the struct which automatically generate by gen-proto // `binding:""` to the struct which automatically generate by gen-proto
return nil return nil
// return validate(obj) // return validate(obj)

18
binding/uri.go Normal file
View File

@ -0,0 +1,18 @@
// 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 binding
type uriBinding struct{}
func (uriBinding) Name() string {
return "uri"
}
func (uriBinding) BindUri(m map[string][]string, obj interface{}) error {
if err := mapUri(obj, m); err != nil {
return err
}
return validate(obj)
}

35
binding/yaml.go Normal file
View File

@ -0,0 +1,35 @@
// 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 binding
import (
"bytes"
"io"
"net/http"
"gopkg.in/yaml.v2"
)
type yamlBinding struct{}
func (yamlBinding) Name() string {
return "yaml"
}
func (yamlBinding) Bind(req *http.Request, obj interface{}) error {
return decodeYAML(req.Body, obj)
}
func (yamlBinding) BindBody(body []byte, obj interface{}) error {
return decodeYAML(bytes.NewReader(body), obj)
}
func decodeYAML(r io.Reader, obj interface{}) error {
decoder := yaml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err
}
return validate(obj)
}

View File

@ -31,6 +31,7 @@ const (
MIMEPlain = binding.MIMEPlain MIMEPlain = binding.MIMEPlain
MIMEPOSTForm = binding.MIMEPOSTForm MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
MIMEYAML = binding.MIMEYAML
BodyBytesKey = "_gin-gonic/gin/bodybyteskey" BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
) )
@ -104,8 +105,9 @@ func (c *Context) Handler() HandlerFunc {
// See example in GitHub. // See example in GitHub.
func (c *Context) Next() { func (c *Context) Next() {
c.index++ c.index++
for s := int8(len(c.handlers)); c.index < s; c.index++ { for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c) c.handlers[c.index](c)
c.index++
} }
} }
@ -414,8 +416,11 @@ func (c *Context) PostFormArray(key string) []string {
// a boolean value whether at least one value exists for the given key. // a boolean value whether at least one value exists for the given key.
func (c *Context) GetPostFormArray(key string) ([]string, bool) { func (c *Context) GetPostFormArray(key string) ([]string, bool) {
req := c.Request req := c.Request
req.ParseForm() if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
req.ParseMultipartForm(c.engine.MaxMultipartMemory) if err != http.ErrNotMultipart {
debugPrint("error on parse multipart form array: %v", err)
}
}
if values := req.PostForm[key]; len(values) > 0 { if values := req.PostForm[key]; len(values) > 0 {
return values, true return values, true
} }
@ -437,8 +442,11 @@ func (c *Context) PostFormMap(key string) map[string]string {
// whether at least one value exists for the given key. // whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) { func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
req := c.Request req := c.Request
req.ParseForm() if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
req.ParseMultipartForm(c.engine.MaxMultipartMemory) if err != http.ErrNotMultipart {
debugPrint("error on parse multipart form map: %v", err)
}
}
dicts, exist := c.get(req.PostForm, key) dicts, exist := c.get(req.PostForm, key)
if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil { if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil {
@ -465,6 +473,11 @@ func (c *Context) get(m map[string][]string, key string) (map[string]string, boo
// FormFile returns the first file for the provided form key. // FormFile returns the first file for the provided form key.
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) { func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
if c.Request.MultipartForm == nil {
if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
return nil, err
}
}
_, fh, err := c.Request.FormFile(name) _, fh, err := c.Request.FormFile(name)
return fh, err return fh, err
} }
@ -489,8 +502,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
} }
defer out.Close() defer out.Close()
io.Copy(out, src) _, err = io.Copy(out, src)
return nil return err
} }
// Bind checks the Content-Type to select a binding engine automatically, // Bind checks the Content-Type to select a binding engine automatically,
@ -521,15 +534,30 @@ func (c *Context) BindQuery(obj interface{}) error {
return c.MustBindWith(obj, binding.Query) return c.MustBindWith(obj, binding.Query)
} }
// MustBindWith binds the passed struct pointer using the specified binding engine. // BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML).
// It will abort the request with HTTP 400 if any error ocurrs. func (c *Context) BindYAML(obj interface{}) error {
// See the binding package. return c.MustBindWith(obj, binding.YAML)
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) { }
if err = c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)
}
return // BindUri binds the passed struct pointer using binding.Uri.
// It will abort the request with HTTP 400 if any error occurs.
func (c *Context) BindUri(obj interface{}) error {
if err := c.ShouldBindUri(obj); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck
return err
}
return nil
}
// MustBindWith binds the passed struct pointer using the specified binding engine.
// It will abort the request with HTTP 400 if any error occurs.
// See the binding package.
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error {
if err := c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck
return err
}
return nil
} }
// ShouldBind checks the Content-Type to select a binding engine automatically, // ShouldBind checks the Content-Type to select a binding engine automatically,
@ -560,6 +588,20 @@ func (c *Context) ShouldBindQuery(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Query) return c.ShouldBindWith(obj, binding.Query)
} }
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
func (c *Context) ShouldBindYAML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.YAML)
}
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
func (c *Context) ShouldBindUri(obj interface{}) error {
m := make(map[string][]string)
for _, v := range c.Params {
m[v.Key] = []string{v.Value}
}
return binding.Uri.BindUri(m, obj)
}
// ShouldBindWith binds the passed struct pointer using the specified binding engine. // ShouldBindWith binds the passed struct pointer using the specified binding engine.
// See the binding package. // See the binding package.
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error { func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
@ -571,9 +613,7 @@ func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
// //
// NOTE: This method reads the body before binding. So you should use // NOTE: This method reads the body before binding. So you should use
// ShouldBindWith for better performance if you need to call only once. // ShouldBindWith for better performance if you need to call only once.
func (c *Context) ShouldBindBodyWith( func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) {
obj interface{}, bb binding.BindingBody,
) (err error) {
var body []byte var body []byte
if cb, ok := c.Get(BodyBytesKey); ok { if cb, ok := c.Get(BodyBytesKey); ok {
if cbb, ok := cb.([]byte); ok { if cbb, ok := cb.([]byte); ok {
@ -882,7 +922,7 @@ func (c *Context) Negotiate(code int, config Negotiate) {
c.XML(code, data) c.XML(code, data)
default: default:
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck
} }
} }

View File

@ -23,5 +23,5 @@ func TestContextRenderPureJSON(t *testing.T) {
c.PureJSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"}) c.PureJSON(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\":\"<b>\"}\n", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }

View File

@ -70,7 +70,8 @@ func TestContextFormFile(t *testing.T) {
mw := multipart.NewWriter(buf) mw := multipart.NewWriter(buf)
w, err := mw.CreateFormFile("file", "test") w, err := mw.CreateFormFile("file", "test")
if assert.NoError(t, err) { if assert.NoError(t, err) {
w.Write([]byte("test")) _, err = w.Write([]byte("test"))
assert.NoError(t, err)
} }
mw.Close() mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
@ -84,13 +85,27 @@ func TestContextFormFile(t *testing.T) {
assert.NoError(t, c.SaveUploadedFile(f, "test")) assert.NoError(t, c.SaveUploadedFile(f, "test"))
} }
func TestContextFormFileFailed(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
c.engine.MaxMultipartMemory = 8 << 20
f, err := c.FormFile("file")
assert.Error(t, err)
assert.Nil(t, f)
}
func TestContextMultipartForm(t *testing.T) { func TestContextMultipartForm(t *testing.T) {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf) mw := multipart.NewWriter(buf)
mw.WriteField("foo", "bar") assert.NoError(t, mw.WriteField("foo", "bar"))
w, err := mw.CreateFormFile("file", "test") w, err := mw.CreateFormFile("file", "test")
if assert.NoError(t, err) { if assert.NoError(t, err) {
w.Write([]byte("test")) _, err = w.Write([]byte("test"))
assert.NoError(t, err)
} }
mw.Close() mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
@ -124,7 +139,8 @@ func TestSaveUploadedCreateFailed(t *testing.T) {
mw := multipart.NewWriter(buf) mw := multipart.NewWriter(buf)
w, err := mw.CreateFormFile("file", "test") w, err := mw.CreateFormFile("file", "test")
if assert.NoError(t, err) { if assert.NoError(t, err) {
w.Write([]byte("test")) _, err = w.Write([]byte("test"))
assert.NoError(t, err)
} }
mw.Close() mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
@ -146,7 +162,7 @@ func TestContextReset(t *testing.T) {
c.index = 2 c.index = 2
c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()} c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()}
c.Params = Params{Param{}} c.Params = Params{Param{}}
c.Error(errors.New("test")) c.Error(errors.New("test")) // nolint: errcheck
c.Set("foo", "bar") c.Set("foo", "bar")
c.reset() c.reset()
@ -628,7 +644,7 @@ func TestContextRenderJSON(t *testing.T) {
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\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
// Tests that the response is serialized as JSONP // Tests that the response is serialized as JSONP
@ -642,7 +658,7 @@ func TestContextRenderJSONP(t *testing.T) {
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "x({\"foo\":\"bar\"})", w.Body.String()) assert.Equal(t, "x({\"foo\":\"bar\"})", w.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
} }
// Tests that the response is serialized as JSONP // Tests that the response is serialized as JSONP
@ -656,7 +672,7 @@ func TestContextRenderJSONPWithoutCallback(t *testing.T) {
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\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
// Tests that no JSON is rendered if code is 204 // Tests that no JSON is rendered if code is 204
@ -668,7 +684,7 @@ func TestContextRenderNoContentJSON(t *testing.T) {
assert.Equal(t, http.StatusNoContent, w.Code) assert.Equal(t, http.StatusNoContent, w.Code)
assert.Empty(t, w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
// Tests that the response is serialized as JSON // Tests that the response is serialized as JSON
@ -682,7 +698,7 @@ func TestContextRenderAPIJSON(t *testing.T) {
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\"}", w.Body.String())
assert.Equal(t, "application/vnd.api+json", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type"))
} }
// Tests that no Custom JSON is rendered if code is 204 // Tests that no Custom JSON is rendered if code is 204
@ -695,7 +711,7 @@ func TestContextRenderNoContentAPIJSON(t *testing.T) {
assert.Equal(t, http.StatusNoContent, w.Code) assert.Equal(t, http.StatusNoContent, w.Code)
assert.Empty(t, w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json") assert.Equal(t, w.Header().Get("Content-Type"), "application/vnd.api+json")
} }
// Tests that the response is serialized as JSON // Tests that the response is serialized as JSON
@ -708,7 +724,7 @@ func TestContextRenderIndentedJSON(t *testing.T) {
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\",\n \"nested\": {\n \"foo\": \"bar\"\n }\n}", w.Body.String()) assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\",\n \"nested\": {\n \"foo\": \"bar\"\n }\n}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
// Tests that no Custom JSON is rendered if code is 204 // Tests that no Custom JSON is rendered if code is 204
@ -720,7 +736,7 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) {
assert.Equal(t, http.StatusNoContent, w.Code) assert.Equal(t, http.StatusNoContent, w.Code)
assert.Empty(t, w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
// Tests that the response is serialized as Secure JSON // Tests that the response is serialized as Secure JSON
@ -734,7 +750,7 @@ func TestContextRenderSecureJSON(t *testing.T) {
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "&&&START&&&[\"foo\",\"bar\"]", w.Body.String()) assert.Equal(t, "&&&START&&&[\"foo\",\"bar\"]", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
// Tests that no Custom JSON is rendered if code is 204 // Tests that no Custom JSON is rendered if code is 204
@ -746,7 +762,7 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) {
assert.Equal(t, http.StatusNoContent, w.Code) assert.Equal(t, http.StatusNoContent, w.Code)
assert.Empty(t, w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestContextRenderNoContentAsciiJSON(t *testing.T) { func TestContextRenderNoContentAsciiJSON(t *testing.T) {
@ -757,7 +773,7 @@ func TestContextRenderNoContentAsciiJSON(t *testing.T) {
assert.Equal(t, http.StatusNoContent, w.Code) assert.Equal(t, http.StatusNoContent, w.Code)
assert.Empty(t, w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, "application/json", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
} }
// Tests that the response executes the templates // Tests that the response executes the templates
@ -773,7 +789,7 @@ func TestContextRenderHTML(t *testing.T) {
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "Hello alexandernyquist", w.Body.String()) assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestContextRenderHTML2(t *testing.T) { func TestContextRenderHTML2(t *testing.T) {
@ -785,7 +801,7 @@ func TestContextRenderHTML2(t *testing.T) {
assert.Len(t, router.trees, 1) assert.Len(t, router.trees, 1)
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
re := captureOutput(func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
router.SetHTMLTemplate(templ) router.SetHTMLTemplate(templ)
SetMode(TestMode) SetMode(TestMode)
@ -797,7 +813,7 @@ func TestContextRenderHTML2(t *testing.T) {
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "Hello alexandernyquist", w.Body.String()) assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
// Tests that no HTML is rendered if code is 204 // Tests that no HTML is rendered if code is 204
@ -811,7 +827,7 @@ func TestContextRenderNoContentHTML(t *testing.T) {
assert.Equal(t, http.StatusNoContent, w.Code) assert.Equal(t, http.StatusNoContent, w.Code)
assert.Empty(t, w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
// TestContextXML tests that the response is serialized as XML // TestContextXML tests that the response is serialized as XML
@ -824,7 +840,7 @@ func TestContextRenderXML(t *testing.T) {
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String()) assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String())
assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
} }
// Tests that no XML is rendered if code is 204 // Tests that no XML is rendered if code is 204
@ -836,7 +852,7 @@ func TestContextRenderNoContentXML(t *testing.T) {
assert.Equal(t, http.StatusNoContent, w.Code) assert.Equal(t, http.StatusNoContent, w.Code)
assert.Empty(t, w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
} }
// TestContextString tests that the response is returned // TestContextString tests that the response is returned
@ -849,7 +865,7 @@ func TestContextRenderString(t *testing.T) {
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "test string 2", w.Body.String()) assert.Equal(t, "test string 2", w.Body.String())
assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
} }
// Tests that no String is rendered if code is 204 // Tests that no String is rendered if code is 204
@ -861,7 +877,7 @@ func TestContextRenderNoContentString(t *testing.T) {
assert.Equal(t, http.StatusNoContent, w.Code) assert.Equal(t, http.StatusNoContent, w.Code)
assert.Empty(t, w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
} }
// TestContextString tests that the response is returned // TestContextString tests that the response is returned
@ -875,7 +891,7 @@ func TestContextRenderHTMLString(t *testing.T) {
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "<html>string 3</html>", w.Body.String()) assert.Equal(t, "<html>string 3</html>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
// Tests that no HTML String is rendered if code is 204 // Tests that no HTML String is rendered if code is 204
@ -888,7 +904,7 @@ func TestContextRenderNoContentHTMLString(t *testing.T) {
assert.Equal(t, http.StatusNoContent, w.Code) assert.Equal(t, http.StatusNoContent, w.Code)
assert.Empty(t, w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
// TestContextData tests that the response can be written from `bytesting` // TestContextData tests that the response can be written from `bytesting`
@ -901,7 +917,7 @@ func TestContextRenderData(t *testing.T) {
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", w.Body.String())
assert.Equal(t, "text/csv", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "text/csv", w.Header().Get("Content-Type"))
} }
// Tests that no Custom Data is rendered if code is 204 // Tests that no Custom Data is rendered if code is 204
@ -913,7 +929,7 @@ func TestContextRenderNoContentData(t *testing.T) {
assert.Equal(t, http.StatusNoContent, w.Code) assert.Equal(t, http.StatusNoContent, w.Code)
assert.Empty(t, w.Body.String()) assert.Empty(t, w.Body.String())
assert.Equal(t, "text/csv", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "text/csv", w.Header().Get("Content-Type"))
} }
func TestContextRenderSSE(t *testing.T) { func TestContextRenderSSE(t *testing.T) {
@ -942,7 +958,7 @@ func TestContextRenderFile(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {") assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
} }
// TestContextRenderYAML tests that the response is serialized as YAML // TestContextRenderYAML tests that the response is serialized as YAML
@ -955,7 +971,7 @@ func TestContextRenderYAML(t *testing.T) {
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "foo: bar\n", w.Body.String()) assert.Equal(t, "foo: bar\n", w.Body.String())
assert.Equal(t, "application/x-yaml; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
} }
// TestContextRenderProtoBuf tests that the response is serialized as ProtoBuf // TestContextRenderProtoBuf tests that the response is serialized as ProtoBuf
@ -979,7 +995,7 @@ func TestContextRenderProtoBuf(t *testing.T) {
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, string(protoData), w.Body.String()) assert.Equal(t, string(protoData), w.Body.String())
assert.Equal(t, "application/x-protobuf", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
} }
func TestContextHeaders(t *testing.T) { func TestContextHeaders(t *testing.T) {
@ -1062,7 +1078,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\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestContextNegotiationWithXML(t *testing.T) { func TestContextNegotiationWithXML(t *testing.T) {
@ -1077,7 +1093,7 @@ func TestContextNegotiationWithXML(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String()) assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String())
assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestContextNegotiationWithHTML(t *testing.T) { func TestContextNegotiationWithHTML(t *testing.T) {
@ -1095,7 +1111,7 @@ func TestContextNegotiationWithHTML(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "Hello gin", w.Body.String()) assert.Equal(t, "Hello gin", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestContextNegotiationNotSupport(t *testing.T) { func TestContextNegotiationNotSupport(t *testing.T) {
@ -1131,7 +1147,7 @@ func TestContextNegotiationFormatWithAccept(t *testing.T) {
assert.Empty(t, c.NegotiateFormat(MIMEJSON)) assert.Empty(t, c.NegotiateFormat(MIMEJSON))
} }
func TestContextNegotiationFormatCustum(t *testing.T) { func TestContextNegotiationFormatCustom(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil) c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
@ -1198,7 +1214,8 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", contentType) assert.Equal(t, "application/json; charset=utf-8", contentType)
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
buf.ReadFrom(w.Body) _, err := buf.ReadFrom(w.Body)
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"}`), jsonStringBody)
} }
@ -1207,11 +1224,11 @@ func TestContextError(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
assert.Empty(t, c.Errors) assert.Empty(t, c.Errors)
c.Error(errors.New("first error")) c.Error(errors.New("first error")) // nolint: errcheck
assert.Len(t, c.Errors, 1) assert.Len(t, c.Errors, 1)
assert.Equal(t, "Error #01: first error\n", c.Errors.String()) assert.Equal(t, "Error #01: first error\n", c.Errors.String())
c.Error(&Error{ c.Error(&Error{ // nolint: errcheck
Err: errors.New("second error"), Err: errors.New("second error"),
Meta: "some data 2", Meta: "some data 2",
Type: ErrorTypePublic, Type: ErrorTypePublic,
@ -1233,13 +1250,13 @@ func TestContextError(t *testing.T) {
t.Error("didn't panic") t.Error("didn't panic")
} }
}() }()
c.Error(nil) c.Error(nil) // nolint: errcheck
} }
func TestContextTypedError(t *testing.T) { func TestContextTypedError(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) // nolint: errcheck
c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) // nolint: errcheck
for _, err := range c.Errors.ByType(ErrorTypePublic) { for _, err := range c.Errors.ByType(ErrorTypePublic) {
assert.Equal(t, ErrorTypePublic, err.Type) assert.Equal(t, ErrorTypePublic, err.Type)
@ -1254,7 +1271,7 @@ func TestContextAbortWithError(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
c.AbortWithError(http.StatusUnauthorized, errors.New("bad input")).SetMeta("some input") c.AbortWithError(http.StatusUnauthorized, errors.New("bad input")).SetMeta("some input") // nolint: errcheck
assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, abortIndex, c.index) assert.Equal(t, abortIndex, c.index)
@ -1367,6 +1384,23 @@ func TestContextBindWithQuery(t *testing.T) {
assert.Equal(t, 0, w.Body.Len()) assert.Equal(t, 0, w.Body.Len())
} }
func TestContextBindWithYAML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("foo: bar\nbar: foo"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct {
Foo string `yaml:"foo"`
Bar string `yaml:"bar"`
}
assert.NoError(t, c.BindYAML(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBadAutoBind(t *testing.T) { func TestContextBadAutoBind(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
@ -1427,7 +1461,7 @@ func TestContextShouldBindWithXML(t *testing.T) {
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8"?> c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8"?>
<root> <root>
<foo>FOO</foo> <foo>FOO</foo>
<bar>BAR</bar> <bar>BAR</bar>
</root>`)) </root>`))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
@ -1445,15 +1479,36 @@ func TestContextShouldBindWithQuery(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused")) c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo&Foo=bar1&Bar=foo1", bytes.NewBufferString("foo=unused"))
var obj struct { var obj struct {
Foo string `form:"foo"` Foo string `form:"foo"`
Bar string `form:"bar"` Bar string `form:"bar"`
Foo1 string `form:"Foo"`
Bar1 string `form:"Bar"`
} }
assert.NoError(t, c.ShouldBindQuery(&obj)) assert.NoError(t, c.ShouldBindQuery(&obj))
assert.Equal(t, "foo", obj.Bar) assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, "foo1", obj.Bar1)
assert.Equal(t, "bar1", obj.Foo1)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextShouldBindWithYAML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("foo: bar\nbar: foo"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct {
Foo string `yaml:"foo"`
Bar string `yaml:"bar"`
}
assert.NoError(t, c.ShouldBindYAML(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len()) assert.Equal(t, 0, w.Body.Len())
} }
@ -1627,9 +1682,9 @@ func TestContextRenderDataFromReader(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, body, w.Body.String()) assert.Equal(t, body, w.Body.String())
assert.Equal(t, contentType, w.HeaderMap.Get("Content-Type")) assert.Equal(t, contentType, w.Header().Get("Content-Type"))
assert.Equal(t, fmt.Sprintf("%d", contentLength), w.HeaderMap.Get("Content-Length")) assert.Equal(t, fmt.Sprintf("%d", contentLength), w.Header().Get("Content-Length"))
assert.Equal(t, extraHeaders["Content-Disposition"], w.HeaderMap.Get("Content-Disposition")) assert.Equal(t, extraHeaders["Content-Disposition"], w.Header().Get("Content-Disposition"))
} }
type TestResponseRecorder struct { type TestResponseRecorder struct {
@ -1662,7 +1717,8 @@ func TestContextStream(t *testing.T) {
stopStream = false stopStream = false
}() }()
w.Write([]byte("test")) _, err := w.Write([]byte("test"))
assert.NoError(t, err)
return stopStream return stopStream
}) })
@ -1679,10 +1735,23 @@ func TestContextStreamWithClientGone(t *testing.T) {
w.closeClient() w.closeClient()
}() }()
writer.Write([]byte("test")) _, err := writer.Write([]byte("test"))
assert.NoError(t, err)
return true return true
}) })
assert.Equal(t, "test", w.Body.String()) assert.Equal(t, "test", w.Body.String())
} }
func TestContextResetInHandler(t *testing.T) {
w := CreateTestResponseRecorder()
c, _ := CreateTestContext(w)
c.handlers = []HandlerFunc{
func(c *Context) { c.reset() },
}
assert.NotPanics(t, func() {
c.Next()
})
}

View File

@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -e
echo "mode: count" > coverage.out
for d in $(go list ./... | grep -E 'gin$|binding$|render$' | grep -v 'examples'); do
go test -v -covermode=count -coverprofile=profile.out $d
if [ -f profile.out ]; then
cat profile.out | grep -v "mode:" >> coverage.out
rm profile.out
fi
done

View File

@ -8,8 +8,14 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"html/template" "html/template"
"os"
"runtime"
"strconv"
"strings"
) )
const ginSupportMinGoVer = 6
// 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.
func IsDebugging() bool { func IsDebugging() bool {
@ -45,14 +51,28 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
func debugPrint(format string, values ...interface{}) { func debugPrint(format string, values ...interface{}) {
if IsDebugging() { if IsDebugging() {
fmt.Printf("[GIN-debug] "+format, values...) if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(os.Stderr, "[GIN-debug] "+format, values...)
} }
} }
func getMinVer(v string) (uint64, error) {
first := strings.IndexByte(v, '.')
last := strings.LastIndexByte(v, '.')
if first == last {
return strconv.ParseUint(v[first+1:], 10, 64)
}
return strconv.ParseUint(v[first+1:last], 10, 64)
}
func debugPrintWARNINGDefault() { func debugPrintWARNINGDefault() {
debugPrint(`[WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon. 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] Creating an Engine instance with the Logger and Recovery middleware already attached. debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
`) `)

View File

@ -11,6 +11,7 @@ import (
"io" "io"
"log" "log"
"os" "os"
"runtime"
"sync" "sync"
"testing" "testing"
@ -31,21 +32,21 @@ func TestIsDebugging(t *testing.T) {
} }
func TestDebugPrint(t *testing.T) { func TestDebugPrint(t *testing.T) {
re := captureOutput(func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
SetMode(ReleaseMode) SetMode(ReleaseMode)
debugPrint("DEBUG this!") debugPrint("DEBUG this!")
SetMode(TestMode) SetMode(TestMode)
debugPrint("DEBUG this!") debugPrint("DEBUG this!")
SetMode(DebugMode) SetMode(DebugMode)
debugPrint("these are %d %s\n", 2, "error messages") debugPrint("these are %d %s", 2, "error messages")
SetMode(TestMode) SetMode(TestMode)
}) })
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re) assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re)
} }
func TestDebugPrintError(t *testing.T) { func TestDebugPrintError(t *testing.T) {
re := captureOutput(func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
debugPrintError(nil) debugPrintError(nil)
debugPrintError(errors.New("this is an error")) debugPrintError(errors.New("this is an error"))
@ -55,7 +56,7 @@ func TestDebugPrintError(t *testing.T) {
} }
func TestDebugPrintRoutes(t *testing.T) { func TestDebugPrintRoutes(t *testing.T) {
re := captureOutput(func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest}) debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode) SetMode(TestMode)
@ -64,7 +65,7 @@ func TestDebugPrintRoutes(t *testing.T) {
} }
func TestDebugPrintLoadTemplate(t *testing.T) { func TestDebugPrintLoadTemplate(t *testing.T) {
re := captureOutput(func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./testdata/template/hello.tmpl")) templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./testdata/template/hello.tmpl"))
debugPrintLoadTemplate(templ) debugPrintLoadTemplate(templ)
@ -74,7 +75,7 @@ func TestDebugPrintLoadTemplate(t *testing.T) {
} }
func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) { func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) {
re := captureOutput(func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
debugPrintWARNINGSetHTMLTemplate() debugPrintWARNINGSetHTMLTemplate()
SetMode(TestMode) SetMode(TestMode)
@ -83,16 +84,21 @@ func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) {
} }
func TestDebugPrintWARNINGDefault(t *testing.T) { func TestDebugPrintWARNINGDefault(t *testing.T) {
re := captureOutput(func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
debugPrintWARNINGDefault() debugPrintWARNINGDefault()
SetMode(TestMode) SetMode(TestMode)
}) })
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) m, e := getMinVer(runtime.Version())
if e == nil && m <= ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
} }
func TestDebugPrintWARNINGNew(t *testing.T) { func TestDebugPrintWARNINGNew(t *testing.T) {
re := captureOutput(func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
debugPrintWARNINGNew() debugPrintWARNINGNew()
SetMode(TestMode) SetMode(TestMode)
@ -100,7 +106,7 @@ func TestDebugPrintWARNINGNew(t *testing.T) {
assert.Equal(t, "[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n", re) assert.Equal(t, "[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n", re)
} }
func captureOutput(f func()) string { func captureOutput(t *testing.T, f func()) string {
reader, writer, err := os.Pipe() reader, writer, err := os.Pipe()
if err != nil { if err != nil {
panic(err) panic(err)
@ -121,7 +127,8 @@ func captureOutput(f func()) string {
go func() { go func() {
var buf bytes.Buffer var buf bytes.Buffer
wg.Done() wg.Done()
io.Copy(&buf, reader) _, err := io.Copy(&buf, reader)
assert.NoError(t, err)
out <- buf.String() out <- buf.String()
}() }()
wg.Wait() wg.Wait()
@ -129,3 +136,18 @@ func captureOutput(f func()) string {
writer.Close() writer.Close()
return <-out return <-out
} }
func TestGetMinVer(t *testing.T) {
var m uint64
var e error
_, e = getMinVer("go1")
assert.NotNil(t, e)
m, e = getMinVer("go1.1")
assert.Equal(t, uint64(1), m)
assert.Nil(t, e)
m, e = getMinVer("go1.1.1")
assert.Nil(t, e)
assert.Equal(t, uint64(1), m)
_, e = getMinVer("go1.1.1.1")
assert.NotNil(t, e)
}

View File

@ -24,7 +24,9 @@ func TestBindWith(t *testing.T) {
Foo string `form:"foo"` Foo string `form:"foo"`
Bar string `form:"bar"` Bar string `form:"bar"`
} }
assert.NoError(t, c.BindWith(&obj, binding.Form)) captureOutput(t, func() {
assert.NoError(t, c.BindWith(&obj, binding.Form))
})
assert.Equal(t, "foo", obj.Bar) assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len()) assert.Equal(t, 0, w.Body.Len())

View File

@ -0,0 +1,137 @@
# How to build one effective middleware?
## Consitituent part
The middleware has two parts:
- part one is what is executed once, when you initialize your middleware. That's where you set up all the global objects, logicals etc. Everything that happens one per application lifetime.
- part two is what executes on every request. For example, a database middleware you simply inject your "global" database object into the context. Once it's inside the context, you can retrieve it from within other middlewares and your handler function.
```go
func funcName(params string) gin.HandlerFunc {
// <---
// This is part one
// --->
// The follow code is an example
if err := check(params); err != nil {
panic(err)
}
return func(c *gin.Context) {
// <---
// This is part two
// --->
// The follow code is an example
c.Set("TestVar", params)
c.Next()
}
}
```
## Execution process
Firstly, we have the follow example code:
```go
func main() {
router := gin.Default()
router.Use(globalMiddleware())
router.GET("/rest/n/api/*some", mid1(), mid2(), handler)
router.Run()
}
func globalMiddleware() gin.HandlerFunc {
fmt.Println("globalMiddleware...1")
return func(c *gin.Context) {
fmt.Println("globalMiddleware...2")
c.Next()
fmt.Println("globalMiddleware...3")
}
}
func handler(c *gin.Context) {
fmt.Println("exec handler.")
}
func mid1() gin.HandlerFunc {
fmt.Println("mid1...1")
return func(c *gin.Context) {
fmt.Println("mid1...2")
c.Next()
fmt.Println("mid1...3")
}
}
func mid2() gin.HandlerFunc {
fmt.Println("mid2...1")
return func(c *gin.Context) {
fmt.Println("mid2...2")
c.Next()
fmt.Println("mid2...3")
}
}
```
According to [Consitituent part](#consitituent-part) said, when we run the gin process, **part one** will execute firstly and will print the follow information:
```go
globalMiddleware...1
mid1...1
mid2...1
```
And init order are:
```go
globalMiddleware...1
|
v
mid1...1
|
v
mid2...1
```
When we curl one request `curl -v localhost:8080/rest/n/api/some`, **part two** will execute their middleware and output the following information:
```go
globalMiddleware...2
mid1...2
mid2...2
exec handler.
mid2...3
mid1...3
globalMiddleware...3
```
In other words, run order are:
```go
globalMiddleware...2
|
v
mid1...2
|
v
mid2...2
|
v
exec handler.
|
v
mid2...3
|
v
mid1...3
|
v
globalMiddleware...3
```

View File

@ -24,9 +24,10 @@ const (
ErrorTypePrivate ErrorType = 1 << 0 ErrorTypePrivate ErrorType = 1 << 0
// ErrorTypePublic indicates a public error. // ErrorTypePublic indicates a public error.
ErrorTypePublic ErrorType = 1 << 1 ErrorTypePublic ErrorType = 1 << 1
// ErrorTypeAny indicates other any error. // ErrorTypeAny indicates any other error.
ErrorTypeAny ErrorType = 1<<64 - 1 ErrorTypeAny ErrorType = 1<<64 - 1
ErrorTypeNu = 2 // ErrorTypeNu indicates any other error.
ErrorTypeNu = 2
) )
// Error represents a error's specification. // Error represents a error's specification.
@ -52,6 +53,7 @@ func (msg *Error) SetMeta(data interface{}) *Error {
return msg return msg
} }
// JSON creates a properly formated 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

@ -34,7 +34,7 @@ func TestError(t *testing.T) {
jsonBytes, _ := json.Marshal(err) jsonBytes, _ := json.Marshal(err)
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes)) assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
err.SetMeta(H{ err.SetMeta(H{ // nolint: errcheck
"status": "200", "status": "200",
"data": "some data", "data": "some data",
}) })
@ -44,7 +44,7 @@ func TestError(t *testing.T) {
"data": "some data", "data": "some data",
}, err.JSON()) }, err.JSON())
err.SetMeta(H{ err.SetMeta(H{ // nolint: errcheck
"error": "custom error", "error": "custom error",
"status": "200", "status": "200",
"data": "some data", "data": "some data",
@ -59,7 +59,7 @@ func TestError(t *testing.T) {
status string status string
data string data string
} }
err.SetMeta(customError{status: "200", data: "other data"}) err.SetMeta(customError{status: "200", data: "other data"}) // nolint: errcheck
assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON()) assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON())
} }

View File

@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"syscall"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -35,7 +36,10 @@ 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)
signal.Notify(quit, os.Interrupt) // kill (no param) default send syscanll.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit <-quit
log.Println("Shutdown Server ...") log.Println("Shutdown Server ...")

View File

@ -19,7 +19,7 @@ func main() {
defer conn.Close() defer conn.Close()
client := pb.NewGreeterClient(conn) client := pb.NewGreeterClient(conn)
// Set up a http setver. // Set up a http server.
r := gin.Default() r := gin.Default()
r.GET("/rest/n/:name", func(c *gin.Context) { r.GET("/rest/n/:name", func(c *gin.Context) {
name := c.Param("name") name := c.Param("name")

View File

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@ -20,9 +20,9 @@
<!-- Latest compiled and minified JavaScript --> <!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
<!-- Primjs --> <!-- Primjs -->
<link href="/static/prismjs.min.css" rel="stylesheet" /> <link href="/static/prismjs.min.css" rel="stylesheet">
<script type="text/javascript"> <script>
$(document).ready(function() { $(document).ready(function() {
StartRealtime({{.roomid}}, {{.timestamp}}); StartRealtime({{.roomid}}, {{.timestamp}});
}); });
@ -49,7 +49,7 @@
<li><a href="http://www.w3.org/TR/2009/WD-eventsource-20091029/">W3 Standard</a></li> <li><a href="http://www.w3.org/TR/2009/WD-eventsource-20091029/">W3 Standard</a></li>
<li><a href="http://caniuse.com/#feat=eventsource">Browser Support</a></li> <li><a href="http://caniuse.com/#feat=eventsource">Browser Support</a></li>
<li><a href="http://gin-gonic.github.io/gin/">Gin Framework</a></li> <li><a href="http://gin-gonic.github.io/gin/">Gin Framework</a></li>
<li><a href="https://github.com/gin-gonic/gin/tree/develop/examples/realtime-advanced">Github</a></li> <li><a href="https://github.com/gin-gonic/gin/tree/develop/examples/realtime-advanced">GitHub</a></li>
</ul> </ul>
</div><!-- /.nav-collapse --> </div><!-- /.nav-collapse -->
</div><!-- /.container --> </div><!-- /.container -->
@ -59,7 +59,7 @@
<div class="container"> <div class="container">
<h1>Server-Sent Events in Go</h1> <h1>Server-Sent Events in Go</h1>
<p>Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection. It is not websockets. <a href="http://www.html5rocks.com/en/tutorials/eventsource/basics/">Learn more.</a></p> <p>Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection. It is not websockets. <a href="http://www.html5rocks.com/en/tutorials/eventsource/basics/">Learn more.</a></p>
<p>The chat and the charts data is provided in realtime using the SSE implemention of <a href="https://github.com/gin-gonic/gin/blob/15b0c49da556d58a3d934b86e3aa552ff224026d/examples/realtime-chat/main.go#L23-L32">Gin Framework</a>.</p> <p>The chat and the charts data is provided in realtime using the SSE implementation of <a href="https://github.com/gin-gonic/gin/blob/15b0c49da556d58a3d934b86e3aa552ff224026d/examples/realtime-chat/main.go#L23-L32">Gin Framework</a>.</p>
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8">
<div id="chat-scroll" style="overflow-y:scroll; overflow-x:scroll; height:290px"> <div id="chat-scroll" style="overflow-y:scroll; overflow-x:scroll; height:290px">
@ -79,19 +79,19 @@
<label class="sr-only" for="chat-message">Message</label> <label class="sr-only" for="chat-message">Message</label>
<div class="input-group"> <div class="input-group">
<div class="input-group-addon">{{.nick}}</div> <div class="input-group-addon">{{.nick}}</div>
<input type="text" name="message" id="chat-message" class="form-control" placeholder="a message" value="" /> <input type="text" name="message" id="chat-message" class="form-control" placeholder="a message" value="">
</div> </div>
</div> </div>
<input type="submit" class="btn btn-primary" value="Send" /> <input type="submit" class="btn btn-primary" value="Send">
</form> </form>
{{else}} {{else}}
<form action="" method="get" class="form-inline"> <form action="" method="get" class="form-inline">
<legend>Join the SSE real-time chat</legend> <legend>Join the SSE real-time chat</legend>
<div class="form-group"> <div class="form-group">
<input value='' name="nick" id="nick" placeholder="Your Name" type="text" class="form-control" /> <input value='' name="nick" id="nick" placeholder="Your Name" type="text" class="form-control">
</div> </div>
<div class="form-group text-center"> <div class="form-group text-center">
<input type="submit" class="btn btn-success btn-login-submit" value="Join" /> <input type="submit" class="btn btn-success btn-login-submit" value="Join">
</div> </div>
</form> </form>
{{end}} {{end}}

View File

@ -6,7 +6,7 @@ var html = template.Must(template.New("chat_room").Parse(`
<html> <html>
<head> <head>
<title>{{.roomid}}</title> <title>{{.roomid}}</title>
<link rel="stylesheet" type="text/css" href="http://meyerweb.com/eric/tools/css/reset/reset.css"/> <link rel="stylesheet" type="text/css" href="http://meyerweb.com/eric/tools/css/reset/reset.css">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.js"></script> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.js"></script>
<script src="http://malsup.github.com/jquery.form.js"></script> <script src="http://malsup.github.com/jquery.form.js"></script>
<script> <script>
@ -35,9 +35,9 @@ var html = template.Must(template.New("chat_room").Parse(`
<h1>Welcome to {{.roomid}} room</h1> <h1>Welcome to {{.roomid}} room</h1>
<div id="messages"></div> <div id="messages"></div>
<form id="myForm" action="/room/{{.roomid}}" method="post"> <form id="myForm" action="/room/{{.roomid}}" method="post">
User: <input id="user_form" name="user" value="{{.userid}}"></input> User: <input id="user_form" name="user" value="{{.userid}}">
Message: <input id="message_form" name="message"></input> Message: <input id="message_form" name="message">
<input type="submit" value="Submit" /> <input type="submit" value="Submit">
</form> </form>
</body> </body>
</html> </html>

View File

@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"path/filepath"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -25,7 +26,8 @@ func main() {
files := form.File["files"] files := form.File["files"]
for _, file := range files { for _, file := range files {
if err := c.SaveUploadedFile(file, file.Filename); err != nil { filename := filepath.Base(file.Filename)
if err := c.SaveUploadedFile(file, filename); err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error())) c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
return return
} }

View File

@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"path/filepath"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -23,7 +24,8 @@ func main() {
return return
} }
if err := c.SaveUploadedFile(file, file.Filename); err != nil { filename := filepath.Base(file.Filename)
if err := c.SaveUploadedFile(file, filename); err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error())) c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
return return
} }

42
gin.go
View File

@ -5,6 +5,7 @@
package gin package gin
import ( import (
"fmt"
"html/template" "html/template"
"net" "net"
"net/http" "net/http"
@ -38,9 +39,10 @@ func (c HandlersChain) Last() HandlerFunc {
// RouteInfo represents a request route's specification which contains method and path and its handler. // RouteInfo represents a request route's specification which contains method and path and its handler.
type RouteInfo struct { type RouteInfo struct {
Method string Method string
Path string Path string
Handler string Handler string
HandlerFunc HandlerFunc
} }
// RoutesInfo defines a RouteInfo array. // RoutesInfo defines a RouteInfo array.
@ -266,10 +268,12 @@ func (engine *Engine) Routes() (routes RoutesInfo) {
func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo { func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
path += root.path path += root.path
if len(root.handlers) > 0 { if len(root.handlers) > 0 {
handlerFunc := root.handlers.Last()
routes = append(routes, RouteInfo{ routes = append(routes, RouteInfo{
Method: method, Method: method,
Path: path, Path: path,
Handler: nameOfFunction(root.handlers.Last()), Handler: nameOfFunction(handlerFunc),
HandlerFunc: handlerFunc,
}) })
} }
for _, child := range root.children { for _, child := range root.children {
@ -318,6 +322,23 @@ func (engine *Engine) RunUnix(file string) (err error) {
return return
} }
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified file descriptor.
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) RunFd(fd int) (err error) {
debugPrint("Listening and serving HTTP on fd@%d", fd)
defer func() { debugPrintError(err) }()
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
listener, err := net.FileListener(f)
if err != nil {
return
}
defer listener.Close()
err = http.Serve(listener, engine)
return
}
// ServeHTTP conforms to the http.Handler interface. // ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context) c := engine.pool.Get().(*Context)
@ -334,9 +355,11 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// This can be done by setting c.Request.URL.Path to your new target. // This can be done by setting c.Request.URL.Path to your new target.
// Disclaimer: You can loop yourself to death with this, use wisely. // Disclaimer: You can loop yourself to death with this, use wisely.
func (engine *Engine) HandleContext(c *Context) { func (engine *Engine) HandleContext(c *Context) {
oldIndexValue := c.index
c.reset() c.reset()
engine.handleHTTPRequest(c) engine.handleHTTPRequest(c)
engine.pool.Put(c)
c.index = oldIndexValue
} }
func (engine *Engine) handleHTTPRequest(c *Context) { func (engine *Engine) handleHTTPRequest(c *Context) {
@ -402,7 +425,10 @@ func serveError(c *Context, code int, defaultMessage []byte) {
} }
if c.writermem.Status() == code { if c.writermem.Status() == code {
c.writermem.Header()["Content-Type"] = mimePlain c.writermem.Header()["Content-Type"] = mimePlain
c.Writer.Write(defaultMessage) _, err := c.Writer.Write(defaultMessage)
if err != nil {
debugPrint("cannot write message to writer during serve error: %v", err)
}
return return
} }
c.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()

View File

@ -47,8 +47,8 @@ func NoMethod(handlers ...gin.HandlerFunc) {
engine().NoMethod(handlers...) engine().NoMethod(handlers...)
} }
// Group creates a new router group. You should add all the routes that have common middlwares or the same path prefix. // Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
// For example, all the routes that use a common middlware for authorization could be grouped. // For example, all the routes that use a common middleware for authorization could be grouped.
func Group(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup { func Group(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup {
return engine().Group(relativePath, handlers...) return engine().Group(relativePath, handlers...)
} }
@ -127,21 +127,21 @@ func Use(middlewares ...gin.HandlerFunc) gin.IRoutes {
// Run : The router is attached to a http.Server and starts listening and serving HTTP requests. // Run : The router is attached to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router) // It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine undefinitelly unless an error happens. // Note: this method will block the calling goroutine 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 : The router is attached 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 undefinitelly 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 : The router is attached to a http.Server and starts listening and serving HTTP requests
// through the specified unix socket (ie. a file) // through the specified unix socket (ie. a file)
// Note: this method will block the calling goroutine undefinitelly unless an error happens. // Note: this method will block the calling goroutine 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)
} }

View File

@ -6,12 +6,14 @@ package gin
import ( import (
"bufio" "bufio"
"crypto/tls"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"sync"
"testing" "testing"
"time" "time"
@ -19,7 +21,14 @@ import (
) )
func testRequest(t *testing.T, url string) { func testRequest(t *testing.T, url string) {
resp, err := http.Get(url) tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: tr}
resp, err := client.Get(url)
assert.NoError(t, err) assert.NoError(t, err)
defer resp.Body.Close() defer resp.Body.Close()
@ -44,6 +53,22 @@ func TestRunEmpty(t *testing.T) {
testRequest(t, "http://localhost:8080/example") testRequest(t, "http://localhost:8080/example")
} }
func TestRunTLS(t *testing.T) {
router := New()
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunTLS(":8443", "./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(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8443/example")
}
func TestRunEmptyWithEnv(t *testing.T) { func TestRunEmptyWithEnv(t *testing.T) {
os.Setenv("PORT", "3123") os.Setenv("PORT", "3123")
router := New() router := New()
@ -62,7 +87,7 @@ func TestRunEmptyWithEnv(t *testing.T) {
func TestRunTooMuchParams(t *testing.T) { func TestRunTooMuchParams(t *testing.T) {
router := New() router := New()
assert.Panics(t, func() { assert.Panics(t, func() {
router.Run("2", "2") assert.NoError(t, router.Run("2", "2"))
}) })
} }
@ -109,6 +134,42 @@ func TestBadUnixSocket(t *testing.T) {
assert.Error(t, router.RunUnix("#/tmp/unix_unit_test")) assert.Error(t, router.RunUnix("#/tmp/unix_unit_test"))
} }
func TestFileDescriptor(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
socketFile, err := listener.File()
assert.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunFd(int(socketFile.Fd())))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String())
assert.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
for scanner.Scan() {
response += scanner.Text()
}
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
func TestBadFileDescriptor(t *testing.T) {
router := New()
assert.Error(t, router.RunFd(0))
}
func TestWithHttptestWithAutoSelectedPort(t *testing.T) { func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
router := New() router := New()
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
@ -119,6 +180,26 @@ func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
testRequest(t, ts.URL+"/example") testRequest(t, ts.URL+"/example")
} }
func TestConcurrentHandleContext(t *testing.T) {
router := New()
router.GET("/", func(c *Context) {
c.Request.URL.Path = "/example"
router.HandleContext(c)
})
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
var wg sync.WaitGroup
iterations := 200
wg.Add(iterations)
for i := 0; i < iterations; i++ {
go func() {
testGetRequestHandler(t, router, "/")
wg.Done()
}()
}
wg.Wait()
}
// func TestWithHttptestWithSpecifiedPort(t *testing.T) { // func TestWithHttptestWithSpecifiedPort(t *testing.T) {
// router := New() // router := New()
// router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) // router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
@ -133,3 +214,14 @@ func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
// testRequest(t, "http://localhost:8033/example") // testRequest(t, "http://localhost:8033/example")
// } // }
func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
req, err := http.NewRequest("GET", url, nil)
assert.NoError(t, err)
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200")
}

View File

@ -10,7 +10,10 @@ import (
"html/template" "html/template"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest"
"reflect" "reflect"
"strconv"
"sync/atomic"
"testing" "testing"
"time" "time"
@ -22,15 +25,18 @@ func formatAsDate(t time.Time) string {
return fmt.Sprintf("%d/%02d/%02d", year, month, day) return fmt.Sprintf("%d/%02d/%02d", year, month, day)
} }
func setupHTMLFiles(t *testing.T, mode string, tls bool) func() { func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine)) *httptest.Server {
go func() { SetMode(mode)
SetMode(mode) defer SetMode(TestMode)
router := New()
var router *Engine
captureOutput(t, func() {
router = New()
router.Delims("{[{", "}]}") router.Delims("{[{", "}]}")
router.SetFuncMap(template.FuncMap{ router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate, "formatAsDate": formatAsDate,
}) })
router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl") loadMethod(router)
router.GET("/test", func(c *Context) { router.GET("/test", func(c *Context) {
c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"}) c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
}) })
@ -39,88 +45,90 @@ func setupHTMLFiles(t *testing.T, mode string, tls bool) func() {
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
}) })
}) })
if tls { })
// these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1`
router.RunTLS(":9999", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem") var ts *httptest.Server
} else {
router.Run(":8888") if tls {
} ts = httptest.NewTLSServer(router)
}() } else {
t.Log("waiting 1 second for server startup") ts = httptest.NewServer(router)
time.Sleep(1 * time.Second) }
return func() {}
return ts
} }
func setupHTMLGlob(t *testing.T, mode string, tls bool) func() { func TestLoadHTMLGlobDebugMode(t *testing.T) {
go func() { ts := setupHTMLFiles(
SetMode(mode) t,
router := New() DebugMode,
router.Delims("{[{", "}]}") false,
router.SetFuncMap(template.FuncMap{ func(router *Engine) {
"formatAsDate": formatAsDate, router.LoadHTMLGlob("./testdata/template/*")
}) },
router.LoadHTMLGlob("./testdata/template/*") )
router.GET("/test", func(c *Context) { defer ts.Close()
c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
})
router.GET("/raw", func(c *Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
})
})
if tls {
// these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1`
router.RunTLS(":9999", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")
} else {
router.Run(":8888")
}
}()
t.Log("waiting 1 second for server startup")
time.Sleep(1 * time.Second)
return func() {}
}
func TestLoadHTMLGlob(t *testing.T) { res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
td := setupHTMLGlob(t, DebugMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLGlob2(t *testing.T) { func TestLoadHTMLGlobTestMode(t *testing.T) {
td := setupHTMLGlob(t, TestMode, false) ts := setupHTMLFiles(
res, err := http.Get("http://127.0.0.1:8888/test") t,
TestMode,
false,
func(router *Engine) {
router.LoadHTMLGlob("./testdata/template/*")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLGlob3(t *testing.T) { func TestLoadHTMLGlobReleaseMode(t *testing.T) {
td := setupHTMLGlob(t, ReleaseMode, false) ts := setupHTMLFiles(
res, err := http.Get("http://127.0.0.1:8888/test") t,
ReleaseMode,
false,
func(router *Engine) {
router.LoadHTMLGlob("./testdata/template/*")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLGlobUsingTLS(t *testing.T) { func TestLoadHTMLGlobUsingTLS(t *testing.T) {
td := setupHTMLGlob(t, DebugMode, true) ts := setupHTMLFiles(
t,
DebugMode,
true,
func(router *Engine) {
router.LoadHTMLGlob("./testdata/template/*")
},
)
defer ts.Close()
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error // Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{ tr := &http.Transport{
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
@ -128,29 +136,33 @@ func TestLoadHTMLGlobUsingTLS(t *testing.T) {
}, },
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
res, err := client.Get("https://127.0.0.1:9999/test") res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLGlobFromFuncMap(t *testing.T) { func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
time.Now() ts := setupHTMLFiles(
td := setupHTMLGlob(t, DebugMode, false) t,
res, err := http.Get("http://127.0.0.1:8888/raw") DebugMode,
false,
func(router *Engine) {
router.LoadHTMLGlob("./testdata/template/*")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01\n", string(resp)) assert.Equal(t, "Date: 2017/07/01\n", string(resp))
td()
} }
func init() { func init() {
@ -164,59 +176,77 @@ func TestCreateEngine(t *testing.T) {
assert.Empty(t, router.Handlers) assert.Empty(t, router.Handlers)
} }
// func TestLoadHTMLDebugMode(t *testing.T) { func TestLoadHTMLFilesTestMode(t *testing.T) {
// router := New() ts := setupHTMLFiles(
// SetMode(DebugMode) t,
// router.LoadHTMLGlob("*.testtmpl") TestMode,
// r := router.HTMLRender.(render.HTMLDebug) false,
// assert.Empty(t, r.Files) func(router *Engine) {
// assert.Equal(t, "*.testtmpl", r.Glob) router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
// },
// router.LoadHTMLFiles("index.html.testtmpl", "login.html.testtmpl") )
// r = router.HTMLRender.(render.HTMLDebug) defer ts.Close()
// assert.Empty(t, r.Glob)
// assert.Equal(t, []string{"index.html", "login.html"}, r.Files)
// SetMode(TestMode)
// }
func TestLoadHTMLFiles(t *testing.T) { res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
td := setupHTMLFiles(t, TestMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLFiles2(t *testing.T) { func TestLoadHTMLFilesDebugMode(t *testing.T) {
td := setupHTMLFiles(t, DebugMode, false) ts := setupHTMLFiles(
res, err := http.Get("http://127.0.0.1:8888/test") t,
DebugMode,
false,
func(router *Engine) {
router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLFiles3(t *testing.T) { func TestLoadHTMLFilesReleaseMode(t *testing.T) {
td := setupHTMLFiles(t, ReleaseMode, false) ts := setupHTMLFiles(
res, err := http.Get("http://127.0.0.1:8888/test") t,
ReleaseMode,
false,
func(router *Engine) {
router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLFilesUsingTLS(t *testing.T) { func TestLoadHTMLFilesUsingTLS(t *testing.T) {
td := setupHTMLFiles(t, TestMode, true) ts := setupHTMLFiles(
t,
TestMode,
true,
func(router *Engine) {
router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
},
)
defer ts.Close()
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error // Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{ tr := &http.Transport{
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
@ -224,28 +254,33 @@ func TestLoadHTMLFilesUsingTLS(t *testing.T) {
}, },
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
res, err := client.Get("https://127.0.0.1:9999/test") res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLFilesFuncMap(t *testing.T) { func TestLoadHTMLFilesFuncMap(t *testing.T) {
time.Now() ts := setupHTMLFiles(
td := setupHTMLFiles(t, TestMode, false) t,
res, err := http.Get("http://127.0.0.1:8888/raw") TestMode,
false,
func(router *Engine) {
router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01\n", string(resp)) assert.Equal(t, "Date: 2017/07/01\n", string(resp))
td()
} }
func TestAddRoute(t *testing.T) { func TestAddRoute(t *testing.T) {
@ -443,6 +478,60 @@ func TestListOfRoutes(t *testing.T) {
}) })
} }
func TestEngineHandleContext(t *testing.T) {
r := New()
r.GET("/", func(c *Context) {
c.Request.URL.Path = "/v2"
r.HandleContext(c)
})
v2 := r.Group("/v2")
{
v2.GET("/", func(c *Context) {})
}
assert.NotPanics(t, func() {
w := performRequest(r, "GET", "/")
assert.Equal(t, 301, w.Code)
})
}
func TestEngineHandleContextManyReEntries(t *testing.T) {
expectValue := 10000
var handlerCounter, middlewareCounter int64
r := New()
r.Use(func(c *Context) {
atomic.AddInt64(&middlewareCounter, 1)
})
r.GET("/:count", func(c *Context) {
countStr := c.Param("count")
count, err := strconv.Atoi(countStr)
assert.NoError(t, err)
n, err := c.Writer.Write([]byte("."))
assert.NoError(t, err)
assert.Equal(t, 1, n)
switch {
case count > 0:
c.Request.URL.Path = "/" + strconv.Itoa(count-1)
r.HandleContext(c)
}
}, func(c *Context) {
atomic.AddInt64(&handlerCounter, 1)
})
assert.NotPanics(t, func() {
w := performRequest(r, "GET", "/"+strconv.Itoa(expectValue-1)) // include 0 value
assert.Equal(t, 200, w.Code)
assert.Equal(t, expectValue, w.Body.Len())
})
assert.Equal(t, int64(expectValue), handlerCounter)
assert.Equal(t, int64(expectValue), middlewareCounter)
}
func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) { func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) {
for _, gotRoute := range gotRoutes { for _, gotRoute := range gotRoutes {
if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method { if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method {

View File

@ -285,6 +285,67 @@ var githubAPI = []route{
{"DELETE", "/user/keys/:id"}, {"DELETE", "/user/keys/:id"},
} }
func TestShouldBindUri(t *testing.T) {
DefaultWriter = os.Stdout
router := New()
type Person struct {
Name string `uri:"name" binding:"required"`
Id string `uri:"id" binding:"required"`
}
router.Handle("GET", "/rest/:name/:id", func(c *Context) {
var person Person
assert.NoError(t, c.ShouldBindUri(&person))
assert.True(t, "" != person.Name)
assert.True(t, "" != person.Id)
c.String(http.StatusOK, "ShouldBindUri test OK")
})
path, _ := exampleFromPath("/rest/:name/:id")
w := performRequest(router, "GET", path)
assert.Equal(t, "ShouldBindUri test OK", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code)
}
func TestBindUri(t *testing.T) {
DefaultWriter = os.Stdout
router := New()
type Person struct {
Name string `uri:"name" binding:"required"`
Id string `uri:"id" binding:"required"`
}
router.Handle("GET", "/rest/:name/:id", func(c *Context) {
var person Person
assert.NoError(t, c.BindUri(&person))
assert.True(t, "" != person.Name)
assert.True(t, "" != person.Id)
c.String(http.StatusOK, "BindUri test OK")
})
path, _ := exampleFromPath("/rest/:name/:id")
w := performRequest(router, "GET", path)
assert.Equal(t, "BindUri test OK", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code)
}
func TestBindUriError(t *testing.T) {
DefaultWriter = os.Stdout
router := New()
type Member struct {
Number string `uri:"num" binding:"required,uuid"`
}
router.Handle("GET", "/new/rest/:num", func(c *Context) {
var m Member
assert.Error(t, c.BindUri(&m))
})
path1, _ := exampleFromPath("/new/rest/:num")
w1 := performRequest(router, "GET", path1)
assert.Equal(t, http.StatusBadRequest, w1.Code)
}
func githubConfigRouter(router *Engine) { func githubConfigRouter(router *Engine) {
for _, route := range githubAPI { for _, route := range githubAPI {
router.Handle(route.method, route.path, func(c *Context) { router.Handle(route.method, route.path, func(c *Context) {
@ -300,7 +361,7 @@ func githubConfigRouter(router *Engine) {
func TestGithubAPI(t *testing.T) { func TestGithubAPI(t *testing.T) {
DefaultWriter = os.Stdout DefaultWriter = os.Stdout
router := Default() router := New()
githubConfigRouter(router) githubConfigRouter(router)
for _, route := range githubAPI { for _, route := range githubAPI {
@ -375,7 +436,7 @@ func BenchmarkParallelGithub(b *testing.B) {
func BenchmarkParallelGithubDefault(b *testing.B) { func BenchmarkParallelGithubDefault(b *testing.B) {
DefaultWriter = os.Stdout DefaultWriter = os.Stdout
router := Default() router := New()
githubConfigRouter(router) githubConfigRouter(router)
req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil) req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil)

42
go.mod
View File

@ -1,28 +1,32 @@
module github.com/gin-gonic/gin module github.com/gin-gonic/gin
require ( require (
github.com/davecgh/go-spew v0.0.0-20180221232628-8991bc29aa16 // indirect github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482
github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7
github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b
github.com/golang/protobuf v1.2.0 github.com/golang/protobuf v1.2.0
github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 github.com/json-iterator/go v1.1.5
github.com/json-iterator/go v0.0.0-20180806060727-1624edc4454b github.com/mattn/go-isatty v0.0.4
github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227
github.com/mattn/go-isatty v0.0.3
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/newrelic/go-agent v2.1.0+incompatible github.com/stretchr/testify v1.3.0
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2
github.com/stretchr/testify v1.2.2 golang.org/x/net v0.0.0-20190119204137-ed066c81e75e
github.com/thinkerou/favicon v0.0.0-20170710140520-94a442a49da6 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4
github.com/ugorji/go v1.1.1 golang.org/x/sys v0.0.0-20190124100055-b90733256f2e // indirect
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e // indirect
google.golang.org/grpc v1.15.0
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.1 gopkg.in/yaml.v2 v2.2.2
)
exclude (
github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160
github.com/client9/misspell v0.3.4
github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66
github.com/gin-gonic/autotls v0.0.0-20190119125636-0b5f4fc15768
github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15
github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227
github.com/newrelic/go-agent v2.5.0+incompatible
github.com/thinkerou/favicon v0.1.0
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1
google.golang.org/grpc v1.18.0
) )

67
go.sum
View File

@ -1,67 +1,54 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160 h1:HJpuhXOHC4EkXDARsLjmXAV9FhlY6qFDnKI/MJM6eoE=
github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v0.0.0-20180221232628-8991bc29aa16 h1:HqkufMBR7waVfFFRABWqHa1WgTvjtVDJTLJe3CR576A= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v0.0.0-20180221232628-8991bc29aa16/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66 h1:QnnoVdChKs+GeTvN4rPYTW6b5U6M3HMEvQ/+x4IGtfY= github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482 h1:iOz5sIQUvuOlpiC7Q6+MmJQpWnlneYX98QIGf+2m50Y=
github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66/go.mod h1:kTEh6M2J/mh7nsskr28alwLCXm/DSG5OSA/o31yy2XU= github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b h1:dm/NYytoj7p8Jc6zMvyRz3PCQrTTCXnVRvEzyBcM890=
github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b/go.mod h1:vwfeXwKgEIWq63oVfwaBjoByS4dZzYbHHROHjV4IjNY=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 h1:cW/amwGEJK5MSKntPXRjX4dxs/nGxGT8gXKIsKFmHGc= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15/go.mod h1:Fdm/oWRW+CH8PRbLntksCNtmcCBximKPkVQYvmMl80k=
github.com/json-iterator/go v0.0.0-20180806060727-1624edc4454b h1:X61dhFTE1Au92SvyF8HyAwdjWqiSdfBgFR7wTxC0+uU=
github.com/json-iterator/go v0.0.0-20180806060727-1624edc4454b/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227 h1:KIaAZ/V+/0/6BOULrmBQ9T1ed8BkKqGIjIKW923nJuo= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227/go.mod h1:ruMr5t05gVho4tuDv0PbI0Bb8nOxc/5Y6JzRHe/yfA0= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
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/newrelic/go-agent v2.1.0+incompatible h1:fCuxXeM4eeIKPbzffOWW6y2Dj+eYfc3yylgNZACZqkM=
github.com/newrelic/go-agent v2.1.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ=
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/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/thinkerou/favicon v0.0.0-20170710140520-94a442a49da6 h1:d/LEgOfWe+AlOCz/kzmkvlO+gq9LRGhjSHqt2nx8Muc= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/thinkerou/favicon v0.0.0-20170710140520-94a442a49da6/go.mod h1:HL7Pap5kOluZv1ku34pZo/AJ44GaxMEPFZ3pmuexV2s= github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4=
github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w= github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc=
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e h1:MDa3fSUp6MdYHouVmCCNz/zaH2a6CRcxY3VhT/K3C5Q=
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20190124100055-b90733256f2e h1:3GIlrlVLfkoipSReOMNAgApI0ajnalyLa/EZHHca/XI=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.15.0 h1:Az/KuahOM4NAidTEuJCv/RonAA7rYsTPkqXVjr+8OOw= google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA=
google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
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=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

129
logger.go
View File

@ -17,7 +17,7 @@ import (
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, 55, 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})
@ -26,6 +26,65 @@ var (
disableColor = false disableColor = false
) )
// LoggerConfig defines the config for Logger middleware.
type LoggerConfig struct {
// Optional. Default value is gin.defaultLogFormatter
Formatter LogFormatter
// Output is a writer where logs are written.
// Optional. Default value is gin.DefaultWriter.
Output io.Writer
// SkipPaths is a url path array which logs are not written.
// Optional.
SkipPaths []string
}
// LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter
type LogFormatter func(params LogFormatterParams) string
// LogFormatterParams is the structure any formatter will be handed when time to log comes
type LogFormatterParams struct {
Request *http.Request
// TimeStamp shows the time after the server returns a response.
TimeStamp time.Time
// StatusCode is HTTP response code.
StatusCode int
// Latency is how much time the server cost to process a certain request.
Latency time.Duration
// ClientIP equals Context's ClientIP method.
ClientIP string
// Method is the HTTP method given to the request.
Method string
// Path is a path the client requests.
Path string
// ErrorMessage is set if error has occurred in processing the request.
ErrorMessage string
// IsTerm shows whether does gin's output descriptor refers to a terminal.
IsTerm bool
}
// defaultLogFormatter is the default log format function Logger middleware uses.
var defaultLogFormatter = func(param LogFormatterParams) string {
var statusColor, methodColor, resetColor string
if param.IsTerm {
statusColor = colorForStatus(param.StatusCode)
methodColor = colorForMethod(param.Method)
resetColor = reset
}
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"),
statusColor, param.StatusCode, resetColor,
param.Latency,
param.ClientIP,
methodColor, param.Method, resetColor,
param.Path,
param.ErrorMessage,
)
}
// DisableConsoleColor disables color output in the console. // DisableConsoleColor disables color output in the console.
func DisableConsoleColor() { func DisableConsoleColor() {
disableColor = true disableColor = true
@ -50,12 +109,39 @@ func ErrorLoggerT(typ ErrorType) HandlerFunc {
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter. // Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
// By default gin.DefaultWriter = os.Stdout. // By default gin.DefaultWriter = os.Stdout.
func Logger() HandlerFunc { func Logger() HandlerFunc {
return LoggerWithWriter(DefaultWriter) return LoggerWithConfig(LoggerConfig{})
} }
// LoggerWithWriter instance a Logger middleware with the specified writter buffer. // LoggerWithFormatter instance a Logger middleware with the specified log format function.
func LoggerWithFormatter(f LogFormatter) HandlerFunc {
return LoggerWithConfig(LoggerConfig{
Formatter: f,
})
}
// LoggerWithWriter instance a Logger middleware with the specified writer buffer.
// Example: os.Stdout, a file opened in write mode, a socket... // Example: os.Stdout, a file opened in write mode, a socket...
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc { func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
return LoggerWithConfig(LoggerConfig{
Output: out,
SkipPaths: notlogged,
})
}
// LoggerWithConfig instance a Logger middleware with config.
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
formatter := conf.Formatter
if formatter == nil {
formatter = defaultLogFormatter
}
out := conf.Output
if out == nil {
out = DefaultWriter
}
notlogged := conf.SkipPaths
isTerm := true isTerm := true
if w, ok := out.(*os.File); !ok || if w, ok := out.(*os.File); !ok ||
@ -85,34 +171,27 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
// Log only when path is not being skipped // Log only when path is not being skipped
if _, ok := skip[path]; !ok { if _, ok := skip[path]; !ok {
// Stop timer param := LogFormatterParams{
end := time.Now() Request: c.Request,
latency := end.Sub(start) IsTerm: isTerm,
clientIP := c.ClientIP()
method := c.Request.Method
statusCode := c.Writer.Status()
var statusColor, methodColor, resetColor string
if isTerm {
statusColor = colorForStatus(statusCode)
methodColor = colorForMethod(method)
resetColor = reset
} }
comment := c.Errors.ByType(ErrorTypePrivate).String()
// Stop timer
param.TimeStamp = time.Now()
param.Latency = param.TimeStamp.Sub(start)
param.ClientIP = c.ClientIP()
param.Method = c.Request.Method
param.StatusCode = c.Writer.Status()
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
if raw != "" { if raw != "" {
path = path + "?" + raw path = path + "?" + raw
} }
fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", param.Path = path
end.Format("2006/01/02 - 15:04:05"),
statusColor, statusCode, resetColor, fmt.Fprint(out, formatter(param))
latency,
clientIP,
methodColor, method, resetColor,
path,
comment,
)
} }
} }
} }

View File

@ -7,8 +7,10 @@ package gin
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"net/http" "net/http"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -79,13 +81,185 @@ func TestLogger(t *testing.T) {
assert.Contains(t, buffer.String(), "404") assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET") assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/notfound") assert.Contains(t, buffer.String(), "/notfound")
}
func TestLoggerWithConfig(t *testing.T) {
buffer := new(bytes.Buffer)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{Output: buffer}))
router.GET("/example", func(c *Context) {})
router.POST("/example", func(c *Context) {})
router.PUT("/example", func(c *Context) {})
router.DELETE("/example", func(c *Context) {})
router.PATCH("/example", func(c *Context) {})
router.HEAD("/example", func(c *Context) {})
router.OPTIONS("/example", func(c *Context) {})
performRequest(router, "GET", "/example?a=100")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
// I wrote these first (extending the above) but then realized they are more
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
buffer.Reset()
performRequest(router, "POST", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "PUT", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "DELETE", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "PATCH", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PATCH")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "HEAD", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "HEAD")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "OPTIONS", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "OPTIONS")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "GET", "/notfound")
assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/notfound")
}
func TestLoggerWithFormatter(t *testing.T) {
buffer := new(bytes.Buffer)
d := DefaultWriter
DefaultWriter = buffer
defer func() {
DefaultWriter = d
}()
router := New()
router.Use(LoggerWithFormatter(func(param LogFormatterParams) string {
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
param.StatusCode,
param.Latency,
param.ClientIP,
param.Method,
param.Path,
param.ErrorMessage,
)
}))
router.GET("/example", func(c *Context) {})
performRequest(router, "GET", "/example?a=100")
// output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
}
func TestLoggerWithConfigFormatting(t *testing.T) {
var gotParam LogFormatterParams
buffer := new(bytes.Buffer)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
Formatter: func(param LogFormatterParams) string {
// for assert test
gotParam = param
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
param.StatusCode,
param.Latency,
param.ClientIP,
param.Method,
param.Path,
param.ErrorMessage,
)
},
}))
router.GET("/example", func(c *Context) {
// set dummy ClientIP
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20")
})
performRequest(router, "GET", "/example?a=100")
// output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
// LogFormatterParams test
assert.NotNil(t, gotParam.Request)
assert.NotEmpty(t, gotParam.TimeStamp)
assert.Equal(t, 200, gotParam.StatusCode)
assert.NotEmpty(t, gotParam.Latency)
assert.Equal(t, "20.20.20.20", gotParam.ClientIP)
assert.Equal(t, "GET", gotParam.Method)
assert.Equal(t, "/example?a=100", gotParam.Path)
assert.Empty(t, gotParam.ErrorMessage)
}
func TestDefaultLogFormatter(t *testing.T) {
timeStamp := time.Unix(1544173902, 0).UTC()
termFalseParam := LogFormatterParams{
TimeStamp: timeStamp,
StatusCode: 200,
Latency: time.Second * 5,
ClientIP: "20.20.20.20",
Method: "GET",
Path: "/",
ErrorMessage: "",
IsTerm: false,
}
termTrueParam := LogFormatterParams{
TimeStamp: timeStamp,
StatusCode: 200,
Latency: time.Second * 5,
ClientIP: "20.20.20.20",
Method: "GET",
Path: "/",
ErrorMessage: "",
IsTerm: true,
}
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 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam))
} }
func TestColorForMethod(t *testing.T) { func TestColorForMethod(t *testing.T) {
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow") assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForMethod("DELETE"), "delete should be red") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForMethod("PATCH"), "patch should be green") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), colorForMethod("HEAD"), "head should be magenta") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), colorForMethod("HEAD"), "head should be magenta")
@ -96,7 +270,7 @@ func TestColorForMethod(t *testing.T) {
func TestColorForStatus(t *testing.T) { func TestColorForStatus(t *testing.T) {
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white") assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow") assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red")
} }
@ -104,13 +278,13 @@ func TestErrorLogger(t *testing.T) {
router := New() router := New()
router.Use(ErrorLogger()) router.Use(ErrorLogger())
router.GET("/error", func(c *Context) { router.GET("/error", func(c *Context) {
c.Error(errors.New("this is an error")) c.Error(errors.New("this is an error")) // nolint: errcheck
}) })
router.GET("/abort", func(c *Context) { router.GET("/abort", func(c *Context) {
c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized")) c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized")) // nolint: errcheck
}) })
router.GET("/print", func(c *Context) { router.GET("/print", func(c *Context) {
c.Error(errors.New("this is an error")) c.Error(errors.New("this is an error")) // nolint: errcheck
c.String(http.StatusInternalServerError, "hola!") c.String(http.StatusInternalServerError, "hola!")
}) })
@ -127,7 +301,7 @@ func TestErrorLogger(t *testing.T) {
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String()) assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
} }
func TestSkippingPaths(t *testing.T) { func TestLoggerWithWriterSkippingPaths(t *testing.T) {
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
router := New() router := New()
router.Use(LoggerWithWriter(buffer, "/skipped")) router.Use(LoggerWithWriter(buffer, "/skipped"))
@ -142,6 +316,24 @@ func TestSkippingPaths(t *testing.T) {
assert.Contains(t, buffer.String(), "") assert.Contains(t, buffer.String(), "")
} }
func TestLoggerWithConfigSkippingPaths(t *testing.T) {
buffer := new(bytes.Buffer)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
SkipPaths: []string{"/skipped"},
}))
router.GET("/logged", func(c *Context) {})
router.GET("/skipped", func(c *Context) {})
performRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
performRequest(router, "GET", "/skipped")
assert.Contains(t, buffer.String(), "")
}
func TestDisableConsoleColor(t *testing.T) { func TestDisableConsoleColor(t *testing.T) {
New() New()
assert.False(t, disableColor) assert.False(t, disableColor)

View File

@ -208,7 +208,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
router := New() router := New()
router.Use(func(context *Context) { router.Use(func(context *Context) {
signature += "A" signature += "A"
context.AbortWithError(http.StatusInternalServerError, errors.New("foo")) context.AbortWithError(http.StatusInternalServerError, errors.New("foo")) // nolint: errcheck
}) })
router.Use(func(context *Context) { router.Use(func(context *Context) {
signature += "B" signature += "B"

View File

@ -17,7 +17,7 @@ const ENV_GIN_MODE = "GIN_MODE"
const ( const (
// DebugMode indicates gin mode is debug. // DebugMode indicates gin mode is debug.
DebugMode = "debug" DebugMode = "debug"
// ReleaseMode indicates gin mode is relase. // ReleaseMode indicates gin mode is release.
ReleaseMode = "release" ReleaseMode = "release"
// TestMode indicates gin mode is test. // TestMode indicates gin mode is test.
TestMode = "test" TestMode = "test"
@ -28,7 +28,7 @@ const (
testCode testCode
) )
// DefaultWriter is the default io.Writer used the Gin for debug output and // DefaultWriter is the default io.Writer used by Gin for debug output and
// middleware output like Logger() or Recovery(). // middleware output like Logger() or Recovery().
// Note that both Logger and Recovery provides custom ways to configure their // Note that both Logger and Recovery provides custom ways to configure their
// output io.Writer. // output io.Writer.
@ -36,6 +36,8 @@ const (
// import "github.com/mattn/go-colorable" // import "github.com/mattn/go-colorable"
// gin.DefaultWriter = colorable.NewColorableStdout() // gin.DefaultWriter = colorable.NewColorableStdout()
var DefaultWriter io.Writer = os.Stdout var DefaultWriter io.Writer = os.Stdout
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors
var DefaultErrorWriter io.Writer = os.Stderr var DefaultErrorWriter io.Writer = os.Stderr
var ginMode = debugCode var ginMode = debugCode

View File

@ -10,9 +10,12 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"os"
"runtime" "runtime"
"strings"
"time" "time"
) )
@ -37,16 +40,37 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
return func(c *Context) { return func(c *Context) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
if logger != nil { // Check for a broken connection, as it is not really a
stack := stack(3) // condition that warrants a panic stack trace.
if IsDebugging() { var brokenPipe bool
httprequest, _ := httputil.DumpRequest(c.Request, false) if ne, ok := err.(*net.OpError); ok {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", timeFormat(time.Now()), string(httprequest), err, stack, reset) if se, ok := ne.Err.(*os.SyscallError); ok {
} else { if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", timeFormat(time.Now()), err, stack, reset) brokenPipe = true
}
} }
} }
c.AbortWithStatus(http.StatusInternalServerError) if logger != nil {
stack := stack(3)
httprequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
logger.Printf("%s\n%s%s", err, string(httprequest), reset)
} else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), string(httprequest), err, stack, reset)
} else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset)
}
}
// If the connection is dead, we can't write a status to it.
if brokenPipe {
c.Error(err.(error)) // nolint: errcheck
c.Abort()
} else {
c.AbortWithStatus(http.StatusInternalServerError)
}
} }
}() }()
c.Next() c.Next()

View File

@ -2,11 +2,17 @@
// 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"
"net"
"net/http" "net/http"
"os"
"strings"
"syscall"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -37,6 +43,7 @@ func TestPanicInHandler(t *testing.T) {
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
SetMode(TestMode)
} }
// TestPanicWithAbort assert that panic has been recovered even if context.Abort was used. // TestPanicWithAbort assert that panic has been recovered even if context.Abort was used.
@ -72,3 +79,38 @@ func TestFunction(t *testing.T) {
bs := function(1) bs := function(1)
assert.Equal(t, []byte("???"), bs) assert.Equal(t, []byte("???"), bs)
} }
// TestPanicWithBrokenPipe asserts that recovery specifically handles
// writing responses to broken pipes
func TestPanicWithBrokenPipe(t *testing.T) {
const expectCode = 204
expectMsgs := map[syscall.Errno]string{
syscall.EPIPE: "broken pipe",
syscall.ECONNRESET: "connection reset by peer",
}
for errno, expectMsg := range expectMsgs {
t.Run(expectMsg, func(t *testing.T) {
var buf bytes.Buffer
router := New()
router.Use(RecoveryWithWriter(&buf))
router.GET("/recovery", func(c *Context) {
// Start writing response
c.Header("X-Test", "Value")
c.Status(expectCode)
// Oops. Client connection closed
e := &net.OpError{Err: &os.SyscallError{Err: errno}}
panic(e)
})
// RUN
w := performRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, expectCode, w.Code)
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
})
}
}

View File

@ -67,8 +67,8 @@ func WriteJSON(w http.ResponseWriter, obj interface{}) error {
if err != nil { if err != nil {
return err return err
} }
w.Write(jsonBytes) _, err = w.Write(jsonBytes)
return nil return err
} }
// Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType. // Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType.
@ -78,8 +78,8 @@ func (r IndentedJSON) Render(w http.ResponseWriter) error {
if err != nil { if err != nil {
return err return err
} }
w.Write(jsonBytes) _, err = w.Write(jsonBytes)
return nil return err
} }
// WriteContentType (IndentedJSON) writes JSON ContentType. // WriteContentType (IndentedJSON) writes JSON ContentType.
@ -96,10 +96,13 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
} }
// if the jsonBytes is array values // if the jsonBytes is array values
if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) { if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) {
w.Write([]byte(r.Prefix)) _, err = w.Write([]byte(r.Prefix))
if err != nil {
return err
}
} }
w.Write(jsonBytes) _, err = w.Write(jsonBytes)
return nil return err
} }
// WriteContentType (SecureJSON) writes JSON ContentType. // WriteContentType (SecureJSON) writes JSON ContentType.
@ -116,15 +119,27 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
} }
if r.Callback == "" { if r.Callback == "" {
w.Write(ret) _, err = w.Write(ret)
return nil return err
} }
callback := template.JSEscapeString(r.Callback) callback := template.JSEscapeString(r.Callback)
w.Write([]byte(callback)) _, err = w.Write([]byte(callback))
w.Write([]byte("(")) if err != nil {
w.Write(ret) return err
w.Write([]byte(")")) }
_, err = w.Write([]byte("("))
if err != nil {
return err
}
_, err = w.Write(ret)
if err != nil {
return err
}
_, err = w.Write([]byte(")"))
if err != nil {
return err
}
return nil return nil
} }
@ -151,8 +166,8 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
buffer.WriteString(cvt) buffer.WriteString(cvt)
} }
w.Write(buffer.Bytes()) _, err = w.Write(buffer.Bytes())
return nil return err
} }
// WriteContentType (AsciiJSON) writes JSON ContentType. // WriteContentType (AsciiJSON) writes JSON ContentType.

View File

@ -26,8 +26,8 @@ func (r ProtoBuf) Render(w http.ResponseWriter) error {
return err return err
} }
w.Write(bytes) _, err = w.Write(bytes)
return nil return err
} }
// WriteContentType (ProtoBuf) writes ProtoBuf ContentType. // WriteContentType (ProtoBuf) writes ProtoBuf ContentType.

View File

@ -71,7 +71,7 @@ func TestRenderJSONPanics(t *testing.T) {
data := make(chan int) data := make(chan int)
// json: unsupported type: chan int // json: unsupported type: chan int
assert.Panics(t, func() { (JSON{data}).Render(w) }) assert.Panics(t, func() { assert.NoError(t, (JSON{data}).Render(w)) })
} }
func TestRenderIndentedJSON(t *testing.T) { func TestRenderIndentedJSON(t *testing.T) {
@ -335,7 +335,7 @@ func TestRenderRedirect(t *testing.T) {
} }
w = httptest.NewRecorder() w = httptest.NewRecorder()
assert.Panics(t, func() { data2.Render(w) }) assert.Panics(t, func() { assert.NoError(t, data2.Render(w)) })
// only improve coverage // only improve coverage
data2.WriteContentType(w) data2.WriteContentType(w)
@ -480,7 +480,7 @@ func TestRenderReader(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, body, w.Body.String()) assert.Equal(t, body, w.Body.String())
assert.Equal(t, "image/png", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.Equal(t, strconv.Itoa(len(body)), w.HeaderMap.Get("Content-Length")) assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length"))
assert.Equal(t, headers["Content-Disposition"], w.HeaderMap.Get("Content-Disposition")) assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
} }

View File

@ -20,8 +20,7 @@ var plainContentType = []string{"text/plain; charset=utf-8"}
// Render (String) writes data with custom ContentType. // Render (String) writes data with custom ContentType.
func (r String) Render(w http.ResponseWriter) error { func (r String) Render(w http.ResponseWriter) error {
WriteString(w, r.Format, r.Data) return WriteString(w, r.Format, r.Data)
return nil
} }
// WriteContentType (String) writes Plain ContentType. // WriteContentType (String) writes Plain ContentType.
@ -30,11 +29,12 @@ func (r String) WriteContentType(w http.ResponseWriter) {
} }
// WriteString writes data according to its format and write custom ContentType. // WriteString writes data according to its format and write custom ContentType.
func WriteString(w http.ResponseWriter, format string, data []interface{}) { func WriteString(w http.ResponseWriter, format string, data []interface{}) (err error) {
writeContentType(w, plainContentType) writeContentType(w, plainContentType)
if len(data) > 0 { if len(data) > 0 {
fmt.Fprintf(w, format, data...) _, err = fmt.Fprintf(w, format, data...)
return return
} }
io.WriteString(w, format) _, err = io.WriteString(w, format)
return
} }

View File

@ -26,8 +26,8 @@ func (r YAML) Render(w http.ResponseWriter) error {
return err return err
} }
w.Write(bytes) _, err = w.Write(bytes)
return nil return err
} }
// WriteContentType (YAML) writes YAML ContentType for response. // WriteContentType (YAML) writes YAML ContentType for response.

View File

@ -103,7 +103,8 @@ func TestResponseWriterHijack(t *testing.T) {
w := ResponseWriter(writer) w := ResponseWriter(writer)
assert.Panics(t, func() { assert.Panics(t, func() {
w.Hijack() _, _, err := w.Hijack()
assert.NoError(t, err)
}) })
assert.True(t, w.Written()) assert.True(t, w.Written())

View File

@ -47,14 +47,14 @@ type RouterGroup struct {
var _ IRouter = &RouterGroup{} var _ IRouter = &RouterGroup{}
// Use adds middleware to the group, see example code in github. // Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...) group.Handlers = append(group.Handlers, middleware...)
return group.returnObj() return group.returnObj()
} }
// Group creates a new router group. You should add all the routes that have common middlwares or the same path prefix. // Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
// For example, all the routes that use a common middlware for authorization could be grouped. // For example, all the routes that use a common middleware for authorization could be grouped.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{ return &RouterGroup{
Handlers: group.combineHandlers(handlers), Handlers: group.combineHandlers(handlers),
@ -78,7 +78,7 @@ func (group *RouterGroup) handle(httpMethod, relativePath string, handlers Handl
// Handle registers a new request handle and middleware with the given path and method. // Handle registers a new request handle and middleware with the given path and method.
// The last handler should be the real handler, the other ones should be middleware that can and should be shared among different routes. // The last handler should be the real handler, the other ones should be middleware that can and should be shared among different routes.
// See the example code in github. // See the example code in GitHub.
// //
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
// functions can be used. // functions can be used.
@ -185,11 +185,22 @@ func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRou
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc { func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {
absolutePath := group.calculateAbsolutePath(relativePath) absolutePath := group.calculateAbsolutePath(relativePath)
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs)) fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
_, nolisting := fs.(*onlyfilesFS)
return func(c *Context) { return func(c *Context) {
if nolisting { if _, nolisting := fs.(*onlyfilesFS); nolisting {
c.Writer.WriteHeader(http.StatusNotFound) c.Writer.WriteHeader(http.StatusNotFound)
} }
file := c.Param("filepath")
// Check if file exists and/or if we have permission to access it
if _, err := fs.Open(file); err != nil {
c.Writer.WriteHeader(http.StatusNotFound)
c.handlers = group.engine.allNoRoute
// Reset index
c.index = -1
return
}
fileServer.ServeHTTP(c.Writer, c.Request) fileServer.ServeHTTP(c.Writer, c.Request)
} }
} }

View File

@ -251,7 +251,8 @@ func TestRouteStaticFile(t *testing.T) {
t.Error(err) t.Error(err)
} }
defer os.Remove(f.Name()) defer os.Remove(f.Name())
f.WriteString("Gin Web Framework") _, err = f.WriteString("Gin Web Framework")
assert.NoError(t, err)
f.Close() f.Close()
dir, filename := filepath.Split(f.Name()) dir, filename := filepath.Split(f.Name())
@ -267,7 +268,7 @@ func TestRouteStaticFile(t *testing.T) {
assert.Equal(t, w, w2) assert.Equal(t, w, w2)
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "Gin Web Framework", w.Body.String()) assert.Equal(t, "Gin Web Framework", w.Body.String())
assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
w3 := performRequest(router, "HEAD", "/using_static/"+filename) w3 := performRequest(router, "HEAD", "/using_static/"+filename)
w4 := performRequest(router, "HEAD", "/result") w4 := performRequest(router, "HEAD", "/result")
@ -285,7 +286,7 @@ func TestRouteStaticListingDir(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "gin.go") assert.Contains(t, w.Body.String(), "gin.go")
assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
// TestHandleHeadToDir - ensure the root/sub dir handles properly // TestHandleHeadToDir - ensure the root/sub dir handles properly
@ -312,10 +313,10 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "package gin") assert.Contains(t, w.Body.String(), "package gin")
assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
assert.NotEqual(t, w.HeaderMap.Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST") assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.HeaderMap.Get("Expires")) assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires"))
assert.Equal(t, "Gin Framework", w.HeaderMap.Get("x-GIN")) assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
} }
func TestRouteNotAllowedEnabled(t *testing.T) { func TestRouteNotAllowedEnabled(t *testing.T) {
@ -411,6 +412,31 @@ func TestRouterNotFound(t *testing.T) {
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
} }
func TestRouterStaticFSNotFound(t *testing.T) {
router := New()
router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/")))
router.NoRoute(func(c *Context) {
c.String(404, "non existent")
})
w := performRequest(router, "GET", "/nonexistent")
assert.Equal(t, "non existent", w.Body.String())
w = performRequest(router, "HEAD", "/nonexistent")
assert.Equal(t, "non existent", w.Body.String())
}
func TestRouterStaticFSFileNotFound(t *testing.T) {
router := New()
router.StaticFS("/", http.FileSystem(http.Dir(".")))
assert.NotPanics(t, func() {
performRequest(router, "GET", "/nonexistent")
})
}
func TestRouteRawPath(t *testing.T) { func TestRouteRawPath(t *testing.T) {
route := New() route := New()
route.UseRawPath = true route.UseRawPath = true

View File

@ -12,7 +12,7 @@ import (
"testing" "testing"
) )
// Used as a workaround since we can't compare functions or their addressses // Used as a workaround since we can't compare functions or their addresses
var fakeHandlerValue string var fakeHandlerValue string
func fakeHandler(val string) HandlersChain { func fakeHandler(val string) HandlersChain {
@ -170,19 +170,19 @@ func TestTreeWildcard(t *testing.T) {
checkRequests(t, tree, testRequests{ checkRequests(t, tree, testRequests{
{"/", false, "/", nil}, {"/", false, "/", nil},
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}}, {"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
{"/cmd/test", true, "", Params{Param{"tool", "test"}}}, {"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}}, {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
{"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}}, {"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
{"/search/", false, "/search/", nil}, {"/search/", false, "/search/", nil},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, {"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
{"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}}, {"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}},
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}}, {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
{"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}}, {"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}}, {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
}) })
checkPriorities(t, tree) checkPriorities(t, tree)
@ -209,18 +209,18 @@ func TestUnescapeParameters(t *testing.T) {
unescape := true unescape := true
checkRequests(t, tree, testRequests{ checkRequests(t, tree, testRequests{
{"/", false, "/", nil}, {"/", false, "/", nil},
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}}, {"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
{"/cmd/test", true, "", Params{Param{"tool", "test"}}}, {"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
{"/src/some/file+test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file test.png"}}}, {"/src/some/file+test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file test.png"}}},
{"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file++++%%%%test.png"}}}, {"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file++++%%%%test.png"}}},
{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file/test.png"}}}, {"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file/test.png"}}},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng in ünìcodé"}}}, {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng in ünìcodé"}}},
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}}, {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{"user", "slash/gordon"}}}, {"/info/slash%2Fgordon", false, "/info/:user", Params{Param{Key: "user", Value: "slash/gordon"}}},
{"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash/gordon"}, Param{"project", "Project #1"}}}, {"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash/gordon"}, Param{Key: "project", Value: "Project #1"}}},
{"/info/slash%%%%", false, "/info/:user", Params{Param{"user", "slash%%%%"}}}, {"/info/slash%%%%", false, "/info/:user", Params{Param{Key: "user", Value: "slash%%%%"}}},
{"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash%%%%2Fgordon"}, Param{"project", "Project%%%%20%231"}}}, {"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash%%%%2Fgordon"}, Param{Key: "project", Value: "Project%%%%20%231"}}},
}, unescape) }, unescape)
checkPriorities(t, tree) checkPriorities(t, tree)
@ -326,9 +326,9 @@ func TestTreeDupliatePath(t *testing.T) {
checkRequests(t, tree, testRequests{ checkRequests(t, tree, testRequests{
{"/", false, "/", nil}, {"/", false, "/", nil},
{"/doc/", false, "/doc/", nil}, {"/doc/", false, "/doc/", nil},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, {"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
}) })
} }

43
vendor/vendor.json vendored
View File

@ -6,7 +6,9 @@
"checksumSHA1": "CSPbwbyzqA6sfORicn4HFtIhF/c=", "checksumSHA1": "CSPbwbyzqA6sfORicn4HFtIhF/c=",
"path": "github.com/davecgh/go-spew/spew", "path": "github.com/davecgh/go-spew/spew",
"revision": "8991bc29aa16c548c550c7ff78260e27b9ab7c73", "revision": "8991bc29aa16c548c550c7ff78260e27b9ab7c73",
"revisionTime": "2018-02-21T22:46:20Z" "revisionTime": "2018-02-21T22:46:20Z",
"version": "v1.1",
"versionExact": "v1.1.1"
}, },
{ {
"checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=", "checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=",
@ -15,12 +17,12 @@
"revisionTime": "2017-01-09T09:34:21Z" "revisionTime": "2017-01-09T09:34:21Z"
}, },
{ {
"checksumSHA1": "Pyou8mceOASSFxc7GeXZuVdSMi0=", "checksumSHA1": "mE9XW26JSpe4meBObM6J/Oeq0eg=",
"path": "github.com/golang/protobuf/proto", "path": "github.com/golang/protobuf/proto",
"revision": "b4deda0973fb4c70b50d226b1af49f3da59f5265", "revision": "aa810b61a9c79d51363740d207bb46cf8e620ed5",
"revisionTime": "2018-04-30T18:52:41Z", "revisionTime": "2018-08-14T21:14:27Z",
"version": "v1.1.0", "version": "v1.2",
"versionExact": "v1.1.0" "versionExact": "v1.2.0"
}, },
{ {
"checksumSHA1": "WqeEgS7pqqkwK8mlrAZmDgtWJMY=", "checksumSHA1": "WqeEgS7pqqkwK8mlrAZmDgtWJMY=",
@ -31,19 +33,19 @@
"versionExact": "v1.1.5" "versionExact": "v1.1.5"
}, },
{ {
"checksumSHA1": "y/A5iuvwjytQE2CqVuphQRXR2nI=", "checksumSHA1": "w5RcOnfv5YDr3j2bd1YydkPiZx4=",
"path": "github.com/mattn/go-isatty", "path": "github.com/mattn/go-isatty",
"revision": "0360b2af4f38e8d38c7fce2a9f4e702702d73a39", "revision": "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c",
"revisionTime": "2017-09-25T05:34:41Z", "revisionTime": "2017-11-07T05:05:31Z",
"version": "v0.0.3", "version": "v0.0",
"versionExact": "v0.0.3" "versionExact": "v0.0.4"
}, },
{ {
"checksumSHA1": "c6pbpF7eowwO59phRTpF8cQ80Z0=", "checksumSHA1": "c6pbpF7eowwO59phRTpF8cQ80Z0=",
"path": "github.com/stretchr/testify/assert", "path": "github.com/stretchr/testify/assert",
"revision": "f35b8ab0b5a2cef36673838d662e249dd9c94686", "revision": "f35b8ab0b5a2cef36673838d662e249dd9c94686",
"revisionTime": "2018-05-06T18:05:49Z", "revisionTime": "2018-05-06T18:05:49Z",
"version": "v1.2.2", "version": "v1.2",
"versionExact": "v1.2.2" "versionExact": "v1.2.2"
}, },
{ {
@ -51,15 +53,20 @@
"path": "github.com/ugorji/go/codec", "path": "github.com/ugorji/go/codec",
"revision": "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab", "revision": "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab",
"revisionTime": "2018-04-07T10:07:33Z", "revisionTime": "2018-04-07T10:07:33Z",
"version": "v1.1.1", "version": "v1.1",
"versionExact": "v1.1.1" "versionExact": "v1.1.1"
}, },
{ {
"checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=", "checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=",
"comment": "release-branch.go1.7",
"path": "golang.org/x/net/context", "path": "golang.org/x/net/context",
"revision": "d4c55e66d8c3a2f3382d264b08e3e3454a66355a", "revision": "49bb7cea24b1df9410e1712aa6433dae904ff66a",
"revisionTime": "2016-10-18T08:54:36Z" "revisionTime": "2018-10-11T05:27:23Z"
},
{
"checksumSHA1": "SiJNkx+YGtq3Gtr6Ldu6OW83O+U=",
"path": "golang.org/x/sys/unix",
"revision": "fa43e7bc11baaae89f3f902b2b4d832b68234844",
"revisionTime": "2018-10-11T14:35:51Z"
}, },
{ {
"checksumSHA1": "P/k5ZGf0lEBgpKgkwy++F7K1PSg=", "checksumSHA1": "P/k5ZGf0lEBgpKgkwy++F7K1PSg=",
@ -74,7 +81,7 @@
"path": "gopkg.in/yaml.v2", "path": "gopkg.in/yaml.v2",
"revision": "5420a8b6744d3b0345ab293f6fcba19c978f1183", "revision": "5420a8b6744d3b0345ab293f6fcba19c978f1183",
"revisionTime": "2018-03-28T19:50:20Z", "revisionTime": "2018-03-28T19:50:20Z",
"version": "v2.2.1", "version": "v2.2",
"versionExact": "v2.2.1" "versionExact": "v2.2.1"
} }
], ],

View File

@ -1 +0,0 @@
box: wercker/default