Merge branch 'master' into master

This commit is contained in:
田欧 2019-02-19 20:10:29 +08:00 committed by GitHub
commit 7401dafc3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 2099 additions and 520 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

@ -7,12 +7,24 @@ go:
- 1.9.x - 1.9.x
- 1.10.x - 1.10.x
- 1.11.x - 1.11.x
- master
matrix:
fast_finish: true
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;

180
README.md
View File

@ -9,6 +9,7 @@
[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) [![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
[![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin) [![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin)
[![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases)
Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
@ -34,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)
@ -58,6 +61,8 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Bind form-data request with custom struct](#bind-form-data-request-with-custom-struct) - [Bind form-data request with custom struct](#bind-form-data-request-with-custom-struct)
- [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)
- [Set and get a cookie](#set-and-get-a-cookie)
- [Testing](#testing) - [Testing](#testing)
- [Users](#users) - [Users](#users)
@ -359,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()
@ -524,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).
@ -534,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`.
@ -574,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
@ -657,6 +703,7 @@ import (
"gopkg.in/go-playground/validator.v8" "gopkg.in/go-playground/validator.v8"
) )
// Booking contains binded and validated data.
type Booking struct { type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"` CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"` CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
@ -789,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)
@ -820,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>
``` ```
@ -1018,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")
} }
``` ```
@ -1156,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
@ -1718,11 +1799,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
} }
``` ```
@ -1835,6 +1916,78 @@ func main() {
} }
``` ```
### Define format for the log of routes
The default log of routes is:
```
[GIN-debug] POST /foo --> main.main.func1 (3 handlers)
[GIN-debug] GET /bar --> main.main.func2 (3 handlers)
[GIN-debug] GET /status --> main.main.func3 (3 handlers)
```
If you want to log this information in given format (e.g. JSON, key values or something else), then you can define this format with `gin.DebugPrintRouteFunc`.
In the example below, we log all routes with standard log package but you can use another log tools that suits of your needs.
```go
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
r.POST("/foo", func(c *gin.Context) {
c.JSON(http.StatusOK, "foo")
})
r.GET("/bar", func(c *gin.Context) {
c.JSON(http.StatusOK, "bar")
})
r.GET("/status", func(c *gin.Context) {
c.JSON(http.StatusOK, "ok")
})
// Listen and Server in http://0.0.0.0:8080
r.Run()
}
```
### 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
The `net/http/httptest` package is preferable way for HTTP testing. The `net/http/httptest` package is preferable way for HTTP testing.
@ -1888,3 +2041,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 {
@ -711,6 +751,7 @@ func (c *Context) Cookie(name string) (string, error) {
return val, nil return val, nil
} }
// Render writes the response headers and calls render.Render to render data.
func (c *Context) Render(code int, r render.Render) { func (c *Context) Render(code int, r render.Render) {
c.Status(code) c.Status(code)
@ -833,6 +874,7 @@ func (c *Context) SSEvent(name string, message interface{}) {
}) })
} }
// Stream sends a streaming response.
func (c *Context) Stream(step func(w io.Writer) bool) { func (c *Context) Stream(step func(w io.Writer) bool) {
w := c.Writer w := c.Writer
clientGone := w.CloseNotify() clientGone := w.CloseNotify()
@ -854,6 +896,7 @@ func (c *Context) Stream(step func(w io.Writer) bool) {
/******** CONTENT NEGOTIATION *******/ /******** CONTENT NEGOTIATION *******/
/************************************/ /************************************/
// Negotiate contains all negotiations data.
type Negotiate struct { type Negotiate struct {
Offered []string Offered []string
HTMLName string HTMLName string
@ -863,6 +906,7 @@ type Negotiate struct {
Data interface{} Data interface{}
} }
// Negotiate calls different Render according acceptable Accept format.
func (c *Context) Negotiate(code int, config Negotiate) { func (c *Context) Negotiate(code int, config Negotiate) {
switch c.NegotiateFormat(config.Offered...) { switch c.NegotiateFormat(config.Offered...) {
case binding.MIMEJSON: case binding.MIMEJSON:
@ -878,10 +922,11 @@ 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
} }
} }
// NegotiateFormat returns an acceptable Accept format.
func (c *Context) NegotiateFormat(offered ...string) string { func (c *Context) NegotiateFormat(offered ...string) string {
assert1(len(offered) > 0, "you must provide at least one offer") assert1(len(offered) > 0, "you must provide at least one offer")
@ -901,6 +946,7 @@ func (c *Context) NegotiateFormat(offered ...string) string {
return "" return ""
} }
// SetAccepted sets Accept header data.
func (c *Context) SetAccepted(formats ...string) { func (c *Context) SetAccepted(formats ...string) {
c.Accepted = formats c.Accepted = formats
} }

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) {
@ -784,20 +800,20 @@ func TestContextRenderHTML2(t *testing.T) {
router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}}) router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 1) assert.Len(t, router.trees, 1)
var b bytes.Buffer
setup(&b)
defer teardown()
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ) re := captureOutput(t, func() {
SetMode(DebugMode)
router.SetHTMLTemplate(templ)
SetMode(TestMode)
})
assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", b.String()) assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", re)
c.HTML(http.StatusCreated, "t", H{"name": "alexandernyquist"}) c.HTML(http.StatusCreated, "t", H{"name": "alexandernyquist"})
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
@ -978,8 +994,8 @@ func TestContextRenderProtoBuf(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
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) {
@ -1147,7 +1163,7 @@ func TestContextNegotiationFormatWithAccept(t *testing.T) {
} }
} }
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")
@ -1214,7 +1230,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)
} }
@ -1223,11 +1240,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,
@ -1249,13 +1266,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)
@ -1270,7 +1287,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)
@ -1383,6 +1400,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)
@ -1443,7 +1477,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
@ -1461,15 +1495,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())
} }
@ -1643,9 +1698,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 {
@ -1678,7 +1733,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
}) })
@ -1695,10 +1751,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

@ -6,13 +6,15 @@ package gin
import ( import (
"bytes" "bytes"
"fmt"
"html/template" "html/template"
"log" "os"
"runtime"
"strconv"
"strings"
) )
func init() { const ginSupportMinGoVer = 6
log.SetFlags(0)
}
// 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.
@ -20,11 +22,18 @@ func IsDebugging() bool {
return ginMode == debugCode return ginMode == debugCode
} }
// DebugPrintRouteFunc indicates debug log output format.
var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
if IsDebugging() { if IsDebugging() {
nuHandlers := len(handlers) nuHandlers := len(handlers)
handlerName := nameOfFunction(handlers.Last()) handlerName := nameOfFunction(handlers.Last())
debugPrint("%-6s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) if DebugPrintRouteFunc == nil {
debugPrint("%-6s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
} else {
DebugPrintRouteFunc(httpMethod, absolutePath, handlerName, nuHandlers)
}
} }
} }
@ -42,14 +51,28 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
func debugPrint(format string, values ...interface{}) { func debugPrint(format string, values ...interface{}) {
if IsDebugging() { if IsDebugging() {
log.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,8 @@ import (
"io" "io"
"log" "log"
"os" "os"
"runtime"
"sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -30,86 +32,122 @@ func TestIsDebugging(t *testing.T) {
} }
func TestDebugPrint(t *testing.T) { func TestDebugPrint(t *testing.T) {
var w bytes.Buffer re := captureOutput(t, func() {
setup(&w) SetMode(DebugMode)
defer teardown() SetMode(ReleaseMode)
debugPrint("DEBUG this!")
SetMode(ReleaseMode) SetMode(TestMode)
debugPrint("DEBUG this!") debugPrint("DEBUG this!")
SetMode(TestMode) SetMode(DebugMode)
debugPrint("DEBUG this!") debugPrint("these are %d %s", 2, "error messages")
assert.Empty(t, w.String()) SetMode(TestMode)
})
SetMode(DebugMode) assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re)
debugPrint("these are %d %s\n", 2, "error messages")
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", w.String())
} }
func TestDebugPrintError(t *testing.T) { func TestDebugPrintError(t *testing.T) {
var w bytes.Buffer re := captureOutput(t, func() {
setup(&w) SetMode(DebugMode)
defer teardown() debugPrintError(nil)
debugPrintError(errors.New("this is an error"))
SetMode(DebugMode) SetMode(TestMode)
debugPrintError(nil) })
assert.Empty(t, w.String()) assert.Equal(t, "[GIN-debug] [ERROR] this is an error\n", re)
debugPrintError(errors.New("this is an error"))
assert.Equal(t, "[GIN-debug] [ERROR] this is an error\n", w.String())
} }
func TestDebugPrintRoutes(t *testing.T) { func TestDebugPrintRoutes(t *testing.T) {
var w bytes.Buffer re := captureOutput(t, func() {
setup(&w) SetMode(DebugMode)
defer teardown() debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode)
debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest}) })
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, w.String()) assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
} }
func TestDebugPrintLoadTemplate(t *testing.T) { func TestDebugPrintLoadTemplate(t *testing.T) {
var w bytes.Buffer re := captureOutput(t, func() {
setup(&w) SetMode(DebugMode)
defer teardown() templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./testdata/template/hello.tmpl"))
debugPrintLoadTemplate(templ)
templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./testdata/template/hello.tmpl")) SetMode(TestMode)
debugPrintLoadTemplate(templ) })
assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, w.String()) assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, re)
} }
func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) { func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) {
var w bytes.Buffer re := captureOutput(t, func() {
setup(&w) SetMode(DebugMode)
defer teardown() debugPrintWARNINGSetHTMLTemplate()
SetMode(TestMode)
debugPrintWARNINGSetHTMLTemplate() })
assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", w.String()) assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", re)
} }
func TestDebugPrintWARNINGDefault(t *testing.T) { func TestDebugPrintWARNINGDefault(t *testing.T) {
var w bytes.Buffer re := captureOutput(t, func() {
setup(&w) SetMode(DebugMode)
defer teardown() debugPrintWARNINGDefault()
SetMode(TestMode)
debugPrintWARNINGDefault() })
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", w.String()) 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) {
var w bytes.Buffer re := captureOutput(t, func() {
setup(&w) SetMode(DebugMode)
defer teardown() debugPrintWARNINGNew()
SetMode(TestMode)
debugPrintWARNINGNew() })
assert.Equal(t, "[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n", w.String()) 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 setup(w io.Writer) { func captureOutput(t *testing.T, f func()) string {
SetMode(DebugMode) reader, writer, err := os.Pipe()
log.SetOutput(w) if err != nil {
panic(err)
}
stdout := os.Stdout
stderr := os.Stderr
defer func() {
os.Stdout = stdout
os.Stderr = stderr
log.SetOutput(os.Stderr)
}()
os.Stdout = writer
os.Stderr = writer
log.SetOutput(writer)
out := make(chan string)
wg := new(sync.WaitGroup)
wg.Add(1)
go func() {
var buf bytes.Buffer
wg.Done()
_, err := io.Copy(&buf, reader)
assert.NoError(t, err)
out <- buf.String()
}()
wg.Wait()
f()
writer.Close()
return <-out
} }
func teardown() { func TestGetMinVer(t *testing.T) {
SetMode(TestMode) var m uint64
log.SetOutput(os.Stdout) 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

@ -12,18 +12,25 @@ import (
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
) )
// ErrorType is an unsigned 64-bit error code as defined in the gin spec.
type ErrorType uint64 type ErrorType uint64
const ( const (
ErrorTypeBind ErrorType = 1 << 63 // used when c.Bind() fails // ErrorTypeBind is used when Context.Bind() fails.
ErrorTypeRender ErrorType = 1 << 62 // used when c.Render() fails ErrorTypeBind ErrorType = 1 << 63
// ErrorTypeRender is used when Context.Render() fails.
ErrorTypeRender ErrorType = 1 << 62
// ErrorTypePrivate indicates a private error.
ErrorTypePrivate ErrorType = 1 << 0 ErrorTypePrivate ErrorType = 1 << 0
ErrorTypePublic ErrorType = 1 << 1 // ErrorTypePublic indicates a public error.
ErrorTypePublic ErrorType = 1 << 1
// 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.
type Error struct { type Error struct {
Err error Err error
Type ErrorType Type ErrorType
@ -34,16 +41,19 @@ type errorMsgs []*Error
var _ error = &Error{} var _ error = &Error{}
// SetType sets the error's type.
func (msg *Error) SetType(flags ErrorType) *Error { func (msg *Error) SetType(flags ErrorType) *Error {
msg.Type = flags msg.Type = flags
return msg return msg
} }
// SetMeta sets the error's meta data.
func (msg *Error) SetMeta(data interface{}) *Error { func (msg *Error) SetMeta(data interface{}) *Error {
msg.Meta = data msg.Meta = data
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 {
@ -70,11 +80,12 @@ func (msg *Error) MarshalJSON() ([]byte, error) {
return json.Marshal(msg.JSON()) return json.Marshal(msg.JSON())
} }
// Error implements the error interface // Error implements the error interface.
func (msg Error) Error() string { func (msg Error) Error() string {
return msg.Err.Error() return msg.Err.Error()
} }
// IsType judges one error.
func (msg *Error) IsType(flags ErrorType) bool { func (msg *Error) IsType(flags ErrorType) bool {
return (msg.Type & flags) > 0 return (msg.Type & flags) > 0
} }
@ -138,6 +149,7 @@ func (a errorMsgs) JSON() interface{} {
} }
} }
// MarshalJSON implements the json.Marshaller interface.
func (a errorMsgs) MarshalJSON() ([]byte, error) { func (a errorMsgs) MarshalJSON() ([]byte, error) {
return json.Marshal(a.JSON()) return json.Marshal(a.JSON())
} }

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

@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var DB = make(map[string]string) var db = make(map[string]string)
func setupRouter() *gin.Engine { func setupRouter() *gin.Engine {
// Disable Console Color // Disable Console Color
@ -21,7 +21,7 @@ func setupRouter() *gin.Engine {
// Get user value // Get user value
r.GET("/user/:name", func(c *gin.Context) { r.GET("/user/:name", func(c *gin.Context) {
user := c.Params.ByName("name") user := c.Params.ByName("name")
value, ok := DB[user] value, ok := db[user]
if ok { if ok {
c.JSON(http.StatusOK, gin.H{"user": user, "value": value}) c.JSON(http.StatusOK, gin.H{"user": user, "value": value})
} else { } else {
@ -50,7 +50,7 @@ func setupRouter() *gin.Engine {
} }
if c.Bind(&json) == nil { if c.Bind(&json) == nil {
DB[user] = json.Value db[user] = json.Value
c.JSON(http.StatusOK, gin.H{"status": "ok"}) c.JSON(http.StatusOK, gin.H{"status": "ok"})
} }
}) })

View File

@ -10,6 +10,7 @@ import (
"gopkg.in/go-playground/validator.v8" "gopkg.in/go-playground/validator.v8"
) )
// Booking contains binded and validated data.
type Booking struct { type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"` CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"` CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`

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

@ -13,16 +13,19 @@ func main() {
StartGin() StartGin()
} }
// ConfigRuntime sets the number of operating system threads.
func ConfigRuntime() { func ConfigRuntime() {
nuCPU := runtime.NumCPU() nuCPU := runtime.NumCPU()
runtime.GOMAXPROCS(nuCPU) runtime.GOMAXPROCS(nuCPU)
fmt.Printf("Running with %d CPUs\n", nuCPU) fmt.Printf("Running with %d CPUs\n", nuCPU)
} }
// StartWorkers start starsWorker by goroutine.
func StartWorkers() { func StartWorkers() {
go statsWorker() go statsWorker()
} }
// StartGin starts gin web server with setting router.
func StartGin() { func StartGin() {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)

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

@ -50,6 +50,7 @@ func connectedUsers() uint64 {
return uint64(connected) return uint64(connected)
} }
// Stats returns savedStats data.
func Stats() map[string]uint64 { func Stats() map[string]uint64 {
mutexStats.RLock() mutexStats.RLock()
defer mutexStats.RUnlock() defer mutexStats.RUnlock()

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

54
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"
@ -14,11 +15,7 @@ import (
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
) )
const ( const defaultMultipartMemory = 32 << 20 // 32 MB
// Version is Framework's version.
Version = "v1.3.0"
defaultMultipartMemory = 32 << 20 // 32 MB
)
var ( var (
default404Body = []byte("404 page not found") default404Body = []byte("404 page not found")
@ -26,7 +23,10 @@ var (
defaultAppEngine bool defaultAppEngine bool
) )
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context) type HandlerFunc func(*Context)
// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc type HandlersChain []HandlerFunc
// Last returns the last handler in the chain. ie. the last handler is the main own. // Last returns the last handler in the chain. ie. the last handler is the main own.
@ -37,12 +37,15 @@ func (c HandlersChain) Last() HandlerFunc {
return nil return nil
} }
// 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.
type RoutesInfo []RouteInfo type RoutesInfo []RouteInfo
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings. // Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
@ -155,6 +158,7 @@ func (engine *Engine) allocateContext() *Context {
return &Context{engine: engine} return &Context{engine: engine}
} }
// Delims sets template left and right delims and returns a Engine instance.
func (engine *Engine) Delims(left, right string) *Engine { func (engine *Engine) Delims(left, right string) *Engine {
engine.delims = render.Delims{Left: left, Right: right} engine.delims = render.Delims{Left: left, Right: right}
return engine return engine
@ -264,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 {
@ -316,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)
@ -332,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) {
@ -400,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

@ -22,14 +22,17 @@ func engine() *gin.Engine {
return internalEngine return internalEngine
} }
// LoadHTMLGlob is a wrapper for Engine.LoadHTMLGlob.
func LoadHTMLGlob(pattern string) { func LoadHTMLGlob(pattern string) {
engine().LoadHTMLGlob(pattern) engine().LoadHTMLGlob(pattern)
} }
// LoadHTMLFiles is a wrapper for Engine.LoadHTMLFiles.
func LoadHTMLFiles(files ...string) { func LoadHTMLFiles(files ...string) {
engine().LoadHTMLFiles(files...) engine().LoadHTMLFiles(files...)
} }
// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate.
func SetHTMLTemplate(templ *template.Template) { func SetHTMLTemplate(templ *template.Template) {
engine().SetHTMLTemplate(templ) engine().SetHTMLTemplate(templ)
} }
@ -39,17 +42,18 @@ func NoRoute(handlers ...gin.HandlerFunc) {
engine().NoRoute(handlers...) engine().NoRoute(handlers...)
} }
// NoMethod sets the handlers called when... TODO // NoMethod is a wrapper for Engine.NoMethod.
func NoMethod(handlers ...gin.HandlerFunc) { func NoMethod(handlers ...gin.HandlerFunc) {
engine().NoMethod(handlers...) engine().NoMethod(handlers...)
} }
// Group creates a new router group. You should add all the routes that have common middlwares or the same path prefix. // Group creates a new router group. You should add all the routes that have common 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...)
} }
// Handle is a wrapper for Engine.Handle.
func Handle(httpMethod, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { func Handle(httpMethod, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().Handle(httpMethod, relativePath, handlers...) return engine().Handle(httpMethod, relativePath, handlers...)
} }
@ -89,10 +93,12 @@ func HEAD(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().HEAD(relativePath, handlers...) return engine().HEAD(relativePath, handlers...)
} }
// Any is a wrapper for Engine.Any.
func Any(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { func Any(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().Any(relativePath, handlers...) return engine().Any(relativePath, handlers...)
} }
// StaticFile is a wrapper for Engine.StaticFile.
func StaticFile(relativePath, filepath string) gin.IRoutes { func StaticFile(relativePath, filepath string) gin.IRoutes {
return engine().StaticFile(relativePath, filepath) return engine().StaticFile(relativePath, filepath)
} }
@ -107,6 +113,7 @@ func Static(relativePath, root string) gin.IRoutes {
return engine().Static(relativePath, root) return engine().Static(relativePath, root)
} }
// StaticFS is a wrapper for Engine.StaticFS.
func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes { func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes {
return engine().StaticFS(relativePath, fs) return engine().StaticFS(relativePath, fs)
} }
@ -120,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)

31
go.mod Normal file
View File

@ -0,0 +1,31 @@
module github.com/gin-gonic/gin
require (
github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482
github.com/golang/protobuf v1.2.0
github.com/json-iterator/go v1.1.5
github.com/mattn/go-isatty v0.0.4
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/stretchr/testify v1.3.0
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2
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/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
)

54
go.sum Normal file
View File

@ -0,0 +1,54 @@
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/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482 h1:iOz5sIQUvuOlpiC7Q6+MmJQpWnlneYX98QIGf+2m50Y=
github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
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/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/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
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/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-20190124100055-b90733256f2e h1:3GIlrlVLfkoipSReOMNAgApI0ajnalyLa/EZHHca/XI=
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/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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/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/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA=
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
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=

View File

@ -9,8 +9,12 @@ package json
import "encoding/json" import "encoding/json"
var ( var (
Marshal = json.Marshal // Marshal is exported by gin/json package.
Marshal = json.Marshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent MarshalIndent = json.MarshalIndent
NewDecoder = json.NewDecoder // NewDecoder is exported by gin/json package.
NewEncoder = json.NewEncoder NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
) )

View File

@ -9,9 +9,13 @@ package json
import "github.com/json-iterator/go" import "github.com/json-iterator/go"
var ( var (
json = jsoniter.ConfigCompatibleWithStandardLibrary json = jsoniter.ConfigCompatibleWithStandardLibrary
Marshal = json.Marshal // Marshal is exported by gin/json package.
Marshal = json.Marshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent MarshalIndent = json.MarshalIndent
NewDecoder = json.NewDecoder // NewDecoder is exported by gin/json package.
NewEncoder = json.NewEncoder NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
) )

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"

17
mode.go
View File

@ -11,12 +11,16 @@ import (
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
) )
// ENV_GIN_MODE indicates environment name for gin mode.
const ENV_GIN_MODE = "GIN_MODE" const ENV_GIN_MODE = "GIN_MODE"
const ( const (
DebugMode = "debug" // DebugMode indicates gin mode is debug.
DebugMode = "debug"
// ReleaseMode indicates gin mode is release.
ReleaseMode = "release" ReleaseMode = "release"
TestMode = "test" // TestMode indicates gin mode is test.
TestMode = "test"
) )
const ( const (
debugCode = iota debugCode = iota
@ -24,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.
@ -32,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
@ -42,6 +48,7 @@ func init() {
SetMode(mode) SetMode(mode)
} }
// SetMode sets gin mode according to input string.
func SetMode(value string) { func SetMode(value string) {
switch value { switch value {
case DebugMode, "": case DebugMode, "":
@ -59,14 +66,18 @@ func SetMode(value string) {
modeName = value modeName = value
} }
// DisableBindValidation closes the default validator.
func DisableBindValidation() { func DisableBindValidation() {
binding.Validator = nil binding.Validator = nil
} }
// EnableJsonDecoderUseNumber sets true for binding.EnableDecoderUseNumberto to
// call the UseNumber method on the JSON Decoder instance.
func EnableJsonDecoderUseNumber() { func EnableJsonDecoderUseNumber() {
binding.EnableDecoderUseNumber = true binding.EnableDecoderUseNumber = true
} }
// Mode returns currently gin mode.
func Mode() string { func Mode() string {
return modeName return modeName
} }

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,12 +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 {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
if logger != nil { if logger != nil {
stack := stack(3) stack := stack(3)
httprequest, _ := httputil.DumpRequest(c.Request, false) httprequest, _ := httputil.DumpRequest(c.Request, false)
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", timeFormat(time.Now()), string(httprequest), err, stack, reset) 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.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"
@ -24,9 +30,20 @@ func TestPanicInHandler(t *testing.T) {
w := performRequest(router, "GET", "/recovery") w := performRequest(router, "GET", "/recovery")
// TEST // TEST
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(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "TestPanicInHandler") assert.Contains(t, buffer.String(), "TestPanicInHandler")
assert.NotContains(t, buffer.String(), "GET /recovery")
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = performRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusInternalServerError, w.Code)
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.
@ -62,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

@ -6,6 +6,7 @@ package render
import "net/http" import "net/http"
// Data contains ContentType and bytes data.
type Data struct { type Data struct {
ContentType string ContentType string
Data []byte Data []byte
@ -18,6 +19,7 @@ func (r Data) Render(w http.ResponseWriter) (err error) {
return return
} }
// WriteContentType (Data) writes custom ContentType.
func (r Data) WriteContentType(w http.ResponseWriter) { func (r Data) WriteContentType(w http.ResponseWriter) {
writeContentType(w, []string{r.ContentType}) writeContentType(w, []string{r.ContentType})
} }

View File

@ -9,20 +9,27 @@ import (
"net/http" "net/http"
) )
// Delims represents a set of Left and Right delimiters for HTML template rendering.
type Delims struct { type Delims struct {
Left string // Left delimiter, defaults to {{.
Left string
// Right delimiter, defaults to }}.
Right string Right string
} }
// HTMLRender interface is to be implemented by HTMLProduction and HTMLDebug.
type HTMLRender interface { type HTMLRender interface {
// Instance returns an HTML instance.
Instance(string, interface{}) Render Instance(string, interface{}) Render
} }
// HTMLProduction contains template reference and its delims.
type HTMLProduction struct { type HTMLProduction struct {
Template *template.Template Template *template.Template
Delims Delims Delims Delims
} }
// HTMLDebug contains template delims and pattern and function with file list.
type HTMLDebug struct { type HTMLDebug struct {
Files []string Files []string
Glob string Glob string
@ -30,6 +37,7 @@ type HTMLDebug struct {
FuncMap template.FuncMap FuncMap template.FuncMap
} }
// HTML contains template reference and its name with given interface object.
type HTML struct { type HTML struct {
Template *template.Template Template *template.Template
Name string Name string
@ -38,6 +46,7 @@ type HTML struct {
var htmlContentType = []string{"text/html; charset=utf-8"} var htmlContentType = []string{"text/html; charset=utf-8"}
// Instance (HTMLProduction) returns an HTML instance which it realizes Render interface.
func (r HTMLProduction) Instance(name string, data interface{}) Render { func (r HTMLProduction) Instance(name string, data interface{}) Render {
return HTML{ return HTML{
Template: r.Template, Template: r.Template,
@ -46,6 +55,7 @@ func (r HTMLProduction) Instance(name string, data interface{}) Render {
} }
} }
// Instance (HTMLDebug) returns an HTML instance which it realizes Render interface.
func (r HTMLDebug) Instance(name string, data interface{}) Render { func (r HTMLDebug) Instance(name string, data interface{}) Render {
return HTML{ return HTML{
Template: r.loadTemplate(), Template: r.loadTemplate(),
@ -66,6 +76,7 @@ func (r HTMLDebug) loadTemplate() *template.Template {
panic("the HTML debug render was created without files or glob pattern") panic("the HTML debug render was created without files or glob pattern")
} }
// Render (HTML) executes template and writes its result with custom ContentType for response.
func (r HTML) Render(w http.ResponseWriter) error { func (r HTML) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
@ -75,6 +86,7 @@ func (r HTML) Render(w http.ResponseWriter) error {
return r.Template.ExecuteTemplate(w, r.Name, r.Data) return r.Template.ExecuteTemplate(w, r.Name, r.Data)
} }
// WriteContentType (HTML) writes HTML ContentType.
func (r HTML) WriteContentType(w http.ResponseWriter) { func (r HTML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, htmlContentType) writeContentType(w, htmlContentType)
} }

View File

@ -13,34 +13,41 @@ import (
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
) )
// JSON contains the given interface object.
type JSON struct { type JSON struct {
Data interface{} Data interface{}
} }
// IndentedJSON contains the given interface object.
type IndentedJSON struct { type IndentedJSON struct {
Data interface{} Data interface{}
} }
// SecureJSON contains the given interface object and its prefix.
type SecureJSON struct { type SecureJSON struct {
Prefix string Prefix string
Data interface{} Data interface{}
} }
// JsonpJSON contains the given interface object its callback.
type JsonpJSON struct { type JsonpJSON struct {
Callback string Callback string
Data interface{} Data interface{}
} }
// AsciiJSON contains the given interface object.
type AsciiJSON struct { type AsciiJSON struct {
Data interface{} Data interface{}
} }
// SecureJSONPrefix is a string which represents SecureJSON prefix.
type SecureJSONPrefix string type SecureJSONPrefix string
var jsonContentType = []string{"application/json; charset=utf-8"} var jsonContentType = []string{"application/json; charset=utf-8"}
var jsonpContentType = []string{"application/javascript; charset=utf-8"} var jsonpContentType = []string{"application/javascript; charset=utf-8"}
var jsonAsciiContentType = []string{"application/json"} var jsonAsciiContentType = []string{"application/json"}
// Render (JSON) writes data with custom ContentType.
func (r JSON) Render(w http.ResponseWriter) (err error) { func (r JSON) Render(w http.ResponseWriter) (err error) {
if err = WriteJSON(w, r.Data); err != nil { if err = WriteJSON(w, r.Data); err != nil {
panic(err) panic(err)
@ -48,34 +55,39 @@ func (r JSON) Render(w http.ResponseWriter) (err error) {
return return
} }
// WriteContentType (JSON) writes JSON ContentType.
func (r JSON) WriteContentType(w http.ResponseWriter) { func (r JSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
} }
// WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj interface{}) error { func WriteJSON(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
jsonBytes, err := json.Marshal(obj) jsonBytes, err := json.Marshal(obj)
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.
func (r IndentedJSON) Render(w http.ResponseWriter) error { func (r IndentedJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
jsonBytes, err := json.MarshalIndent(r.Data, "", " ") jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
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.
func (r IndentedJSON) WriteContentType(w http.ResponseWriter) { func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
} }
// Render (SecureJSON) marshals the given interface object and writes it with custom ContentType.
func (r SecureJSON) Render(w http.ResponseWriter) error { func (r SecureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
jsonBytes, err := json.Marshal(r.Data) jsonBytes, err := json.Marshal(r.Data)
@ -84,16 +96,21 @@ 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.
func (r SecureJSON) WriteContentType(w http.ResponseWriter) { func (r SecureJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
} }
// Render (JsonpJSON) marshals the given interface object and writes it and its callback with custom ContentType.
func (r JsonpJSON) Render(w http.ResponseWriter) (err error) { func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w) r.WriteContentType(w)
ret, err := json.Marshal(r.Data) ret, err := json.Marshal(r.Data)
@ -102,23 +119,37 @@ 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
} }
// WriteContentType (JsonpJSON) writes Javascript ContentType.
func (r JsonpJSON) WriteContentType(w http.ResponseWriter) { func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonpContentType) writeContentType(w, jsonpContentType)
} }
// Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType.
func (r AsciiJSON) Render(w http.ResponseWriter) (err error) { func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w) r.WriteContentType(w)
ret, err := json.Marshal(r.Data) ret, err := json.Marshal(r.Data)
@ -135,10 +166,11 @@ 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.
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) { func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonAsciiContentType) writeContentType(w, jsonAsciiContentType)
} }

View File

@ -12,10 +12,12 @@ import (
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
) )
// PureJSON contains the given interface object.
type PureJSON struct { type PureJSON struct {
Data interface{} Data interface{}
} }
// Render (PureJSON) writes custom ContentType and encodes the given interface object.
func (r PureJSON) Render(w http.ResponseWriter) error { func (r PureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
encoder := json.NewEncoder(w) encoder := json.NewEncoder(w)
@ -23,6 +25,7 @@ func (r PureJSON) Render(w http.ResponseWriter) error {
return encoder.Encode(r.Data) return encoder.Encode(r.Data)
} }
// WriteContentType (PureJSON) writes custom ContentType.
func (r PureJSON) WriteContentType(w http.ResponseWriter) { func (r PureJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
} }

View File

@ -10,20 +10,24 @@ import (
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
) )
// MsgPack contains the given interface object.
type MsgPack struct { type MsgPack struct {
Data interface{} Data interface{}
} }
var msgpackContentType = []string{"application/msgpack; charset=utf-8"} var msgpackContentType = []string{"application/msgpack; charset=utf-8"}
// WriteContentType (MsgPack) writes MsgPack ContentType.
func (r MsgPack) WriteContentType(w http.ResponseWriter) { func (r MsgPack) WriteContentType(w http.ResponseWriter) {
writeContentType(w, msgpackContentType) writeContentType(w, msgpackContentType)
} }
// Render (MsgPack) encodes the given interface object and writes data with custom ContentType.
func (r MsgPack) Render(w http.ResponseWriter) error { func (r MsgPack) Render(w http.ResponseWriter) error {
return WriteMsgPack(w, r.Data) return WriteMsgPack(w, r.Data)
} }
// WriteMsgPack writes MsgPack ContentType and encodes the given interface object.
func WriteMsgPack(w http.ResponseWriter, obj interface{}) error { func WriteMsgPack(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, msgpackContentType) writeContentType(w, msgpackContentType)
var mh codec.MsgpackHandle var mh codec.MsgpackHandle

View File

@ -10,12 +10,14 @@ import (
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
) )
// ProtoBuf contains the given interface object.
type ProtoBuf struct { type ProtoBuf struct {
Data interface{} Data interface{}
} }
var protobufContentType = []string{"application/x-protobuf"} var protobufContentType = []string{"application/x-protobuf"}
// Render (ProtoBuf) marshals the given interface object and writes data with custom ContentType.
func (r ProtoBuf) Render(w http.ResponseWriter) error { func (r ProtoBuf) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
@ -24,10 +26,11 @@ 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.
func (r ProtoBuf) WriteContentType(w http.ResponseWriter) { func (r ProtoBuf) WriteContentType(w http.ResponseWriter) {
writeContentType(w, protobufContentType) writeContentType(w, protobufContentType)
} }

View File

@ -10,6 +10,7 @@ import (
"strconv" "strconv"
) )
// Reader contains the IO reader and its length, and custom ContentType and other headers.
type Reader struct { type Reader struct {
ContentType string ContentType string
ContentLength int64 ContentLength int64
@ -26,10 +27,12 @@ func (r Reader) Render(w http.ResponseWriter) (err error) {
return return
} }
// WriteContentType (Reader) writes custom ContentType.
func (r Reader) WriteContentType(w http.ResponseWriter) { func (r Reader) WriteContentType(w http.ResponseWriter) {
writeContentType(w, []string{r.ContentType}) writeContentType(w, []string{r.ContentType})
} }
// writeHeaders writes custom Header.
func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) { func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) {
header := w.Header() header := w.Header()
for k, v := range headers { for k, v := range headers {

View File

@ -9,12 +9,14 @@ import (
"net/http" "net/http"
) )
// Redirect contains the http request reference and redirects status code and location.
type Redirect struct { type Redirect struct {
Code int Code int
Request *http.Request Request *http.Request
Location string Location string
} }
// Render (Redirect) redirects the http request to new location and writes redirect response.
func (r Redirect) Render(w http.ResponseWriter) error { func (r Redirect) Render(w http.ResponseWriter) error {
// todo(thinkerou): go1.6 not support StatusPermanentRedirect(308) // todo(thinkerou): go1.6 not support StatusPermanentRedirect(308)
// when we upgrade go version we can use http.StatusPermanentRedirect // when we upgrade go version we can use http.StatusPermanentRedirect
@ -25,4 +27,5 @@ func (r Redirect) Render(w http.ResponseWriter) error {
return nil return nil
} }
// WriteContentType (Redirect) don't write any ContentType.
func (r Redirect) WriteContentType(http.ResponseWriter) {} func (r Redirect) WriteContentType(http.ResponseWriter) {}

View File

@ -6,8 +6,11 @@ package render
import "net/http" import "net/http"
// Render interface is to be implemented by JSON, XML, HTML, YAML and so on.
type Render interface { type Render interface {
// Render writes data with custom ContentType.
Render(http.ResponseWriter) error Render(http.ResponseWriter) error
// WriteContentType writes custom ContentType.
WriteContentType(w http.ResponseWriter) WriteContentType(w http.ResponseWriter)
} }

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) {
@ -287,7 +287,7 @@ func TestRenderProtoBuf(t *testing.T) {
err = (ProtoBuf{data}).Render(w) err = (ProtoBuf{data}).Render(w)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, string(protoData[:]), w.Body.String()) assert.Equal(t, string(protoData), w.Body.String())
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
} }
@ -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

@ -10,6 +10,7 @@ import (
"net/http" "net/http"
) )
// String contains the given interface object slice and its format.
type String struct { type String struct {
Format string Format string
Data []interface{} Data []interface{}
@ -17,20 +18,23 @@ type String struct {
var plainContentType = []string{"text/plain; charset=utf-8"} var plainContentType = []string{"text/plain; charset=utf-8"}
// 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.
func (r String) WriteContentType(w http.ResponseWriter) { func (r String) WriteContentType(w http.ResponseWriter) {
writeContentType(w, plainContentType) writeContentType(w, plainContentType)
} }
func WriteString(w http.ResponseWriter, format string, data []interface{}) { // WriteString writes data according to its format and write custom ContentType.
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

@ -9,17 +9,20 @@ import (
"net/http" "net/http"
) )
// XML contains the given interface object.
type XML struct { type XML struct {
Data interface{} Data interface{}
} }
var xmlContentType = []string{"application/xml; charset=utf-8"} var xmlContentType = []string{"application/xml; charset=utf-8"}
// Render (XML) encodes the given interface object and writes data with custom ContentType.
func (r XML) Render(w http.ResponseWriter) error { func (r XML) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
return xml.NewEncoder(w).Encode(r.Data) return xml.NewEncoder(w).Encode(r.Data)
} }
// WriteContentType (XML) writes XML ContentType for response.
func (r XML) WriteContentType(w http.ResponseWriter) { func (r XML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, xmlContentType) writeContentType(w, xmlContentType)
} }

View File

@ -10,12 +10,14 @@ import (
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
// YAML contains the given interface object.
type YAML struct { type YAML struct {
Data interface{} Data interface{}
} }
var yamlContentType = []string{"application/x-yaml; charset=utf-8"} var yamlContentType = []string{"application/x-yaml; charset=utf-8"}
// Render (YAML) marshals the given interface object and writes data with custom ContentType.
func (r YAML) Render(w http.ResponseWriter) error { func (r YAML) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
@ -24,10 +26,11 @@ 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.
func (r YAML) WriteContentType(w http.ResponseWriter) { func (r YAML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, yamlContentType) writeContentType(w, yamlContentType)
} }

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

@ -11,11 +11,13 @@ import (
"strings" "strings"
) )
// IRouter defines all router handle interface includes single and group router.
type IRouter interface { type IRouter interface {
IRoutes IRoutes
Group(string, ...HandlerFunc) *RouterGroup Group(string, ...HandlerFunc) *RouterGroup
} }
// IRoutes defines all router handle interface.
type IRoutes interface { type IRoutes interface {
Use(...HandlerFunc) IRoutes Use(...HandlerFunc) IRoutes
@ -34,8 +36,8 @@ type IRoutes interface {
StaticFS(string, http.FileSystem) IRoutes StaticFS(string, http.FileSystem) IRoutes
} }
// RouterGroup is used internally to configure router, a RouterGroup is associated with a prefix // RouterGroup is used internally to configure router, a RouterGroup is associated with
// and an array of handlers (middleware). // a prefix and an array of handlers (middleware).
type RouterGroup struct { type RouterGroup struct {
Handlers HandlersChain Handlers HandlersChain
basePath string basePath string
@ -45,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),
@ -61,6 +63,8 @@ func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *R
} }
} }
// BasePath returns the base path of router group.
// For example, if v := router.Group("/rest/n/v1/api"), v.BasePath() is "/rest/n/v1/api".
func (group *RouterGroup) BasePath() string { func (group *RouterGroup) BasePath() string {
return group.basePath return group.basePath
} }
@ -74,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.
@ -181,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

11
tree.go
View File

@ -193,9 +193,16 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
} }
} }
panic("path segment '" + path + pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(path, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
"' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path + "' conflicts with existing wildcard '" + n.path +
"' in path '" + fullPath + "'") "' in existing prefix '" + prefix +
"'")
} }
c := path[0] c := path[0]

View File

@ -5,12 +5,14 @@
package gin package gin
import ( import (
"fmt"
"reflect" "reflect"
"regexp"
"strings" "strings"
"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 {
@ -125,8 +127,6 @@ func TestTreeAddAndGet(t *testing.T) {
tree.addRoute(route, fakeHandler(route)) tree.addRoute(route, fakeHandler(route))
} }
//printChildren(tree, "")
checkRequests(t, tree, testRequests{ checkRequests(t, tree, testRequests{
{"/a", false, "/a", nil}, {"/a", false, "/a", nil},
{"/", true, "", nil}, {"/", true, "", nil},
@ -168,23 +168,21 @@ func TestTreeWildcard(t *testing.T) {
tree.addRoute(route, fakeHandler(route)) tree.addRoute(route, fakeHandler(route))
} }
//printChildren(tree, "")
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)
@ -208,22 +206,21 @@ func TestUnescapeParameters(t *testing.T) {
tree.addRoute(route, fakeHandler(route)) tree.addRoute(route, fakeHandler(route))
} }
//printChildren(tree, "")
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)
@ -260,8 +257,6 @@ func testRoutes(t *testing.T, routes []testRoute) {
t.Errorf("unexpected panic for route '%s': %v", route.path, recv) t.Errorf("unexpected panic for route '%s': %v", route.path, recv)
} }
} }
//printChildren(tree, "")
} }
func TestTreeWildcardConflict(t *testing.T) { func TestTreeWildcardConflict(t *testing.T) {
@ -328,14 +323,12 @@ func TestTreeDupliatePath(t *testing.T) {
} }
} }
//printChildren(tree, "")
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"}}},
}) })
} }
@ -444,8 +437,6 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
} }
} }
//printChildren(tree, "")
tsrRoutes := [...]string{ tsrRoutes := [...]string{
"/hi/", "/hi/",
"/b", "/b",
@ -664,3 +655,43 @@ func TestTreeInvalidNodeType(t *testing.T) {
t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
} }
} }
func TestTreeWildcardConflictEx(t *testing.T) {
conflicts := [...]struct {
route string
segPath string
existPath string
existSegPath string
}{
{"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
{"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
{"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
{"/conxxx", "xxx", `/con:tact`, `:tact`},
{"/conooo/xxx", "ooo", `/con:tact`, `:tact`},
}
for _, conflict := range conflicts {
// I have to re-create a 'tree', because the 'tree' will be
// in an inconsistent state when the loop recovers from the
// panic which threw by 'addRoute' function.
tree := &node{}
routes := [...]string{
"/con:tact",
"/who/are/*you",
"/who/foo/hello",
}
for _, route := range routes {
tree.addRoute(route, fakeHandler(route))
}
recv := catchPanic(func() {
tree.addRoute(conflict.route, fakeHandler(conflict.route))
})
if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'",
conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) {
t.Fatalf("invalid wildcard conflict error (%v)", recv)
}
}
}

View File

@ -14,8 +14,10 @@ import (
"strings" "strings"
) )
// BindKey indicates a default bind key.
const BindKey = "_gin-gonic/gin/bindkey" const BindKey = "_gin-gonic/gin/bindkey"
// Bind is a helper function for given interface object and returns a Gin middleware.
func Bind(val interface{}) HandlerFunc { func Bind(val interface{}) HandlerFunc {
value := reflect.ValueOf(val) value := reflect.ValueOf(val)
if value.Kind() == reflect.Ptr { if value.Kind() == reflect.Ptr {
@ -33,16 +35,14 @@ func Bind(val interface{}) HandlerFunc {
} }
} }
// WrapF is a helper function for wrapping http.HandlerFunc // WrapF is a helper function for wrapping http.HandlerFunc and returns a Gin middleware.
// Returns a Gin middleware
func WrapF(f http.HandlerFunc) HandlerFunc { func WrapF(f http.HandlerFunc) HandlerFunc {
return func(c *Context) { return func(c *Context) {
f(c.Writer, c.Request) f(c.Writer, c.Request)
} }
} }
// WrapH is a helper function for wrapping http.Handler // WrapH is a helper function for wrapping http.Handler and returns a Gin middleware.
// Returns a Gin middleware
func WrapH(h http.Handler) HandlerFunc { func WrapH(h http.Handler) HandlerFunc {
return func(c *Context) { return func(c *Context) {
h.ServeHTTP(c.Writer, c.Request) h.ServeHTTP(c.Writer, c.Request)

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"
} }
], ],

8
version.go Normal file
View File

@ -0,0 +1,8 @@
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
// Version is the current gin framework's version.
const Version = "v1.4.0-dev"

View File

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